From 2b52e24cbd5c02f54236f7cb672790e5b47435f1 Mon Sep 17 00:00:00 2001 From: homersimpsons Date: Mon, 1 Apr 2024 22:57:12 +0000 Subject: [PATCH 1/2] :construction: test generator --- .devcontainer/devcontainer.json | 9 + exercises/practice/list-ops/ListOpsTest.php | 79 ++++----- .../practice/list-ops/ListOpsTest.php.twig | 164 ++++++++++++++++++ test-generator/.gitignore | 3 + test-generator/README.md | 26 +++ test-generator/composer.json | 41 +++++ test-generator/main.php | 8 + test-generator/phpcs.xml.dist | 37 ++++ test-generator/phpstan.neon | 2 + test-generator/phpunit.xml.dist | 24 +++ test-generator/src/Application.php | 150 ++++++++++++++++ test-generator/src/TomlParser.php | 79 +++++++++ test-generator/tests/ApplicationTest.php | 33 ++++ 13 files changed, 612 insertions(+), 43 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 exercises/practice/list-ops/ListOpsTest.php.twig create mode 100644 test-generator/.gitignore create mode 100644 test-generator/README.md create mode 100644 test-generator/composer.json create mode 100644 test-generator/main.php create mode 100644 test-generator/phpcs.xml.dist create mode 100644 test-generator/phpstan.neon create mode 100644 test-generator/phpunit.xml.dist create mode 100644 test-generator/src/Application.php create mode 100644 test-generator/src/TomlParser.php create mode 100644 test-generator/tests/ApplicationTest.php diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..a103e3979 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/php:1": { + "version": "8.3", + "installComposer": true + } + } + } \ No newline at end of file diff --git a/exercises/practice/list-ops/ListOpsTest.php b/exercises/practice/list-ops/ListOpsTest.php index 4d0f35ead..89ee592f7 100644 --- a/exercises/practice/list-ops/ListOpsTest.php +++ b/exercises/practice/list-ops/ListOpsTest.php @@ -36,7 +36,7 @@ public static function setUpBeforeClass(): void /** * @testdox append entries to a list and return the new list -> empty lists */ - public function testAppendEmptyLists() + public function testAppendEntriesToAListAndReturnTheNewListWithEmptyLists() { $listOps = new ListOps(); $this->assertEquals([], $listOps->append([], [])); @@ -45,25 +45,25 @@ public function testAppendEmptyLists() /** * @testdox append entries to a list and return the new list -> list to empty list */ - public function testAppendNonEmptyListToEmptyList() + public function testAppendEntriesToAListAndReturnTheNewListWithListToEmptyList() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 3, 4], $listOps->append([1, 2, 3, 4], [])); + $this->assertEquals([1, 2, 3, 4], $listOps->append([], [1, 2, 3, 4])); } /** * @testdox append entries to a list and return the new list -> empty list to list */ - public function testAppendEmptyListToNonEmptyList() + public function testAppendEntriesToAListAndReturnTheNewListWithEmptyListToList() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 3, 4], $listOps->append([], [1, 2, 3, 4])); + $this->assertEquals([1, 2, 3, 4], $listOps->append([1, 2, 3, 4], [])); } /** * @testdox append entries to a list and return the new list -> non-empty lists */ - public function testAppendNonEmptyLists() + public function testAppendEntriesToAListAndReturnTheNewListWithNonEmptyLists() { $listOps = new ListOps(); $this->assertEquals([1, 2, 2, 3, 4, 5], $listOps->append([1, 2], [2, 3, 4, 5])); @@ -72,16 +72,16 @@ public function testAppendNonEmptyLists() /** * @testdox concatenate a list of lists -> empty list */ - public function testConcatEmptyLists() + public function testConcatenateAListOfListsWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals([], $listOps->concat([], [])); + $this->assertEquals([], $listOps->concat()); } /** * @testdox concatenate a list of lists -> list of lists */ - public function testConcatLists() + public function testConcatenateAListOfListsWithListOfLists() { $listOps = new ListOps(); $this->assertEquals([1, 2, 3, 4, 5, 6], $listOps->concat([1, 2], [3], [], [4, 5, 6])); @@ -90,7 +90,7 @@ public function testConcatLists() /** * @testdox concatenate a list of lists -> list of nested lists */ - public function testConcatNestedLists() + public function testConcatenateAListOfListsWithListOfNestedLists() { $listOps = new ListOps(); $this->assertEquals([[1], [2], [3], [], [4, 5, 6]], $listOps->concat([[1], [2]], [[3]], [[]], [[4, 5, 6]])); @@ -99,7 +99,7 @@ public function testConcatNestedLists() /** * @testdox filter list returning only values that satisfy the filter function -> empty list */ - public function testFilterEmptyList() + public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWithEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -109,9 +109,9 @@ public function testFilterEmptyList() } /** - * @testdox filter list returning only values that satisfy the filter function -> non empty list + * @testdox filter list returning only values that satisfy the filter function -> non-empty list */ - public function testFilterNonEmptyList() + public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWithNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -123,7 +123,7 @@ public function testFilterNonEmptyList() /** * @testdox returns the length of a list -> empty list */ - public function testLengthEmptyList() + public function testReturnsTheLengthOfAListWithEmptyList() { $listOps = new ListOps(); $this->assertEquals(0, $listOps->length([])); @@ -132,16 +132,16 @@ public function testLengthEmptyList() /** * @testdox returns the length of a list -> non-empty list */ - public function testLengthNonEmptyList() + public function testReturnsTheLengthOfAListWithNonEmptyList() { $listOps = new ListOps(); $this->assertEquals(4, $listOps->length([1, 2, 3, 4])); } /** - * @testdox returns a list of elements whose values equal the list value transformed by the mapping function -> empty list + * @testdox return a list of elements whose values equal the list value transformed by the mapping function -> empty list */ - public function testMapEmptyList() + public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformedByTheMappingFunctionWithEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -151,9 +151,9 @@ public function testMapEmptyList() } /** - * @testdox returns a list of elements whose values equal the list value transformed by the mapping function -> non-empty list + * @testdox return a list of elements whose values equal the list value transformed by the mapping function -> non-empty list */ - public function testMapNonEmptyList() + public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformedByTheMappingFunctionWithNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -165,7 +165,7 @@ public function testMapNonEmptyList() /** * @testdox folds (reduces) the given list from the left with a function -> empty list */ - public function testFoldlEmptyList() + public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -177,19 +177,19 @@ public function testFoldlEmptyList() /** * @testdox folds (reduces) the given list from the left with a function -> direction independent function applied to non-empty list */ - public function testFoldlDirectionIndependentNonEmptyList() + public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectionIndependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( 15, - $listOps->foldl(static fn ($acc, $el) => $acc + $el, [1, 2, 3, 4], 5) + $listOps->foldl(static fn ($acc, $el) => $el + $acc, [1, 2, 3, 4], 5) ); } /** * @testdox folds (reduces) the given list from the left with a function -> direction dependent function applied to non-empty list */ - public function testFoldlDirectionDependentNonEmptyList() + public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectionDependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -201,7 +201,7 @@ public function testFoldlDirectionDependentNonEmptyList() /** * @testdox folds (reduces) the given list from the right with a function -> empty list */ - public function testFoldrEmptyList() + public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -209,23 +209,21 @@ public function testFoldrEmptyList() $listOps->foldr(static fn ($acc, $el) => $el * $acc, [], 2) ); } - /** * @testdox folds (reduces) the given list from the right with a function -> direction independent function applied to non-empty list */ - public function testFoldrDirectionIndependentNonEmptyList() + public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithDirectionIndependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( 15, - $listOps->foldr(static fn ($acc, $el) => $acc + $el, [1, 2, 3, 4], 5) + $listOps->foldr(static fn ($acc, $el) => $el + $acc, [1, 2, 3, 4], 5) ); } - /** * @testdox folds (reduces) the given list from the right with a function -> direction dependent function applied to non-empty list */ - public function testFoldrDirectionDependentNonEmptyList() + public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithDirectionDependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); $this->assertEquals( @@ -233,29 +231,24 @@ public function testFoldrDirectionDependentNonEmptyList() $listOps->foldr(static fn ($acc, $el) => $el / $acc, [1, 2, 3, 4], 24) ); } - - /** - * @testdox reverse the elements of a list -> empty list + /** + * @testdox reverse the elements of the list -> empty list */ - public function testReverseEmptyList() + public function testReverseTheElementsOfTheListWithEmptyList() { $listOps = new ListOps(); $this->assertEquals([], $listOps->reverse([])); - } - - /** - * @testdox reverse the elements of a list -> non-empty list + } /** + * @testdox reverse the elements of the list -> non-empty list */ - public function testReverseNonEmptyList() + public function testReverseTheElementsOfTheListWithNonEmptyList() { $listOps = new ListOps(); $this->assertEquals([7, 5, 3, 1], $listOps->reverse([1, 3, 5, 7])); - } - - /** - * @testdox reverse the elements of a list -> list of lists is not flattened + } /** + * @testdox reverse the elements of the list -> list of lists is not flattened */ - public function testReverseNonEmptyListIsNotFlattened() + public function testReverseTheElementsOfTheListWithListOfListsIsNotFlattened() { $listOps = new ListOps(); $this->assertEquals([[4, 5, 6], [], [3], [1, 2]], $listOps->reverse([[1, 2], [3], [], [4, 5, 6]])); diff --git a/exercises/practice/list-ops/ListOpsTest.php.twig b/exercises/practice/list-ops/ListOpsTest.php.twig new file mode 100644 index 000000000..9527727ff --- /dev/null +++ b/exercises/practice/list-ops/ListOpsTest.php.twig @@ -0,0 +1,164 @@ + x modulo 2 == 1': 'static fn ($el) => $el % 2 === 1', + '(x) -> x + 1': 'static fn ($el) => $el + 1', + '(acc, el) -> el * acc': 'static fn ($acc, $el) => $el * $acc', + '(acc, el) -> el + acc': 'static fn ($acc, $el) => $el + $acc', + '(acc, el) -> el / acc': 'static fn ($acc, $el) => $el / $acc', +} +-%} + +/* + * By adding type hints and enabling strict type checking, code can become + * easier to read, self-documenting and reduce the number of potential bugs. + * By default, type declarations are non-strict, which means they will attempt + * to change the original type to match the type specified by the + * type-declaration. + * + * In other words, if you pass a string to a function requiring a float, + * it will attempt to convert the string value to a float. + * + * To enable strict mode, a single declare directive must be placed at the top + * of the file. + * This means that the strictness of typing is configured on a per-file basis. + * This directive not only affects the type declarations of parameters, but also + * a function's return type. + * + * For more info review the Concept on strict type checking in the PHP track + * . + * + * To disable strict typing, comment out the directive below. + */ + +declare(strict_types=1); + +use PHPUnit\Framework\ExpectationFailedException; + +class ListOpsTest extends PHPUnit\Framework\TestCase +{ + public static function setUpBeforeClass(): void + { + require_once 'ListOps.php'; + } + + {% set case0 = cases[0] -%} + {% for case in case0.cases -%} + /** + * @testdox {{ case0.description }} -> {{ case.description }} + */ + public function {{ testfn(case0.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list1) }}, {{ export(case.input.list2) }})); + } + + {% endfor -%} + + {% set case1 = cases[1] -%} + {% for case in case1.cases -%} + /** + * @testdox {{ case1.description }} -> {{ case.description }} + */ + public function {{ testfn(case1.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ case.input.lists | map(l => export(l)) | join(', ') }})); + } + + {% endfor -%} + + + {% set case2 = cases[2] -%} + {% for case in case2.cases -%} + /** + * @testdox {{ case2.description }} -> {{ case.description }} + */ + public function {{ testfn(case2.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals( + {{ export(case.expected) }}, + $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}) + ); + } + + {% endfor -%} + + + {% set case3 = cases[3] -%} + {% for case in case3.cases -%} + /** + * @testdox {{ case3.description }} -> {{ case.description }} + */ + public function {{ testfn(case3.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list) }})); + } + + {% endfor -%} + + + {% set case4 = cases[4] -%} + {% for case in case4.cases -%} + /** + * @testdox {{ case4.description }} -> {{ case.description }} + */ + public function {{ testfn(case4.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals( + {{ export(case.expected) }}, + $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}) + ); + } + + {% endfor -%} + + + {% set case5 = cases[5] -%} + {% for case in case5.cases -%} + /** + * @testdox {{ case5.description }} -> {{ case.description }} + */ + public function {{ testfn(case5.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals( + {{ export(case.expected) }}, + $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}, {{ export(case.input.initial) }}) + ); + } + + {% endfor -%} + + + {% set case6 = cases[6] -%} + {% for case in case6.cases -%} + /** + * @testdox {{ case6.description }} -> {{ case.description }} + */ + public function {{ testfn(case6.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals( + {{ export(case.expected) }}, + $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}, {{ export(case.input.initial) }}) + ); + } + {% endfor -%} + + {%- set case7 = cases[7] %} + {%- for case in case7.cases %} + /** + * @testdox {{ case7.description }} -> {{ case.description }} + */ + public function {{ testfn(case7.description ~ ' with ' ~ case.description) }}() + { + $listOps = new ListOps(); + $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list) }})); + } + {%- endfor %} + +} diff --git a/test-generator/.gitignore b/test-generator/.gitignore new file mode 100644 index 000000000..1940dfd2f --- /dev/null +++ b/test-generator/.gitignore @@ -0,0 +1,3 @@ +.phpunit.cache/ +.phpcs-cache +vendor/ diff --git a/test-generator/README.md b/test-generator/README.md new file mode 100644 index 000000000..dd1b0a357 --- /dev/null +++ b/test-generator/README.md @@ -0,0 +1,26 @@ +TODO: +- [ ] Readme + - [ ] Requirements (php 8.3) + - [ ] Usage `php test-generator/main.php exercises/practice/list-ops/ /home/codespace/.cache/exercism/configlet/problem-specifications/exercises/list-ops/canonical-data.json -vv` + - [ ] https://twig.symfony.com/ + - [ ] custom functions `export` / `testf` +- [ ] CI (generator) + - [ ] `phpstan` + - [ ] `phpcs` + - [ ] `phpunit` +- [ ] CI (exercises): iterate over each exercise and run the generator in check mode +- [ ] Write tests +- [ ] Path to convert existing exercises to the test-generator +- [ ] `@TODO` +- [ ] Upgrade https://github.com/brick/varexporter +- [ ] TOML Library for php (does not seem to exist any maitained library) +- [ ] Default templates: + - [ ] Test function header (automatic docblock, automatic name) +- [ ] Going further + - [ ] Skip re-implements + - [x] Read .meta/tests.toml to skip `include=false` cases by uuid + - [ ] Ensure correctness between toml and effectively generated files + - [ ] Default templates to include (strict_types header, require_once based on config, testfn header [testdox, uuid, task_id]) + - [ ] devcontainer for easy contribution in github codespace directly + - [ ] Automatically fetch configlet and exercise informations + - [x] Disable twig automatic isset diff --git a/test-generator/composer.json b/test-generator/composer.json new file mode 100644 index 000000000..b6a4ecb5d --- /dev/null +++ b/test-generator/composer.json @@ -0,0 +1,41 @@ +{ + "name": "exercism/test-generator", + "type": "project", + "require": { + "brick/varexporter": "^0.4.0", + "league/flysystem": "^3.26", + "league/flysystem-memory": "^3.25", + "psr/log": "^3.0", + "symfony/console": "^6.0", + "twig/twig": "^3.8" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests" + } + }, + "scripts": { + "phpstan": "phpstan analyse src tests --configuration phpstan.neon --memory-limit=2G", + "test": "phpunit", + "lint": "phpcs", + "lint:fix": "phpcbf" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + } +} diff --git a/test-generator/main.php b/test-generator/main.php new file mode 100644 index 000000000..a9a519e68 --- /dev/null +++ b/test-generator/main.php @@ -0,0 +1,8 @@ +run(); diff --git a/test-generator/phpcs.xml.dist b/test-generator/phpcs.xml.dist new file mode 100644 index 000000000..8a22e1cb3 --- /dev/null +++ b/test-generator/phpcs.xml.dist @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/ + tests/ + \ No newline at end of file diff --git a/test-generator/phpstan.neon b/test-generator/phpstan.neon new file mode 100644 index 000000000..22254bcd0 --- /dev/null +++ b/test-generator/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: max diff --git a/test-generator/phpunit.xml.dist b/test-generator/phpunit.xml.dist new file mode 100644 index 000000000..1444de8ba --- /dev/null +++ b/test-generator/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + ./tests + + + + + src/ + + + + + + + + + + \ No newline at end of file diff --git a/test-generator/src/Application.php b/test-generator/src/Application.php new file mode 100644 index 000000000..0fc9049e0 --- /dev/null +++ b/test-generator/src/Application.php @@ -0,0 +1,150 @@ +setVersion('1.0.0'); + // @TODO + $this->addArgument('exercise-path', InputArgument::REQUIRED, 'Path of the exercise.'); + $this->addArgument('canonical-data', InputArgument::REQUIRED, 'Path of the canonical data for the exercise. (Use `bin/configlet -verbosity info --offline`)'); + $this->addOption('check', null, InputOption::VALUE_NONE, 'Checks whether the existing files are the same as generated one.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $exercisePath = $input->getArgument('exercise-path'); + $canonicalPath = $input->getArgument('canonical-data'); + $exerciseCheck = $input->getOption('check'); + assert(is_string($exercisePath), 'exercise-path must be a string'); + assert(is_string($canonicalPath), 'canonical-data must be a string'); + assert(is_bool($exerciseCheck), 'check must be a bool'); + + $logger = new ConsoleLogger($output); + $logger->info('Exercise path: ' . $exercisePath); + $logger->info('canonical-data path: ' . $canonicalPath); + + $canonicalDataJson = file_get_contents($canonicalPath); + if ($canonicalDataJson === false) { + throw new RuntimeException('Faield to fetch canonical-data.json, check you `canonical-data` argument.'); + } + + $canonicalData = json_decode($canonicalDataJson, true, flags: JSON_THROW_ON_ERROR); + assert(is_array($canonicalData), 'json_decode(..., true) should return an array'); + $exerciseAdapter = new LocalFilesystemAdapter($exercisePath); + $exerciseFilesystem = new Filesystem($exerciseAdapter); + + $success = $this->generate($exerciseFilesystem, $exerciseCheck, $canonicalData, $logger); + + return $success ? self::SUCCESS : self::FAILURE; + } + + /** @param array $canonicalData */ + public function generate(Filesystem $exerciseDir, bool $check, array $canonicalData, LoggerInterface $logger): bool + { + // 1. Read config.json + $configJson = $exerciseDir->read('/.meta/config.json'); + $config = json_decode($configJson, true, flags: JSON_THROW_ON_ERROR); + assert(is_array($config), 'json_decode(..., true) should return an array'); + + if (! isset($config['files']['test']) || ! is_array($config['files']['test'])) { + throw new RuntimeException('.meta/config.json: missing or invalid `files.test` key'); + } + + $testsPaths = $config['files']['test']; + $logger->info('.meta/config.json: tests files: ' . implode(', ', $testsPaths)); + + if (empty($testsPaths)) { + $logger->warning('.meta/config.json: `files.test` key is empty'); + } + + // 2. Read test.toml + $testsToml = $exerciseDir->read('/.meta/tests.toml'); + $tests = TomlParser::parse($testsToml); + + // 3. Remove `include = false` tests + $excludedTests = array_filter($tests, static fn (array $props) => isset($props['include']) && $props['include'] === false); + $this->removeExcludedTests($excludedTests, $canonicalData['cases']); + + // 4. foreach tests files, check if there is a twig file + $twigLoader = new ArrayLoader(); + $twigEnvironment = new Environment($twigLoader, ['strict_variables' => true, 'autoescape' => false]); + $twigEnvironment->addFunction(new TwigFunction('export', static fn (mixed $value) => VarExporter::export($value, VarExporter::INLINE_ARRAY))); + $twigEnvironment->addFunction(new TwigFunction('testfn', static fn (string $label) => 'test' . str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9]/', ' ', $label))))); + foreach ($testsPaths as $testPath) { + // 5. generate the file + $twigFilename = $testPath . '.twig'; + // @TODO warning or error if it does not exist + $testTemplate = $exerciseDir->read($twigFilename); + $rendered = $twigEnvironment->createTemplate($testTemplate, $twigFilename)->render($canonicalData); + + if ($check) { + // 6. Compare it if check mode + if ($exerciseDir->read($testPath) !== $rendered) { + // return false; + throw new Exception('Differences between generated and existing file'); + } + } else { + $exerciseDir->write($testPath, $rendered); + } + } + + return true; + } + + private function removeExcludedTests(array $tests, array &$cases): void + { + foreach ($cases as $key => &$case) { + if (array_key_exists('cases', $case)) { + $this->removeExcludedTests($tests, $case['cases']); + } else { + assert(array_key_exists('uuid', $case)); + if (array_key_exists($case['uuid'], $tests)) { + unset($cases[$key]); + } + } + } + } +} diff --git a/test-generator/src/TomlParser.php b/test-generator/src/TomlParser.php new file mode 100644 index 000000000..1d88fc550 --- /dev/null +++ b/test-generator/src/TomlParser.php @@ -0,0 +1,79 @@ +write('.meta/config.json', '{"files":{"test":["test.php"]}}'); + $exerciseFs->write('.meta/tests.toml', ''); + $exerciseFs->write('test.php.twig', ' [1, 2], 'l' => 'this-Is_a test fn', 'cases' => []]; + + $application = new Application(); + $success = $application->generate($exerciseFs, false, $canonicalData, new NullLogger()); + + $this->assertTrue($success); + $this->assertSame('read('/test.php')); + } +} From 807b4095e66ec8eaa1e25b62dcbcd28f67f86604 Mon Sep 17 00:00:00 2001 From: homersimpsons Date: Sat, 13 Apr 2024 00:12:36 +0000 Subject: [PATCH 2/2] :alembic: Minimise twig template and follow AAA tests AAA stands for Arrange-Act-Assert pattern --- exercises/practice/list-ops/ListOpsTest.php | 199 +++++++++++++----- .../practice/list-ops/ListOpsTest.php.twig | 113 +--------- 2 files changed, 150 insertions(+), 162 deletions(-) diff --git a/exercises/practice/list-ops/ListOpsTest.php b/exercises/practice/list-ops/ListOpsTest.php index 89ee592f7..e2a2cb8fd 100644 --- a/exercises/practice/list-ops/ListOpsTest.php +++ b/exercises/practice/list-ops/ListOpsTest.php @@ -33,13 +33,19 @@ public static function setUpBeforeClass(): void require_once 'ListOps.php'; } + /** * @testdox append entries to a list and return the new list -> empty lists */ public function testAppendEntriesToAListAndReturnTheNewListWithEmptyLists() { $listOps = new ListOps(); - $this->assertEquals([], $listOps->append([], [])); + $list1 = []; + $list2 = []; + + $result = $listOps->append($list1, $list2); + + $this->assertEquals([], $result); } /** @@ -48,7 +54,12 @@ public function testAppendEntriesToAListAndReturnTheNewListWithEmptyLists() public function testAppendEntriesToAListAndReturnTheNewListWithListToEmptyList() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 3, 4], $listOps->append([], [1, 2, 3, 4])); + $list1 = []; + $list2 = [1, 2, 3, 4]; + + $result = $listOps->append($list1, $list2); + + $this->assertEquals([1, 2, 3, 4], $result); } /** @@ -57,7 +68,12 @@ public function testAppendEntriesToAListAndReturnTheNewListWithListToEmptyList() public function testAppendEntriesToAListAndReturnTheNewListWithEmptyListToList() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 3, 4], $listOps->append([1, 2, 3, 4], [])); + $list1 = [1, 2, 3, 4]; + $list2 = []; + + $result = $listOps->append($list1, $list2); + + $this->assertEquals([1, 2, 3, 4], $result); } /** @@ -66,7 +82,12 @@ public function testAppendEntriesToAListAndReturnTheNewListWithEmptyListToList() public function testAppendEntriesToAListAndReturnTheNewListWithNonEmptyLists() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 2, 3, 4, 5], $listOps->append([1, 2], [2, 3, 4, 5])); + $list1 = [1, 2]; + $list2 = [2, 3, 4, 5]; + + $result = $listOps->append($list1, $list2); + + $this->assertEquals([1, 2, 2, 3, 4, 5], $result); } /** @@ -75,7 +96,11 @@ public function testAppendEntriesToAListAndReturnTheNewListWithNonEmptyLists() public function testConcatenateAListOfListsWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals([], $listOps->concat()); + $lists = []; + + $result = $listOps->concat($lists); + + $this->assertEquals([], $result); } /** @@ -84,7 +109,11 @@ public function testConcatenateAListOfListsWithEmptyList() public function testConcatenateAListOfListsWithListOfLists() { $listOps = new ListOps(); - $this->assertEquals([1, 2, 3, 4, 5, 6], $listOps->concat([1, 2], [3], [], [4, 5, 6])); + $lists = [[1, 2], [3], [], [4, 5, 6]]; + + $result = $listOps->concat($lists); + + $this->assertEquals([1, 2, 3, 4, 5, 6], $result); } /** @@ -93,7 +122,11 @@ public function testConcatenateAListOfListsWithListOfLists() public function testConcatenateAListOfListsWithListOfNestedLists() { $listOps = new ListOps(); - $this->assertEquals([[1], [2], [3], [], [4, 5, 6]], $listOps->concat([[1], [2]], [[3]], [[]], [[4, 5, 6]])); + $lists = [[[1], [2]], [[3]], [[]], [[4, 5, 6]]]; + + $result = $listOps->concat($lists); + + $this->assertEquals([[1], [2], [3], [], [4, 5, 6]], $result); } /** @@ -102,10 +135,12 @@ public function testConcatenateAListOfListsWithListOfNestedLists() public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - [], - $listOps->filter(static fn ($el) => $el % 2 === 1, []) - ); + $list = []; + $function = static fn ($el) => $el % 2 === 1; + + $result = $listOps->filter($list, $function); + + $this->assertEquals([], $result); } /** @@ -114,10 +149,12 @@ public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWit public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWithNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - [1, 3, 5], - $listOps->filter(static fn ($el) => $el % 2 === 1, [1, 2, 3, 5]) - ); + $list = [1, 2, 3, 5]; + $function = static fn ($el) => $el % 2 === 1; + + $result = $listOps->filter($list, $function); + + $this->assertEquals([1, 3, 5], $result); } /** @@ -126,7 +163,11 @@ public function testFilterListReturningOnlyValuesThatSatisfyTheFilterFunctionWit public function testReturnsTheLengthOfAListWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals(0, $listOps->length([])); + $list = []; + + $result = $listOps->length($list); + + $this->assertEquals(0, $result); } /** @@ -135,7 +176,11 @@ public function testReturnsTheLengthOfAListWithEmptyList() public function testReturnsTheLengthOfAListWithNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals(4, $listOps->length([1, 2, 3, 4])); + $list = [1, 2, 3, 4]; + + $result = $listOps->length($list); + + $this->assertEquals(4, $result); } /** @@ -144,10 +189,12 @@ public function testReturnsTheLengthOfAListWithNonEmptyList() public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformedByTheMappingFunctionWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - [], - $listOps->map(static fn ($el) => $el + 1, []) - ); + $list = []; + $function = static fn ($el) => $el + 1; + + $result = $listOps->map($list, $function); + + $this->assertEquals([], $result); } /** @@ -156,10 +203,12 @@ public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformed public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformedByTheMappingFunctionWithNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - [2, 4, 6, 8], - $listOps->map(static fn ($el) => $el + 1, [1, 3, 5, 7]) - ); + $list = [1, 3, 5, 7]; + $function = static fn ($el) => $el + 1; + + $result = $listOps->map($list, $function); + + $this->assertEquals([2, 4, 6, 8], $result); } /** @@ -168,10 +217,13 @@ public function testReturnAListOfElementsWhoseValuesEqualTheListValueTransformed public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 2, - $listOps->foldl(static fn ($acc, $el) => $el * $acc, [], 2) - ); + $list = []; + $initial = 2; + $function = static fn ($acc, $el) => $el * $acc; + + $result = $listOps->foldl($list, $initial, $function); + + $this->assertEquals(2, $result); } /** @@ -180,10 +232,13 @@ public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithEmptyLis public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectionIndependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 15, - $listOps->foldl(static fn ($acc, $el) => $el + $acc, [1, 2, 3, 4], 5) - ); + $list = [1, 2, 3, 4]; + $initial = 5; + $function = static fn ($acc, $el) => $el + $acc; + + $result = $listOps->foldl($list, $initial, $function); + + $this->assertEquals(15, $result); } /** @@ -192,10 +247,13 @@ public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectio public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectionDependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 64, - $listOps->foldl(static fn ($acc, $el) => $el / $acc, [1, 2, 3, 4], 24) - ); + $list = [1, 2, 3, 4]; + $initial = 24; + $function = static fn ($acc, $el) => $el / $acc; + + $result = $listOps->foldl($list, $initial, $function); + + $this->assertEquals(64, $result); } /** @@ -204,53 +262,82 @@ public function testFoldsReducesTheGivenListFromTheLeftWithAFunctionWithDirectio public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 2, - $listOps->foldr(static fn ($acc, $el) => $el * $acc, [], 2) - ); + $list = []; + $initial = 2; + $function = static fn ($acc, $el) => $el * $acc; + + $result = $listOps->foldr($list, $initial, $function); + + $this->assertEquals(2, $result); } + /** * @testdox folds (reduces) the given list from the right with a function -> direction independent function applied to non-empty list */ public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithDirectionIndependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 15, - $listOps->foldr(static fn ($acc, $el) => $el + $acc, [1, 2, 3, 4], 5) - ); + $list = [1, 2, 3, 4]; + $initial = 5; + $function = static fn ($acc, $el) => $el + $acc; + + $result = $listOps->foldr($list, $initial, $function); + + $this->assertEquals(15, $result); } + /** * @testdox folds (reduces) the given list from the right with a function -> direction dependent function applied to non-empty list */ public function testFoldsReducesTheGivenListFromTheRightWithAFunctionWithDirectionDependentFunctionAppliedToNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals( - 9, - $listOps->foldr(static fn ($acc, $el) => $el / $acc, [1, 2, 3, 4], 24) - ); + $list = [1, 2, 3, 4]; + $initial = 24; + $function = static fn ($acc, $el) => $el / $acc; + + $result = $listOps->foldr($list, $initial, $function); + + $this->assertEquals(9, $result); } - /** + + /** * @testdox reverse the elements of the list -> empty list */ public function testReverseTheElementsOfTheListWithEmptyList() { $listOps = new ListOps(); - $this->assertEquals([], $listOps->reverse([])); - } /** + $list = []; + + $result = $listOps->reverse($list); + + $this->assertEquals([], $result); + } + + /** * @testdox reverse the elements of the list -> non-empty list */ public function testReverseTheElementsOfTheListWithNonEmptyList() { $listOps = new ListOps(); - $this->assertEquals([7, 5, 3, 1], $listOps->reverse([1, 3, 5, 7])); - } /** + $list = [1, 3, 5, 7]; + + $result = $listOps->reverse($list); + + $this->assertEquals([7, 5, 3, 1], $result); + } + + /** * @testdox reverse the elements of the list -> list of lists is not flattened */ public function testReverseTheElementsOfTheListWithListOfListsIsNotFlattened() { $listOps = new ListOps(); - $this->assertEquals([[4, 5, 6], [], [3], [1, 2]], $listOps->reverse([[1, 2], [3], [], [4, 5, 6]])); + $list = [[1, 2], [3], [], [4, 5, 6]]; + + $result = $listOps->reverse($list); + + $this->assertEquals([[4, 5, 6], [], [3], [1, 2]], $result); + } + } -} diff --git a/exercises/practice/list-ops/ListOpsTest.php.twig b/exercises/practice/list-ops/ListOpsTest.php.twig index 9527727ff..056bf27df 100644 --- a/exercises/practice/list-ops/ListOpsTest.php.twig +++ b/exercises/practice/list-ops/ListOpsTest.php.twig @@ -42,7 +42,8 @@ class ListOpsTest extends PHPUnit\Framework\TestCase require_once 'ListOps.php'; } - {% set case0 = cases[0] -%} + + {% for case0 in cases -%} {% for case in case0.cases -%} /** * @testdox {{ case0.description }} -> {{ case.description }} @@ -50,115 +51,15 @@ class ListOpsTest extends PHPUnit\Framework\TestCase public function {{ testfn(case0.description ~ ' with ' ~ case.description) }}() { $listOps = new ListOps(); - $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list1) }}, {{ export(case.input.list2) }})); - } + {% for property, value in case.input -%} + ${{ property }} = {{ property == 'function' ? callbacks[value] : export(value) }}; + {% endfor %} - {% endfor -%} + $result = $listOps->{{ case.property }}({{ case.input | keys | map(p => '$' ~ p) | join(', ')}}); - {% set case1 = cases[1] -%} - {% for case in case1.cases -%} - /** - * @testdox {{ case1.description }} -> {{ case.description }} - */ - public function {{ testfn(case1.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ case.input.lists | map(l => export(l)) | join(', ') }})); + $this->assertEquals({{ export(case.expected) }}, $result); } {% endfor -%} - - - {% set case2 = cases[2] -%} - {% for case in case2.cases -%} - /** - * @testdox {{ case2.description }} -> {{ case.description }} - */ - public function {{ testfn(case2.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals( - {{ export(case.expected) }}, - $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}) - ); - } - {% endfor -%} - - - {% set case3 = cases[3] -%} - {% for case in case3.cases -%} - /** - * @testdox {{ case3.description }} -> {{ case.description }} - */ - public function {{ testfn(case3.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list) }})); - } - - {% endfor -%} - - - {% set case4 = cases[4] -%} - {% for case in case4.cases -%} - /** - * @testdox {{ case4.description }} -> {{ case.description }} - */ - public function {{ testfn(case4.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals( - {{ export(case.expected) }}, - $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}) - ); - } - - {% endfor -%} - - - {% set case5 = cases[5] -%} - {% for case in case5.cases -%} - /** - * @testdox {{ case5.description }} -> {{ case.description }} - */ - public function {{ testfn(case5.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals( - {{ export(case.expected) }}, - $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}, {{ export(case.input.initial) }}) - ); - } - - {% endfor -%} - - - {% set case6 = cases[6] -%} - {% for case in case6.cases -%} - /** - * @testdox {{ case6.description }} -> {{ case.description }} - */ - public function {{ testfn(case6.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals( - {{ export(case.expected) }}, - $listOps->{{ case.property }}({{ callbacks[case.input.function] }}, {{ export(case.input.list) }}, {{ export(case.input.initial) }}) - ); - } - {% endfor -%} - - {%- set case7 = cases[7] %} - {%- for case in case7.cases %} - /** - * @testdox {{ case7.description }} -> {{ case.description }} - */ - public function {{ testfn(case7.description ~ ' with ' ~ case.description) }}() - { - $listOps = new ListOps(); - $this->assertEquals({{ export(case.expected) }}, $listOps->{{ case.property }}({{ export(case.input.list) }})); - } - {%- endfor %} - }