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