diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml
index b6daa6997..6a5bd41c3 100644
--- a/.github/workflows/test-php.yml
+++ b/.github/workflows/test-php.yml
@@ -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 }}
@@ -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
- - 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
diff --git a/php/.gitignore b/php/.gitignore
index 4a6f7c9af..f4467a9d5 100644
--- a/php/.gitignore
+++ b/php/.gitignore
@@ -1,5 +1,4 @@
build
-acceptance
vendor
composer.lock
.phpunit.cache
diff --git a/php/Makefile b/php/Makefile
index 034aa6b59..526b54913 100644
--- a/php/Makefile
+++ b/php/Makefile
@@ -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
@@ -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
+ 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
@@ -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 "." $@)
diff --git a/php/bin/gherkin-generate-tokens b/php/bin/gherkin-generate-tokens
deleted file mode 100755
index 0c9ce2ed8..000000000
--- a/php/bin/gherkin-generate-tokens
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env php
-parse(
- $fileName,
- new StringTokenScanner(file_get_contents($fileName)),
- new TokenMatcher(),
- );
- echo $result;
-}
diff --git a/php/composer.json b/php/composer.json
index 5452d158a..302750490 100644
--- a/php/composer.json
+++ b/php/composer.json
@@ -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": [
{
@@ -34,5 +36,10 @@
}
}
}
- ]
+ ],
+ "config": {
+ "allow-plugins": {
+ "infection/extension-installer": false
+ }
+ }
}
diff --git a/php/infection.json5 b/php/infection.json5
new file mode 100644
index 000000000..6c187660f
--- /dev/null
+++ b/php/infection.json5
@@ -0,0 +1,11 @@
+{
+ "$schema": "vendor/infection/infection/resources/schema.json",
+ "source": {
+ "directories": [
+ "src",
+ ]
+ },
+ "mutators": {
+ "@default": true
+ }
+}
diff --git a/php/phpunit.xml b/php/phpunit.xml
index fd5e5582f..7e569f1af 100644
--- a/php/phpunit.xml
+++ b/php/phpunit.xml
@@ -12,8 +12,11 @@
failOnWarning="true"
verbose="true">
-
- tests
+
+ tests/unit
+
+
+ tests/acceptance/TestDataTest.php
diff --git a/php/psalm.xml b/php/psalm.xml
index 6686ed61d..f267e4ebb 100644
--- a/php/psalm.xml
+++ b/php/psalm.xml
@@ -7,7 +7,6 @@
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
-
diff --git a/php/tests/acceptance/TestDataTest.php b/php/tests/acceptance/TestDataTest.php
new file mode 100644
index 000000000..0eaa7b607
--- /dev/null
+++ b/php/tests/acceptance/TestDataTest.php
@@ -0,0 +1,144 @@
+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
+ */
+ public function provideGoodFeatureFiles(): iterable
+ {
+ return $this->provideFeatureFiles("good");
+ }
+
+ /**
+ * @return iterable
+ */
+ public function provideBadFeatureFiles(): iterable
+ {
+ return $this->provideFeatureFiles("bad");
+ }
+
+ /**
+ * @param 'good'|'bad' $subDir
+ *
+ * @return iterable
+ */
+ 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 $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], '');
+ }
+ }
+ }
+}
diff --git a/php/tests/AstNodeTest.php b/php/tests/unit/AstNodeTest.php
similarity index 100%
rename from php/tests/AstNodeTest.php
rename to php/tests/unit/AstNodeTest.php
diff --git a/php/tests/GherkinDialectProviderTest.php b/php/tests/unit/GherkinDialectProviderTest.php
similarity index 100%
rename from php/tests/GherkinDialectProviderTest.php
rename to php/tests/unit/GherkinDialectProviderTest.php
diff --git a/php/tests/GherkinDialectTest.php b/php/tests/unit/GherkinDialectTest.php
similarity index 100%
rename from php/tests/GherkinDialectTest.php
rename to php/tests/unit/GherkinDialectTest.php
diff --git a/php/tests/GherkinLineSpanTest.php b/php/tests/unit/GherkinLineSpanTest.php
similarity index 100%
rename from php/tests/GherkinLineSpanTest.php
rename to php/tests/unit/GherkinLineSpanTest.php
diff --git a/php/tests/Parser/RuleTypeTest.php b/php/tests/unit/Parser/RuleTypeTest.php
similarity index 100%
rename from php/tests/Parser/RuleTypeTest.php
rename to php/tests/unit/Parser/RuleTypeTest.php
diff --git a/php/tests/ParserException/CompositeParserExceptionTest.php b/php/tests/unit/ParserException/CompositeParserExceptionTest.php
similarity index 100%
rename from php/tests/ParserException/CompositeParserExceptionTest.php
rename to php/tests/unit/ParserException/CompositeParserExceptionTest.php
diff --git a/php/tests/ParserException/NoSuchLanguageExceptionTest.php b/php/tests/unit/ParserException/NoSuchLanguageExceptionTest.php
similarity index 100%
rename from php/tests/ParserException/NoSuchLanguageExceptionTest.php
rename to php/tests/unit/ParserException/NoSuchLanguageExceptionTest.php
diff --git a/php/tests/ParserException/UnexpectedEofExceptionTest.php b/php/tests/unit/ParserException/UnexpectedEofExceptionTest.php
similarity index 100%
rename from php/tests/ParserException/UnexpectedEofExceptionTest.php
rename to php/tests/unit/ParserException/UnexpectedEofExceptionTest.php
diff --git a/php/tests/ParserException/UnexpectedTokenExceptionTest.php b/php/tests/unit/ParserException/UnexpectedTokenExceptionTest.php
similarity index 100%
rename from php/tests/ParserException/UnexpectedTokenExceptionTest.php
rename to php/tests/unit/ParserException/UnexpectedTokenExceptionTest.php
diff --git a/php/tests/StringGherkinLineTest.php b/php/tests/unit/StringGherkinLineTest.php
similarity index 100%
rename from php/tests/StringGherkinLineTest.php
rename to php/tests/unit/StringGherkinLineTest.php
diff --git a/php/tests/StringTokenScannerTest.php b/php/tests/unit/StringTokenScannerTest.php
similarity index 100%
rename from php/tests/StringTokenScannerTest.php
rename to php/tests/unit/StringTokenScannerTest.php
diff --git a/php/tests/StringUtilsTest.php b/php/tests/unit/StringUtilsTest.php
similarity index 100%
rename from php/tests/StringUtilsTest.php
rename to php/tests/unit/StringUtilsTest.php
diff --git a/php/tests/TokenFormatterBuilderTest.php b/php/tests/unit/TokenFormatterBuilderTest.php
similarity index 100%
rename from php/tests/TokenFormatterBuilderTest.php
rename to php/tests/unit/TokenFormatterBuilderTest.php
diff --git a/php/tests/TokenFormatterTest.php b/php/tests/unit/TokenFormatterTest.php
similarity index 100%
rename from php/tests/TokenFormatterTest.php
rename to php/tests/unit/TokenFormatterTest.php
diff --git a/php/tests/TokenMatchTest.php b/php/tests/unit/TokenMatchTest.php
similarity index 100%
rename from php/tests/TokenMatchTest.php
rename to php/tests/unit/TokenMatchTest.php
diff --git a/php/tests/TokenMatcherTest.php b/php/tests/unit/TokenMatcherTest.php
similarity index 100%
rename from php/tests/TokenMatcherTest.php
rename to php/tests/unit/TokenMatcherTest.php
diff --git a/php/tests/TokenTest.php b/php/tests/unit/TokenTest.php
similarity index 100%
rename from php/tests/TokenTest.php
rename to php/tests/unit/TokenTest.php