Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHP - Introduce Mutation testing #52

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/test-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php }}"
ini-values: "memory_limit=-1"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down Expand Up @@ -56,8 +57,23 @@ jobs:
run: |
vendor/bin/php-cs-fixer --dry-run --diff fix
vendor/bin/psalm --no-cache
vendor/bin/phpunit
vendor/bin/phpunit --testsuite unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can run all the tests in a regular run. The separation between acceptance and unit existed only because we had different methods for testing.


- name: run acceptance tests
- name: Run acceptance tests
run: make acceptance
working-directory: php

- name: Mutation tests - minimum thresholds
run: |
vendor/bin/roave-infection-static-analysis-plugin \
--min-msi=90 \
--min-covered-msi=90
working-directory: php

- name: Mutation tests - modifications
run: |
git fetch --depth=1 origin $GITHUB_BASE_REF
vendor/bin/roave-infection-static-analysis-plugin \
--git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF \
--logger-github --ignore-msi-with-no-mutations
working-directory: php
1 change: 0 additions & 1 deletion php/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
build
acceptance
vendor
composer.lock
.phpunit.cache
Expand Down
42 changes: 4 additions & 38 deletions php/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@ GHERKIN_RAZOR = gherkin-php.razor
SOURCE_FILES = $(shell find . -name "*.php" | grep -v $(GHERKIN_PARSER))

GHERKIN = bin/gherkin
GHERKIN_GENERATE_TOKENS = bin/gherkin-generate-tokens

GOOD_FEATURE_FILES = $(shell find ../testdata/good -name "*.feature")
BAD_FEATURE_FILES = $(shell find ../testdata/bad -name "*.feature")

TOKENS = $(patsubst ../testdata/%,acceptance/testdata/%.tokens,$(GOOD_FEATURE_FILES))
ASTS = $(patsubst ../testdata/%,acceptance/testdata/%.ast.ndjson,$(GOOD_FEATURE_FILES))
PICKLES = $(patsubst ../testdata/%,acceptance/testdata/%.pickles.ndjson,$(GOOD_FEATURE_FILES))
SOURCES = $(patsubst ../testdata/%,acceptance/testdata/%.source.ndjson,$(GOOD_FEATURE_FILES))
ERRORS = $(patsubst ../testdata/%,acceptance/testdata/%.errors.ndjson,$(BAD_FEATURE_FILES))

.DEFAULT_GOAL = help

Expand All @@ -39,12 +29,13 @@ clean: ## Remove all build artifacts and files generated by the acceptance tests

.DELETE_ON_ERROR:

acceptance: .built $(TOKENS) $(ASTS) $(PICKLES) $(ERRORS) $(SOURCES) ## Build acceptance test dir and compare results with reference
acceptance: .built ## Test parser against test data
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this should be .PHONY now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the goal entirety.

Do update the php/README.md with info about the structure.

vendor/bin/phpunit --testsuite acceptance

.built: vendor $(SOURCE_FILES)
.built: vendor/autoload.php $(SOURCE_FILES)
touch $@

vendor: composer.json
vendor/autoload.php: composer.json
composer update

$(GHERKIN_PARSER): $(GHERKIN_RAZOR) ../gherkin.berp
Expand All @@ -57,28 +48,3 @@ $(GHERKIN_PARSER): $(GHERKIN_RAZOR) ../gherkin.berp

$(GHERKIN_LANGUAGES_JSON):
cp ../gherkin-languages.json $@

acceptance/testdata/%.tokens: ../testdata/% ../testdata/%.tokens
mkdir -p $(@D)
$(GHERKIN_GENERATE_TOKENS) $< > $@
diff --unified $<.tokens $@

acceptance/testdata/%.ast.ndjson: ../testdata/% ../testdata/%.ast.ndjson
mkdir -p $(@D)
$(GHERKIN) --no-source --no-pickles --predictable-ids $< | jq --sort-keys --compact-output "." > $@
diff --unified <(jq "." $<.ast.ndjson) <(jq "." $@)

acceptance/testdata/%.pickles.ndjson: ../testdata/% ../testdata/%.pickles.ndjson
mkdir -p $(@D)
$(GHERKIN) --no-source --no-ast --predictable-ids $< | jq --sort-keys --compact-output "." > $@
diff --unified <(jq "." $<.pickles.ndjson) <(jq "." $@)

acceptance/testdata/%.source.ndjson: ../testdata/% ../testdata/%.source.ndjson
mkdir -p $(@D)
$(GHERKIN) --no-ast --no-pickles --predictable-ids $< | jq --sort-keys --compact-output "." > $@
diff --unified <(jq "." $<.source.ndjson) <(jq "." $@)

acceptance/testdata/%.errors.ndjson: ../testdata/% ../testdata/%.errors.ndjson
mkdir -p $(@D)
$(GHERKIN) --no-source --predictable-ids $< | jq --sort-keys --compact-output "." > $@
diff --unified <(jq "." $<.errors.ndjson) <(jq "." $@)
24 changes: 0 additions & 24 deletions php/bin/gherkin-generate-tokens

This file was deleted.

11 changes: 9 additions & 2 deletions php/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"vimeo/psalm": "^4.24",
"friendsofphp/php-cs-fixer": "^3.5",
"psalm/plugin-phpunit": "^0.18.0",
"nikic/php-parser": "^4.14"
"nikic/php-parser": "^4.14",
"infection/infection": "^0.26.16",
"roave/infection-static-analysis-plugin": "^1.25"
},
"repositories": [
{
Expand All @@ -34,5 +36,10 @@
}
}
}
]
],
"config": {
"allow-plugins": {
"infection/extension-installer": false
}
}
}
11 changes: 11 additions & 0 deletions php/infection.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "vendor/infection/infection/resources/schema.json",
"source": {
"directories": [
"src",
]
},
"mutators": {
"@default": true
}
}
7 changes: 5 additions & 2 deletions php/phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
<testsuite name="unit">
<directory>tests/unit</directory>
</testsuite>
<testsuite name="acceptance">
<file>tests/acceptance/TestDataTest.php</file>
</testsuite>
</testsuites>

Expand Down
1 change: 0 additions & 1 deletion php/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<file name="bin/gherkin-generate-tokens"/>
<file name="bin/gherkin"/>
<directory name="src"/>
<directory name="src-generated"/>
Expand Down
144 changes: 144 additions & 0 deletions php/tests/acceptance/TestDataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

namespace Cucumber\Gherkin;

use Cucumber\Messages\Envelope;
use Cucumber\Messages\Source;
use Cucumber\Messages\Streams\NdJson\NdJsonStreamWriter;
use PHPUnit\Framework\TestCase;

final class TestDataTest extends TestCase
{
/**
* @dataProvider provideGoodFeatureFiles
*/
public function testTokensAreSameAsTestData(string $fullPath): void
{
$result = (new Parser(new TokenFormatterBuilder()))->parse(
$fullPath,
new StringTokenScanner(file_get_contents($fullPath)),
new TokenMatcher(),
);

self::assertStringEqualsFile($fullPath . '.tokens', $result);
}

/**
* @dataProvider provideGoodFeatureFiles
*/
public function testAstsAreSameAsTestData(string $fullPath, Source $source): void
{
$envelopes = (new GherkinParser(
predictableIds: true,
includeSource: false,
includeGherkinDocument: true,
includePickles: false,
))->parse([$source]);

self::assertEnvelopesMatchNdJsonFile($envelopes, $fullPath . '.ast.ndjson');
}

/**
* @dataProvider provideGoodFeatureFiles
*/
public function testSourcesAreSameAsTestData(string $fullPath, Source $source): void
{
$envelopes = (new GherkinParser(
predictableIds: true,
includeSource: true,
includeGherkinDocument: false,
includePickles: false,
))->parse([$source]);

self::assertEnvelopesMatchNdJsonFile($envelopes, $fullPath . '.source.ndjson');
}

/**
* @dataProvider provideGoodFeatureFiles
*/
public function testPicklesAreSameAsTestData(string $fullPath, Source $source): void
{
$envelopes = (new GherkinParser(
predictableIds: true,
includeSource: false,
includeGherkinDocument: false,
includePickles: true,
))->parse([$source]);

self::assertEnvelopesMatchNdJsonFile($envelopes, $fullPath . '.pickles.ndjson');
}

/**
* @dataProvider provideBadFeatureFiles
*/
public function testErrorsAreSameAsTestData(string $fullPath, Source $source): void
{
$envelopes = (new GherkinParser(
predictableIds: true,
includeSource: false,
includeGherkinDocument: false,
includePickles: true,
))->parse([$source]);

self::assertEnvelopesMatchNdJsonFile($envelopes, $fullPath . '.errors.ndjson');
}

/**
* @return iterable<string, array{0: string, 1: Source}>
*/
public function provideGoodFeatureFiles(): iterable
{
return $this->provideFeatureFiles("good");
}

/**
* @return iterable<string, array{0: string, 1: Source}>
*/
public function provideBadFeatureFiles(): iterable
{
return $this->provideFeatureFiles("bad");
}

/**
* @param 'good'|'bad' $subDir
*
* @return iterable<string, array{0: string, 1: Source}>
*/
private function provideFeatureFiles(string $subDir): iterable
{
foreach (glob(__DIR__ . "/../../../testdata/$subDir/*.feature") as $fullPath) {
$shortPath = substr($fullPath, strlen(__DIR__ . '/../../'));

yield $shortPath => [$fullPath, new Source($shortPath, file_get_contents($fullPath))];
}
}

/**
* @param iterable<Envelope> $envelopes
*/
private static function assertEnvelopesMatchNdJsonFile(iterable $envelopes, string $expectedfile): void
{
$output = fopen('php://memory', 'w');
NdJsonStreamWriter::fromFileHandle($output)->writeEnvelopes($envelopes);
rewind($output);

$actual = stream_get_contents($output);
$expected = file_get_contents($expectedfile);

// rather than compare the full file, compare line by line to get better JSON diffs on error
$actualLines = explode("\n", $actual);
$expectedLines = explode("\n", $expected);

self::assertSame(count($actualLines), count($expectedLines));

foreach ($actualLines as $i => $actualLine) {
if ($actualLine !== '') {
self::assertJsonStringEqualsJsonString($expectedLines[$i], $actualLine);
} else {
self::assertEquals($expectedLines[$i], '');
}
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.