From 06b500ffac3db5e1411c8eb5834ca1533e92e8e1 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 22:27:18 +0100 Subject: [PATCH 01/29] Import test generator POC of tomasnorre No changes, only removed composer.lock --- contribution/generator/.env | 20 +++ contribution/generator/.env.test | 6 + contribution/generator/.gitignore | 17 +++ contribution/generator/README.md | 20 +++ contribution/generator/bin/console | 17 +++ contribution/generator/composer.json | 71 ++++++++++ contribution/generator/config/bundles.php | 6 + .../generator/config/packages/cache.yaml | 19 +++ .../generator/config/packages/framework.yaml | 16 +++ .../generator/config/packages/routing.yaml | 10 ++ contribution/generator/config/preload.php | 5 + contribution/generator/config/routes.yaml | 5 + .../generator/config/routes/framework.yaml | 4 + contribution/generator/config/services.yaml | 24 ++++ contribution/generator/phpunit.xml.dist | 27 ++++ contribution/generator/public/index.php | 9 ++ .../src/Command/CreateTestsCommand.php | 121 ++++++++++++++++++ .../generator/src/Command/NucleotideCount.php | 22 ++++ .../generator/src/Command/canonical-data.json | 72 +++++++++++ .../generator/src/Controller/.gitignore | 0 contribution/generator/src/Kernel.php | 11 ++ contribution/generator/symfony.lock | 81 ++++++++++++ contribution/generator/tests/bootstrap.php | 11 ++ 23 files changed, 594 insertions(+) create mode 100644 contribution/generator/.env create mode 100644 contribution/generator/.env.test create mode 100644 contribution/generator/.gitignore create mode 100644 contribution/generator/README.md create mode 100755 contribution/generator/bin/console create mode 100644 contribution/generator/composer.json create mode 100644 contribution/generator/config/bundles.php create mode 100644 contribution/generator/config/packages/cache.yaml create mode 100644 contribution/generator/config/packages/framework.yaml create mode 100644 contribution/generator/config/packages/routing.yaml create mode 100644 contribution/generator/config/preload.php create mode 100644 contribution/generator/config/routes.yaml create mode 100644 contribution/generator/config/routes/framework.yaml create mode 100644 contribution/generator/config/services.yaml create mode 100644 contribution/generator/phpunit.xml.dist create mode 100644 contribution/generator/public/index.php create mode 100644 contribution/generator/src/Command/CreateTestsCommand.php create mode 100644 contribution/generator/src/Command/NucleotideCount.php create mode 100644 contribution/generator/src/Command/canonical-data.json create mode 100644 contribution/generator/src/Controller/.gitignore create mode 100644 contribution/generator/src/Kernel.php create mode 100644 contribution/generator/symfony.lock create mode 100644 contribution/generator/tests/bootstrap.php diff --git a/contribution/generator/.env b/contribution/generator/.env new file mode 100644 index 00000000..900185a0 --- /dev/null +++ b/contribution/generator/.env @@ -0,0 +1,20 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=1be0b2dbd34333efb23cfefcff0ff718 +###< symfony/framework-bundle ### diff --git a/contribution/generator/.env.test b/contribution/generator/.env.test new file mode 100644 index 00000000..9e7162f0 --- /dev/null +++ b/contribution/generator/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/contribution/generator/.gitignore b/contribution/generator/.gitignore new file mode 100644 index 00000000..d2d8d075 --- /dev/null +++ b/contribution/generator/.gitignore @@ -0,0 +1,17 @@ +# IDEs +.idea + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### diff --git a/contribution/generator/README.md b/contribution/generator/README.md new file mode 100644 index 00000000..09fa6828 --- /dev/null +++ b/contribution/generator/README.md @@ -0,0 +1,20 @@ +# Auto Creating of tests for Exercism PHP Track + +This is a small poc on how we could auto generate tests for the PHP track based on the https://github.com/exercism/problem-specifications/. + +How to test it: + +``` +git clone https://github.com/tomasnorre/exercism-tests-generation.git +cd exercism-tests-generation +composer install +bin/console app:create-tests +vendor/bin/phpunit src/Command/NucleotideCountTest.php +``` + +If you now make a `git status` you will see that the `src/Command/NucleotideCountTest.php` and you can now inspect the auto generated tests. + +It's all based on the `nikic/php-parser` and the https://github.com/exercism/problem-specifications/ repository, I have made a local copy of that on file +for now to spare the http-requests. + +Let me know what you think. diff --git a/contribution/generator/bin/console b/contribution/generator/bin/console new file mode 100755 index 00000000..c933dc53 --- /dev/null +++ b/contribution/generator/bin/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +=8.2", + "ext-ctype": "*", + "ext-iconv": "*", + "nikic/php-parser": "^5.0", + "symfony/console": "7.0.*", + "symfony/dotenv": "7.0.*", + "symfony/flex": "^2", + "symfony/framework-bundle": "7.0.*", + "symfony/runtime": "7.0.*", + "symfony/yaml": "7.0.*" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/maker-bundle": "^1.54" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.0.*" + } + } +} diff --git a/contribution/generator/config/bundles.php b/contribution/generator/config/bundles.php new file mode 100644 index 00000000..ffeb8610 --- /dev/null +++ b/contribution/generator/config/bundles.php @@ -0,0 +1,6 @@ + ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], +]; diff --git a/contribution/generator/config/packages/cache.yaml b/contribution/generator/config/packages/cache.yaml new file mode 100644 index 00000000..6899b720 --- /dev/null +++ b/contribution/generator/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/contribution/generator/config/packages/framework.yaml b/contribution/generator/config/packages/framework.yaml new file mode 100644 index 00000000..877eb25d --- /dev/null +++ b/contribution/generator/config/packages/framework.yaml @@ -0,0 +1,16 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + + # Note that the session will be started ONLY if you read or write from it. + session: true + + #esi: true + #fragments: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/contribution/generator/config/packages/routing.yaml b/contribution/generator/config/packages/routing.yaml new file mode 100644 index 00000000..8166181c --- /dev/null +++ b/contribution/generator/config/packages/routing.yaml @@ -0,0 +1,10 @@ +framework: + router: + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/contribution/generator/config/preload.php b/contribution/generator/config/preload.php new file mode 100644 index 00000000..5ebcdb21 --- /dev/null +++ b/contribution/generator/config/preload.php @@ -0,0 +1,5 @@ + + + + + + + + + + + + + + + + tests + + + + + + diff --git a/contribution/generator/public/index.php b/contribution/generator/public/index.php new file mode 100644 index 00000000..9982c218 --- /dev/null +++ b/contribution/generator/public/index.php @@ -0,0 +1,9 @@ +success('Generating Tests - Started'); + + $this->createTests(); + + $io->success('Generating Tests - Finished'); + return Command::SUCCESS; + } + + /** + * @throws \JsonException + */ + private function createTests(): void + { + $jsonData = file_get_contents(__DIR__ . "/canonical-data.json"); + $data = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR); + + $factory = new BuilderFactory(); + $classBuilder = $factory->class($this->generateClassName($data['exercise']) . "Test")->makeFinal()->extend('\PHPUnit\Framework\TestCase'); + + // Include Setup Method + $methodSetup = 'setUpBeforeClass'; + $method = $factory->method($methodSetup) + ->makePublic() + ->makeStatic() + ->setReturnType('void') + ->addStmt( + $factory->funcCall( + "require_once", + [$this->generateClassName($data['exercise']) . ".php"] + ), + ); + + $classBuilder->addStmt($method); + + foreach ($data['cases'] as $case) { + // Generate a method for each test case + $description = $case['description']; + $methodName = ucfirst(str_replace('-', ' ', $description)); + $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); + $uuid = $case['uuid']; + + $exceptionClassName = new Node\Name\FullyQualified('Exception'); + if (isset($case['expected']['error'])) { + $method = $factory->method($methodName) + ->makePublic() + ->setReturnType('void') + ->addStmt( + $factory->funcCall('$this->expectException', + [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] + ) + ) + ->addStmt($factory->funcCall($case['property'], [$case['input']['strand']])) + ->setDocComment("/**\n * uuid: $uuid\n */"); + } else { + $method = $factory->method($methodName) + ->makePublic() + ->setReturnType('void') + ->addStmt( + $factory->funcCall('$this->assertEquals', [ + $case['expected'], + $factory->funcCall($case['property'], [$case['input']['strand']]) + ]) + ) + ->setDocComment("/**\n * uuid: $uuid\n */"); + } + $classBuilder->addStmt($method); + } + + $class = $classBuilder->getNode(); + + $namespace = new Namespace_(new Node\Name('Tests')); + $namespace->stmts[] = $class; + + $printer = new PrettyPrinter\Standard(); + + // Write to file + $file = fopen(__DIR__ . "/" . $this->generateClassName($data['exercise']) . "Test.php", "w") or die("Unable to open file!"); + fwrite($file, $printer->prettyPrintFile([$namespace]) . PHP_EOL); + fclose($file); + + } + + private function generateClassName(string $name): string + { + $name = str_replace("-", " ", $name); + $name = ucwords($name); + return str_replace(" ", "", $name); + } +} diff --git a/contribution/generator/src/Command/NucleotideCount.php b/contribution/generator/src/Command/NucleotideCount.php new file mode 100644 index 00000000..2fedbdc3 --- /dev/null +++ b/contribution/generator/src/Command/NucleotideCount.php @@ -0,0 +1,22 @@ + 0, + 'C' => 0, + 'T' => 0, + 'G' => 0 + ]; + + foreach (str_split($input) as $char) { + $key = strtoupper($char); + if (!array_key_exists($key, $result)) { + throw new \RuntimeException('Input contains invalid character.'); + } + $result[$key]++; + } + return $result; +} diff --git a/contribution/generator/src/Command/canonical-data.json b/contribution/generator/src/Command/canonical-data.json new file mode 100644 index 00000000..9caf8575 --- /dev/null +++ b/contribution/generator/src/Command/canonical-data.json @@ -0,0 +1,72 @@ +{ + "exercise": "nucleotide-count", + "cases": [ + { + "uuid": "3e5c30a8-87e2-4845-a815-a49671ade970", + "description": "empty strand", + "property": "nucleotideCount", + "input": { + "strand": "" + }, + "expected": { + "A": 0, + "C": 0, + "G": 0, + "T": 0 + } + }, + { + "uuid": "a0ea42a6-06d9-4ac6-828c-7ccaccf98fec", + "description": "can count one nucleotide in single-character input", + "property": "nucleotideCount", + "input": { + "strand": "G" + }, + "expected": { + "A": 0, + "C": 0, + "G": 1, + "T": 0 + } + }, + { + "uuid": "eca0d565-ed8c-43e7-9033-6cefbf5115b5", + "description": "strand with repeated nucleotide", + "property": "nucleotideCount", + "input": { + "strand": "GGGGGGG" + }, + "expected": { + "A": 0, + "C": 0, + "G": 7, + "T": 0 + } + }, + { + "uuid": "40a45eac-c83f-4740-901a-20b22d15a39f", + "description": "strand with multiple nucleotides", + "property": "nucleotideCount", + "input": { + "strand": "AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC" + }, + "expected": { + "A": 20, + "C": 12, + "G": 17, + "T": 21 + } + }, + { + "uuid": "b4c47851-ee9e-4b0a-be70-a86e343bd851", + "description": "strand with invalid nucleotides", + "property": "nucleotideCount", + "input": { + "strand": "AGXXACT" + }, + "expected": { + "error": "Invalid nucleotide in strand" + } + } + ] +} diff --git a/contribution/generator/src/Controller/.gitignore b/contribution/generator/src/Controller/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/contribution/generator/src/Kernel.php b/contribution/generator/src/Kernel.php new file mode 100644 index 00000000..779cd1f2 --- /dev/null +++ b/contribution/generator/src/Kernel.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +} From 1f344bc30c121a00c51671b4d3b7bea0d9dff780 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 22:38:52 +0100 Subject: [PATCH 02/29] Mark PHPUnit config in root as unused --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index 5cd996e7..a8045207 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,4 +1,5 @@ + From 44ea6e7099a420458498c73c1b207e579768ae0f Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 23:20:08 +0100 Subject: [PATCH 03/29] Delete hardcoded POC data, disable test generation --- .../src/Command/CreateTestsCommand.php | 2 +- .../generator/src/Command/NucleotideCount.php | 22 ------ .../generator/src/Command/canonical-data.json | 72 ------------------- 3 files changed, 1 insertion(+), 95 deletions(-) delete mode 100644 contribution/generator/src/Command/NucleotideCount.php delete mode 100644 contribution/generator/src/Command/canonical-data.json diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 92b1ae7c..f11160d5 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -32,7 +32,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $io->success('Generating Tests - Started'); - $this->createTests(); + // $this->createTests(); $io->success('Generating Tests - Finished'); return Command::SUCCESS; diff --git a/contribution/generator/src/Command/NucleotideCount.php b/contribution/generator/src/Command/NucleotideCount.php deleted file mode 100644 index 2fedbdc3..00000000 --- a/contribution/generator/src/Command/NucleotideCount.php +++ /dev/null @@ -1,22 +0,0 @@ - 0, - 'C' => 0, - 'T' => 0, - 'G' => 0 - ]; - - foreach (str_split($input) as $char) { - $key = strtoupper($char); - if (!array_key_exists($key, $result)) { - throw new \RuntimeException('Input contains invalid character.'); - } - $result[$key]++; - } - return $result; -} diff --git a/contribution/generator/src/Command/canonical-data.json b/contribution/generator/src/Command/canonical-data.json deleted file mode 100644 index 9caf8575..00000000 --- a/contribution/generator/src/Command/canonical-data.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "exercise": "nucleotide-count", - "cases": [ - { - "uuid": "3e5c30a8-87e2-4845-a815-a49671ade970", - "description": "empty strand", - "property": "nucleotideCount", - "input": { - "strand": "" - }, - "expected": { - "A": 0, - "C": 0, - "G": 0, - "T": 0 - } - }, - { - "uuid": "a0ea42a6-06d9-4ac6-828c-7ccaccf98fec", - "description": "can count one nucleotide in single-character input", - "property": "nucleotideCount", - "input": { - "strand": "G" - }, - "expected": { - "A": 0, - "C": 0, - "G": 1, - "T": 0 - } - }, - { - "uuid": "eca0d565-ed8c-43e7-9033-6cefbf5115b5", - "description": "strand with repeated nucleotide", - "property": "nucleotideCount", - "input": { - "strand": "GGGGGGG" - }, - "expected": { - "A": 0, - "C": 0, - "G": 7, - "T": 0 - } - }, - { - "uuid": "40a45eac-c83f-4740-901a-20b22d15a39f", - "description": "strand with multiple nucleotides", - "property": "nucleotideCount", - "input": { - "strand": "AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC" - }, - "expected": { - "A": 20, - "C": 12, - "G": 17, - "T": 21 - } - }, - { - "uuid": "b4c47851-ee9e-4b0a-be70-a86e343bd851", - "description": "strand with invalid nucleotides", - "property": "nucleotideCount", - "input": { - "strand": "AGXXACT" - }, - "expected": { - "error": "Invalid nucleotide in strand" - } - } - ] -} From 8b18ec323c33a64973aae63abd6fcd03a4481a16 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 23:20:48 +0100 Subject: [PATCH 04/29] Require exercise slug as argument --- .../generator/src/Command/CreateTestsCommand.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index f11160d5..0a4a2e57 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -11,7 +11,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PhpParser\PrettyPrinter; - +use Symfony\Component\Console\Input\InputArgument; #[AsCommand( name: 'app:create-tests', @@ -24,13 +24,18 @@ public function __construct() parent::__construct(); } + protected function configure(): void + { + $this->addArgument('exercise', InputArgument::REQUIRED, 'Exercise slug'); + } + /** * @throws \JsonException */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $io->success('Generating Tests - Started'); + $io->writeln('Generating tests for ' . $input->getArgument('exercise')); // $this->createTests(); From d13002cd8353115f0fe71e70cec6d5d03cf80ecc Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 23:46:46 +0100 Subject: [PATCH 05/29] Sort `use` by alphabet --- contribution/generator/src/Command/CreateTestsCommand.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 0a4a2e57..6dee025c 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -3,15 +3,15 @@ namespace App\Command; use PhpParser\BuilderFactory; +use PhpParser\Node; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\PrettyPrinter; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use PhpParser\Node; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\PrettyPrinter; -use Symfony\Component\Console\Input\InputArgument; #[AsCommand( name: 'app:create-tests', From 761964cd15a78f4523b5fb538df5cd20de171f8e Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Fri, 23 Feb 2024 23:47:26 +0100 Subject: [PATCH 06/29] Make BuilderFactory a private class instance --- .../src/Command/CreateTestsCommand.php | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 6dee025c..dc67df1a 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -19,8 +19,12 @@ )] class CreateTestsCommand extends Command { + private BuilderFactory $builderFactory; + public function __construct() { + $this->builderFactory = new BuilderFactory(); + parent::__construct(); } @@ -51,17 +55,16 @@ private function createTests(): void $jsonData = file_get_contents(__DIR__ . "/canonical-data.json"); $data = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR); - $factory = new BuilderFactory(); - $classBuilder = $factory->class($this->generateClassName($data['exercise']) . "Test")->makeFinal()->extend('\PHPUnit\Framework\TestCase'); + $classBuilder = $this->builderFactory->class($this->generateClassName($data['exercise']) . "Test")->makeFinal()->extend('\PHPUnit\Framework\TestCase'); // Include Setup Method $methodSetup = 'setUpBeforeClass'; - $method = $factory->method($methodSetup) + $method = $this->builderFactory->method($methodSetup) ->makePublic() ->makeStatic() ->setReturnType('void') ->addStmt( - $factory->funcCall( + $this->builderFactory->funcCall( "require_once", [$this->generateClassName($data['exercise']) . ".php"] ), @@ -78,24 +81,24 @@ private function createTests(): void $exceptionClassName = new Node\Name\FullyQualified('Exception'); if (isset($case['expected']['error'])) { - $method = $factory->method($methodName) + $method = $this->builderFactory->method($methodName) ->makePublic() ->setReturnType('void') ->addStmt( - $factory->funcCall('$this->expectException', + $this->builderFactory->funcCall('$this->expectException', [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] ) ) - ->addStmt($factory->funcCall($case['property'], [$case['input']['strand']])) + ->addStmt($this->builderFactory->funcCall($case['property'], [$case['input']['strand']])) ->setDocComment("/**\n * uuid: $uuid\n */"); } else { - $method = $factory->method($methodName) + $method = $this->builderFactory->method($methodName) ->makePublic() ->setReturnType('void') ->addStmt( - $factory->funcCall('$this->assertEquals', [ + $this->builderFactory->funcCall('$this->assertEquals', [ $case['expected'], - $factory->funcCall($case['property'], [$case['input']['strand']]) + $this->builderFactory->funcCall($case['property'], [$case['input']['strand']]) ]) ) ->setDocComment("/**\n * uuid: $uuid\n */"); From 3f722236be5026a819d051b7592c8ec00bbf2100 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 11:59:31 +0100 Subject: [PATCH 07/29] Ensure configlet is usable Will use configlet to get path to the canonical data cache. --- .../generator/src/Command/CreateTestsCommand.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index dc67df1a..b22dd435 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -38,6 +38,15 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { + $this->exerciseSlug = $input->getArgument('exercise'); + + // TODO: Make this relative to $PWD === track root + $pathToConfiglet = '../../bin/configlet'; + if (!(is_executable($pathToConfiglet) && is_file($pathToConfiglet))) + throw new RuntimeException( + 'configlet not found. Fetch configlet and create exercise with configlet first!' + ); + $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $input->getArgument('exercise')); From 25b3d184caf78c7ad853f22d5d9180690fd0d607 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 13:48:07 +0100 Subject: [PATCH 08/29] Ensure exercise directory is usable Generating the exercise with configlet makes sure the cached is filled. --- .../generator/src/Command/CreateTestsCommand.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index b22dd435..fc1be095 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -47,8 +47,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'configlet not found. Fetch configlet and create exercise with configlet first!' ); + // TODO: Make this relative to $PWD === track root + $pathToPracticeExercise = '../../exercises/practice/' . $this->exerciseSlug; + if (!(is_writable($pathToPracticeExercise) && is_dir($pathToPracticeExercise))) + throw new RuntimeException( + 'Cannot write to exercise directory. Create exercise with configlet first or check access rights!' + ); + + $io = new SymfonyStyle($input, $output); - $io->writeln('Generating tests for ' . $input->getArgument('exercise')); + $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $pathToPracticeExercise); // $this->createTests(); From 65887a4fdb6cddea533384dbd8895e6086fa9871 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 15:47:22 +0100 Subject: [PATCH 09/29] Construct path to canonical data from configlet When running configlet with detailed output (-v d) and a command that requires problem specification data (e.g. info), it prints the location of the cache as the first line. To avoid an HTTP call, use the offline mode (-o). Pipe the output through 'head' to get the first line only, then 'cut' the 5th field to get the path only. configlet may fail when there is no cached data (offline mode), which tells us, that the exercise hasn't been generated before (the cache is required for that, too). Assemble the cache directory with the exercise location and if that's not there, we don't have a usable exercise slug or no access rights. --- .../src/Command/CreateTestsCommand.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index fc1be095..87fe36a8 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PhpParser\PrettyPrinter; +use RuntimeException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -20,6 +21,7 @@ class CreateTestsCommand extends Command { private BuilderFactory $builderFactory; + private string $exerciseSlug; public function __construct() { @@ -58,12 +60,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $pathToPracticeExercise); - // $this->createTests(); + $pathToCanonicalData = $this->pathToCachedCanonicalData(); + $io->writeln('Constructed path to canonical data: ' . $pathToCanonicalData); + + if (!(is_readable($pathToCanonicalData) && is_file($pathToCanonicalData))) + throw new RuntimeException( + 'Cannot read "configlet" provided cached canonical data from ' + . $pathToCanonicalData + .'. Check exercise slug or access rights!' + ); $io->success('Generating Tests - Finished'); return Command::SUCCESS; } + private function pathToCachedCanonicalData(): string + { + // TODO: Make this relative to $PWD === track root + $command = 'bash -c \'set -eo pipefail; ../../bin/configlet -v d -t ../.. info -o | head -1 | cut -d " " -f 5\''; + $resultCode = 1; + $configletCache = \exec(command: $command, result_code: $resultCode); + if ($configletCache === false || $resultCode !== Command::SUCCESS) + throw new RuntimeException( + '"configlet" could not provide cached canonical data. Create exercise with configlet first!' + ); + + return \sprintf('%1$s/exercises/%2$s/canonical-data.json', $configletCache, $this->exerciseSlug); + } + /** * @throws \JsonException */ From 7c273f78e3b85f4ff69b73228ac621d2c13de130 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 16:01:41 +0100 Subject: [PATCH 10/29] Extract ensureConfigletCanBeUsed() --- .../src/Command/CreateTestsCommand.php | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 87fe36a8..ae5ea878 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -1,5 +1,7 @@ trackRoot = '../..'; + $this->pathToConfiglet = $this->trackRoot . '/bin/configlet'; + $this->builderFactory = new BuilderFactory(); parent::__construct(); @@ -41,13 +49,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $this->exerciseSlug = $input->getArgument('exercise'); - - // TODO: Make this relative to $PWD === track root - $pathToConfiglet = '../../bin/configlet'; - if (!(is_executable($pathToConfiglet) && is_file($pathToConfiglet))) - throw new RuntimeException( - 'configlet not found. Fetch configlet and create exercise with configlet first!' - ); + $this->ensureConfigletCanBeUsed(); // TODO: Make this relative to $PWD === track root $pathToPracticeExercise = '../../exercises/practice/' . $this->exerciseSlug; @@ -74,6 +76,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + private function ensureConfigletCanBeUsed(): void + { + if ( + !( + is_executable($this->pathToConfiglet) + && is_file($this->pathToConfiglet) + ) + ) { + throw new RuntimeException( + 'configlet not found. Run the generator from track root.' + . ' Fetch configlet and create exercise with configlet first!' + ); + } + } + private function pathToCachedCanonicalData(): string { // TODO: Make this relative to $PWD === track root From 69564c9407e63d8033f63124bbd907f1647b987f Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 16:11:45 +0100 Subject: [PATCH 11/29] Extract ensurePracticeExerciseCanBeUsed() --- .../src/Command/CreateTestsCommand.php | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index ae5ea878..f2c8594c 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -25,6 +25,8 @@ class CreateTestsCommand extends Command private BuilderFactory $builderFactory; private string $trackRoot; private string $pathToConfiglet = ''; + private string $pathToPracticeExercises = ''; + private string $pathToPracticeExercise = ''; private string $exerciseSlug; public function __construct() @@ -32,6 +34,7 @@ public function __construct() // TODO: Make this $PWD (being injected by DI) and check for $PWD === track root $this->trackRoot = '../..'; $this->pathToConfiglet = $this->trackRoot . '/bin/configlet'; + $this->pathToPracticeExercises = $this->trackRoot . '/exercises/practice/'; $this->builderFactory = new BuilderFactory(); @@ -49,18 +52,14 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $this->exerciseSlug = $input->getArgument('exercise'); - $this->ensureConfigletCanBeUsed(); - - // TODO: Make this relative to $PWD === track root - $pathToPracticeExercise = '../../exercises/practice/' . $this->exerciseSlug; - if (!(is_writable($pathToPracticeExercise) && is_dir($pathToPracticeExercise))) - throw new RuntimeException( - 'Cannot write to exercise directory. Create exercise with configlet first or check access rights!' - ); + $this->pathToPracticeExercise = + $this->pathToPracticeExercises . $this->exerciseSlug; + $this->ensureConfigletCanBeUsed(); + $this->ensurePracticeExerciseCanBeUsed(); $io = new SymfonyStyle($input, $output); - $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $pathToPracticeExercise); + $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $this->pathToPracticeExercise); $pathToCanonicalData = $this->pathToCachedCanonicalData(); $io->writeln('Constructed path to canonical data: ' . $pathToCanonicalData); @@ -76,6 +75,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + private function ensurePracticeExerciseCanBeUsed(): void + { + if ( + !( + is_writable($this->pathToPracticeExercise) + && is_dir($this->pathToPracticeExercise) + ) + ) { + throw new RuntimeException( + 'Cannot write to exercise directory. Create exercise with' + . ' configlet first or check access rights!' + ); + } + } + private function ensureConfigletCanBeUsed(): void { if ( From 3815922724e05b0fe7416239653dadc8d74eb835 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 16:42:40 +0100 Subject: [PATCH 12/29] Extract canonical data handling --- .../src/Command/CreateTestsCommand.php | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index f2c8594c..87fba8d1 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -27,6 +27,8 @@ class CreateTestsCommand extends Command private string $pathToConfiglet = ''; private string $pathToPracticeExercises = ''; private string $pathToPracticeExercise = ''; + private string $pathToCanonicalData = ''; + private string $exerciseSlug; public function __construct() @@ -47,7 +49,7 @@ protected function configure(): void } /** - * @throws \JsonException + * @throws RuntimeException */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -57,22 +59,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->ensureConfigletCanBeUsed(); $this->ensurePracticeExerciseCanBeUsed(); + $this->pathToCachedCanonicalDataFromConfiglet(); + $this->ensurePathToCanonicalDataCanBeUsed(); $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $this->pathToPracticeExercise); + $io->writeln('Constructed path to canonical data: ' . $this->pathToCanonicalData); - $pathToCanonicalData = $this->pathToCachedCanonicalData(); - $io->writeln('Constructed path to canonical data: ' . $pathToCanonicalData); + $io->success('Generating Tests - Finished'); + return Command::SUCCESS; + } - if (!(is_readable($pathToCanonicalData) && is_file($pathToCanonicalData))) + private function ensurePathToCanonicalDataCanBeUsed(): void + { + if ( + !( + is_readable($this->pathToCanonicalData) + && is_file($this->pathToCanonicalData) + ) + ) { throw new RuntimeException( 'Cannot read "configlet" provided cached canonical data from ' - . $pathToCanonicalData + . $this->pathToCanonicalData .'. Check exercise slug or access rights!' ); - - $io->success('Generating Tests - Finished'); - return Command::SUCCESS; + } } private function ensurePracticeExerciseCanBeUsed(): void @@ -105,18 +116,29 @@ private function ensureConfigletCanBeUsed(): void } } - private function pathToCachedCanonicalData(): string + private function pathToCachedCanonicalDataFromConfiglet(): void { - // TODO: Make this relative to $PWD === track root - $command = 'bash -c \'set -eo pipefail; ../../bin/configlet -v d -t ../.. info -o | head -1 | cut -d " " -f 5\''; - $resultCode = 1; + $command = 'bash -c \'set -eo pipefail; ' + . $this->pathToConfiglet + . ' -v d -t ' + . $this->trackRoot + . ' info -o | head -1 | cut -d " " -f 5\'' + ; + $resultCode = Command::FAILURE; + $configletCache = \exec(command: $command, result_code: $resultCode); - if ($configletCache === false || $resultCode !== Command::SUCCESS) + if ($configletCache === false || $resultCode !== Command::SUCCESS) { throw new RuntimeException( - '"configlet" could not provide cached canonical data. Create exercise with configlet first!' + '"configlet" could not provide cached canonical data.' + . ' Create exercise with configlet first!' ); + } - return \sprintf('%1$s/exercises/%2$s/canonical-data.json', $configletCache, $this->exerciseSlug); + $this->pathToCanonicalData = \sprintf( + '%1$s/exercises/%2$s/canonical-data.json', + $configletCache, + $this->exerciseSlug + ); } /** From b7be21f91abd89722770a66735cd7802968c9b50 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 18:38:14 +0100 Subject: [PATCH 13/29] Sort out test creation things The test creation will move to its own class. Right now it is not used. --- .../generator/src/Command/CreateTestsCommand.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 87fba8d1..f51d7dff 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -22,7 +22,6 @@ )] class CreateTestsCommand extends Command { - private BuilderFactory $builderFactory; private string $trackRoot; private string $pathToConfiglet = ''; private string $pathToPracticeExercises = ''; @@ -38,8 +37,6 @@ public function __construct() $this->pathToConfiglet = $this->trackRoot . '/bin/configlet'; $this->pathToPracticeExercises = $this->trackRoot . '/exercises/practice/'; - $this->builderFactory = new BuilderFactory(); - parent::__construct(); } @@ -141,11 +138,17 @@ private function pathToCachedCanonicalDataFromConfiglet(): void ); } + // TODO: Move to own class + + private BuilderFactory $builderFactory; + /** * @throws \JsonException */ private function createTests(): void { + $this->builderFactory = new BuilderFactory(); + $jsonData = file_get_contents(__DIR__ . "/canonical-data.json"); $data = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR); From 1f8bc159a862727da2c05e065811cd399373beaf Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 24 Feb 2024 18:57:20 +0100 Subject: [PATCH 14/29] Inject project dir and make track root from it --- contribution/generator/config/services.yaml | 2 ++ .../generator/src/Command/CreateTestsCommand.php | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/contribution/generator/config/services.yaml b/contribution/generator/config/services.yaml index 2d6a76f9..ba14e9fa 100644 --- a/contribution/generator/config/services.yaml +++ b/contribution/generator/config/services.yaml @@ -10,6 +10,8 @@ services: _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + bind: + $projectDir: '%kernel.project_dir%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index f51d7dff..b04b6eb3 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -30,10 +30,10 @@ class CreateTestsCommand extends Command private string $exerciseSlug; - public function __construct() - { - // TODO: Make this $PWD (being injected by DI) and check for $PWD === track root - $this->trackRoot = '../..'; + public function __construct( + private string $projectDir, + ) { + $this->trackRoot = realpath($projectDir . '/../..'); $this->pathToConfiglet = $this->trackRoot . '/bin/configlet'; $this->pathToPracticeExercises = $this->trackRoot . '/exercises/practice/'; @@ -60,6 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->ensurePathToCanonicalDataCanBeUsed(); $io = new SymfonyStyle($input, $output); + $io->writeln('Project dir: ' . $this->projectDir); $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $this->pathToPracticeExercise); $io->writeln('Constructed path to canonical data: ' . $this->pathToCanonicalData); From eec68e4e65cd0534804a6602467c012f5444f3da Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 22:07:59 +0100 Subject: [PATCH 15/29] Extract class PracticeExercise --- .../src/Command/CreateTestsCommand.php | 102 ++---------------- .../generator/src/TrackData/Exercise.php | 19 ++++ .../src/TrackData/PracticeExercise.php | 101 +++++++++++++++++ 3 files changed, 128 insertions(+), 94 deletions(-) create mode 100644 contribution/generator/src/TrackData/Exercise.php create mode 100644 contribution/generator/src/TrackData/PracticeExercise.php diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index b04b6eb3..9a861eaf 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -4,11 +4,11 @@ namespace App\Command; +use App\TrackData\PracticeExercise; use PhpParser\BuilderFactory; use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PhpParser\PrettyPrinter; -use RuntimeException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -23,19 +23,11 @@ class CreateTestsCommand extends Command { private string $trackRoot; - private string $pathToConfiglet = ''; - private string $pathToPracticeExercises = ''; - private string $pathToPracticeExercise = ''; - private string $pathToCanonicalData = ''; - - private string $exerciseSlug; public function __construct( private string $projectDir, ) { $this->trackRoot = realpath($projectDir . '/../..'); - $this->pathToConfiglet = $this->trackRoot . '/bin/configlet'; - $this->pathToPracticeExercises = $this->trackRoot . '/exercises/practice/'; parent::__construct(); } @@ -45,100 +37,22 @@ protected function configure(): void $this->addArgument('exercise', InputArgument::REQUIRED, 'Exercise slug'); } - /** - * @throws RuntimeException - */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->exerciseSlug = $input->getArgument('exercise'); - $this->pathToPracticeExercise = - $this->pathToPracticeExercises . $this->exerciseSlug; - - $this->ensureConfigletCanBeUsed(); - $this->ensurePracticeExerciseCanBeUsed(); - $this->pathToCachedCanonicalDataFromConfiglet(); - $this->ensurePathToCanonicalDataCanBeUsed(); + // TODO: Move things to `TestGenerator` + $exerciseSlug = $input->getArgument('exercise'); + $exercise = new PracticeExercise( + $this->trackRoot, + $exerciseSlug, + ); $io = new SymfonyStyle($input, $output); - $io->writeln('Project dir: ' . $this->projectDir); - $io->writeln('Generating tests for ' . $this->exerciseSlug . ' in ' . $this->pathToPracticeExercise); - $io->writeln('Constructed path to canonical data: ' . $this->pathToCanonicalData); + $io->writeln('Generating tests for ' . $exerciseSlug . ' in ' . $exercise->pathToExercise()); $io->success('Generating Tests - Finished'); return Command::SUCCESS; } - private function ensurePathToCanonicalDataCanBeUsed(): void - { - if ( - !( - is_readable($this->pathToCanonicalData) - && is_file($this->pathToCanonicalData) - ) - ) { - throw new RuntimeException( - 'Cannot read "configlet" provided cached canonical data from ' - . $this->pathToCanonicalData - .'. Check exercise slug or access rights!' - ); - } - } - - private function ensurePracticeExerciseCanBeUsed(): void - { - if ( - !( - is_writable($this->pathToPracticeExercise) - && is_dir($this->pathToPracticeExercise) - ) - ) { - throw new RuntimeException( - 'Cannot write to exercise directory. Create exercise with' - . ' configlet first or check access rights!' - ); - } - } - - private function ensureConfigletCanBeUsed(): void - { - if ( - !( - is_executable($this->pathToConfiglet) - && is_file($this->pathToConfiglet) - ) - ) { - throw new RuntimeException( - 'configlet not found. Run the generator from track root.' - . ' Fetch configlet and create exercise with configlet first!' - ); - } - } - - private function pathToCachedCanonicalDataFromConfiglet(): void - { - $command = 'bash -c \'set -eo pipefail; ' - . $this->pathToConfiglet - . ' -v d -t ' - . $this->trackRoot - . ' info -o | head -1 | cut -d " " -f 5\'' - ; - $resultCode = Command::FAILURE; - - $configletCache = \exec(command: $command, result_code: $resultCode); - if ($configletCache === false || $resultCode !== Command::SUCCESS) { - throw new RuntimeException( - '"configlet" could not provide cached canonical data.' - . ' Create exercise with configlet first!' - ); - } - - $this->pathToCanonicalData = \sprintf( - '%1$s/exercises/%2$s/canonical-data.json', - $configletCache, - $this->exerciseSlug - ); - } - // TODO: Move to own class private BuilderFactory $builderFactory; diff --git a/contribution/generator/src/TrackData/Exercise.php b/contribution/generator/src/TrackData/Exercise.php new file mode 100644 index 00000000..9e16e58d --- /dev/null +++ b/contribution/generator/src/TrackData/Exercise.php @@ -0,0 +1,19 @@ +pathToConfiglet = $trackRoot . '/bin/configlet'; + $this->pathToPracticeExercises = $trackRoot . '/exercises/practice/'; + $this->pathToExercise = + $this->pathToPracticeExercises . $this->exerciseSlug; + } + + public function pathToExercise(): string + { + return $this->pathToExercise; + } + + private function ensureConfigletCanBeUsed(): void + { + if ( + !( + is_executable($this->pathToConfiglet) + && is_file($this->pathToConfiglet) + ) + ) { + throw new RuntimeException( + 'configlet not found. Run the generator from track root.' + . ' Fetch configlet and create exercise with configlet first!' + ); + } + } + + private function ensurePracticeExerciseCanBeUsed(): void + { + if ( + !( + is_writable($this->pathToExercise) + && is_dir($this->pathToExercise) + ) + ) { + throw new RuntimeException( + 'Cannot write to exercise directory. Create exercise with' + . ' configlet first or check access rights!' + ); + } + } + + private function pathToCachedCanonicalDataFromConfiglet(): void + { + $command = 'bash -c \'set -eo pipefail; ' + . $this->pathToConfiglet + . ' -v d -t ' + . $this->trackRoot + . ' info -o | head -1 | cut -d " " -f 5\'' + ; + $resultCode = 1; + + $configletCache = \exec(command: $command, result_code: $resultCode); + if ($configletCache === false || $resultCode !== 0) { + throw new RuntimeException( + '"configlet" could not provide cached canonical data.' + . ' Create exercise with configlet first!' + ); + } + + $this->pathToCanonicalData = \sprintf( + '%1$s/exercises/%2$s/canonical-data.json', + $configletCache, + $this->exerciseSlug + ); + } + + private function ensurePathToCanonicalDataCanBeUsed(): void + { + if ( + !( + is_readable($this->pathToCanonicalData) + && is_file($this->pathToCanonicalData) + ) + ) { + throw new RuntimeException( + 'Cannot read "configlet" provided cached canonical data from ' + . $this->pathToCanonicalData + .'. Check exercise slug or access rights!' + ); + } + } +} From 74c2153c9d3d4bf0d955e60586b944fee514fddf Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 22:52:50 +0100 Subject: [PATCH 16/29] Extract class TestGenerator --- .../src/Command/CreateTestsCommand.php | 93 +----------------- contribution/generator/src/TestGenerator.php | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+), 91 deletions(-) create mode 100644 contribution/generator/src/TestGenerator.php diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 9a861eaf..c002d4a6 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -4,11 +4,8 @@ namespace App\Command; +use App\TestGenerator; use App\TrackData\PracticeExercise; -use PhpParser\BuilderFactory; -use PhpParser\Node; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\PrettyPrinter; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -39,12 +36,12 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - // TODO: Move things to `TestGenerator` $exerciseSlug = $input->getArgument('exercise'); $exercise = new PracticeExercise( $this->trackRoot, $exerciseSlug, ); + $testGenerator = new TestGenerator($exerciseSlug); $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $exerciseSlug . ' in ' . $exercise->pathToExercise()); @@ -52,90 +49,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->success('Generating Tests - Finished'); return Command::SUCCESS; } - - // TODO: Move to own class - - private BuilderFactory $builderFactory; - - /** - * @throws \JsonException - */ - private function createTests(): void - { - $this->builderFactory = new BuilderFactory(); - - $jsonData = file_get_contents(__DIR__ . "/canonical-data.json"); - $data = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR); - - $classBuilder = $this->builderFactory->class($this->generateClassName($data['exercise']) . "Test")->makeFinal()->extend('\PHPUnit\Framework\TestCase'); - - // Include Setup Method - $methodSetup = 'setUpBeforeClass'; - $method = $this->builderFactory->method($methodSetup) - ->makePublic() - ->makeStatic() - ->setReturnType('void') - ->addStmt( - $this->builderFactory->funcCall( - "require_once", - [$this->generateClassName($data['exercise']) . ".php"] - ), - ); - - $classBuilder->addStmt($method); - - foreach ($data['cases'] as $case) { - // Generate a method for each test case - $description = $case['description']; - $methodName = ucfirst(str_replace('-', ' ', $description)); - $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); - $uuid = $case['uuid']; - - $exceptionClassName = new Node\Name\FullyQualified('Exception'); - if (isset($case['expected']['error'])) { - $method = $this->builderFactory->method($methodName) - ->makePublic() - ->setReturnType('void') - ->addStmt( - $this->builderFactory->funcCall('$this->expectException', - [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] - ) - ) - ->addStmt($this->builderFactory->funcCall($case['property'], [$case['input']['strand']])) - ->setDocComment("/**\n * uuid: $uuid\n */"); - } else { - $method = $this->builderFactory->method($methodName) - ->makePublic() - ->setReturnType('void') - ->addStmt( - $this->builderFactory->funcCall('$this->assertEquals', [ - $case['expected'], - $this->builderFactory->funcCall($case['property'], [$case['input']['strand']]) - ]) - ) - ->setDocComment("/**\n * uuid: $uuid\n */"); - } - $classBuilder->addStmt($method); - } - - $class = $classBuilder->getNode(); - - $namespace = new Namespace_(new Node\Name('Tests')); - $namespace->stmts[] = $class; - - $printer = new PrettyPrinter\Standard(); - - // Write to file - $file = fopen(__DIR__ . "/" . $this->generateClassName($data['exercise']) . "Test.php", "w") or die("Unable to open file!"); - fwrite($file, $printer->prettyPrintFile([$namespace]) . PHP_EOL); - fclose($file); - - } - - private function generateClassName(string $name): string - { - $name = str_replace("-", " ", $name); - $name = ucwords($name); - return str_replace(" ", "", $name); - } } diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php new file mode 100644 index 00000000..f8d2b642 --- /dev/null +++ b/contribution/generator/src/TestGenerator.php @@ -0,0 +1,98 @@ +builderFactory = new BuilderFactory(); + + $classBuilder = $this->builderFactory->class( + $this->slugInPascalCase() . "Test" + )->makeFinal()->extend('\PHPUnit\Framework\TestCase'); + + // Include Setup Method + $methodSetup = 'setUpBeforeClass'; + $method = $this->builderFactory->method($methodSetup) + ->makePublic() + ->makeStatic() + ->setReturnType('void') + ->addStmt( + $this->builderFactory->funcCall( + "require_once", + [$this->slugInPascalCase() . ".php"] + ), + ); + + $classBuilder->addStmt($method); + + foreach ($canonicalData->testCases as $case) { + // Generate a method for each test case + $description = $case->description; + $methodName = ucfirst(str_replace('-', ' ', $description)); + $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); + $uuid = $case->uuid; + + $exceptionClassName = new Node\Name\FullyQualified('Exception'); + if (isset($case->expected->error)) { + $method = $this->builderFactory->method($methodName) + ->makePublic() + ->setReturnType('void') + ->addStmt( + $this->builderFactory->funcCall('$this->expectException', + [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] + ) + ) + ->addStmt($this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown'])) + ->setDocComment("/**\n * uuid: $uuid\n */"); + } else { + $method = $this->builderFactory->method($methodName) + ->makePublic() + ->setReturnType('void') + ->addStmt( + $this->builderFactory->funcCall('$this->assertEquals', [ + $case->expected, + $this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown']) + ]) + ) + ->setDocComment("/**\n * uuid: $uuid\n */"); + } + $classBuilder->addStmt($method); + } + + $class = $classBuilder->getNode(); + + $namespace = new Namespace_(new Node\Name('Tests')); + $namespace->stmts[] = $class; + + $printer = new PrettyPrinter\Standard(); + + return $printer->prettyPrintFile([$namespace]) . PHP_EOL; + } + + public function slugInPascalCase(): string + { + $name = str_replace("-", " ", $this->exerciseSlug); + $name = ucwords($name); + return str_replace(" ", "", $name); + } +} From 0d54518e5793be6da0862fa194e13ce4b5227d6e Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:01:46 +0100 Subject: [PATCH 17/29] Generate test stubs from problem specification --- .../src/Command/CreateTestsCommand.php | 2 + contribution/generator/src/TestGenerator.php | 50 +++++++++---------- .../generator/src/TrackData/CanonicalData.php | 17 +++++++ .../src/TrackData/CanonicalData/TestCase.php | 17 +++++++ .../generator/src/TrackData/Exercise.php | 3 ++ .../src/TrackData/PracticeExercise.php | 40 +++++++++++++++ 6 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 contribution/generator/src/TrackData/CanonicalData.php create mode 100644 contribution/generator/src/TrackData/CanonicalData/TestCase.php diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index c002d4a6..ba5b3a44 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -46,6 +46,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $exerciseSlug . ' in ' . $exercise->pathToExercise()); + \file_put_contents($exercise->pathToExercise() . '/' . $testGenerator->slugInPascalCase() . 'Test.php', $testGenerator->createTestsFor($exercise->canonicalData())); + $io->success('Generating Tests - Finished'); return Command::SUCCESS; } diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index f8d2b642..e227f873 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -19,9 +19,6 @@ public function __construct( ) { } - /** - * @throws \JsonException - */ public function createTestsFor(CanonicalData $canonicalData): string { $this->builderFactory = new BuilderFactory(); @@ -52,30 +49,29 @@ public function createTestsFor(CanonicalData $canonicalData): string $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); $uuid = $case->uuid; - $exceptionClassName = new Node\Name\FullyQualified('Exception'); - if (isset($case->expected->error)) { - $method = $this->builderFactory->method($methodName) - ->makePublic() - ->setReturnType('void') - ->addStmt( - $this->builderFactory->funcCall('$this->expectException', - [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] - ) - ) - ->addStmt($this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown'])) - ->setDocComment("/**\n * uuid: $uuid\n */"); - } else { - $method = $this->builderFactory->method($methodName) - ->makePublic() - ->setReturnType('void') - ->addStmt( - $this->builderFactory->funcCall('$this->assertEquals', [ - $case->expected, - $this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown']) - ]) - ) - ->setDocComment("/**\n * uuid: $uuid\n */"); - } + // $exceptionClassName = new Node\Name\FullyQualified('Exception'); + $method = $this->builderFactory->method($methodName) + ->makePublic() + ->setReturnType('void') + ->setDocComment("/**\n * uuid: $uuid\n */") + ; + // if (isset($case->expected->error)) { + // $method->addStmt( + // $this->builderFactory->funcCall('$this->expectException', + // [new Node\Arg(new Node\Expr\ClassConstFetch($exceptionClassName, 'class'))] + // ) + // ) + // ->addStmt($this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown'])) + // ; + // } else { + // $method->addStmt( + // $this->builderFactory->funcCall('$this->assertEquals', [ + // $case->expected, + // $this->builderFactory->funcCall($case->property, [$case->input->strand ?? 'unknown']) + // ]) + // ); + // } + $classBuilder->addStmt($method); } diff --git a/contribution/generator/src/TrackData/CanonicalData.php b/contribution/generator/src/TrackData/CanonicalData.php new file mode 100644 index 00000000..7a7dcf08 --- /dev/null +++ b/contribution/generator/src/TrackData/CanonicalData.php @@ -0,0 +1,17 @@ +pathToExercise; } + public function canonicalData(): CanonicalData + { + $this->ensureConfigletCanBeUsed(); + $this->ensurePracticeExerciseCanBeUsed(); + $this->pathToCachedCanonicalDataFromConfiglet(); + $this->ensurePathToCanonicalDataCanBeUsed(); + + return $this->hydratedCanonicalData(); + } + + private function hydratedCanonicalData(): CanonicalData + { + $canonicalData = \json_decode( + json: \file_get_contents($this->pathToCanonicalData), + flags: JSON_THROW_ON_ERROR + ); + + // TODO: Validate + return new CanonicalData( + $canonicalData->exercise, + $this->hydrateTestCasesFrom($canonicalData->cases), + $canonicalData->comments, + ); + } + + private function hydrateTestCasesFrom(array $rawData): array + { + // TODO: Validate + return array_map(fn ($case) => new TestCase( + $case->uuid ?? null, + $case->description ?? null, + $case->property ?? null, + $case->input ?? null, + $case->expected ?? null, + $case->comments ?? [], + ), $rawData); + } + private function ensureConfigletCanBeUsed(): void { if ( From 2faf87d526aa4b17407ed43ff4954315f5418b18 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:19:43 +0100 Subject: [PATCH 18/29] Move method inPascalCase to command --- .../src/Command/CreateTestsCommand.php | 19 ++++++++++++++-- contribution/generator/src/TestGenerator.php | 22 +++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index ba5b3a44..0d88519b 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -41,14 +41,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->trackRoot, $exerciseSlug, ); - $testGenerator = new TestGenerator($exerciseSlug); + $testGenerator = new TestGenerator(); $io = new SymfonyStyle($input, $output); $io->writeln('Generating tests for ' . $exerciseSlug . ' in ' . $exercise->pathToExercise()); - \file_put_contents($exercise->pathToExercise() . '/' . $testGenerator->slugInPascalCase() . 'Test.php', $testGenerator->createTestsFor($exercise->canonicalData())); + \file_put_contents( + $exercise->pathToExercise() + . '/' + . $this->inPascalCase($exerciseSlug) + . 'Test.php' + , + $testGenerator->createTestsFor( + $exercise->canonicalData(), + $this->inPascalCase($exerciseSlug) + ), + ); $io->success('Generating Tests - Finished'); return Command::SUCCESS; } + + private function inPascalCase(string $text): string + { + return \str_replace(" ", "", \ucwords(\str_replace("-", " ", $text))); + } } diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index e227f873..bd213226 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -14,17 +14,14 @@ class TestGenerator { private BuilderFactory $builderFactory; - public function __construct( - private string $exerciseSlug, - ) { - } - - public function createTestsFor(CanonicalData $canonicalData): string - { + public function createTestsFor( + CanonicalData $canonicalData, + string $exerciseClass + ): string { $this->builderFactory = new BuilderFactory(); $classBuilder = $this->builderFactory->class( - $this->slugInPascalCase() . "Test" + $exerciseClass . "Test" )->makeFinal()->extend('\PHPUnit\Framework\TestCase'); // Include Setup Method @@ -36,7 +33,7 @@ public function createTestsFor(CanonicalData $canonicalData): string ->addStmt( $this->builderFactory->funcCall( "require_once", - [$this->slugInPascalCase() . ".php"] + [$exerciseClass . ".php"] ), ); @@ -84,11 +81,4 @@ public function createTestsFor(CanonicalData $canonicalData): string return $printer->prettyPrintFile([$namespace]) . PHP_EOL; } - - public function slugInPascalCase(): string - { - $name = str_replace("-", " ", $this->exerciseSlug); - $name = ucwords($name); - return str_replace(" ", "", $name); - } } From ec018962c3889f124e79fc89cd0432f45a79809a Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:20:21 +0100 Subject: [PATCH 19/29] Rename $classBuilder to $class --- contribution/generator/src/TestGenerator.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index bd213226..f3f3c184 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -20,7 +20,7 @@ public function createTestsFor( ): string { $this->builderFactory = new BuilderFactory(); - $classBuilder = $this->builderFactory->class( + $class = $this->builderFactory->class( $exerciseClass . "Test" )->makeFinal()->extend('\PHPUnit\Framework\TestCase'); @@ -37,7 +37,7 @@ public function createTestsFor( ), ); - $classBuilder->addStmt($method); + $class->addStmt($method); foreach ($canonicalData->testCases as $case) { // Generate a method for each test case @@ -69,13 +69,11 @@ public function createTestsFor( // ); // } - $classBuilder->addStmt($method); + $class->addStmt($method); } - $class = $classBuilder->getNode(); - $namespace = new Namespace_(new Node\Name('Tests')); - $namespace->stmts[] = $class; + $namespace->stmts[] = $class->getNode(); $printer = new PrettyPrinter\Standard(); From 2bd8ede0f2c4dff384e0b14485bb8bef5c33ca7f Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:21:03 +0100 Subject: [PATCH 20/29] Use class constant instead of string --- contribution/generator/src/TestGenerator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index f3f3c184..587fcc01 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -9,6 +9,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PhpParser\PrettyPrinter; +use PHPUnit\Framework\TestCase; class TestGenerator { @@ -22,7 +23,7 @@ public function createTestsFor( $class = $this->builderFactory->class( $exerciseClass . "Test" - )->makeFinal()->extend('\PHPUnit\Framework\TestCase'); + )->makeFinal()->extend(TestCase::class); // Include Setup Method $methodSetup = 'setUpBeforeClass'; From 66e8eb5d1a8f75475d23f4341b5ae296040b554c Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:28:43 +0100 Subject: [PATCH 21/29] Add test class doc block from exercise comment --- contribution/generator/src/TestGenerator.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index 587fcc01..9b1fb60b 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -24,6 +24,9 @@ public function createTestsFor( $class = $this->builderFactory->class( $exerciseClass . "Test" )->makeFinal()->extend(TestCase::class); + $class->setDocComment( + "/**\n * " . implode("\n * ", $canonicalData->comments) . "\n */" + ); // Include Setup Method $methodSetup = 'setUpBeforeClass'; @@ -45,13 +48,12 @@ public function createTestsFor( $description = $case->description; $methodName = ucfirst(str_replace('-', ' ', $description)); $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); - $uuid = $case->uuid; // $exceptionClassName = new Node\Name\FullyQualified('Exception'); $method = $this->builderFactory->method($methodName) ->makePublic() ->setReturnType('void') - ->setDocComment("/**\n * uuid: $uuid\n */") + ->setDocComment("/**\n * uuid: $case->uuid\n */") ; // if (isset($case->expected->error)) { // $method->addStmt( From 78a44b6a77b9dd64dbb616cff52c1e63736a45d0 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:40:40 +0100 Subject: [PATCH 22/29] Add testdox to test methods --- contribution/generator/src/TestGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index 9b1fb60b..65a7313f 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -45,7 +45,7 @@ public function createTestsFor( foreach ($canonicalData->testCases as $case) { // Generate a method for each test case - $description = $case->description; + $description = \ucfirst($case->description); $methodName = ucfirst(str_replace('-', ' ', $description)); $methodName = 'test' . str_replace(' ', '', ucwords($methodName)); @@ -53,7 +53,7 @@ public function createTestsFor( $method = $this->builderFactory->method($methodName) ->makePublic() ->setReturnType('void') - ->setDocComment("/**\n * uuid: $case->uuid\n */") + ->setDocComment("/**\n * uuid: {$case->uuid}\n * @testdox {$description}\n */") ; // if (isset($case->expected->error)) { // $method->addStmt( From cec0d8a9e84c8c53a793eb82b1fdfda0b4aec591 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:41:43 +0100 Subject: [PATCH 23/29] Add use for PHPUnit TestCase --- contribution/generator/src/TestGenerator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index 65a7313f..fa9d38c5 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -23,7 +23,7 @@ public function createTestsFor( $class = $this->builderFactory->class( $exerciseClass . "Test" - )->makeFinal()->extend(TestCase::class); + )->makeFinal()->extend('TestCase'); $class->setDocComment( "/**\n * " . implode("\n * ", $canonicalData->comments) . "\n */" ); @@ -76,6 +76,7 @@ public function createTestsFor( } $namespace = new Namespace_(new Node\Name('Tests')); + $namespace->stmts[] = $this->builderFactory->use(TestCase::class)->getNode(); $namespace->stmts[] = $class->getNode(); $printer = new PrettyPrinter\Standard(); From bd22d0cae8cdf8f0766817a76546f5aae4327214 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:48:16 +0100 Subject: [PATCH 24/29] Mark generated tests as incomplete --- contribution/generator/src/TestGenerator.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contribution/generator/src/TestGenerator.php b/contribution/generator/src/TestGenerator.php index fa9d38c5..e65ea06f 100644 --- a/contribution/generator/src/TestGenerator.php +++ b/contribution/generator/src/TestGenerator.php @@ -54,6 +54,12 @@ public function createTestsFor( ->makePublic() ->setReturnType('void') ->setDocComment("/**\n * uuid: {$case->uuid}\n * @testdox {$description}\n */") + ->addStmt( + $this->builderFactory->funcCall( + '$this->markTestIncomplete', + [ 'This test has not been implemented yet.' ], + ) + ) ; // if (isset($case->expected->error)) { // $method->addStmt( From 4bb8fa8e257a04fa793687a1971194b696e6360a Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 25 Feb 2024 23:58:26 +0100 Subject: [PATCH 25/29] Update README, more TODOs --- README.md | 15 +++++++++++++++ .../generator/src/Command/CreateTestsCommand.php | 3 +++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index 906b6caa..d9d17082 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The following system dependencies are required: - `composer`, as recommended in the [PHP track installation docs][exercism-track-installation-composer]. - [`bash` shell][gnu-bash] +- PHP V8.2+ CLI Run the following commands to get started with this project: @@ -51,6 +52,20 @@ composer lint:fix # Automatically fix codestyle issues - CI is run on all pull requests, it must pass the required checks for merge. - CI is running all tests on PHP 8.0 to PHP 8.2 +## Generating new practice exercises + +Use `bin/configlet create --practice-exercise ` to create the exercism resources required. +This provides you with the directories and files in `exercises/practice/`. +Look into `tests.toml` for which test cases **not** to implement / generate and mark them with `include = false`. + +Test generator MVP used like this: + +```shell +composer -d contribution/generator install +contribution/generator/bin/console app:create-tests '' +composer lint:fix +``` + [exercism-configlet]: https://exercism.org/docs/building/configlet [exercism-docs]: https://exercism.org/docs [exercism-track-home]: https://exercism.org/docs/tracks/php diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index 0d88519b..cec64107 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -47,6 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->writeln('Generating tests for ' . $exerciseSlug . ' in ' . $exercise->pathToExercise()); \file_put_contents( + // TODO: Make '$exercise->pathToTestFile()' $exercise->pathToExercise() . '/' . $this->inPascalCase($exerciseSlug) @@ -57,6 +58,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->inPascalCase($exerciseSlug) ), ); + // TODO: Make '$exercise->pathToStudentsFile()' + // TODO: Make '$testGenerator->studentsFileFor()' $io->success('Generating Tests - Finished'); return Command::SUCCESS; From c7f7a035fe99da18195becd9445df7476f604dc6 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Mon, 26 Feb 2024 00:02:33 +0100 Subject: [PATCH 26/29] Fix code styles --- contribution/generator/src/Command/CreateTestsCommand.php | 3 +-- contribution/generator/src/TrackData/CanonicalData.php | 3 ++- .../generator/src/TrackData/CanonicalData/TestCase.php | 3 ++- contribution/generator/src/TrackData/Exercise.php | 3 ++- contribution/generator/src/TrackData/PracticeExercise.php | 5 +++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contribution/generator/src/Command/CreateTestsCommand.php b/contribution/generator/src/Command/CreateTestsCommand.php index cec64107..ae9604f4 100644 --- a/contribution/generator/src/Command/CreateTestsCommand.php +++ b/contribution/generator/src/Command/CreateTestsCommand.php @@ -51,8 +51,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $exercise->pathToExercise() . '/' . $this->inPascalCase($exerciseSlug) - . 'Test.php' - , + . 'Test.php', $testGenerator->createTestsFor( $exercise->canonicalData(), $this->inPascalCase($exerciseSlug) diff --git a/contribution/generator/src/TrackData/CanonicalData.php b/contribution/generator/src/TrackData/CanonicalData.php index 7a7dcf08..1e970b35 100644 --- a/contribution/generator/src/TrackData/CanonicalData.php +++ b/contribution/generator/src/TrackData/CanonicalData.php @@ -6,7 +6,8 @@ use App\TrackData\CanonicalData\TestCase; -class CanonicalData { +class CanonicalData +{ /** @param TestCase[] $testCases */ public function __construct( public string $exercise, diff --git a/contribution/generator/src/TrackData/CanonicalData/TestCase.php b/contribution/generator/src/TrackData/CanonicalData/TestCase.php index 666c0296..3b29ec40 100644 --- a/contribution/generator/src/TrackData/CanonicalData/TestCase.php +++ b/contribution/generator/src/TrackData/CanonicalData/TestCase.php @@ -4,7 +4,8 @@ namespace App\TrackData\CanonicalData; -class TestCase { +class TestCase +{ public function __construct( public string $uuid, public string $description, diff --git a/contribution/generator/src/TrackData/Exercise.php b/contribution/generator/src/TrackData/Exercise.php index 85227ea8..1c3f6bb1 100644 --- a/contribution/generator/src/TrackData/Exercise.php +++ b/contribution/generator/src/TrackData/Exercise.php @@ -4,7 +4,8 @@ namespace App\TrackData; -interface Exercise { +interface Exercise +{ /** * @param string $trackRoot The absolute location of the track tree * @param string $exerciseSlug The slug of this exercise used in the track tree diff --git a/contribution/generator/src/TrackData/PracticeExercise.php b/contribution/generator/src/TrackData/PracticeExercise.php index 21eab974..e5f32c36 100644 --- a/contribution/generator/src/TrackData/PracticeExercise.php +++ b/contribution/generator/src/TrackData/PracticeExercise.php @@ -9,7 +9,8 @@ use App\TrackData\Exercise; use RuntimeException; -class PracticeExercise implements Exercise { +class PracticeExercise implements Exercise +{ private string $pathToConfiglet = ''; private string $pathToPracticeExercises = ''; private string $pathToExercise = ''; @@ -134,7 +135,7 @@ private function ensurePathToCanonicalDataCanBeUsed(): void throw new RuntimeException( 'Cannot read "configlet" provided cached canonical data from ' . $this->pathToCanonicalData - .'. Check exercise slug or access rights!' + . '. Check exercise slug or access rights!' ); } } From 18295d5e9c303aec13e77b15087256f3bea3c743 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Mon, 26 Feb 2024 09:44:16 +0100 Subject: [PATCH 27/29] Specify array type of $comments --- contribution/generator/src/TrackData/CanonicalData.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contribution/generator/src/TrackData/CanonicalData.php b/contribution/generator/src/TrackData/CanonicalData.php index 1e970b35..cedcee7a 100644 --- a/contribution/generator/src/TrackData/CanonicalData.php +++ b/contribution/generator/src/TrackData/CanonicalData.php @@ -8,7 +8,10 @@ class CanonicalData { - /** @param TestCase[] $testCases */ + /** + * @param TestCase[] $testCases + * @param string[] $comments + */ public function __construct( public string $exercise, public array $testCases = [], From f12ea9f2b9eb70ee6a721bcfbb8037a1fd145372 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Mon, 26 Feb 2024 09:47:42 +0100 Subject: [PATCH 28/29] Explain configlet command in comment --- .../generator/src/TrackData/PracticeExercise.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contribution/generator/src/TrackData/PracticeExercise.php b/contribution/generator/src/TrackData/PracticeExercise.php index e5f32c36..3b2472a8 100644 --- a/contribution/generator/src/TrackData/PracticeExercise.php +++ b/contribution/generator/src/TrackData/PracticeExercise.php @@ -101,6 +101,20 @@ private function ensurePracticeExerciseCanBeUsed(): void private function pathToCachedCanonicalDataFromConfiglet(): void { + /* + When running configlet with detailed output (-v d) and a command that + requires problem specification data (e.g. info), it prints the location + of the cache as the first line. To avoid an HTTP call, use the offline + mode (-o). + + Pipe the output through 'head' to get the first line only, then 'cut' + the 5th field to get the path only. + + configlet may fail when there is no cached data (offline mode), which + tells us, that the exercise hasn't been generated before (the cache is + required for that, too). So BASH must use `-eo pipefail` to get the + failure code back. + */ $command = 'bash -c \'set -eo pipefail; ' . $this->pathToConfiglet . ' -v d -t ' From 51297a43dfaf81b575fdf8c4b1329d3e185b9550 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sun, 3 Mar 2024 11:37:22 +0100 Subject: [PATCH 29/29] Remove unrequired PHPUnit config file The flag `--no-configuration` is explicitly used in 'bin/tests.sh'. Running a test directly for making a new exercise works fine, too. --- phpunit.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 phpunit.xml diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index a8045207..00000000 --- a/phpunit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - ./ - - -