diff --git a/.github/workflows/php-workshop.yml b/.github/workflows/php-workshop.yml index 134c5709..9d319a28 100644 --- a/.github/workflows/php-workshop.yml +++ b/.github/workflows/php-workshop.yml @@ -21,7 +21,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - tools: composer:v2 + tools: php-cs-fixer,composer:v2 coverage: none - name: Install Dependencies diff --git a/.gitignore b/.gitignore index 2fce655d..c865fe62 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ program.php .phpunit.result.cache /build +/vendor-bin/**/vendor/ +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..4687719d --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,13 @@ +in(__DIR__) + ->exclude('test/res') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS2.0' => true, + 'no_unused_imports' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/app/config.php b/app/config.php index ab0aa126..8d246abf 100644 --- a/app/config.php +++ b/app/config.php @@ -38,10 +38,10 @@ use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\ExerciseRenderer; use PhpSchool\PhpWorkshop\ExerciseRepository; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CgiRunnerFactory; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CliRunnerFactory; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CustomVerifyingRunnerFactory; -use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\ServerRunnerFactory; use PhpSchool\PhpWorkshop\ExerciseRunner\RunnerManager; use PhpSchool\PhpWorkshop\Factory\EventDispatcherFactory; use PhpSchool\PhpWorkshop\Factory\MenuFactory; @@ -58,7 +58,6 @@ use PhpSchool\PhpWorkshop\Logger\ConsoleLogger; use PhpSchool\PhpWorkshop\Logger\Logger; use PhpSchool\PhpWorkshop\Markdown\CurrentContext; -use PhpSchool\PhpWorkshop\Markdown\Parser\ContextSpecificBlockParser; use PhpSchool\PhpWorkshop\Markdown\ProblemFileExtension; use PhpSchool\PhpWorkshop\Markdown\Renderer\ContextSpecificRenderer; use PhpSchool\PhpWorkshop\Markdown\Shorthands\Cli\AppName; @@ -71,6 +70,8 @@ use PhpSchool\PhpWorkshop\Output\OutputInterface; use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshop\Patch; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure as CgiGenericFailure; use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure as CgiRequestFailure; @@ -102,6 +103,7 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Filesystem; + use function DI\create; use function DI\factory; use function PhpSchool\PhpWorkshop\canonicalise_path; @@ -132,7 +134,7 @@ $c->get(RunnerManager::class), $c->get(ResultAggregator::class), $c->get(EventDispatcher::class), - $c->get(CheckRepository::class) + $c->get(CheckRepository::class), ); }, ResultAggregator::class => create(ResultAggregator::class), @@ -145,7 +147,7 @@ $c->get(ComposerCheck::class), $c->get(FunctionRequirementsCheck::class), $c->get(DatabaseCheck::class), - $c->get(FileComparisonCheck::class) + $c->get(FileComparisonCheck::class), ]); }, CommandRouter::class => function (ContainerInterface $c) { @@ -156,16 +158,16 @@ new CommandDefinition('print', [], PrintCommand::class), new CommandDefinition('verify', [], VerifyCommand::class), new CommandDefinition('run', [], RunCommand::class), - new CommandDefinition('credits', [], CreditsCommand::class) + new CommandDefinition('credits', [], CreditsCommand::class), ], 'menu', $c->get(EventDispatcher::class), - $c + $c, ); }, Color::class => function () { - $colors = new Color; + $colors = new Color(); $colors->setForceStyle(true); return $colors; }, @@ -177,22 +179,38 @@ return new ExerciseRepository( array_map(function ($exerciseClass) use ($c) { return $c->get($exerciseClass); - }, $c->get('exercises')) + }, $c->get('exercises')), ); }, EventDispatcher::class => factory(EventDispatcherFactory::class), EventDispatcherFactory::class => create(), + EnvironmentManager::class => function (ContainerInterface $c) { + return new EnvironmentManager($c->get(Filesystem::class), $c->get(EventDispatcher::class)); + }, + //Exercise Runners RunnerManager::class => function (ContainerInterface $c) { $manager = new RunnerManager(); - $manager->addFactory(new CliRunnerFactory($c->get(EventDispatcher::class))); - $manager->addFactory(new CgiRunnerFactory($c->get(EventDispatcher::class))); + $manager->addFactory(new CliRunnerFactory( + $c->get(EventDispatcher::class), + $c->get(ProcessFactory::class), + $c->get(EnvironmentManager::class), + )); + $manager->addFactory(new CgiRunnerFactory( + $c->get(EventDispatcher::class), + $c->get(ProcessFactory::class), + $c->get(EnvironmentManager::class), + )); $manager->addFactory(new CustomVerifyingRunnerFactory()); return $manager; }, + ProcessFactory::class => function (ContainerInterface $c) { + return new HostProcessFactory(); + }, + //commands MenuCommand::class => function (ContainerInterface $c) { return new MenuCommand($c->get('menu')); @@ -204,7 +222,7 @@ $c->get(ExerciseRepository::class), $c->get(UserState::class), $c->get(MarkdownRenderer::class), - $c->get(OutputInterface::class) + $c->get(OutputInterface::class), ); }, @@ -215,7 +233,7 @@ $c->get(UserState::class), $c->get(Serializer::class), $c->get(OutputInterface::class), - $c->get(ResultsRenderer::class) + $c->get(ResultsRenderer::class), ); }, @@ -224,7 +242,7 @@ $c->get(ExerciseRepository::class), $c->get(ExerciseDispatcher::class), $c->get(UserState::class), - $c->get(OutputInterface::class) + $c->get(OutputInterface::class), ); }, @@ -233,7 +251,7 @@ $c->get('coreContributors'), $c->get('appContributors'), $c->get(OutputInterface::class), - $c->get(Color::class) + $c->get(Color::class), ); }, @@ -241,7 +259,7 @@ return new HelpCommand( $c->get('appName'), $c->get(OutputInterface::class), - $c->get(Color::class) + $c->get(Color::class), ); }, @@ -249,12 +267,16 @@ InitialCodeListener::class => function (ContainerInterface $c) { return new InitialCodeListener($c->get('currentWorkingDirectory'), $c->get(LoggerInterface::class)); }, - PrepareSolutionListener::class => create(), + PrepareSolutionListener::class => function (ContainerInterface $c) { + return new PrepareSolutionListener( + $c->get(ProcessFactory::class), + ); + }, CodePatchListener::class => function (ContainerInterface $c) { return new CodePatchListener( $c->get(CodePatcher::class), $c->get(LoggerInterface::class), - $c->get('debugMode') + $c->get('debugMode'), ); }, SelfCheckListener::class => function (ContainerInterface $c) { @@ -267,7 +289,7 @@ return new ConfigureCommandListener( $c->get(UserState::class), $c->get(ExerciseRepository::class), - $c->get(RunnerManager::class) + $c->get(RunnerManager::class), ); }, RealPathListener::class => create(), @@ -298,14 +320,14 @@ //Utils Filesystem::class => create(), Parser::class => function () { - $parserFactory = new ParserFactory; + $parserFactory = new ParserFactory(); return $parserFactory->create(ParserFactory::PREFER_PHP7); }, CodePatcher::class => function (ContainerInterface $c) { $patch = (new Patch()) ->withInsertion(new Insertion(Insertion::TYPE_BEFORE, 'ini_set("display_errors", "1");')) ->withInsertion(new Insertion(Insertion::TYPE_BEFORE, 'error_reporting(E_ALL);')) - ->withInsertion(new Insertion(Insertion ::TYPE_BEFORE, 'date_default_timezone_set("Europe/London");')); + ->withInsertion(new Insertion(Insertion::TYPE_BEFORE, 'date_default_timezone_set("Europe/London");')); return new CodePatcher($c->get(Parser::class), new Standard(), $c->get(LoggerInterface::class), $patch); }, @@ -325,7 +347,7 @@ $c->get(Serializer::class), $c->get(MarkdownRenderer::class), $c->get(Color::class), - $c->get(OutputInterface::class) + $c->get(OutputInterface::class), ); }, ContextSpecificRenderer::class => function (ContainerInterface $c) { @@ -342,7 +364,7 @@ $environment = new Environment([ 'renderer' => [ - 'width' => $terminal->getWidth() + 'width' => $terminal->getWidth(), ], ]); @@ -355,8 +377,8 @@ new Documentation(), new Run($c->get('appName')), new Verify($c->get('appName')), - $c->get(Context::class) - ] + $c->get(Context::class), + ], )); return $environment; @@ -364,20 +386,20 @@ MarkdownRenderer::class => function (ContainerInterface $c) { return new MarkdownRenderer( new DocParser($c->get(Environment::class)), - $c->get(ElementRendererInterface::class) + $c->get(ElementRendererInterface::class), ); }, ElementRendererInterface::class => function (ContainerInterface $c) { return new CliRenderer( $c->get(Environment::class), - $c->get(Color::class) + $c->get(Color::class), ); }, Serializer::class => function (ContainerInterface $c) { return new LocalJsonSerializer( getenv('HOME'), $c->get('workshopTitle'), - $c->get(ExerciseRepository::class) + $c->get(ExerciseRepository::class), ); }, UserState::class => function (ContainerInterface $c) { @@ -387,7 +409,7 @@ return new ResetProgress($c->get(Serializer::class)); }, ResultRendererFactory::class => function (ContainerInterface $c) { - $factory = new ResultRendererFactory; + $factory = new ResultRendererFactory(); $factory->registerRenderer(FunctionRequirementsFailure::class, FunctionRequirementsFailureRenderer::class); $factory->registerRenderer(Failure::class, FailureRenderer::class); $factory->registerRenderer( @@ -395,7 +417,7 @@ CgiResultRenderer::class, function (CgiResult $result) use ($c) { return new CgiResultRenderer($result, $c->get(RequestRenderer::class)); - } + }, ); $factory->registerRenderer(CgiGenericFailure::class, FailureRenderer::class); $factory->registerRenderer(CgiRequestFailure::class, CgiRequestFailureRenderer::class); @@ -417,12 +439,12 @@ function (CgiResult $result) use ($c) { $c->get(Terminal::class), $c->get(ExerciseRepository::class), $c->get(KeyLighter::class), - $c->get(ResultRendererFactory::class) + $c->get(ResultRendererFactory::class), ); }, KeyLighter::class => function () { - $keylighter = new KeyLighter; + $keylighter = new KeyLighter(); $keylighter->init(); return $keylighter; }, @@ -431,36 +453,36 @@ function (CgiResult $result) use ($c) { '@AydinHassan' => 'Aydin Hassan', '@mikeymike' => 'Michael Woodward', '@shakeyShane' => 'Shane Osbourne', - '@chris3ailey' => 'Chris Bailey' + '@chris3ailey' => 'Chris Bailey', ], 'appContributors' => [], 'eventListeners' => [ 'realpath-student-submission' => [ 'verify.start' => [ - containerListener(RealPathListener::class) + containerListener(RealPathListener::class), ], 'run.start' => [ - containerListener(RealPathListener::class) + containerListener(RealPathListener::class), ], ], 'check-exercise-assigned' => [ 'route.pre.resolve.args' => [ - containerListener(CheckExerciseAssignedListener::class) + containerListener(CheckExerciseAssignedListener::class), ], ], 'configure-command-arguments' => [ 'route.pre.resolve.args' => [ - containerListener(ConfigureCommandListener::class) + containerListener(ConfigureCommandListener::class), ], ], 'prepare-solution' => [ - 'cli.verify.start' => [ + 'cli.verify.reference-execute.pre' => [ containerListener(PrepareSolutionListener::class), ], 'cli.run.start' => [ containerListener(PrepareSolutionListener::class), ], - 'cgi.verify.start' => [ + 'cgi.verify.reference-execute.pre' => [ containerListener(PrepareSolutionListener::class), ], 'cgi.run.start' => [ @@ -468,7 +490,10 @@ function (CgiResult $result) use ($c) { ], ], 'code-patcher' => [ - 'verify.pre.execute' => [ + 'cli.verify.start' => [ + containerListener(CodePatchListener::class, 'patch'), + ], + 'cgi.verify.start' => [ containerListener(CodePatchListener::class, 'patch'), ], 'verify.post.execute' => [ @@ -486,26 +511,26 @@ function (CgiResult $result) use ($c) { ], 'self-check' => [ 'verify.post.check' => [ - containerListener(SelfCheckListener::class) + containerListener(SelfCheckListener::class), ], ], 'create-initial-code' => [ 'exercise.selected' => [ - containerListener(InitialCodeListener::class) - ] + containerListener(InitialCodeListener::class), + ], ], 'cleanup-filesystem' => [ 'application.tear-down' => [ - containerListener(TearDownListener::class, 'cleanupTempDir') - ] + containerListener(TearDownListener::class, 'cleanupTempDir'), + ], ], 'decorate-run-output' => [ 'cli.run.student-execute.pre' => [ - containerListener(OutputRunInfoListener::class) + containerListener(OutputRunInfoListener::class), ], 'cgi.run.student-execute.pre' => [ - containerListener(OutputRunInfoListener::class) - ] + containerListener(OutputRunInfoListener::class), + ], ], ], ]; diff --git a/composer.json b/composer.json index 1b092da9..f098d5e3 100644 --- a/composer.json +++ b/composer.json @@ -36,10 +36,10 @@ "require-dev": { "phpunit/phpunit": "^8.5", "composer/composer": "^2.0", - "squizlabs/php_codesniffer": "^3.7", "phpstan/phpstan": "^1.8", "phpstan/extension-installer": "^1.0", - "yoast/phpunit-polyfills": "^0.2.0" + "yoast/phpunit-polyfills": "^0.2.0", + "bamarni/composer-bin-plugin": "^1.8" }, "autoload" : { "psr-4" : { @@ -62,19 +62,26 @@ "@static" ], "unit-tests": "phpunit", - "cs" : [ - "phpcs src --standard=PSR12 --encoding=UTF-8 --exclude=Generic.Files.LineLength", - "phpcs test --standard=PSR12 --encoding=UTF-8 --exclude=Generic.Files.LineLength" - ], - "cs-fix" : [ - "phpcbf src --standard=PSR12 --encoding=UTF-8", - "phpcbf test --standard=PSR12 --encoding=UTF-8" - ], + "cs" : "@cs-fix --dry-run", + "cs-fix" : "php-cs-fixer fix", "static": "phpstan --ansi analyse --level max src" }, + "post-install-cmd": [ + "@composer bin all install --ansi" + ], + "post-update-cmd": [ + "@composer bin all update --ansi" + ], "config": { "allow-plugins": { - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false } } } diff --git a/composer.lock b/composer.lock index 3c3e867f..e96b1156 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7ea6de6abfae6d45fbabc34230da77bc", + "content-hash": "5c5d8be82389e614973f05624b5555dd", "packages": [ { "name": "beberlei/assert", @@ -734,21 +734,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", @@ -784,9 +784,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "php-di/invoker", @@ -1283,20 +1283,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -1320,7 +1320,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -1332,9 +1332,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -1485,16 +1485,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -1503,7 +1503,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1532,7 +1532,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -1548,26 +1548,27 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.3", + "version": "v6.4.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + "reference": "78dde75f8f6dbbca4ec436a4b0087f7af02076d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/78dde75f8f6dbbca4ec436a4b0087f7af02076d4", + "reference": "78dde75f8f6dbbca4ec436a4b0087f7af02076d4", "shasum": "" }, "require": { "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "symfony/polyfill-mbstring": "~1.8", + "symfony/process": "^5.4|^6.4" }, "type": "library", "autoload": { @@ -1595,7 +1596,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + "source": "https://github.com/symfony/filesystem/tree/v6.4.7" }, "funding": [ { @@ -1611,7 +1612,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:22:46+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1774,16 +1775,16 @@ }, { "name": "symfony/process", - "version": "v6.4.4", + "version": "v6.4.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "710e27879e9be3395de2b98da3f52a946039f297" + "reference": "cdb1c81c145fd5aa9b0038bab694035020943381" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", - "reference": "710e27879e9be3395de2b98da3f52a946039f297", + "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381", + "reference": "cdb1c81c145fd5aa9b0038bab694035020943381", "shasum": "" }, "require": { @@ -1815,7 +1816,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.4" + "source": "https://github.com/symfony/process/tree/v6.4.7" }, "funding": [ { @@ -1831,34 +1832,91 @@ "type": "tidelift" } ], - "time": "2024-02-20T12:31:00+00:00" + "time": "2024-04-18T09:22:46+00:00" } ], "packages-dev": [ + { + "name": "bamarni/composer-bin-plugin", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/bamarni/composer-bin-plugin.git", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "ext-json": "*", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" + }, + "autoload": { + "psr-4": { + "Bamarni\\Composer\\Bin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "No conflicts for your bin dependencies", + "keywords": [ + "composer", + "conflict", + "dependency", + "executable", + "isolation", + "tool" + ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" + }, + "time": "2022-10-31T08:38:03+00:00" + }, { "name": "composer/ca-bundle", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -1893,7 +1951,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" }, "funding": [ { @@ -1909,20 +1967,20 @@ "type": "tidelift" } ], - "time": "2024-02-23T10:16:52+00:00" + "time": "2024-03-15T14:00:32+00:00" }, { "name": "composer/class-map-generator", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", "shasum": "" }, "require": { @@ -1966,7 +2024,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" }, "funding": [ { @@ -1982,20 +2040,20 @@ "type": "tidelift" } ], - "time": "2023-06-30T13:58:57+00:00" + "time": "2024-03-15T12:53:41+00:00" }, { "name": "composer/composer", - "version": "2.7.1", + "version": "2.7.6", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" + "reference": "fabd995783b633829fd4280e272284b39b6ae702" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "url": "https://api.github.com/repos/composer/composer/zipball/fabd995783b633829fd4280e272284b39b6ae702", + "reference": "fabd995783b633829fd4280e272284b39b6ae702", "shasum": "" }, "require": { @@ -2080,7 +2138,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.1" + "source": "https://github.com/composer/composer/tree/2.7.6" }, "funding": [ { @@ -2096,7 +2154,7 @@ "type": "tidelift" } ], - "time": "2024-02-09T14:26:28+00:00" + "time": "2024-05-04T21:03:15+00:00" }, { "name": "composer/metadata-minifier", @@ -2169,16 +2227,16 @@ }, { "name": "composer/pcre", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { @@ -2220,7 +2278,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.2" + "source": "https://github.com/composer/pcre/tree/3.1.3" }, "funding": [ { @@ -2236,7 +2294,7 @@ "type": "tidelift" } ], - "time": "2024-03-07T15:38:35+00:00" + "time": "2024-03-19T10:26:25+00:00" }, { "name": "composer/semver", @@ -2401,16 +2459,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -2421,7 +2479,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -2445,9 +2503,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -2463,7 +2521,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "doctrine/instantiator", @@ -2828,16 +2886,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.60", + "version": "1.10.67", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe" + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", "shasum": "" }, "require": { @@ -2880,13 +2938,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-03-07T13:30:19+00:00" + "time": "2024-04-16T07:22:02+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3187,16 +3241,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.37", + "version": "8.5.38", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fce30f306cee78be33ba00c8f9a853f41db0491b" + "reference": "1ecad678646c817a29e55a32c930f3601c3f5a8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fce30f306cee78be33ba00c8f9a853f41db0491b", - "reference": "fce30f306cee78be33ba00c8f9a853f41db0491b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1ecad678646c817a29e55a32c930f3601c3f5a8c", + "reference": "1ecad678646c817a29e55a32c930f3601c3f5a8c", "shasum": "" }, "require": { @@ -3265,7 +3319,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.37" + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.38" }, "funding": [ { @@ -3281,7 +3335,7 @@ "type": "tidelift" } ], - "time": "2024-03-06T06:27:42+00:00" + "time": "2024-04-05T04:31:23+00:00" }, { "name": "react/promise", @@ -4257,98 +4311,18 @@ }, "time": "2023-09-03T09:24:00+00:00" }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.9.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" - }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - } - ], - "time": "2024-02-16T15:06:51+00:00" - }, { "name": "symfony/console", - "version": "v7.0.4", + "version": "v7.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f" + "reference": "c981e0e9380ce9f146416bde3150c79197ce9986" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f", - "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f", + "url": "https://api.github.com/repos/symfony/console/zipball/c981e0e9380ce9f146416bde3150c79197ce9986", + "reference": "c981e0e9380ce9f146416bde3150c79197ce9986", "shasum": "" }, "require": { @@ -4412,7 +4386,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.0.4" + "source": "https://github.com/symfony/console/tree/v7.0.7" }, "funding": [ { @@ -4428,20 +4402,20 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:20+00:00" + "time": "2024-04-18T09:29:19+00:00" }, { "name": "symfony/finder", - "version": "v7.0.0", + "version": "v7.0.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56" + "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "url": "https://api.github.com/repos/symfony/finder/zipball/4d58f0f4fe95a30d7b538d71197135483560b97c", + "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c", "shasum": "" }, "require": { @@ -4476,7 +4450,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.0.0" + "source": "https://github.com/symfony/finder/tree/v7.0.7" }, "funding": [ { @@ -4492,7 +4466,7 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:59:56+00:00" + "time": "2024-04-28T11:44:19+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -4887,21 +4861,22 @@ }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -4909,7 +4884,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -4949,7 +4924,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -4965,20 +4940,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "v7.0.4", + "version": "v7.0.7", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63", "shasum": "" }, "require": { @@ -5035,7 +5010,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.4" + "source": "https://github.com/symfony/string/tree/v7.0.7" }, "funding": [ { @@ -5051,7 +5026,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:17:36+00:00" + "time": "2024-04-18T09:29:19+00:00" }, { "name": "theseer/tokenizer", diff --git a/phpstan.neon b/phpstan.neon index 71ba3712..2849b8d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,6 @@ parameters: treatPhpDocTypesAsCertain: false ignoreErrors: - - - message: '#PhpSchool\\PhpWorkshop\\ExerciseRunner\\CliRunner\:\:preserveOldArgFormat\(\) should return#' - path: src/ExerciseRunner/CliRunner.php - message: '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:__toString\(\)#' path: src/Check/FunctionRequirementsCheck.php diff --git a/src/Application.php b/src/Application.php index 8075c0cc..a2af0c00 100644 --- a/src/Application.php +++ b/src/Application.php @@ -133,7 +133,7 @@ public function addResult(string $resultClass, string $resultRendererClass): voi $this->results[] = [ 'resultClass' => $resultClass, - 'resultRendererClass' => $resultRendererClass + 'resultRendererClass' => $resultRendererClass, ]; } @@ -181,7 +181,7 @@ public function configure(bool $debugMode = false): ContainerInterface foreach ($this->exercises as $exercise) { if (false === $container->has($exercise)) { throw new RuntimeException( - sprintf('No DI config found for exercise: "%s". Register a factory.', $exercise) + sprintf('No DI config found for exercise: "%s". Register a factory.', $exercise), ); } } @@ -191,7 +191,7 @@ public function configure(bool $debugMode = false): ContainerInterface foreach ($this->checks as $check) { if (false === $container->has($check)) { throw new RuntimeException( - sprintf('No DI config found for check: "%s". Register a factory.', $check) + sprintf('No DI config found for check: "%s". Register a factory.', $check), ); } @@ -202,8 +202,8 @@ public function configure(bool $debugMode = false): ContainerInterface sprintf( 'Check: "%s" does not implement the required interface: "%s".', $check, - CheckInterface::class - ) + CheckInterface::class, + ), ); } @@ -265,8 +265,8 @@ public function run(): int 'Argument%s: "%s" %s missing!', count($e->getMissingArguments()) > 1 ? 's' : '', implode('", "', $e->getMissingArguments()), - count($e->getMissingArguments()) > 1 ? 'are' : 'is' - ) + count($e->getMissingArguments()) > 1 ? 'are' : 'is', + ), ); return 1; } catch (\Throwable $e) { @@ -303,7 +303,7 @@ private function getContainer(bool $debugMode): Container $fwConfig['eventListeners'] = array_merge_recursive( $fwConfig['eventListeners'], - $diConfig['eventListeners'] ?? [] + $diConfig['eventListeners'] ?? [], ); unset($diConfig['eventListeners']); @@ -318,7 +318,7 @@ private function getContainer(bool $debugMode): Container 'workshopLogo' => $this->logo, 'bgColour' => $this->bgColour, 'fgColour' => $this->fgColour, - ] + ], ); $containerBuilder->useAutowiring(false); diff --git a/src/Check/CodeExistsCheck.php b/src/Check/CodeExistsCheck.php index 859e3d29..a3b16621 100644 --- a/src/Check/CodeExistsCheck.php +++ b/src/Check/CodeExistsCheck.php @@ -7,10 +7,10 @@ use PhpParser\Error; use PhpParser\ErrorHandler; use PhpParser\Node\Stmt\InlineHTML; -use PhpParser\NodeFinder; use PhpParser\Parser; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -18,15 +18,7 @@ class CodeExistsCheck implements SimpleCheckInterface { - /** - * @var Parser - */ - private $parser; - - public function __construct(Parser $parser) - { - $this->parser = $parser; - } + public function __construct(private Parser $parser) {} public function getName(): string { @@ -34,18 +26,18 @@ public function getName(): string } /** + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. + * * Check solution provided contains code * Note: We don't care if it's valid code at this point */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { - $noopHandler = new class implements ErrorHandler { - public function handleError(Error $error): void - { - } + $noopHandler = new class () implements ErrorHandler { + public function handleError(Error $error): void {} }; - $code = (string) file_get_contents($input->getRequiredArgument('program')); + $code = (string) file_get_contents($context->getEntryPoint()); $statements = $this->parser->parse($code, $noopHandler); $empty = null === $statements || empty($statements); diff --git a/src/Check/CodeParseCheck.php b/src/Check/CodeParseCheck.php index 670ec1b1..cdf56fbd 100644 --- a/src/Check/CodeParseCheck.php +++ b/src/Check/CodeParseCheck.php @@ -8,6 +8,7 @@ use PhpParser\Parser; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -19,18 +20,7 @@ */ class CodeParseCheck implements SimpleCheckInterface { - /** - * @var Parser - */ - private $parser; - - /** - * @param Parser $parser - */ - public function __construct(Parser $parser) - { - $this->parser = $parser; - } + public function __construct(private Parser $parser) {} /** * Return the check's name @@ -45,18 +35,17 @@ public function getName(): string * attempts to parse it with `nikic/php-parser`. If any exceptions are thrown * by the parser, it is treated as a failure. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { - $code = (string) file_get_contents($input->getRequiredArgument('program')); + $code = (string) file_get_contents($context->getEntryPoint()); try { $this->parser->parse($code); } catch (Error $e) { - return Failure::fromCheckAndCodeParseFailure($this, $e, $input->getRequiredArgument('program')); + return Failure::fromCheckAndCodeParseFailure($this, $e, $context->getEntryPoint()); } return Success::fromCheck($this); diff --git a/src/Check/ComposerCheck.php b/src/Check/ComposerCheck.php index 6711f1b2..c3088861 100644 --- a/src/Check/ComposerCheck.php +++ b/src/Check/ComposerCheck.php @@ -6,9 +6,9 @@ use InvalidArgumentException; use PhpSchool\PhpWorkshop\ComposerUtil\LockFileParser; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\ComposerExerciseCheck; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\ComposerFailure; use PhpSchool\PhpWorkshop\Result\Failure; @@ -34,30 +34,29 @@ public function getName(): string * installed a set of required packages. If they did not a failure is returned, otherwise, * a success is returned. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. - * @noinspection SpellCheckingInspection */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { + $exercise = $context->getExercise(); if (!$exercise instanceof ComposerExerciseCheck) { throw new InvalidArgumentException(); } - if (!file_exists(sprintf('%s/composer.json', dirname($input->getRequiredArgument('program'))))) { + if (!file_exists(sprintf('%s/composer.json', $context->getStudentExecutionDirectory()))) { return ComposerFailure::fromCheckAndMissingFileOrFolder($this, 'composer.json'); } - if (!file_exists(sprintf('%s/composer.lock', dirname($input->getRequiredArgument('program'))))) { + if (!file_exists(sprintf('%s/composer.lock', $context->getStudentExecutionDirectory()))) { return ComposerFailure::fromCheckAndMissingFileOrFolder($this, 'composer.lock'); } - if (!file_exists(sprintf('%s/vendor', dirname($input->getRequiredArgument('program'))))) { + if (!file_exists(sprintf('%s/vendor', $context->getStudentExecutionDirectory()))) { return ComposerFailure::fromCheckAndMissingFileOrFolder($this, 'vendor'); } - $lockFile = new LockFileParser(sprintf('%s/composer.lock', dirname($input->getRequiredArgument('program')))); + $lockFile = new LockFileParser(sprintf('%s/composer.lock', $context->getStudentExecutionDirectory())); $missingPackages = array_filter($exercise->getRequiredPackages(), function ($package) use ($lockFile) { return !$lockFile->hasInstalledPackage($package); }); diff --git a/src/Check/DatabaseCheck.php b/src/Check/DatabaseCheck.php index 0a55f4ec..6d9545fe 100644 --- a/src/Check/DatabaseCheck.php +++ b/src/Check/DatabaseCheck.php @@ -25,30 +25,11 @@ class DatabaseCheck implements ListenableCheckInterface { use TemporaryDirectoryTrait; - /** - * @var string - */ - private $databaseDirectory; - - /** - * @var string - */ - private $userDatabasePath; - - /** - * @var string - */ - private $solutionDatabasePath; - - /** - * @var string - */ - private $userDsn; - - /** - * @var string - */ - private $solutionDsn; + private string $databaseDirectory; + private string $userDatabasePath; + private string $solutionDatabasePath; + private string $userDsn; + private string $solutionDsn; /** * Setup paths and DSN's. @@ -85,7 +66,7 @@ public function attach(EventDispatcher $eventDispatcher): void { if (file_exists($this->databaseDirectory)) { throw new RuntimeException( - sprintf('Database directory: "%s" already exists', $this->databaseDirectory) + sprintf('Database directory: "%s" already exists', $this->databaseDirectory), ); } @@ -121,7 +102,7 @@ public function attach(EventDispatcher $eventDispatcher): void ['cli.verify.student-execute.pre', 'cli.run.student-execute.pre'], function (CliExecuteEvent $e) { $e->prependArg($this->userDsn); - } + }, ); $eventDispatcher->insertVerifier('verify.finish', function (Event $e) use ($db) { @@ -140,14 +121,14 @@ function (CliExecuteEvent $e) { [ 'cli.verify.reference-execute.fail', 'verify.finish', - 'run.finish' + 'run.finish', ], function () use ($db) { unset($db); $this->unlink($this->userDatabasePath); $this->unlink($this->solutionDatabasePath); rmdir($this->databaseDirectory); - } + }, ); } diff --git a/src/Check/FileComparisonCheck.php b/src/Check/FileComparisonCheck.php index 3acefd68..4f01dd08 100644 --- a/src/Check/FileComparisonCheck.php +++ b/src/Check/FileComparisonCheck.php @@ -6,10 +6,9 @@ use InvalidArgumentException; use PhpSchool\PhpWorkshop\Exception\SolutionFileDoesNotExistException; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\ExerciseCheck\FileComparisonExerciseCheck; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\FileComparisonFailure; @@ -34,12 +33,12 @@ public function getName(): string /** * Simply check that the file exists. * - * @param ExerciseInterface&ProvidesSolution $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { + $exercise = $context->getExercise(); if (!$exercise instanceof FileComparisonExerciseCheck) { throw new InvalidArgumentException(); } @@ -47,8 +46,8 @@ public function check(ExerciseInterface $exercise, Input $input): ResultInterfac foreach ($exercise->getFilesToCompare() as $key => $file) { [$options, $file] = $this->getOptionsAndFile($key, $file); - $studentFile = Path::join(dirname($input->getRequiredArgument('program')), $file); - $referenceFile = Path::join($exercise->getSolution()->getBaseDirectory(), $file); + $studentFile = Path::join($context->getStudentExecutionDirectory(), $file); + $referenceFile = Path::join($context->getReferenceExecutionDirectory(), $file); if (!file_exists($referenceFile)) { throw SolutionFileDoesNotExistException::fromExpectedFile($file); diff --git a/src/Check/FileExistsCheck.php b/src/Check/FileExistsCheck.php index 5dba56e8..45a7ce05 100644 --- a/src/Check/FileExistsCheck.php +++ b/src/Check/FileExistsCheck.php @@ -6,6 +6,7 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -27,19 +28,18 @@ public function getName(): string /** * Simply check that the file exists. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { - if (file_exists($input->getRequiredArgument('program'))) { + if (file_exists($context->getEntryPoint())) { return Success::fromCheck($this); } return Failure::fromCheckAndReason( $this, - sprintf('File: "%s" does not exist', $input->getRequiredArgument('program')) + sprintf('File: "%s" does not exist', $context->getEntryPoint()), ); } diff --git a/src/Check/FunctionRequirementsCheck.php b/src/Check/FunctionRequirementsCheck.php index 4c14f851..ce37bcd2 100644 --- a/src/Check/FunctionRequirementsCheck.php +++ b/src/Check/FunctionRequirementsCheck.php @@ -9,9 +9,9 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\NodeTraverser; use PhpParser\Parser; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\NodeVisitor\FunctionVisitor; use PhpSchool\PhpWorkshop\Result\Failure; @@ -25,18 +25,7 @@ */ class FunctionRequirementsCheck implements SimpleCheckInterface { - /** - * @var Parser - */ - private $parser; - - /** - * @param Parser $parser - */ - public function __construct(Parser $parser) - { - $this->parser = $parser; - } + public function __construct(private Parser $parser) {} /** * Return the check's name. @@ -51,12 +40,12 @@ public function getName(): string * required functions and that banned functions are not used. The requirements * are pulled from the exercise. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { + $exercise = $context->getExercise(); if (!$exercise instanceof FunctionRequirementsExerciseCheck) { throw new InvalidArgumentException(); } @@ -64,12 +53,12 @@ public function check(ExerciseInterface $exercise, Input $input): ResultInterfac $requiredFunctions = $exercise->getRequiredFunctions(); $bannedFunctions = $exercise->getBannedFunctions(); - $code = (string) file_get_contents($input->getRequiredArgument('program')); + $code = (string) file_get_contents($context->getEntryPoint()); try { $ast = $this->parser->parse($code) ?? []; } catch (Error $e) { - return Failure::fromCheckAndCodeParseFailure($this, $e, $input->getRequiredArgument('program')); + return Failure::fromCheckAndCodeParseFailure($this, $e, $context->getEntryPoint()); } $visitor = new FunctionVisitor($requiredFunctions, $bannedFunctions); diff --git a/src/Check/PhpLintCheck.php b/src/Check/PhpLintCheck.php index c90c3fbd..f05e427d 100644 --- a/src/Check/PhpLintCheck.php +++ b/src/Check/PhpLintCheck.php @@ -6,6 +6,7 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\ResultInterface; use PhpSchool\PhpWorkshop\Result\Success; @@ -30,14 +31,13 @@ public function getName(): string /** * Simply check the student's solution can be linted with `php -l`. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface + public function check(ExecutionContext $context): ResultInterface { $finder = new ExecutableFinder(); - $process = new Process([$finder->find('php'), '-l', $input->getArgument('program')]); + $process = new Process([$finder->find('php'), '-l', $context->getEntryPoint()]); $process->run(); if ($process->isSuccessful()) { diff --git a/src/Check/SimpleCheckInterface.php b/src/Check/SimpleCheckInterface.php index 5b4bf78a..f977bf71 100644 --- a/src/Check/SimpleCheckInterface.php +++ b/src/Check/SimpleCheckInterface.php @@ -4,8 +4,8 @@ namespace PhpSchool\PhpWorkshop\Check; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -46,11 +46,10 @@ public function canRun(ExerciseType $exerciseType): bool; * successful then an instance of `PhpSchool\PhpWorkshop\Result\FailureInterface` * should be returned. * - * @param ExerciseInterface $exercise The exercise to check against. - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function check(ExerciseInterface $exercise, Input $input): ResultInterface; + public function check(ExecutionContext $context): ResultInterface; /** * Either `static::CHECK_BEFORE` | `static::CHECK_AFTER`. diff --git a/src/CodeInsertion.php b/src/CodeInsertion.php index 981fa79b..9d1d2569 100644 --- a/src/CodeInsertion.php +++ b/src/CodeInsertion.php @@ -53,7 +53,7 @@ public function __construct(string $type, string $code) { if (!in_array($type, $this->types, true)) { throw new InvalidArgumentException( - sprintf('Value "%s" is not an element of the valid values: %s', $type, implode(', ', $this->types)) + sprintf('Value "%s" is not an element of the valid values: %s', $type, implode(', ', $this->types)), ); } diff --git a/src/CodePatcher.php b/src/CodePatcher.php index df4249ce..6d388235 100644 --- a/src/CodePatcher.php +++ b/src/CodePatcher.php @@ -151,7 +151,7 @@ private function applyCodeInsertion(CodeInsertion $codeInsertion, array $stateme } catch (Error $e) { $this->logger->critical( 'Code Insertion could not be parsed: ' . $e->getMessage(), - ['code' => $codeInsertion->getCode()] + ['code' => $codeInsertion->getCode()], ); return $statements; } diff --git a/src/Command/CreditsCommand.php b/src/Command/CreditsCommand.php index 6a7f7f42..7a274cc6 100644 --- a/src/Command/CreditsCommand.php +++ b/src/Command/CreditsCommand.php @@ -75,7 +75,7 @@ public function __invoke(): void } $this->output->writeLine( - $this->color->__invoke("PHP School is bought to you by...")->yellow()->__toString() + $this->color->__invoke("PHP School is bought to you by...")->yellow()->__toString(), ); $this->output->emptyLine(); $this->writeContributors($this->coreContributors); @@ -88,7 +88,7 @@ public function __invoke(): void $this->output->emptyLine(); $this->output->writeLine( - $this->color->__invoke("This workshop is brought to you by...")->yellow()->__toString() + $this->color->__invoke("This workshop is brought to you by...")->yellow()->__toString(), ); $this->output->writeLine(""); $this->writeContributors($this->appContributors); diff --git a/src/Command/HelpCommand.php b/src/Command/HelpCommand.php index bf402c0b..fd4c26aa 100644 --- a/src/Command/HelpCommand.php +++ b/src/Command/HelpCommand.php @@ -58,7 +58,7 @@ public function __invoke(): void $this->output->writeLine(" Show the people who made this happen."); $this->output->writeLine(""); $this->output->writeLine( - (string) $this->color->__invoke('Having trouble with a PHPSchool exercise?')->yellow()->bold() + (string) $this->color->__invoke('Having trouble with a PHPSchool exercise?')->yellow()->bold(), ); $this->output->writeLine(""); $this->output->writeLine(" A team of expert helper elves is eagerly waiting to assist you in"); @@ -73,7 +73,7 @@ public function __invoke(): void $this->output->writeLine(" https://chat.stackoverflow.com/rooms/11/php"); $this->output->writeLine(""); $this->output->writeLine( - (string) $this->color->__invoke('Found a bug with PHPSchool or just want to contribute?')->yellow()->bold() + (string) $this->color->__invoke('Found a bug with PHPSchool or just want to contribute?')->yellow()->bold(), ); $this->output->writeLine(" The official repository for PHPSchool is:"); $this->output->writeLine(" https://github.com/php-school/php-workshop"); diff --git a/src/Command/PrintCommand.php b/src/Command/PrintCommand.php index ebf27c45..fba683cb 100644 --- a/src/Command/PrintCommand.php +++ b/src/Command/PrintCommand.php @@ -4,7 +4,6 @@ namespace PhpSchool\PhpWorkshop\Command; -use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Exception\ProblemFileDoesNotExistException; use PhpSchool\PhpWorkshop\ExerciseRepository; use PhpSchool\PhpWorkshop\MarkdownRenderer; @@ -53,7 +52,7 @@ public function __construct( ExerciseRepository $exerciseRepository, UserState $userState, MarkdownRenderer $markdownRenderer, - OutputInterface $output + OutputInterface $output, ) { $this->appName = $appName; $this->markdownRenderer = $markdownRenderer; diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 6de175eb..b343229d 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -4,12 +4,10 @@ namespace PhpSchool\PhpWorkshop\Command; -use PhpSchool\PhpWorkshop\Check\FileExistsCheck; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\ExerciseRepository; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; -use PhpSchool\PhpWorkshop\Result\Success; use PhpSchool\PhpWorkshop\UserState\UserState; /** @@ -47,7 +45,7 @@ public function __construct( ExerciseRepository $exerciseRepository, ExerciseDispatcher $exerciseDispatcher, UserState $userState, - OutputInterface $output + OutputInterface $output, ) { $this->output = $output; $this->exerciseRepository = $exerciseRepository; diff --git a/src/Command/VerifyCommand.php b/src/Command/VerifyCommand.php index 51f37323..c8349756 100644 --- a/src/Command/VerifyCommand.php +++ b/src/Command/VerifyCommand.php @@ -61,7 +61,7 @@ public function __construct( UserState $userState, Serializer $userStateSerializer, OutputInterface $output, - ResultsRenderer $resultsRenderer + ResultsRenderer $resultsRenderer, ) { $this->output = $output; $this->exerciseRepository = $exerciseRepository; diff --git a/src/CommandDefinition.php b/src/CommandDefinition.php index e8996aaf..a32e77ab 100644 --- a/src/CommandDefinition.php +++ b/src/CommandDefinition.php @@ -53,7 +53,7 @@ public function addArgument($argument): self throw InvalidArgumentException::notValidParameter( 'argument', ['string', CommandArgument::class], - $argument + $argument, ); } @@ -69,7 +69,7 @@ public function addArgument($argument): self $previousArgument = end($this->args); if ($previousArgument->isOptional() && $argument->isRequired()) { throw new InvalidArgumentException(sprintf( - 'A required argument cannot follow an optional argument' + 'A required argument cannot follow an optional argument', )); } diff --git a/src/CommandRouter.php b/src/CommandRouter.php index 9043282c..605a6439 100644 --- a/src/CommandRouter.php +++ b/src/CommandRouter.php @@ -55,7 +55,7 @@ public function __construct( array $commands, string $default, EventDispatcher $eventDispatcher, - ContainerInterface $container + ContainerInterface $container, ) { foreach ($commands as $command) { $this->addCommand($command); diff --git a/src/Event/CgiExecuteEvent.php b/src/Event/CgiExecuteEvent.php index e72551b6..a7e5cf28 100644 --- a/src/Event/CgiExecuteEvent.php +++ b/src/Event/CgiExecuteEvent.php @@ -4,28 +4,32 @@ namespace PhpSchool\PhpWorkshop\Event; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use Psr\Http\Message\RequestInterface; /** * An event to represent events which occur throughout the verification and running process in * `\PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner`. */ -class CgiExecuteEvent extends Event +class CgiExecuteEvent extends CgiExerciseRunnerEvent { - /** - * @var RequestInterface - */ - private $request; + private RequestInterface $request; /** * @param string $name The event name. * @param RequestInterface $request The request that will be performed. - * @param array $parameters The event parameters. + * @param array $parameters The event parameters. */ - public function __construct(string $name, RequestInterface $request, array $parameters = []) - { + public function __construct( + string $name, + ExecutionContext $context, + CgiScenario $scenario, + RequestInterface $request, + array $parameters = [], + ) { $parameters['request'] = $request; - parent::__construct($name, $parameters); + parent::__construct($name, $context, $scenario, $parameters); $this->request = $request; } diff --git a/src/Event/CgiExerciseRunnerEvent.php b/src/Event/CgiExerciseRunnerEvent.php new file mode 100644 index 00000000..dc4925b0 --- /dev/null +++ b/src/Event/CgiExerciseRunnerEvent.php @@ -0,0 +1,29 @@ + $parameters + */ + public function __construct( + string $name, + ExecutionContext $context, + CgiScenario $scenario, + array $parameters = [], + ) { + $this->scenario = $scenario; + parent::__construct($name, $context, $parameters); + } + + public function getScenario(): CgiScenario + { + return $this->scenario; + } +} diff --git a/src/Event/CliExecuteEvent.php b/src/Event/CliExecuteEvent.php index 90fcf347..b9ea0277 100644 --- a/src/Event/CliExecuteEvent.php +++ b/src/Event/CliExecuteEvent.php @@ -4,28 +4,35 @@ namespace PhpSchool\PhpWorkshop\Event; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; +use PhpSchool\PhpWorkshop\Utils\Collection; /** * An event to represent events which occur throughout the verification and running process in * `\PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner`. */ -class CliExecuteEvent extends Event +class CliExecuteEvent extends CliExerciseRunnerEvent { /** - * @var ArrayObject + * @var Collection */ - private $args; + private Collection $args; /** * @param string $name The event name. - * @param ArrayObject $args The arguments that should be/have been passed to the program. - * @param array $parameters The event parameters. + * @param Collection $args The arguments that should be/have been passed to the program. + * @param array $parameters The event parameters. */ - public function __construct(string $name, ArrayObject $args, array $parameters = []) - { + public function __construct( + string $name, + ExecutionContext $context, + CliScenario $scenario, + Collection $args, + array $parameters = [], + ) { $parameters['args'] = $args; - parent::__construct($name, $parameters); + parent::__construct($name, $context, $scenario, $parameters); $this->args = $args; } @@ -52,9 +59,9 @@ public function appendArg(string $arg): void /** * Get the arguments to be passed to the program. * - * @return ArrayObject + * @return Collection */ - public function getArgs(): ArrayObject + public function getArgs(): Collection { return $this->args; } diff --git a/src/Event/CliExerciseRunnerEvent.php b/src/Event/CliExerciseRunnerEvent.php new file mode 100644 index 00000000..a2a46be7 --- /dev/null +++ b/src/Event/CliExerciseRunnerEvent.php @@ -0,0 +1,29 @@ + $parameters + */ + public function __construct( + string $name, + ExecutionContext $context, + CliScenario $scenario, + array $parameters = [], + ) { + $this->scenario = $scenario; + parent::__construct($name, $context, $parameters); + } + + public function getScenario(): CliScenario + { + return $this->scenario; + } +} diff --git a/src/Event/Event.php b/src/Event/Event.php index f01aa4aa..02ef9209 100644 --- a/src/Event/Event.php +++ b/src/Event/Event.php @@ -11,15 +11,12 @@ */ class Event implements EventInterface { - /** - * @var string - */ - private $name; + private string $name; /** * @var array */ - protected $parameters; + protected array $parameters; /** * @param string $name The event name. @@ -52,13 +49,13 @@ public function getParameters(): array } /** - * Get a parameter by it's name. + * Get a parameter by its name. * * @param string $name The name of the parameter. * @return mixed The value. * @throws InvalidArgumentException If the parameter by name does not exist. */ - public function getParameter(string $name) + public function getParameter(string $name): mixed { if (!array_key_exists($name, $this->parameters)) { throw new InvalidArgumentException(sprintf('Parameter: "%s" does not exist', $name)); diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php index ecc7f2ef..25a00ac2 100644 --- a/src/Event/EventDispatcher.php +++ b/src/Event/EventDispatcher.php @@ -16,16 +16,9 @@ class EventDispatcher /** * @var array> */ - private $listeners = []; + private array $listeners = []; + private ResultAggregator $resultAggregator; - /** - * @var ResultAggregator - */ - private $resultAggregator; - - /** - * @param ResultAggregator $resultAggregator - */ public function __construct(ResultAggregator $resultAggregator) { $this->resultAggregator = $resultAggregator; @@ -33,9 +26,6 @@ public function __construct(ResultAggregator $resultAggregator) /** * Dispatch an event. Can be any event object which implements `PhpSchool\PhpWorkshop\Event\EventInterface`. - * - * @param EventInterface $event - * @return EventInterface */ public function dispatch(EventInterface $event): EventInterface { @@ -103,9 +93,6 @@ public function removeListener(string $eventName, callable $callback): void * Insert a verifier callback which will execute at the given event name much like normal listeners. * A verifier should return an object which implements `PhpSchool\PhpWorkshop\Result\FailureInterface` * or `PhpSchool\PhpWorkshop\Result\SuccessInterface`. This result object will be added to the result aggregator. - * - * @param string $eventName - * @param callable $verifier */ public function insertVerifier(string $eventName, callable $verifier): void { diff --git a/src/Event/EventInterface.php b/src/Event/EventInterface.php index e586d93d..a5b7a0c5 100644 --- a/src/Event/EventInterface.php +++ b/src/Event/EventInterface.php @@ -26,11 +26,11 @@ public function getName(): string; public function getParameters(): array; /** - * Get a parameter by it's name. + * Get a parameter by its name. * * @param string $name The name of the parameter. * @return mixed The value. * @throws InvalidArgumentException If the parameter by name does not exist. */ - public function getParameter(string $name); + public function getParameter(string $name): mixed; } diff --git a/src/Event/ExerciseRunnerEvent.php b/src/Event/ExerciseRunnerEvent.php index 0dc9ce4a..7aca6ee9 100644 --- a/src/Event/ExerciseRunnerEvent.php +++ b/src/Event/ExerciseRunnerEvent.php @@ -5,6 +5,7 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; /** @@ -12,30 +13,24 @@ */ class ExerciseRunnerEvent extends Event { - /** - * @var ExerciseInterface - */ - private $exercise; - - /** - * @var Input - */ - private $input; + private ExecutionContext $context; /** * @param string $name - * @param ExerciseInterface $exercise - * @param Input $input - * @param array $parameters + * @param array $parameters */ - public function __construct(string $name, ExerciseInterface $exercise, Input $input, array $parameters = []) + public function __construct(string $name, ExecutionContext $context, array $parameters = []) { - $parameters['input'] = $input; - $parameters['exercise'] = $exercise; + $this->context = $context; + + $parameters['input'] = $context->getInput(); + $parameters['exercise'] = $context->getExercise(); parent::__construct($name, $parameters); + } - $this->exercise = $exercise; - $this->input = $input; + public function getContext(): ExecutionContext + { + return $this->context; } /** @@ -43,7 +38,7 @@ public function __construct(string $name, ExerciseInterface $exercise, Input $in */ public function getInput(): Input { - return $this->input; + return $this->context->getInput(); } /** @@ -51,6 +46,6 @@ public function getInput(): Input */ public function getExercise(): ExerciseInterface { - return $this->exercise; + return $this->context->getExercise(); } } diff --git a/src/Event/functions.php b/src/Event/functions.php index 70d5cd68..5f9436f4 100644 --- a/src/Event/functions.php +++ b/src/Event/functions.php @@ -8,6 +8,6 @@ function containerListener(string $service, string $method = '__invoke'): callable { - return fn () => new ContainerListenerHelper($service, $method); + return fn() => new ContainerListenerHelper($service, $method); } } diff --git a/src/Exception/CheckNotApplicableException.php b/src/Exception/CheckNotApplicableException.php index 68d7f0bd..0a9939c4 100644 --- a/src/Exception/CheckNotApplicableException.php +++ b/src/Exception/CheckNotApplicableException.php @@ -28,8 +28,8 @@ public static function fromCheckAndExercise(CheckInterface $check, ExerciseInter 'Check: "%s" cannot process exercise: "%s" with type: "%s"', $check->getName(), $exercise->getName(), - $exercise->getType() - ) + $exercise->getType(), + ), ); } } diff --git a/src/Exception/CodeExecutionException.php b/src/Exception/CodeExecutionException.php index 2e4e5561..aff5c67a 100644 --- a/src/Exception/CodeExecutionException.php +++ b/src/Exception/CodeExecutionException.php @@ -23,8 +23,8 @@ public static function fromProcess(Process $process): self return new self( sprintf( 'PHP Code failed to execute. Error: "%s"', - trim($process->getErrorOutput() ?: $process->getOutput()) - ) + trim($process->getErrorOutput() ?: $process->getOutput()), + ), ); } } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index ddfab528..f66e96a2 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -22,8 +22,8 @@ public static function typeMisMatch(string $expected, $actual): self sprintf( 'Expected: "%s" Received: "%s"', $expected, - is_object($actual) ? get_class($actual) : gettype($actual) - ) + is_object($actual) ? get_class($actual) : gettype($actual), + ), ); } @@ -42,8 +42,8 @@ public static function notValidParameter(string $parameterName, array $allowedVa 'Parameter: "%s" can only be one of: "%s" Received: "%s"', $parameterName, self::stringify($allowedValues), - self::stringify($actualValue) - ) + self::stringify($actualValue), + ), ); } @@ -58,8 +58,8 @@ public static function missingImplements(object $object, string $requiredInterfa sprintf( '"%s" is required to implement "%s", but it does not', get_class($object), - $requiredInterface - ) + $requiredInterface, + ), ); } diff --git a/src/Exception/MissingArgumentException.php b/src/Exception/MissingArgumentException.php index d4dbcc65..b52d9d91 100644 --- a/src/Exception/MissingArgumentException.php +++ b/src/Exception/MissingArgumentException.php @@ -29,8 +29,8 @@ public function __construct(string $commandName, array $missingArguments) sprintf( 'Command: "%s" is missing the following arguments: "%s"', $commandName, - implode('", "', $missingArguments) - ) + implode('", "', $missingArguments), + ), ); } diff --git a/src/Exception/ProblemFileDoesNotExistException.php b/src/Exception/ProblemFileDoesNotExistException.php index 698e198a..54129972 100644 --- a/src/Exception/ProblemFileDoesNotExistException.php +++ b/src/Exception/ProblemFileDoesNotExistException.php @@ -12,7 +12,7 @@ public static function fromFile(string $file): self { return new self(sprintf( 'Exercise problem file: "%s" does not exist or is not readable', - canonicalise_path($file) + canonicalise_path($file), )); } } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 1907349e..011f5948 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -4,6 +4,4 @@ namespace PhpSchool\PhpWorkshop\Exception; -class RuntimeException extends \RuntimeException -{ -} +class RuntimeException extends \RuntimeException {} diff --git a/src/Exception/SolutionExecutionException.php b/src/Exception/SolutionExecutionException.php index ecfd89c9..44515b77 100644 --- a/src/Exception/SolutionExecutionException.php +++ b/src/Exception/SolutionExecutionException.php @@ -10,6 +10,4 @@ * Represents the situation where a reference solution cannot be executed (this should only really happen during * workshop development). */ -class SolutionExecutionException extends RuntimeException -{ -} +class SolutionExecutionException extends RuntimeException {} diff --git a/src/Exercise/AbstractExercise.php b/src/Exercise/AbstractExercise.php index 87a0b241..30faac17 100644 --- a/src/Exercise/AbstractExercise.php +++ b/src/Exercise/AbstractExercise.php @@ -4,9 +4,7 @@ namespace PhpSchool\PhpWorkshop\Exercise; -use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; use PhpSchool\PhpWorkshop\Event\EventDispatcher; -use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; use ReflectionClass; @@ -41,9 +39,9 @@ public function getSolution(): SolutionInterface sprintf( '%s/../../exercises/%s/solution/solution.php', dirname((string) (new ReflectionClass(static::class))->getFileName()), - self::normaliseName($this->getName()) - ) - ) + self::normaliseName($this->getName()), + ), + ), ); } @@ -66,9 +64,7 @@ public function getProblem(): string * * @return void */ - public function tearDown(): void - { - } + public function tearDown(): void {} /** * @param string $name @@ -87,7 +83,5 @@ public function getRequiredChecks(): array return []; } - public function defineListeners(EventDispatcher $dispatcher): void - { - } + public function defineListeners(EventDispatcher $dispatcher): void {} } diff --git a/src/Exercise/CgiExercise.php b/src/Exercise/CgiExercise.php index 1244712a..14cef8a1 100644 --- a/src/Exercise/CgiExercise.php +++ b/src/Exercise/CgiExercise.php @@ -4,7 +4,7 @@ namespace PhpSchool\PhpWorkshop\Exercise; -use Psr\Http\Message\RequestInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; /** * This interface describes the additional methods a CGI type exercise should implement. @@ -12,10 +12,15 @@ interface CgiExercise extends ProvidesSolution { /** - * This method should return an array of PSR-7 requests, which will be forwarded to the student's - * solution. + * This method should return an instance of CgiScenario which contains PSR-7 requests, + * which will be forwarded to the student's solution. * - * @return array An array of PSR-7 requests. + * Use like so: + * + * ``` + * return (new CgiScenario()) + * ->withExecution($request1) + * ``` */ - public function getRequests(): array; + public function defineTestScenario(): CgiScenario; } diff --git a/src/Exercise/CliExercise.php b/src/Exercise/CliExercise.php index 80a287f9..05449b10 100644 --- a/src/Exercise/CliExercise.php +++ b/src/Exercise/CliExercise.php @@ -4,16 +4,24 @@ namespace PhpSchool\PhpWorkshop\Exercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; + /** * This interface describes the additional methods a CLI type exercise should implement. */ interface CliExercise extends ProvidesSolution { /** - * This method should return an array of an array of strings. - * Each set of arguments will be passed to the students solution as command line arguments. + * This method should return an instance of CliScenario which contains sets of arguments, + * which will be passed to the students solution as command line arguments. + * + * Use like so: * - * @return array> An array of string arguments. + * ``` + * return (new CliScenario()) + * ->withExecution(['arg1', 'arg2']) + * ->withExecution(['round2-arg1', 'round2-arg2']) + * ``` */ - public function getArgs(): array; + public function defineTestScenario(): CliScenario; } diff --git a/src/Exercise/ExerciseInterface.php b/src/Exercise/ExerciseInterface.php index 10443374..58d8d566 100644 --- a/src/Exercise/ExerciseInterface.php +++ b/src/Exercise/ExerciseInterface.php @@ -5,7 +5,6 @@ namespace PhpSchool\PhpWorkshop\Exercise; use PhpSchool\PhpWorkshop\Event\EventDispatcher; -use PhpSchool\PhpWorkshop\ExerciseDispatcher; /** * This interface describes all of the methods an exercise diff --git a/src/Exercise/MockExercise.php b/src/Exercise/MockExercise.php index a8268785..90cbe04f 100644 --- a/src/Exercise/MockExercise.php +++ b/src/Exercise/MockExercise.php @@ -2,8 +2,6 @@ namespace PhpSchool\PhpWorkshop\Exercise; -use PhpSchool\PhpWorkshop\ExerciseDispatcher; - class MockExercise extends AbstractExercise implements ExerciseInterface { public function getName(): string diff --git a/src/ExerciseCheck/SelfCheck.php b/src/ExerciseCheck/SelfCheck.php index 857cf850..ba58893f 100644 --- a/src/ExerciseCheck/SelfCheck.php +++ b/src/ExerciseCheck/SelfCheck.php @@ -4,7 +4,7 @@ namespace PhpSchool\PhpWorkshop\ExerciseCheck; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Result\ResultInterface; /** @@ -21,8 +21,8 @@ interface SelfCheck * The method is passed the absolute file path to the student's solution and should return a result * object which indicates the success or not of the check. * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context. * @return ResultInterface The result of the check. */ - public function check(Input $input): ResultInterface; + public function check(ExecutionContext $context): ResultInterface; } diff --git a/src/ExerciseDispatcher.php b/src/ExerciseDispatcher.php index 112fd68b..b4c7142f 100644 --- a/src/ExerciseDispatcher.php +++ b/src/ExerciseDispatcher.php @@ -14,6 +14,7 @@ use PhpSchool\PhpWorkshop\Exception\ExerciseNotConfiguredException; use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\ExerciseRunner\RunnerManager; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; @@ -29,50 +30,26 @@ class ExerciseDispatcher /** * @var array */ - private $checksToRunBefore = []; + private array $checksToRunBefore = []; /** * @var array */ - private $checksToRunAfter = []; + private array $checksToRunAfter = []; - /** - * @var RunnerManager - */ - private $runnerManager; - - /** - * @var ResultAggregator - */ - private $results; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @var CheckRepository - */ - private $checkRepository; /** * @param RunnerManager $runnerManager Factory capable of building an exercise runner based on the exercise type. - * @param ResultAggregator $resultAggregator + * @param ResultAggregator $results * @param EventDispatcher $eventDispatcher * @param CheckRepository $checkRepository */ public function __construct( - RunnerManager $runnerManager, - ResultAggregator $resultAggregator, - EventDispatcher $eventDispatcher, - CheckRepository $checkRepository - ) { - $this->runnerManager = $runnerManager; - $this->results = $resultAggregator; - $this->eventDispatcher = $eventDispatcher; - $this->checkRepository = $checkRepository; - } + private RunnerManager $runnerManager, + private ResultAggregator $results, + private EventDispatcher $eventDispatcher, + private CheckRepository $checkRepository, + ) {} /** * Queue a specific check to be run when the exercise is verified. When the exercise is verified @@ -102,7 +79,7 @@ public function requireCheck(string $requiredCheck): void throw InvalidArgumentException::notValidParameter( 'position', [SimpleCheckInterface::CHECK_BEFORE, SimpleCheckInterface::CHECK_AFTER], - $check->getPosition() + $check->getPosition(), ); } @@ -129,6 +106,8 @@ public function requireCheck(string $requiredCheck): void */ public function verify(ExerciseInterface $exercise, Input $input): ResultAggregator { + $context = ExecutionContext::fromInputAndExercise($input, $exercise); + $runner = $this->runnerManager->getRunner($exercise); $exercise->defineListeners($this->eventDispatcher); @@ -137,35 +116,35 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega $this->requireCheck($requiredCheck); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.start', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.start', $context)); $this->validateChecks($this->checksToRunBefore, $exercise); $this->validateChecks($this->checksToRunAfter, $exercise); foreach ($this->checksToRunBefore as $check) { - $this->results->add($check->check($exercise, $input)); + $this->results->add($check->check($context)); if (!$this->results->isSuccessful()) { return $this->results; } } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.pre.execute', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.pre.execute', $context)); try { - $this->results->add($runner->verify($input)); + $this->results->add($runner->verify($context)); } finally { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.execute', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.execute', $context)); } foreach ($this->checksToRunAfter as $check) { - $this->results->add($check->check($exercise, $input)); + $this->results->add($check->check($context)); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.check', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.check', $context)); $exercise->tearDown(); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.finish', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.finish', $context)); return $this->results; } @@ -181,24 +160,26 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega */ public function run(ExerciseInterface $exercise, Input $input, OutputInterface $output): bool { + $context = ExecutionContext::fromInputAndExercise($input, $exercise); + $exercise->defineListeners($this->eventDispatcher); /** @var PhpLintCheck $lint */ $lint = $this->checkRepository->getByClass(PhpLintCheck::class); - $result = $lint->check($exercise, $input); + $result = $lint->check($context); if ($result instanceof FailureInterface) { throw CouldNotRunException::fromFailure($result); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.start', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.start', $context)); try { $exitStatus = $this->runnerManager ->getRunner($exercise) - ->run($input, $output); + ->run($context, $output); } finally { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.finish', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.finish', $context)); } return $exitStatus; diff --git a/src/ExerciseRenderer.php b/src/ExerciseRenderer.php index 987b28e5..36dba649 100644 --- a/src/ExerciseRenderer.php +++ b/src/ExerciseRenderer.php @@ -68,7 +68,7 @@ public function __construct( Serializer $userStateSerializer, MarkdownRenderer $markdownRenderer, Color $color, - OutputInterface $output + OutputInterface $output, ) { $this->appName = $appName; $this->exerciseRepository = $exerciseRepository; @@ -133,7 +133,7 @@ private function helpLine(string $text, string $cmd): string " %s %s: %s\n", $this->color->__invoke("»")->bold()->__toString(), $text, - $cmd + $cmd, ); } } diff --git a/src/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index e29e3348..0986cef4 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -10,26 +10,27 @@ use PhpSchool\PhpWorkshop\Check\FileExistsCheck; use PhpSchool\PhpWorkshop\Check\PhpLintCheck; use PhpSchool\PhpWorkshop\Event\CgiExecuteEvent; +use PhpSchool\PhpWorkshop\Event\CgiExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Event\Event; use PhpSchool\PhpWorkshop\Event\EventDispatcher; -use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exception\CodeExecutionException; use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessInput; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure; use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure; use PhpSchool\PhpWorkshop\Result\Cgi\Success; use PhpSchool\PhpWorkshop\Result\Cgi\ResultInterface as CgiResultInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; -use PhpSchool\PhpWorkshop\Utils\RequestRenderer; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; /** @@ -38,25 +39,10 @@ */ class CgiRunner implements ExerciseRunnerInterface { - /** - * @var CgiExercise&ExerciseInterface - */ - private $exercise; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @var string - */ - private $phpLocation; - /** * @var array */ - private static $requiredChecks = [ + private static array $requiredChecks = [ FileExistsCheck::class, CodeExistsCheck::class, PhpLintCheck::class, @@ -68,27 +54,14 @@ class CgiRunner implements ExerciseRunnerInterface * be available. It will check for it's existence in the system's $PATH variable or the same * folder that the CLI php binary lives in. * - * @param CgiExercise $exercise The exercise to be invoked. - * @param EventDispatcher $eventDispatcher The event dispatcher. + * @param CgiExercise&ExerciseInterface $exercise The exercise to be invoked. */ public function __construct( - CgiExercise $exercise, - EventDispatcher $eventDispatcher - ) { - $php = (new ExecutableFinder())->find('php-cgi'); - - if (null === $php) { - throw new RuntimeException( - 'Could not load php-cgi binary. Please install php using your package manager.' - ); - } - - $this->phpLocation = $php; - - /** @var CgiExercise&ExerciseInterface $exercise */ - $this->eventDispatcher = $eventDispatcher; - $this->exercise = $exercise; - } + private CgiExercise $exercise, + private EventDispatcher $eventDispatcher, + private ProcessFactory $processFactory, + private EnvironmentManager $environmentManager, + ) {} /** * @return string @@ -109,33 +82,95 @@ public function getRequiredChecks(): array } /** - * @param RequestInterface $request - * @param string $fileName - * @return CgiResultInterface + * Verifies a solution by invoking PHP via the `php-cgi` binary, populating all the super globals with + * the information from the request objects returned from the exercise. The exercise can return multiple + * requests so the solution will be invoked for however many requests there are. + * + * Events dispatched (for each request): + * + * * cgi.verify.reference-execute.pre + * * cgi.verify.reference.executing + * * cgi.verify.reference-execute.fail (if the reference solution fails to execute) + * * cgi.verify.student-execute.pre + * * cgi.verify.student.executing + * * cgi.verify.student-execute.fail (if the student's solution fails to execute) + * + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. + * @return CgiResult The result of the check. */ - private function checkRequest(RequestInterface $request, string $fileName): CgiResultInterface + public function verify(ExecutionContext $context): ResultInterface + { + $scenario = $this->exercise->defineTestScenario(); + + $this->environmentManager->prepareStudent($context, $scenario); + $this->environmentManager->prepareReference($context, $scenario); + + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.start', $context, $scenario)); + + $result = new CgiResult( + array_map( + function (RequestInterface $request) use ($context, $scenario) { + return $this->doVerify($context, $scenario, $request); + }, + $scenario->getExecutions(), + ), + ); + + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.finish', $context, $scenario)); + return $result; + } + + private function doVerify(ExecutionContext $context, CgiScenario $scenario, RequestInterface $request): CgiResultInterface { try { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.verify.reference-execute.pre', $request) + new CgiExecuteEvent('cgi.verify.reference-execute.pre', $context, $scenario, $request), ); $solutionResponse = $this->executePhpFile( - $this->exercise->getSolution()->getEntryPoint()->getAbsolutePath(), + $context, + $scenario, + $context->getReferenceExecutionDirectory(), + $this->exercise->getSolution()->getEntryPoint()->getRelativePath(), $event->getRequest(), - 'reference' + 'reference', ); } catch (CodeExecutionException $e) { - $this->eventDispatcher->dispatch(new Event('cgi.verify.reference-execute.fail', ['exception' => $e])); + $this->eventDispatcher->dispatch( + new CgiExecuteEvent( + 'cgi.verify.reference-execute.fail', + $context, + $scenario, + $request, + ['exception' => $e], + ), + ); throw new SolutionExecutionException($e->getMessage()); } try { /** @var CgiExecuteEvent $event */ - $event = $this->eventDispatcher->dispatch(new CgiExecuteEvent('cgi.verify.student-execute.pre', $request)); - $userResponse = $this->executePhpFile($fileName, $event->getRequest(), 'student'); + $event = $this->eventDispatcher->dispatch( + new CgiExecuteEvent('cgi.verify.student-execute.pre', $context, $scenario, $request), + ); + $userResponse = $this->executePhpFile( + $context, + $scenario, + $context->getStudentExecutionDirectory(), + $context->getEntryPoint(), + $event->getRequest(), + 'student', + ); } catch (CodeExecutionException $e) { - $this->eventDispatcher->dispatch(new Event('cgi.verify.student-execute.fail', ['exception' => $e])); + $this->eventDispatcher->dispatch( + new CgiExecuteEvent( + 'cgi.verify.student-execute.fail', + $context, + $scenario, + $request, + ['exception' => $e], + ), + ); return GenericFailure::fromRequestAndCodeExecutionFailure($request, $e); } @@ -170,12 +205,20 @@ private function getHeaders(ResponseInterface $response): array * @param string $type * @return ResponseInterface */ - private function executePhpFile(string $fileName, RequestInterface $request, string $type): ResponseInterface - { - $process = $this->getProcess($fileName, $request); + private function executePhpFile( + ExecutionContext $context, + CgiScenario $scenario, + string $workingDirectory, + string $fileName, + RequestInterface $request, + string $type, + ): ResponseInterface { + $process = $this->getPhpProcess($workingDirectory, $fileName, $request); $process->start(); - $this->eventDispatcher->dispatch(new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $request)); + $this->eventDispatcher->dispatch( + new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $context, $scenario, $request), + ); $process->wait(); if (!$process->isSuccessful()) { @@ -196,79 +239,38 @@ private function executePhpFile(string $fileName, RequestInterface $request, str * @param RequestInterface $request * @return Process */ - private function getProcess(string $fileName, RequestInterface $request): Process + private function getPhpProcess(string $workingDirectory, string $fileName, RequestInterface $request): Process { - $env = $this->getDefaultEnv(); - $env += [ + $env = [ 'REQUEST_METHOD' => $request->getMethod(), 'SCRIPT_FILENAME' => $fileName, - 'REDIRECT_STATUS' => 302, + 'REDIRECT_STATUS' => '302', 'QUERY_STRING' => $request->getUri()->getQuery(), 'REQUEST_URI' => $request->getUri()->getPath(), 'XDEBUG_MODE' => 'off', ]; - $cgiBinary = sprintf( - '%s -dalways_populate_raw_post_data=-1 -dhtml_errors=0 -dexpose_php=0', - $this->phpLocation - ); - $content = $request->getBody()->__toString(); - $cmd = sprintf('echo %s | %s', escapeshellarg($content), $cgiBinary); - $env['CONTENT_LENGTH'] = $request->getBody()->getSize(); + $env['CONTENT_LENGTH'] = (string) $request->getBody()->getSize(); $env['CONTENT_TYPE'] = $request->getHeaderLine('Content-Type'); foreach ($request->getHeaders() as $name => $values) { $env[sprintf('HTTP_%s', strtoupper($name))] = implode(", ", $values); } - return Process::fromShellCommandline($cmd, null, $env, null, 10); - } - - /** - * We need to reset env entirely, because Symfony inherits it. We do that by setting all - * the current env vars to false - * - * @return array - */ - private function getDefaultEnv(): array - { - $env = array_map(fn () => false, $_ENV); - $env + array_map(fn () => false, $_SERVER); - - return $env; - } - - /** - * Verifies a solution by invoking PHP via the `php-cgi` binary, populating all the super globals with - * the information from the request objects returned from the exercise. The exercise can return multiple - * requests so the solution will be invoked for however many requests there are. - * - * Events dispatched (for each request): - * - * * cgi.verify.reference-execute.pre - * * cgi.verify.reference.executing - * * cgi.verify.reference-execute.fail (if the reference solution fails to execute) - * * cgi.verify.student-execute.pre - * * cgi.verify.student.executing - * * cgi.verify.student-execute.fail (if the student's solution fails to execute) - * - * @param Input $input The command line arguments passed to the command. - * @return CgiResult The result of the check. - */ - public function verify(Input $input): ResultInterface - { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.verify.start', $this->exercise, $input)); - $result = new CgiResult( - array_map( - function (RequestInterface $request) use ($input) { - return $this->checkRequest($request, $input->getRequiredArgument('program')); - }, - $this->exercise->getRequests() - ) + $processInput = new ProcessInput( + 'php-cgi', + [ + '-dalways_populate_raw_post_data=-1', + '-dhtml_errors=0', + '-dexpose_php=0', + ], + $workingDirectory, + $env, + $content, ); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.verify.finish', $this->exercise, $input)); - return $result; + + return $this->processFactory->create($processInput); } /** @@ -284,24 +286,39 @@ function (RequestInterface $request) use ($input) { * * cgi.run.student-execute.pre * * cgi.run.student.executing * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @param OutputInterface $output A wrapper around STDOUT. * @return bool If the solution was successfully executed, eg. exit code was 0. */ - public function run(Input $input, OutputInterface $output): bool + public function run(ExecutionContext $context, OutputInterface $output): bool { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.run.start', $this->exercise, $input)); + $scenario = $this->exercise->defineTestScenario(); + + $this->environmentManager->prepareStudent($context, $scenario); + + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $context, $scenario)); + $success = true; - foreach ($this->exercise->getRequests() as $i => $request) { + foreach ($scenario->getExecutions() as $i => $request) { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student-execute.pre', $request) + new CgiExecuteEvent('cgi.run.student-execute.pre', $context, $scenario, $request), + ); + $process = $this->getPhpProcess( + $context->getStudentExecutionDirectory(), + $context->getEntryPoint(), + $event->getRequest(), ); - $process = $this->getProcess($input->getRequiredArgument('program'), $event->getRequest()); $process->start(); $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student.executing', $request, ['output' => $output]) + new CgiExecuteEvent( + 'cgi.run.student.executing', + $context, + $scenario, + $request, + ['output' => $output], + ), ); $process->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); @@ -315,10 +332,11 @@ public function run(Input $input, OutputInterface $output): bool $output->lineBreak(); $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student-execute.post', $request) + new CgiExecuteEvent('cgi.run.student-execute.post', $context, $scenario, $request), ); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.run.finish', $this->exercise, $input)); + + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.finish', $context, $scenario)); return $success; } } diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index 4750405e..2afbebde 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -9,24 +9,26 @@ use PhpSchool\PhpWorkshop\Check\FileExistsCheck; use PhpSchool\PhpWorkshop\Check\PhpLintCheck; use PhpSchool\PhpWorkshop\Event\CliExecuteEvent; +use PhpSchool\PhpWorkshop\Event\CliExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Event\Event; use PhpSchool\PhpWorkshop\Event\EventDispatcher; -use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exception\CodeExecutionException; use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessInput; use PhpSchool\PhpWorkshop\Result\Cli\RequestFailure; use PhpSchool\PhpWorkshop\Result\Cli\CliResult; use PhpSchool\PhpWorkshop\Result\Cli\GenericFailure; use PhpSchool\PhpWorkshop\Result\Cli\Success; use PhpSchool\PhpWorkshop\Result\Cli\ResultInterface as CliResultInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; -use RuntimeException; -use Symfony\Component\Process\ExecutableFinder; +use PhpSchool\PhpWorkshop\Utils\Collection; use Symfony\Component\Process\Process; /** @@ -39,25 +41,10 @@ */ class CliRunner implements ExerciseRunnerInterface { - /** - * @var CliExercise&ExerciseInterface - */ - private $exercise; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @var string - */ - private $phpLocation; - /** * @var array */ - private static $requiredChecks = [ + private static array $requiredChecks = [ FileExistsCheck::class, CodeExistsCheck::class, PhpLintCheck::class, @@ -67,25 +54,14 @@ class CliRunner implements ExerciseRunnerInterface /** * Requires the exercise instance and an event dispatcher. * - * @param CliExercise $exercise The exercise to be invoked. - * @param EventDispatcher $eventDispatcher The event dispatcher. + * @param CliExercise&ExerciseInterface $exercise The exercise to be invoked. */ - public function __construct(CliExercise $exercise, EventDispatcher $eventDispatcher) - { - $php = (new ExecutableFinder())->find('php'); - - if (null === $php) { - throw new RuntimeException( - 'Could not load php binary. Please install php using your package manager.' - ); - } - - $this->phpLocation = $php; - - /** @var CliExercise&ExerciseInterface $exercise */ - $this->eventDispatcher = $eventDispatcher; - $this->exercise = $exercise; - } + public function __construct( + private CliExercise $exercise, + private EventDispatcher $eventDispatcher, + private ProcessFactory $processFactory, + private EnvironmentManager $environmentManager, + ) {} /** * @return string @@ -105,59 +81,6 @@ public function getRequiredChecks(): array return self::$requiredChecks; } - /** - * @param string $fileName - * @param ArrayObject $args - * @param string $type - * @return string - */ - private function executePhpFile(string $fileName, ArrayObject $args, string $type): string - { - $process = $this->getPhpProcess($fileName, $args); - - $process->start(); - $this->eventDispatcher->dispatch(new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $args)); - $process->wait(); - - if (!$process->isSuccessful()) { - throw CodeExecutionException::fromProcess($process); - } - - return $process->getOutput(); - } - - /** - * @param string $fileName - * @param ArrayObject $args - * - * @return Process - */ - private function getPhpProcess(string $fileName, ArrayObject $args): Process - { - return new Process( - $args->prepend($fileName)->prepend($this->phpLocation)->getArrayCopy(), - dirname($fileName), - $this->getDefaultEnv() + ['XDEBUG_MODE' => 'off'], - null, - 10 - ); - } - - /** - * We need to reset env entirely, because Symfony inherits it. We do that by setting all - * the current env vars to false - * - * @return array - */ - private function getDefaultEnv(): array - { - $env = array_map(fn () => false, $_ENV); - $env + array_map(fn () => false, $_SERVER); - - return $env; - } - - /** * Verifies a solution by invoking PHP from the CLI passing the arguments gathered from the exercise * as command line arguments to PHP. @@ -171,70 +94,86 @@ private function getDefaultEnv(): array * * cli.verify.student.executing * * cli.verify.student-execute.fail (if the student's solution fails to execute) * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return CliResult The result of the check. */ - public function verify(Input $input): ResultInterface + public function verify(ExecutionContext $context): ResultInterface { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.verify.start', $this->exercise, $input)); + $scenario = $this->exercise->defineTestScenario(); + + $this->environmentManager->prepareStudent($context, $scenario); + $this->environmentManager->prepareReference($context, $scenario); + + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.start', $context, $scenario)); + $result = new CliResult( array_map( - function (array $args) use ($input) { - return $this->doVerify($args, $input); + function (Collection $args) use ($context, $scenario) { + return $this->doVerify($context, $scenario, $args); }, - $this->preserveOldArgFormat($this->exercise->getArgs()) - ) + $scenario->getExecutions(), + ), ); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.verify.finish', $this->exercise, $input)); - return $result; - } - /** - * BC - getArgs only returned 1 set of args in v1 instead of multiple sets of args in v2 - * - * @param array>|array $args - * @return array> - */ - private function preserveOldArgFormat(array $args): array - { - if (isset($args[0]) && !is_array($args[0])) { - $args = [$args]; - } elseif (count($args) === 0) { - $args = [[]]; - } + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.finish', $context, $scenario)); - return $args; + return $result; } /** - * @param array $args - * @param Input $input - * @return CliResultInterface + * @param Collection $args */ - private function doVerify(array $args, Input $input): CliResultInterface + private function doVerify(ExecutionContext $context, CliScenario $scenario, Collection $args): CliResultInterface { - //arrays are not pass-by-ref - $args = new ArrayObject($args); - try { /** @var CliExecuteEvent $event */ - $event = $this->eventDispatcher->dispatch(new CliExecuteEvent('cli.verify.reference-execute.pre', $args)); + $event = $this->eventDispatcher->dispatch( + new CliExecuteEvent('cli.verify.reference-execute.pre', $context, $scenario, $args), + ); $solutionOutput = $this->executePhpFile( - $this->exercise->getSolution()->getEntryPoint()->getAbsolutePath(), + $context, + $scenario, + $context->getReferenceExecutionDirectory(), + $this->exercise->getSolution()->getEntryPoint()->getRelativePath(), $event->getArgs(), - 'reference' + 'reference', ); } catch (CodeExecutionException $e) { - $this->eventDispatcher->dispatch(new Event('cli.verify.reference-execute.fail', ['exception' => $e])); + $this->eventDispatcher->dispatch( + new CliExecuteEvent( + 'cli.verify.reference-execute.fail', + $context, + $scenario, + $args, + ['exception' => $e], + ), + ); throw new SolutionExecutionException($e->getMessage()); } try { /** @var CliExecuteEvent $event */ - $event = $this->eventDispatcher->dispatch(new CliExecuteEvent('cli.verify.student-execute.pre', $args)); - $userOutput = $this->executePhpFile($input->getRequiredArgument('program'), $event->getArgs(), 'student'); + $event = $this->eventDispatcher->dispatch( + new CliExecuteEvent('cli.verify.student-execute.pre', $context, $scenario, $args), + ); + $userOutput = $this->executePhpFile( + $context, + $scenario, + $context->getStudentExecutionDirectory(), + $context->getEntryPoint(), + $event->getArgs(), + 'student', + ); } catch (CodeExecutionException $e) { - $this->eventDispatcher->dispatch(new Event('cli.verify.student-execute.fail', ['exception' => $e])); + $this->eventDispatcher->dispatch( + new CliExecuteEvent( + 'cli.verify.student-execute.fail', + $context, + $scenario, + $args, + ['exception' => $e], + ), + ); return GenericFailure::fromArgsAndCodeExecutionFailure($args, $e); } if ($solutionOutput === $userOutput) { @@ -256,26 +195,36 @@ private function doVerify(array $args, Input $input): CliResultInterface * * cli.run.student-execute.pre * * cli.run.student.executing * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @param OutputInterface $output A wrapper around STDOUT. * @return bool If the solution was successfully executed, eg. exit code was 0. */ - public function run(Input $input, OutputInterface $output): bool + public function run(ExecutionContext $context, OutputInterface $output): bool { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.start', $this->exercise, $input)); + $scenario = $this->exercise->defineTestScenario(); + + $this->environmentManager->prepareStudent($context, $scenario); + + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $context, $scenario)); + $success = true; - foreach ($this->preserveOldArgFormat($this->exercise->getArgs()) as $i => $args) { + foreach ($scenario->getExecutions() as $i => $args) { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student-execute.pre', new ArrayObject($args)) + new CliExecuteEvent('cli.run.student-execute.pre', $context, $scenario, $args), ); $args = $event->getArgs(); - $process = $this->getPhpProcess($input->getRequiredArgument('program'), $args); + $process = $this->getPhpProcess( + $context->getStudentExecutionDirectory(), + $context->getEntryPoint(), + $args, + ); + $process->start(); $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student.executing', $args, ['output' => $output]) + new CliExecuteEvent('cli.run.student.executing', $context, $scenario, $args, ['output' => $output]), ); $process->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); @@ -289,11 +238,42 @@ public function run(Input $input, OutputInterface $output): bool $output->lineBreak(); $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student-execute.post', $args) + new CliExecuteEvent('cli.run.student-execute.post', $context, $scenario, $args), ); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.finish', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.finish', $context, $scenario)); + return $success; } + + /** + * @param Collection $args + */ + private function executePhpFile(ExecutionContext $context, CliScenario $scenario, string $workingDirectory, string $fileName, Collection $args, string $type): string + { + $process = $this->getPhpProcess($workingDirectory, $fileName, $args); + + $process->start(); + $this->eventDispatcher->dispatch( + new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $context, $scenario, $args), + ); + $process->wait(); + + if (!$process->isSuccessful()) { + throw CodeExecutionException::fromProcess($process); + } + + return $process->getOutput(); + } + + /** + * @param Collection $args + */ + private function getPhpProcess(string $workingDirectory, string $fileName, Collection $args): Process + { + return $this->processFactory->create( + new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, []), + ); + } } diff --git a/src/ExerciseRunner/Context/ExecutionContext.php b/src/ExerciseRunner/Context/ExecutionContext.php index ce407aac..ba1c300c 100644 --- a/src/ExerciseRunner/Context/ExecutionContext.php +++ b/src/ExerciseRunner/Context/ExecutionContext.php @@ -14,8 +14,7 @@ public function __construct( private string $referenceExecutionDirectory, private ExerciseInterface $exercise, private Input $input, - ) { - } + ) {} public static function fromInputAndExercise(Input $input, ExerciseInterface $exercise): ExecutionContext { @@ -25,7 +24,7 @@ public static function fromInputAndExercise(Input $input, ExerciseInterface $exe $program, System::randomTempDir(), $exercise, - $input + $input, ); } @@ -52,7 +51,7 @@ public function getEntryPoint(): string return Path::join( $this->studentExecutionDirectory, - basename($this->input->getRequiredArgument('program')) + basename($this->input->getRequiredArgument('program')), ); } diff --git a/src/ExerciseRunner/Context/TestContext.php b/src/ExerciseRunner/Context/TestContext.php index d5ceb0ba..383f0d97 100644 --- a/src/ExerciseRunner/Context/TestContext.php +++ b/src/ExerciseRunner/Context/TestContext.php @@ -2,13 +2,16 @@ namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\RuntimeException; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\ExerciseScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Input\Input; -use PhpSchool\PhpWorkshop\Solution\SolutionInterface; +use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Utils\System; use Symfony\Component\Filesystem\Filesystem; use PhpSchool\PhpWorkshop\Utils\Path; @@ -16,6 +19,7 @@ class TestContext extends ExecutionContext { private Filesystem $filesystem; + private EnvironmentManager $environmentManager; private ExerciseInterface $exercise; private bool $studentSolutionDirWasCreated = false; private bool $referenceSolutionDirWasCreated = false; @@ -28,6 +32,7 @@ public function __construct( $this->exercise = $exercise ?? new MockExercise(); $this->filesystem = new Filesystem(); + $this->environmentManager = new EnvironmentManager($this->filesystem, new EventDispatcher(new ResultAggregator())); if ($studentDirectory === null) { $studentDirectory = System::randomTempDir(); @@ -53,11 +58,27 @@ public function createReferenceSolutionDirectory(): void $this->referenceSolutionDirWasCreated = true; } + public function importReferenceSolution(): void + { + if (!$this->referenceSolutionDirWasCreated) { + throw new RuntimeException( + sprintf('Reference execution directory not created. Call %s::createReferenceSolutionDirectory() first.', self::class), + ); + } + + $scenario = new class () extends ExerciseScenario {}; + if ($this->exercise instanceof CliExercise || $this->exercise instanceof CgiExercise) { + $scenario = $this->exercise->defineTestScenario(); + } + + $this->environmentManager->prepareReference($this, $scenario); + } + public function importStudentFileFromString(string $content, string $filename = 'solution.php'): void { if (!$this->studentSolutionDirWasCreated) { throw new RuntimeException( - sprintf('Student execution directory not created. Call %s::createStudentSolutionDirectory() first.', self::class) + sprintf('Student execution directory not created. Call %s::createStudentSolutionDirectory() first.', self::class), ); } @@ -68,7 +89,7 @@ public function importReferenceFileFromString(string $content, string $filename { if (!$this->referenceSolutionDirWasCreated) { throw new RuntimeException( - sprintf('Reference execution directory not created. Call %s::createReferenceSolutionDirectory() first.', self::class) + sprintf('Reference execution directory not created. Call %s::createReferenceSolutionDirectory() first.', self::class), ); } @@ -85,7 +106,7 @@ public static function fromExerciseAndStudentSolution(ExerciseInterface $exercis return new self( exercise: $exercise, input: $input, - studentDirectory: dirname($file) + studentDirectory: dirname($file), ); } diff --git a/src/ExerciseRunner/CustomVerifyingRunner.php b/src/ExerciseRunner/CustomVerifyingRunner.php index c2fb9afc..d619733d 100644 --- a/src/ExerciseRunner/CustomVerifyingRunner.php +++ b/src/ExerciseRunner/CustomVerifyingRunner.php @@ -5,6 +5,7 @@ namespace PhpSchool\PhpWorkshop\ExerciseRunner; use PhpSchool\PhpWorkshop\Exercise\CustomVerifyingExercise; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -14,18 +15,7 @@ */ class CustomVerifyingRunner implements ExerciseRunnerInterface { - /** - * @var CustomVerifyingExercise - */ - private $exercise; - - /** - * @param CustomVerifyingExercise $exercise - */ - public function __construct(CustomVerifyingExercise $exercise) - { - $this->exercise = $exercise; - } + public function __construct(private CustomVerifyingExercise $exercise) {} /** * Get the name of the exercise runner. @@ -51,10 +41,10 @@ public function getRequiredChecks(): array * Delegate to the exercise for verifying. Verifying could mean checking that a program was installed or that some * other arbitrary task was performed. * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @return ResultInterface The result of the check. */ - public function verify(Input $input): ResultInterface + public function verify(ExecutionContext $context): ResultInterface { return $this->exercise->verify(); } @@ -63,11 +53,11 @@ public function verify(Input $input): ResultInterface * Running a custom verifying exercise does nothing. There is no program required, therefore there is nothing * to run. * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context, containing the exercise, input and working directories. * @param OutputInterface $output A wrapper around STDOUT. * @return bool If the solution was successfully executed, eg. exit code was 0. */ - public function run(Input $input, OutputInterface $output): bool + public function run(ExecutionContext $context, OutputInterface $output): bool { $message = 'Nothing to run here. This exercise does not require a code solution, '; $message .= 'so there is nothing to execute.'; diff --git a/src/ExerciseRunner/EnvironmentManager.php b/src/ExerciseRunner/EnvironmentManager.php new file mode 100644 index 00000000..39bb8119 --- /dev/null +++ b/src/ExerciseRunner/EnvironmentManager.php @@ -0,0 +1,66 @@ +copyExerciseFiles($scenario, $context->getStudentExecutionDirectory()); + + //cleanup the files when the run or verification process is finished + //we do this at late as possible in case any checks or other event listeners need to access the files + $this->eventDispatcher->listen(['run.finish', 'verify.finish'], function () use ($context, $scenario) { + foreach ($scenario->getFiles() as $fileName => $content) { + $this->filesystem->remove(Path::join($context->getStudentExecutionDirectory(), $fileName)); + } + }); + } + + public function prepareReference(ExecutionContext $context, ExerciseScenario $scenario): void + { + $exercise = $context->getExercise(); + + if (!$exercise instanceof ProvidesSolution) { + return; + } + + $this->filesystem->mkdir($context->getReferenceExecutionDirectory()); + + $solution = $exercise->getSolution(); + + foreach ($solution->getFiles() as $file) { + $this->filesystem->copy( + $file->getAbsolutePath(), + Path::join($context->getReferenceExecutionDirectory(), $file->getRelativePath()), + ); + } + + $this->copyExerciseFiles($scenario, $context->getReferenceExecutionDirectory()); + + //cleanup the files when the run or verification process is finished + //we do this at late as possible in case any checks or other event listeners need to access the files + $this->eventDispatcher->listen(['run.finish', 'verify.finish'], function () use ($context) { + $this->filesystem->remove($context->getReferenceExecutionDirectory()); + }); + } + + private function copyExerciseFiles(ExerciseScenario $scenario, string $dir): void + { + foreach ($scenario->getFiles() as $fileName => $content) { + $this->filesystem->dumpFile( + Path::join($dir, $fileName), + $content, + ); + } + } +} diff --git a/src/ExerciseRunner/ExerciseRunnerInterface.php b/src/ExerciseRunner/ExerciseRunnerInterface.php index 66acec07..03dea33d 100644 --- a/src/ExerciseRunner/ExerciseRunnerInterface.php +++ b/src/ExerciseRunner/ExerciseRunnerInterface.php @@ -4,6 +4,7 @@ namespace PhpSchool\PhpWorkshop\ExerciseRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -38,19 +39,19 @@ public function getRequiredChecks(): array; * Other things that could go wrong include the student's solution returning a non-zero * exit code, or a notice/warning being exhibited. * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context * @return ResultInterface The result of the check. */ - public function verify(Input $input): ResultInterface; + public function verify(ExecutionContext $context): ResultInterface; /** * Run a solution to an exercise. This simply run's the student's solution with the correct input from the exercise * (such as the CLI arguments) and prints the output directly. This allows the student to have the environment * setup for them including getting a different set of arguments each time (if the exercise supports that). * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context * @param OutputInterface $output A wrapper around STDOUT. * @return bool If the solution was successfully executed, eg. exit code was 0. */ - public function run(Input $input, OutputInterface $output): bool; + public function run(ExecutionContext $context, OutputInterface $output): bool; } diff --git a/src/ExerciseRunner/Factory/CgiRunnerFactory.php b/src/ExerciseRunner/Factory/CgiRunnerFactory.php index a7917039..a53fec95 100644 --- a/src/ExerciseRunner/Factory/CgiRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CgiRunnerFactory.php @@ -11,8 +11,9 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; -use PhpSchool\PhpWorkshop\Utils\RequestRenderer; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; /** * Factory class for `CgiRunner` @@ -22,20 +23,9 @@ class CgiRunnerFactory implements ExerciseRunnerFactoryInterface /** * @var string */ - private static $type = ExerciseType::CGI; + private static string $type = ExerciseType::CGI; - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @param EventDispatcher $eventDispatcher - */ - public function __construct(EventDispatcher $eventDispatcher) - { - $this->eventDispatcher = $eventDispatcher; - } + public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory, private EnvironmentManager $environmentManager) {} /** * Whether the factory supports this exercise type. @@ -66,6 +56,6 @@ public function configureInput(CommandDefinition $commandDefinition): void */ public function create(ExerciseInterface $exercise): ExerciseRunnerInterface { - return new CgiRunner($exercise, $this->eventDispatcher); + return new CgiRunner($exercise, $this->eventDispatcher, $this->processFactory, $this->environmentManager); } } diff --git a/src/ExerciseRunner/Factory/CliRunnerFactory.php b/src/ExerciseRunner/Factory/CliRunnerFactory.php index 9694d642..4ee13f18 100644 --- a/src/ExerciseRunner/Factory/CliRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CliRunnerFactory.php @@ -11,7 +11,9 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; /** * Factory class for `CliRunner` @@ -21,20 +23,9 @@ class CliRunnerFactory implements ExerciseRunnerFactoryInterface /** * @var string */ - private static $type = ExerciseType::CLI; + private static string $type = ExerciseType::CLI; - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @param EventDispatcher $eventDispatcher - */ - public function __construct(EventDispatcher $eventDispatcher) - { - $this->eventDispatcher = $eventDispatcher; - } + public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory, private EnvironmentManager $environmentManager) {} /** * Whether the factory supports this exercise type. @@ -65,6 +56,6 @@ public function configureInput(CommandDefinition $commandDefinition): void */ public function create(ExerciseInterface $exercise): ExerciseRunnerInterface { - return new CliRunner($exercise, $this->eventDispatcher); + return new CliRunner($exercise, $this->eventDispatcher, $this->processFactory, $this->environmentManager); } } diff --git a/src/ExerciseRunner/Factory/CustomVerifyingRunnerFactory.php b/src/ExerciseRunner/Factory/CustomVerifyingRunnerFactory.php index 6ef11290..80130cca 100644 --- a/src/ExerciseRunner/Factory/CustomVerifyingRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CustomVerifyingRunnerFactory.php @@ -37,9 +37,7 @@ public function supports(ExerciseInterface $exercise): bool * * @param CommandDefinition $commandDefinition */ - public function configureInput(CommandDefinition $commandDefinition): void - { - } + public function configureInput(CommandDefinition $commandDefinition): void {} /** * Create and return an instance of the runner. diff --git a/src/ExerciseRunner/RunnerManager.php b/src/ExerciseRunner/RunnerManager.php index 149022e0..747e30b3 100644 --- a/src/ExerciseRunner/RunnerManager.php +++ b/src/ExerciseRunner/RunnerManager.php @@ -17,7 +17,7 @@ class RunnerManager /** * @var array */ - private $factories = []; + private array $factories = []; /** * @param ExerciseRunnerFactoryInterface $factory @@ -59,7 +59,7 @@ private function getFactory(ExerciseInterface $exercise): ExerciseRunnerFactoryI } throw new InvalidArgumentException( - sprintf('Exercise Type: "%s" not supported', $exercise->getType()->getValue()) + sprintf('Exercise Type: "%s" not supported', $exercise->getType()->getValue()), ); } } diff --git a/src/Factory/EventDispatcherFactory.php b/src/Factory/EventDispatcherFactory.php index a1d3638a..c84d44eb 100644 --- a/src/Factory/EventDispatcherFactory.php +++ b/src/Factory/EventDispatcherFactory.php @@ -78,12 +78,12 @@ private function mergeListenerGroups(array $listeners): array return $carry->set( $event, - array_merge($carry->get($event, []), $listeners) + array_merge($carry->get($event, []), $listeners), ); }, $carry); }, new Collection()); - return $mergedListeners->getArrayCopy(); + return $mergedListeners->getArrayCopy(); } /** @@ -97,26 +97,26 @@ private function attachListeners( string $eventName, array $listeners, ContainerInterface $container, - EventDispatcher $dispatcher + EventDispatcher $dispatcher, ): void { array_walk($listeners, function ($listener) use ($eventName, $dispatcher, $container) { if ($listener instanceof ContainerListenerHelper) { if (!$container->has($listener->getService())) { throw new InvalidArgumentException( - sprintf('Container has no entry named: "%s"', $listener->getService()) + sprintf('Container has no entry named: "%s"', $listener->getService()), ); } $dispatcher->listen( $eventName, - new LazyContainerListener($container, $listener) + new LazyContainerListener($container, $listener), ); return; } if (!is_callable($listener)) { throw new InvalidArgumentException( - 'Listener must be a callable or a container entry for a callable service.' + 'Listener must be a callable or a container entry for a callable service.', ); } $dispatcher->listen($eventName, $listener); diff --git a/src/Factory/MenuFactory.php b/src/Factory/MenuFactory.php index ed8bbf22..0715dfcd 100644 --- a/src/Factory/MenuFactory.php +++ b/src/Factory/MenuFactory.php @@ -78,7 +78,7 @@ function (CliMenu $menu) use ($exerciseRenderer, $eventDispatcher, $exercise) { $exerciseRenderer->__invoke($menu); }, $userState->completedExercise($exercise->getName()), - $this->isExerciseDisabled($exercise, $userState, $workshopType) + $this->isExerciseDisabled($exercise, $userState, $workshopType), ); } /** @var HelpCommand $helpCommand */ @@ -178,7 +178,7 @@ private function dispatchExerciseSelectedEvent(EventDispatcher $eventDispatcher, { $eventDispatcher->dispatch(new Event('exercise.selected', ['exercise' => $exercise])); $eventDispatcher->dispatch( - new Event(sprintf('exercise.selected.%s', AbstractExercise::normaliseName($exercise->getName()))) + new Event(sprintf('exercise.selected.%s', AbstractExercise::normaliseName($exercise->getName()))), ); } } diff --git a/src/Factory/ResultRendererFactory.php b/src/Factory/ResultRendererFactory.php index f1776d52..7232eafc 100644 --- a/src/Factory/ResultRendererFactory.php +++ b/src/Factory/ResultRendererFactory.php @@ -68,8 +68,8 @@ public function create(ResultInterface $result): ResultRendererInterface 'Renderer Factory for "%s" produced "%s" instead of expected "%s"', $class, is_object($renderer) ? get_class($renderer) : gettype($renderer), - $class - ) + $class, + ), ); } diff --git a/src/Listener/CodePatchListener.php b/src/Listener/CodePatchListener.php index eda7abe2..49ca18e2 100644 --- a/src/Listener/CodePatchListener.php +++ b/src/Listener/CodePatchListener.php @@ -8,8 +8,8 @@ use PhpSchool\PhpWorkshop\Event\EventInterface; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; +use PhpSchool\PhpWorkshop\Utils\Path; use Psr\Log\LoggerInterface; -use RuntimeException; /** * Listener which patches internal and student's solutions @@ -53,11 +53,14 @@ public function __construct(CodePatcher $codePatcher, LoggerInterface $logger, b */ public function patch(ExerciseRunnerEvent $event): void { - $files = [$event->getInput()->getArgument('program')]; + $files = [$event->getContext()->getEntryPoint()]; $exercise = $event->getExercise(); if ($exercise instanceof ProvidesSolution) { - $files[] = $exercise->getSolution()->getEntryPoint()->getAbsolutePath(); + $files[] = Path::join( + $event->getContext()->getReferenceExecutionDirectory(), + $exercise->getSolution()->getEntryPoint()->getRelativePath(), + ); } foreach (array_filter($files) as $fileName) { @@ -67,7 +70,7 @@ public function patch(ExerciseRunnerEvent $event): void file_put_contents( $fileName, - $this->codePatcher->patch($event->getExercise(), $this->originalCode[$fileName]) + $this->codePatcher->patch($event->getExercise(), $this->originalCode[$fileName]), ); } } @@ -83,11 +86,12 @@ public function revert(EventInterface $event): void //if we're in debug mode leave the students patch for debugging if ($event instanceof ExerciseRunnerEvent && $this->debugMode) { - unset($this->originalCode[$event->getInput()->getArgument('program')]); + unset($this->originalCode[$event->getContext()->getEntryPoint()]); } foreach ($this->originalCode as $fileName => $contents) { file_put_contents($fileName, $contents); + unset($this->originalCode[$fileName]); } } } diff --git a/src/Listener/ConfigureCommandListener.php b/src/Listener/ConfigureCommandListener.php index 98037100..fcb45f85 100644 --- a/src/Listener/ConfigureCommandListener.php +++ b/src/Listener/ConfigureCommandListener.php @@ -38,7 +38,7 @@ class ConfigureCommandListener public function __construct( UserState $userState, ExerciseRepository $exerciseRepository, - RunnerManager $runnerManager + RunnerManager $runnerManager, ) { $this->userState = $userState; $this->exerciseRepository = $exerciseRepository; @@ -58,7 +58,7 @@ public function __invoke(Event $event): void } $currentExercise = $this->exerciseRepository->findByName( - $this->userState->getCurrentExercise() + $this->userState->getCurrentExercise(), ); $this->runnerManager->configureInput($currentExercise, $command); diff --git a/src/Listener/InitialCodeListener.php b/src/Listener/InitialCodeListener.php index 6fb353e3..ac580be9 100644 --- a/src/Listener/InitialCodeListener.php +++ b/src/Listener/InitialCodeListener.php @@ -7,7 +7,6 @@ use PhpSchool\PhpWorkshop\Event\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ProvidesInitialCode; -use PhpSchool\PhpWorkshop\Solution\SolutionFile; use Psr\Log\LoggerInterface; /** @@ -60,8 +59,8 @@ public function __invoke(Event $event): void [ 'exercise' => $exercise->getName(), 'workingDir' => $this->workingDirectory, - 'file' => $file->getAbsolutePath() - ] + 'file' => $file->getAbsolutePath(), + ], ); } } diff --git a/src/Listener/LazyContainerListener.php b/src/Listener/LazyContainerListener.php index 5a06207f..efc8f29b 100644 --- a/src/Listener/LazyContainerListener.php +++ b/src/Listener/LazyContainerListener.php @@ -34,7 +34,7 @@ public function __invoke(...$args): void if (!method_exists($service, $this->listener->getMethod())) { throw new InvalidArgumentException( - sprintf('Method "%s" does not exist on "%s"', $this->listener->getMethod(), get_class($service)) + sprintf('Method "%s" does not exist on "%s"', $this->listener->getMethod(), get_class($service)), ); } @@ -49,7 +49,7 @@ public function getWrapped(): callable /** @var callable $listener */ $listener = [ $this->container->get($this->listener->getService()), - $this->listener->getMethod() + $this->listener->getMethod(), ]; return $listener; diff --git a/src/Listener/PrepareSolutionListener.php b/src/Listener/PrepareSolutionListener.php index ecac7e59..cd8225de 100644 --- a/src/Listener/PrepareSolutionListener.php +++ b/src/Listener/PrepareSolutionListener.php @@ -7,24 +7,18 @@ use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\Exception\RuntimeException; -use Symfony\Component\Process\Process; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessInput; /** * Listener to install composer deps for an exercise solution */ class PrepareSolutionListener { - /** - * Locations for composer executable - * - * @var array - */ - private static $composerLocations = [ - 'composer', - 'composer.phar', - '/usr/local/bin/composer', - __DIR__ . '/../../vendor/bin/composer', - ]; + public function __construct(private ProcessFactory $processFactory) + { + $this->processFactory = $processFactory; + } /** * @param ExerciseRunnerEvent $event @@ -39,36 +33,22 @@ public function __invoke(ExerciseRunnerEvent $event): void $solution = $exercise->getSolution(); - if ($solution->hasComposerFile()) { - //prepare composer deps - //only install if composer.lock file not available - - if (!file_exists(sprintf('%s/vendor', $solution->getBaseDirectory()))) { - $process = new Process( - [self::locateComposer(), 'install', '--no-interaction'], - $solution->getBaseDirectory() - ); - - try { - $process->mustRun(); - } catch (\Symfony\Component\Process\Exception\RuntimeException $e) { - throw new RuntimeException('Composer dependencies could not be installed', 0, $e); - } - } + if (!$solution->hasComposerFile()) { + return; } - } - /** - * @return string - */ - public static function locateComposer(): string - { - foreach (self::$composerLocations as $location) { - if (file_exists($location) && is_executable($location)) { - return $location; + //prepare composer deps + //only install if vendor folder not available + if (!file_exists(sprintf('%s/vendor', $event->getContext()->getReferenceExecutionDirectory()))) { + $process = $this->processFactory->create( + new ProcessInput('composer', ['install', '--no-interaction'], $event->getContext()->getReferenceExecutionDirectory(), []), + ); + + try { + $process->mustRun(); + } catch (\Symfony\Component\Process\Exception\RuntimeException $e) { + throw new RuntimeException('Composer dependencies could not be installed', 0, $e); } } - - throw new RuntimeException('Composer could not be located on the system'); } } diff --git a/src/Listener/SelfCheckListener.php b/src/Listener/SelfCheckListener.php index b70b0951..200fd00d 100644 --- a/src/Listener/SelfCheckListener.php +++ b/src/Listener/SelfCheckListener.php @@ -4,7 +4,7 @@ namespace PhpSchool\PhpWorkshop\Listener; -use PhpSchool\PhpWorkshop\Event\Event; +use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\ResultAggregator; @@ -14,30 +14,16 @@ */ class SelfCheckListener { - /** - * @var ResultAggregator - */ - private $results; + public function __construct(private ResultAggregator $results) {} - /** - * @param ResultAggregator $results - */ - public function __construct(ResultAggregator $results) + public function __invoke(ExerciseRunnerEvent $event): void { - $this->results = $results; - } - - /** - * @param Event $event - */ - public function __invoke(Event $event): void - { - $exercise = $event->getParameter('exercise'); + $exercise = $event->getContext()->getExercise(); if ($exercise instanceof SelfCheck) { /** @var Input $input */ $input = $event->getParameter('input'); - $this->results->add($exercise->check($input)); + $this->results->add($exercise->check($event->getContext())); } } } diff --git a/src/Listener/TearDownListener.php b/src/Listener/TearDownListener.php index 82dec2ea..db808ef8 100644 --- a/src/Listener/TearDownListener.php +++ b/src/Listener/TearDownListener.php @@ -4,7 +4,6 @@ namespace PhpSchool\PhpWorkshop\Listener; -use PhpSchool\PhpWorkshop\Event\EventInterface; use PhpSchool\PhpWorkshop\Utils\System; use Symfony\Component\Filesystem\Filesystem; diff --git a/src/Logger/ConsoleLogger.php b/src/Logger/ConsoleLogger.php index 0ded0d82..b134ca61 100644 --- a/src/Logger/ConsoleLogger.php +++ b/src/Logger/ConsoleLogger.php @@ -37,9 +37,9 @@ public function log($level, $message, array $context = []): void '%s - %s - %s', $this->color->fg('yellow', (new \DateTime())->format('H:i:s')), $this->color->bg('red', strtoupper($level)), - $this->color->fg('red', $message) + $this->color->fg('red', $message), ), - json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) + json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), ]; $this->output->writeLine(implode("\n", $parts)); diff --git a/src/Logger/Logger.php b/src/Logger/Logger.php index 0bc328b8..93d5acb8 100644 --- a/src/Logger/Logger.php +++ b/src/Logger/Logger.php @@ -37,9 +37,9 @@ public function log($level, $message, array $context = []): void (new \DateTime())->format('d-m-y H:i:s'), $level, $message, - json_encode($context) + json_encode($context), ), - FILE_APPEND + FILE_APPEND, ); } } diff --git a/src/Markdown/CurrentContext.php b/src/Markdown/CurrentContext.php index 6c7efd73..90233e52 100644 --- a/src/Markdown/CurrentContext.php +++ b/src/Markdown/CurrentContext.php @@ -20,7 +20,7 @@ public function __construct(string $context) throw InvalidArgumentException::notValidParameter( 'context', [self::CONTEXT_CLI, self::CONTEXT_CLOUD], - $context + $context, ); } $this->context = $context; diff --git a/src/Markdown/Parser/HandleBarParser.php b/src/Markdown/Parser/HandleBarParser.php index 24a76808..ee0bf251 100644 --- a/src/Markdown/Parser/HandleBarParser.php +++ b/src/Markdown/Parser/HandleBarParser.php @@ -4,9 +4,6 @@ namespace PhpSchool\PhpWorkshop\Markdown\Parser; -use League\CommonMark\Delimiter\Delimiter; -use League\CommonMark\Inline\Element\Code; -use League\CommonMark\Inline\Element\Text; use League\CommonMark\InlineParserContext; use League\CommonMark\Inline\Parser\InlineParserInterface; use PhpSchool\PhpWorkshop\Markdown\Shorthands\ShorthandInterface; @@ -27,7 +24,7 @@ public function __construct(array $shorthands) array_map(function (ShorthandInterface $shorthand) { return $shorthand->getCode(); }, $shorthands), - $shorthands + $shorthands, ) ?: []; } diff --git a/src/Markdown/ProblemFileExtension.php b/src/Markdown/ProblemFileExtension.php index 80d96fc2..948e8f04 100644 --- a/src/Markdown/ProblemFileExtension.php +++ b/src/Markdown/ProblemFileExtension.php @@ -29,7 +29,7 @@ final class ProblemFileExtension implements ExtensionInterface */ public function __construct( ContextSpecificRenderer $contextSpecificRenderer, - array $shorthandExpanders + array $shorthandExpanders, ) { $this->contextSpecificRenderer = $contextSpecificRenderer; $this->shorthandExpanders = $shorthandExpanders; diff --git a/src/Markdown/Shorthands/Cli/AppName.php b/src/Markdown/Shorthands/Cli/AppName.php index b2c68c22..676a2169 100644 --- a/src/Markdown/Shorthands/Cli/AppName.php +++ b/src/Markdown/Shorthands/Cli/AppName.php @@ -8,7 +8,6 @@ use League\CommonMark\Inline\Element\Emphasis; use League\CommonMark\Inline\Element\Strong; use League\CommonMark\Inline\Element\Text; -use League\CommonMark\Node\Node; use PhpSchool\PhpWorkshop\Markdown\Shorthands\ShorthandInterface; final class AppName implements ShorthandInterface diff --git a/src/Markdown/Shorthands/Cloud/AppName.php b/src/Markdown/Shorthands/Cloud/AppName.php index a8d1dadb..2957719d 100644 --- a/src/Markdown/Shorthands/Cloud/AppName.php +++ b/src/Markdown/Shorthands/Cloud/AppName.php @@ -4,10 +4,6 @@ namespace PhpSchool\PhpWorkshop\Markdown\Shorthands\Cloud; -use League\CommonMark\Inline\Element\Code; -use League\CommonMark\Inline\Element\Emphasis; -use League\CommonMark\Inline\Element\Strong; -use League\CommonMark\Inline\Element\Text; use League\CommonMark\Node\Node; use PhpSchool\PhpWorkshop\Markdown\Shorthands\ShorthandInterface; diff --git a/src/Markdown/Shorthands/Documentation.php b/src/Markdown/Shorthands/Documentation.php index b3069231..b71c8308 100644 --- a/src/Markdown/Shorthands/Documentation.php +++ b/src/Markdown/Shorthands/Documentation.php @@ -31,9 +31,9 @@ public function __invoke(array $callArgs): array new Text('Documentation on '), new Code($callout), new Text(' can be found by pointing your browser here:'), - new Newline() + new Newline(), ], - $links + $links, ); } diff --git a/src/Output/StdOutput.php b/src/Output/StdOutput.php index 35119b21..e9cc6439 100644 --- a/src/Output/StdOutput.php +++ b/src/Output/StdOutput.php @@ -67,7 +67,7 @@ public function printException(Throwable $exception): void sprintf("In %s line %d:", $file, $exception->getLine()), sprintf("[%s (%s)]:", get_class($exception), $exception->getCode()), '', - $message + $message, ]; $length = max(array_map('strlen', $lines)) + 2; @@ -85,11 +85,13 @@ public function printException(Throwable $exception): void $this->writeLine( implode( "\n", - array_map(function ($l) { - return " $l"; - }, - explode("\n", $exception->getTraceAsString())) - ) + array_map( + function ($l) { + return " $l"; + }, + explode("\n", $exception->getTraceAsString()), + ), + ), ); } diff --git a/src/Patch/ForceStrictTypes.php b/src/Patch/ForceStrictTypes.php index 445996cf..ec4e314c 100644 --- a/src/Patch/ForceStrictTypes.php +++ b/src/Patch/ForceStrictTypes.php @@ -22,8 +22,8 @@ public function transform(array $statements): array $declare = new \PhpParser\Node\Stmt\Declare_([ new DeclareDeclare( new \PhpParser\Node\Identifier('strict_types'), - new LNumber(1) - ) + new LNumber(1), + ), ]); return array_merge([$declare], $statements); diff --git a/src/Patch/WrapInTryCatch.php b/src/Patch/WrapInTryCatch.php index c3d86428..4988fe73 100644 --- a/src/Patch/WrapInTryCatch.php +++ b/src/Patch/WrapInTryCatch.php @@ -31,8 +31,8 @@ public function __construct(string $exceptionClass = \Exception::class, array $s $this->exceptionClass = $exceptionClass; $this->statements = $statements ?: [ new Echo_([ - new MethodCall(new Variable('e'), 'getMessage') - ]) + new MethodCall(new Variable('e'), 'getMessage'), + ]), ]; } @@ -49,10 +49,10 @@ public function transform(array $statements): array new Catch_( [new Name($this->exceptionClass)], new Variable('e'), - $this->statements - ) - ] - ) + $this->statements, + ), + ], + ), ]; } } diff --git a/src/Process/DockerProcessFactory.php b/src/Process/DockerProcessFactory.php index ed4d965a..5f4ab810 100644 --- a/src/Process/DockerProcessFactory.php +++ b/src/Process/DockerProcessFactory.php @@ -4,10 +4,8 @@ namespace PhpSchool\PhpWorkshop\Process; -use PhpSchool\PhpWorkshop\Utils\Collection; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; -use PhpSchool\PhpWorkshop\Utils\System; final class DockerProcessFactory implements ProcessFactory { @@ -20,7 +18,7 @@ public function __construct( string $basePath, string $projectName, string $composerCacheDir, - ExecutableFinder $executableFinder = null + ExecutableFinder $executableFinder = null, ) { $this->executableFinder = $executableFinder ?? new ExecutableFinder(); $this->basePath = $basePath; @@ -45,12 +43,12 @@ public function create(ProcessInput $processInput): Process ...$this->baseComposeCommand($mounts, $env), 'runtime', $processInput->getExecutable(), - ...$processInput->getArgs() + ...$processInput->getArgs(), ], $this->basePath, ['SOLUTION' => $processInput->getWorkingDirectory()], $processInput->getInput(), - 10 + 10, ); } @@ -78,7 +76,7 @@ private function baseComposeCommand(array $mounts, array $env): array ...$env, '-w', '/solution', - ...array_merge(...array_map(fn ($mount) => ['-v', $mount], $mounts)), + ...array_merge(...array_map(fn($mount) => ['-v', $mount], $mounts)), ]; } } diff --git a/src/Process/HostProcessFactory.php b/src/Process/HostProcessFactory.php index 7ea0405b..453bffc1 100644 --- a/src/Process/HostProcessFactory.php +++ b/src/Process/HostProcessFactory.php @@ -4,7 +4,6 @@ namespace PhpSchool\PhpWorkshop\Process; -use PhpSchool\PhpWorkshop\Utils\Collection; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -40,8 +39,8 @@ public function create(ProcessInput $processInput): Process */ private function getDefaultEnv(): array { - $env = array_map(fn () => false, $_ENV); - $env + array_map(fn () => false, $_SERVER); + $env = array_map(fn() => false, $_ENV); + $env + array_map(fn() => false, $_SERVER); return $env; } diff --git a/src/Process/ProcessFactory.php b/src/Process/ProcessFactory.php index 37a79d92..8e7faffa 100644 --- a/src/Process/ProcessFactory.php +++ b/src/Process/ProcessFactory.php @@ -4,8 +4,6 @@ namespace PhpSchool\PhpWorkshop\Process; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; -use PhpSchool\PhpWorkshop\Utils\Collection; use Symfony\Component\Process\Process; interface ProcessFactory diff --git a/src/Process/ProcessInput.php b/src/Process/ProcessInput.php index 98fa781d..067031d2 100644 --- a/src/Process/ProcessInput.php +++ b/src/Process/ProcessInput.php @@ -13,9 +13,8 @@ public function __construct( private array $args, private string $workingDirectory, private array $env, - private ?string $input = null - ) { - } + private ?string $input = null, + ) {} public function getExecutable(): string { diff --git a/src/Result/Cgi/CgiResult.php b/src/Result/Cgi/CgiResult.php index 12b570af..bef5b1c7 100644 --- a/src/Result/Cgi/CgiResult.php +++ b/src/Result/Cgi/CgiResult.php @@ -65,7 +65,7 @@ public function isSuccessful(): bool return count( array_filter($this->results, function ($result) { return $result instanceof FailureInterface; - }) + }), ) === 0; } diff --git a/src/Result/Cgi/GenericFailure.php b/src/Result/Cgi/GenericFailure.php index 48982bb6..7c186c4e 100644 --- a/src/Result/Cgi/GenericFailure.php +++ b/src/Result/Cgi/GenericFailure.php @@ -54,7 +54,7 @@ public static function fromRequestAndReason(RequestInterface $request, string $r */ public static function fromRequestAndCodeExecutionFailure( RequestInterface $request, - CodeExecutionException $e + CodeExecutionException $e, ): self { return new self($request, $e->getMessage()); } diff --git a/src/Result/Cgi/RequestFailure.php b/src/Result/Cgi/RequestFailure.php index 76d4d023..6774ea43 100644 --- a/src/Result/Cgi/RequestFailure.php +++ b/src/Result/Cgi/RequestFailure.php @@ -49,7 +49,7 @@ public function __construct( string $expectedOutput, string $actualOutput, array $expectedHeaders, - array $actualHeaders + array $actualHeaders, ) { $this->request = $request; $this->expectedOutput = $expectedOutput; diff --git a/src/Result/Cgi/SuccessInterface.php b/src/Result/Cgi/SuccessInterface.php index 1e35910b..ca2c232f 100644 --- a/src/Result/Cgi/SuccessInterface.php +++ b/src/Result/Cgi/SuccessInterface.php @@ -8,6 +8,4 @@ * This interface represents a success. Any result implementing this interface will * be treated as a success. */ -interface SuccessInterface extends ResultInterface -{ -} +interface SuccessInterface extends ResultInterface {} diff --git a/src/Result/Cli/CliResult.php b/src/Result/Cli/CliResult.php index af33c959..e9a4d50e 100644 --- a/src/Result/Cli/CliResult.php +++ b/src/Result/Cli/CliResult.php @@ -65,7 +65,7 @@ public function isSuccessful(): bool return count( array_filter($this->results, function ($result) { return $result instanceof FailureInterface; - }) + }), ) === 0; } diff --git a/src/Result/Cli/RequestFailure.php b/src/Result/Cli/RequestFailure.php index 62d83adb..981f5ffa 100644 --- a/src/Result/Cli/RequestFailure.php +++ b/src/Result/Cli/RequestFailure.php @@ -104,7 +104,7 @@ public function toArray(): array return [ 'args' => $this->getArgs()->getArrayCopy(), 'expected_output' => $this->getExpectedOutput(), - 'actual_output' => $this->getActualOutput() + 'actual_output' => $this->getActualOutput(), ]; } } diff --git a/src/Result/Cli/SuccessInterface.php b/src/Result/Cli/SuccessInterface.php index ddc1bd36..65508532 100644 --- a/src/Result/Cli/SuccessInterface.php +++ b/src/Result/Cli/SuccessInterface.php @@ -8,6 +8,4 @@ * This interface represents a success. Any result implementing this interface will * be treated as a success. */ -interface SuccessInterface extends ResultInterface -{ -} +interface SuccessInterface extends ResultInterface {} diff --git a/src/Result/ComparisonFailure.php b/src/Result/ComparisonFailure.php index 9af73a6f..57ba567c 100644 --- a/src/Result/ComparisonFailure.php +++ b/src/Result/ComparisonFailure.php @@ -84,7 +84,7 @@ public function toArray(): array { return [ 'expected_value' => $this->getExpectedValue(), - 'actual_value' => $this->getActualValue() + 'actual_value' => $this->getActualValue(), ]; } } diff --git a/src/Result/ComposerFailure.php b/src/Result/ComposerFailure.php index 9b60d4f7..2fefe188 100644 --- a/src/Result/ComposerFailure.php +++ b/src/Result/ComposerFailure.php @@ -43,7 +43,7 @@ public function __construct(CheckInterface $check, string $missingComponent = nu throw InvalidArgumentException::notValidParameter( 'missingComponent', self::$validComponents, - $missingComponent + $missingComponent, ); } $this->missingComponent = $missingComponent; @@ -99,7 +99,7 @@ public function toArray(): array 'is_missing_component' => $this->isMissingComponent(), 'is_missing_packages' => $this->isMissingPackages(), 'missing_component' => $this->getMissingComponent(), - 'missing_packages' => $this->getMissingPackages() + 'missing_packages' => $this->getMissingPackages(), ]; } } diff --git a/src/Result/Failure.php b/src/Result/Failure.php index ef6b281e..85bf227b 100644 --- a/src/Result/Failure.php +++ b/src/Result/Failure.php @@ -84,11 +84,11 @@ public static function fromNameAndCodeExecutionFailure(string $name, CodeExecuti public static function fromCheckAndCodeParseFailure( CheckInterface $check, ParseErrorException $e, - string $file + string $file, ): self { return new self( $check->getName(), - sprintf('File: "%s" could not be parsed. Error: "%s"', $file, $e->getMessage()) + sprintf('File: "%s" could not be parsed. Error: "%s"', $file, $e->getMessage()), ); } diff --git a/src/Result/FileComparisonFailure.php b/src/Result/FileComparisonFailure.php index 6f38a28d..93851761 100644 --- a/src/Result/FileComparisonFailure.php +++ b/src/Result/FileComparisonFailure.php @@ -80,7 +80,7 @@ public function toArray(): array return [ 'file_name' => $this->getFileName(), 'expected_value' => $this->getExpectedValue(), - 'actual_value' => $this->getActualValue() + 'actual_value' => $this->getActualValue(), ]; } } diff --git a/src/Result/FunctionRequirementsFailure.php b/src/Result/FunctionRequirementsFailure.php index 1fbc403e..2d745c4a 100644 --- a/src/Result/FunctionRequirementsFailure.php +++ b/src/Result/FunctionRequirementsFailure.php @@ -66,7 +66,7 @@ public function toArray(): array { return [ 'banned_functions' => $this->getBannedFunctions(), - 'missing_functions' => $this->getMissingFunctions() + 'missing_functions' => $this->getMissingFunctions(), ]; } } diff --git a/src/Result/SuccessInterface.php b/src/Result/SuccessInterface.php index 675fcaef..1248466c 100644 --- a/src/Result/SuccessInterface.php +++ b/src/Result/SuccessInterface.php @@ -8,6 +8,4 @@ * This interface represents a success. Any result implementing this interface will * be treated as a success. */ -interface SuccessInterface extends ResultInterface -{ -} +interface SuccessInterface extends ResultInterface {} diff --git a/src/ResultAggregator.php b/src/ResultAggregator.php index c750b82d..0c014334 100644 --- a/src/ResultAggregator.php +++ b/src/ResultAggregator.php @@ -47,7 +47,7 @@ public function isSuccessful(): bool return !$result->isSuccessful(); } return $result instanceof FailureInterface; - }) + }), ) === 0; } diff --git a/src/ResultRenderer/Cgi/RequestFailureRenderer.php b/src/ResultRenderer/Cgi/RequestFailureRenderer.php index 0bcd9cb7..a3864fc8 100644 --- a/src/ResultRenderer/Cgi/RequestFailureRenderer.php +++ b/src/ResultRenderer/Cgi/RequestFailureRenderer.php @@ -41,7 +41,7 @@ public function render(ResultsRenderer $renderer): string $renderer->style('YOUR HEADERS:', ['bold', 'yellow']), $this->headers($this->result->getActualHeaders(), $renderer), $renderer->style('EXPECTED HEADERS:', ['bold', 'yellow']), - $this->headers($this->result->getExpectedHeaders(), $renderer, false) + $this->headers($this->result->getExpectedHeaders(), $renderer, false), ); } @@ -55,7 +55,7 @@ public function render(ResultsRenderer $renderer): string $renderer->style('YOUR OUTPUT:', ['bold', 'yellow']), $renderer->style(sprintf('"%s"', $this->result->getActualOutput()), 'red'), $renderer->style('EXPECTED OUTPUT:', ['bold', 'yellow']), - $renderer->style(sprintf('"%s"', $this->result->getExpectedOutput()), 'green') + $renderer->style(sprintf('"%s"', $this->result->getExpectedOutput()), 'green'), ); } diff --git a/src/ResultRenderer/Cli/RequestFailureRenderer.php b/src/ResultRenderer/Cli/RequestFailureRenderer.php index 19f807eb..1bd12127 100644 --- a/src/ResultRenderer/Cli/RequestFailureRenderer.php +++ b/src/ResultRenderer/Cli/RequestFailureRenderer.php @@ -39,7 +39,7 @@ public function render(ResultsRenderer $renderer): string $renderer->style('YOUR OUTPUT:', ['bold', 'yellow']), $this->indent($renderer->style(sprintf('"%s"', $this->result->getActualOutput()), 'red')), $renderer->style('EXPECTED OUTPUT:', ['bold', 'yellow']), - $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedOutput()), 'green')) + $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedOutput()), 'green')), ); } @@ -55,8 +55,8 @@ private function indent(string $string): string function ($line) { return sprintf(" %s", $line); }, - explode("\n", $string) - ) + explode("\n", $string), + ), ); } } diff --git a/src/ResultRenderer/ComparisonFailureRenderer.php b/src/ResultRenderer/ComparisonFailureRenderer.php index b6559e31..a3f0ae7a 100644 --- a/src/ResultRenderer/ComparisonFailureRenderer.php +++ b/src/ResultRenderer/ComparisonFailureRenderer.php @@ -37,7 +37,7 @@ public function render(ResultsRenderer $renderer): string $renderer->style('YOUR OUTPUT:', ['bold', 'yellow']), $this->indent($renderer->style(sprintf('"%s"', $this->result->getActualValue()), 'red')), $renderer->style('EXPECTED OUTPUT:', ['bold', 'yellow']), - $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedValue()), 'green')) + $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedValue()), 'green')), ); } @@ -53,8 +53,8 @@ private function indent(string $string): string function ($line) { return sprintf(' %s', $line); }, - explode("\n", $string) - ) + explode("\n", $string), + ), ); } } diff --git a/src/ResultRenderer/ComposerFailureRenderer.php b/src/ResultRenderer/ComposerFailureRenderer.php index 750115f7..d60cce86 100644 --- a/src/ResultRenderer/ComposerFailureRenderer.php +++ b/src/ResultRenderer/ComposerFailureRenderer.php @@ -36,7 +36,7 @@ public function render(ResultsRenderer $renderer): string /** @var string $component */ $component = $this->result->getMissingComponent(); - $type = str_contains($component, '.') ? 'file' : 'folder'; + $type = str_contains($component, '.') ? 'file' : 'folder'; return $renderer->center("No $component $type found") . "\n"; } @@ -46,7 +46,7 @@ public function render(ResultsRenderer $renderer): string return $renderer->center(sprintf( "Lockfile doesn't include the following packages at any version: \"%s\"\n", - implode('", "', $missingPackages) + implode('", "', $missingPackages), )); } diff --git a/src/ResultRenderer/FileComparisonFailureRenderer.php b/src/ResultRenderer/FileComparisonFailureRenderer.php index 09914cba..9a2f6035 100644 --- a/src/ResultRenderer/FileComparisonFailureRenderer.php +++ b/src/ResultRenderer/FileComparisonFailureRenderer.php @@ -39,7 +39,7 @@ public function render(ResultsRenderer $renderer): string $this->indent($renderer->style(sprintf('"%s"', $this->result->getActualValue()), 'red')), $renderer->style('EXPECTED OUTPUT FOR: ', ['bold', 'yellow']), $renderer->style($this->result->getFileName(), ['bold', 'green']), - $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedValue()), 'green')) + $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedValue()), 'green')), ); } @@ -55,8 +55,8 @@ private function indent(string $string): string function ($line) { return sprintf(' %s', $line); }, - explode("\n", $string) - ) + explode("\n", $string), + ), ); } } diff --git a/src/ResultRenderer/FunctionRequirementsFailureRenderer.php b/src/ResultRenderer/FunctionRequirementsFailureRenderer.php index fb75f66e..7a908c1b 100644 --- a/src/ResultRenderer/FunctionRequirementsFailureRenderer.php +++ b/src/ResultRenderer/FunctionRequirementsFailureRenderer.php @@ -38,11 +38,11 @@ public function render(ResultsRenderer $renderer): string " %s\n%s\n", $renderer->style( "Some functions were used which should not be used in this exercise", - ['bold', 'underline', 'yellow'] + ['bold', 'underline', 'yellow'], ), implode("\n", array_map(function (array $call) { return sprintf(' %s on line %s', $call['function'], $call['line']); - }, $bannedFunctions)) + }, $bannedFunctions)), ); } @@ -51,11 +51,11 @@ public function render(ResultsRenderer $renderer): string " %s\n%s\n", $renderer->style( "Some function requirements were missing. You should use the functions", - ['bold', 'underline', 'yellow'] + ['bold', 'underline', 'yellow'], ), implode("\n", array_map(function ($function) { return sprintf(' %s', $function); - }, $missingFunctions)) + }, $missingFunctions)), ); } diff --git a/src/ResultRenderer/ResultsRenderer.php b/src/ResultRenderer/ResultsRenderer.php index 192d7692..76ac13e7 100644 --- a/src/ResultRenderer/ResultsRenderer.php +++ b/src/ResultRenderer/ResultsRenderer.php @@ -72,7 +72,7 @@ public function __construct( Terminal $terminal, ExerciseRepository $exerciseRepository, KeyLighter $keyLighter, - ResultRendererFactory $resultRendererFactory + ResultRendererFactory $resultRendererFactory, ) { $this->color = $color; $this->terminal = $terminal; @@ -95,7 +95,7 @@ public function render( ResultAggregator $results, ExerciseInterface $exercise, UserState $userState, - OutputInterface $output + OutputInterface $output, ): void { $successes = []; $failures = []; @@ -120,7 +120,7 @@ public function render( foreach ($successes as $success) { $output->writeLine($this->center($this->style(str_repeat(' ', $longest), ['bg_green']))); $output->writeLine( - $this->center($this->style(mb_str_pad($success, $longest), ['bg_green', 'white', 'bold'])) + $this->center($this->style(mb_str_pad($success, $longest), ['bg_green', 'white', 'bold'])), ); $output->writeLine($this->center($this->style(str_repeat(' ', $longest), ['bg_green']))); $output->emptyLine(); @@ -143,7 +143,7 @@ private function renderErrorInformation( array $failures, int $padLength, ExerciseInterface $exercise, - OutputInterface $output + OutputInterface $output, ): void { foreach ($failures as [$failure, $message]) { $output->writeLine($this->center($this->style(str_repeat(' ', $padLength), ['bg_red']))); @@ -161,7 +161,7 @@ private function renderErrorInformation( $output->emptyLine(); $output->writeLine( - $this->center(sprintf(" Your solution to %s didn't pass. Try again!", $exercise->getName())) + $this->center(sprintf(" Your solution to %s didn't pass. Try again!", $exercise->getName())), ); $output->emptyLine(); $output->emptyLine(); @@ -175,7 +175,7 @@ private function renderErrorInformation( private function renderSuccessInformation( ExerciseInterface $exercise, UserState $userState, - OutputInterface $output + OutputInterface $output, ): void { $output->lineBreak(); $output->emptyLine(); @@ -195,7 +195,7 @@ private function renderSuccessInformation( $code = $this->keyLighter->highlight( $file->getContents(), $this->keyLighter->languageByExt('.' . $file->getExtension()), - new CliFormatter() + new CliFormatter(), ); //make sure there is a new line at the end @@ -251,10 +251,10 @@ private function fullWidthBlock(OutputInterface $output, string $string, array $ '%s%s%s', str_repeat(' ', (int) $start), $string, - str_repeat(' ', (int) ($this->terminal->getWidth() - $stringLength - $start)) + str_repeat(' ', (int) ($this->terminal->getWidth() - $stringLength - $start)), ), - $style - ) + $style, + ), ); $output->writeLine($this->style(str_repeat(' ', $this->terminal->getWidth()), $style)); } diff --git a/src/Solution/DirectorySolution.php b/src/Solution/DirectorySolution.php index 2e8c9d8a..5f1425f3 100644 --- a/src/Solution/DirectorySolution.php +++ b/src/Solution/DirectorySolution.php @@ -50,7 +50,7 @@ public function __construct(string $directory, string $entryPoint, array $exclus new RecursiveCallbackFilterIterator($dir, function (SplFileInfo $current) use ($exclusions) { return !in_array($current->getBasename(), $exclusions, true); }), - RecursiveIteratorIterator::SELF_FIRST + RecursiveIteratorIterator::SELF_FIRST, ); $files = []; @@ -64,7 +64,7 @@ public function __construct(string $directory, string $entryPoint, array $exclus if (!in_array($entryPoint, $files, true)) { throw new InvalidArgumentException( - sprintf('Entry point: "%s" does not exist in: "%s"', $entryPoint, $directory) + sprintf('Entry point: "%s" does not exist in: "%s"', $entryPoint, $directory), ); } @@ -88,7 +88,6 @@ public function __construct(string $directory, string $entryPoint, array $exclus */ public static function fromDirectory(string $directory, array $exclusions = [], $entryPoint = 'solution.php'): self { - $directory = InTempSolutionMapper::mapDirectory($directory); return new self($directory, $entryPoint, array_merge($exclusions, ['composer.lock', 'vendor'])); } diff --git a/src/Solution/InTempSolutionMapper.php b/src/Solution/InTempSolutionMapper.php deleted file mode 100644 index 2511bc22..00000000 --- a/src/Solution/InTempSolutionMapper.php +++ /dev/null @@ -1,58 +0,0 @@ -mkdir($tempDir); - - $dirIterator = new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS); - $iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); - - foreach ($iterator as $file) { - /** @var \SplFileInfo $file */ - $target = Path::join($tempDir, $iterator->getSubPathName()); - - if ($fileSystem->exists($target)) { - continue; - } - - $file->isDir() - ? $fileSystem->mkdir($target) - : $fileSystem->copy($file->getPathname(), $target); - } - - return $tempDir; - } - - public static function mapFile(string $file): string - { - $fileSystem = new Filesystem(); - $tempFile = Path::join(self::getDeterministicTempDir($file), basename($file)); - - if ($fileSystem->exists($tempFile)) { - return $tempFile; - } - - $fileSystem->mkdir(dirname($tempFile)); - $fileSystem->copy($file, $tempFile); - - return $tempFile; - } - - private static function getDeterministicTempDir(string $path): string - { - return Path::join(System::tempDir(), md5($path)); - } -} diff --git a/src/Solution/SingleFileSolution.php b/src/Solution/SingleFileSolution.php index 3964c91d..82e57e06 100644 --- a/src/Solution/SingleFileSolution.php +++ b/src/Solution/SingleFileSolution.php @@ -38,7 +38,7 @@ public function __construct(string $file) */ public static function fromFile(string $file): self { - return new self(InTempSolutionMapper::mapFile($file)); + return new self($file); } /** diff --git a/src/TestUtils/WorkshopExerciseTest.php b/src/TestUtils/WorkshopExerciseTest.php index b98a0930..46189cc6 100644 --- a/src/TestUtils/WorkshopExerciseTest.php +++ b/src/TestUtils/WorkshopExerciseTest.php @@ -13,7 +13,6 @@ use PhpSchool\PhpWorkshop\ExerciseCheck\ComposerExerciseCheck; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\ExerciseRepository; -use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; use PhpSchool\PhpWorkshop\Result\Cli\CliResult; use PhpSchool\PhpWorkshop\Result\Failure; @@ -21,7 +20,6 @@ use PhpSchool\PhpWorkshop\Result\ResultGroupInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; use PhpSchool\PhpWorkshop\ResultAggregator; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; use PhpSchool\PhpWorkshop\Utils\Collection; use PhpSchool\PhpWorkshop\Utils\System; use PHPUnit\Framework\ExpectationFailedException; @@ -29,6 +27,7 @@ use Psr\Container\ContainerInterface; use PhpSchool\PhpWorkshop\Input\Input; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; use function PhpSchool\PhpWorkshop\collect; @@ -82,7 +81,7 @@ public function runExercise(string $submissionFile): void '%s/test/solutions/%s/%s', rtrim($this->container->get('basePath'), '/'), AbstractExercise::normaliseName($exercise->getName()), - $submissionFile + $submissionFile, ); if (!file_exists($submissionFileAbsolute)) { @@ -90,8 +89,8 @@ public function runExercise(string $submissionFile): void sprintf( 'Submission file "%s" does not exist in "%s"', $submissionFile, - dirname($submissionFileAbsolute) - ) + dirname($submissionFileAbsolute), + ), ); } @@ -100,7 +99,7 @@ public function runExercise(string $submissionFile): void } $input = new Input($this->container->get('appName'), [ - 'program' => $submissionFileAbsolute + 'program' => $submissionFileAbsolute, ]); $this->results = $this->container->get(ExerciseDispatcher::class) @@ -114,9 +113,12 @@ public function runExercise(string $submissionFile): void private function installDeps(ExerciseInterface $exercise, string $directory): void { if (file_exists("$directory/composer.json") && !file_exists("$directory/vendor")) { + $execFinder = new ExecutableFinder(); + $execFinder->addSuffix('.phar'); + $process = new Process( - [PrepareSolutionListener::locateComposer(), 'install', '--no-interaction'], - $directory + [$execFinder->find('composer'), 'install', '--no-interaction'], + $directory, ); $process->mustRun(); } @@ -132,7 +134,7 @@ public function assertVerifyWasSuccessful(): void return sprintf( ' * %s%s', get_class($failure), - $failure instanceof Failure ? ": {$failure->getReason()}" : '' + $failure instanceof Failure ? ": {$failure->getReason()}" : '', ); }); @@ -171,7 +173,7 @@ public function assertResultsHasFailure(string $resultClass, string $reason): vo return sprintf( ' * %s%s', get_class($failure), - $failure instanceof Failure ? ": {$failure->getReason()}" : '' + $failure instanceof Failure ? ": {$failure->getReason()}" : '', ); }); @@ -242,7 +244,7 @@ public function removeSolutionAsset(string $file): void '%s/test/solutions/%s/%s', rtrim($this->container->get('basePath'), '/'), AbstractExercise::normaliseName($this->getExercise()->getName()), - $file + $file, ); (new Filesystem())->remove($path); diff --git a/src/UserState/LocalJsonSerializer.php b/src/UserState/LocalJsonSerializer.php index 99368303..1695ef69 100644 --- a/src/UserState/LocalJsonSerializer.php +++ b/src/UserState/LocalJsonSerializer.php @@ -44,7 +44,7 @@ class LocalJsonSerializer implements Serializer public function __construct( string $saveFileDirectory, string $workshopName, - ExerciseRepository $exerciseRepository + ExerciseRepository $exerciseRepository, ) { $this->workshopName = $workshopName; $this->path = $saveFileDirectory; @@ -129,7 +129,7 @@ public function deSerialize(): UserState return new UserState( $json['completed_exercises'], - $json['current_exercise'] + $json['current_exercise'], ); } @@ -172,7 +172,7 @@ private function migrateData(string $legacySaveFile): ?UserState $userState = new UserState( $data['completed_exercises'], - is_string($data['current_exercise']) ? $data['current_exercise'] : null + is_string($data['current_exercise']) ? $data['current_exercise'] : null, ); $this->serialize($userState); diff --git a/src/Utils/Collection.php b/src/Utils/Collection.php index f0dc1136..1029ac2c 100644 --- a/src/Utils/Collection.php +++ b/src/Utils/Collection.php @@ -10,6 +10,4 @@ * @template T * @extends ArrayObject */ -class Collection extends ArrayObject -{ -} +class Collection extends ArrayObject {} diff --git a/src/Utils/Path.php b/src/Utils/Path.php index d3a4db77..a12e4f7f 100644 --- a/src/Utils/Path.php +++ b/src/Utils/Path.php @@ -12,8 +12,8 @@ public static function join(string $base, string ...$parts): string [rtrim($base, '/')], array_map(function (string $part) { return trim($part, '/'); - }, array_filter($parts)) - ) + }, array_filter($parts)), + ), ); } } diff --git a/src/Utils/StringUtils.php b/src/Utils/StringUtils.php index d3f9cffd..e2073f5a 100644 --- a/src/Utils/StringUtils.php +++ b/src/Utils/StringUtils.php @@ -57,9 +57,9 @@ public static function pluralise(string $string, array $items, string ...$args): str_replace( array_keys(self::$pluraliseSearchReplace), array_values(self::$pluraliseSearchReplace), - $string + $string, ), - $args + $args, ); } } diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index dd43f743..d8edbc04 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -14,10 +14,7 @@ use PhpSchool\PhpWorkshop\Output\OutputInterface; use PhpSchool\PhpWorkshopTest\Asset\MockEventDispatcher; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use PhpSchool\PhpWorkshop\Logger\ConsoleLogger; -use PhpSchool\PhpWorkshop\Logger\Logger; -use PHPUnit\Framework\TestCase; class ApplicationTest extends BaseTest { @@ -66,9 +63,9 @@ public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void 'entry1', 'entry2', 'entry3', - ] + ], ], - $eventListeners + $eventListeners, ); } @@ -139,8 +136,8 @@ public function testLoggingExceptionDuringTearDown(): void static function () use ($exception) { throw $exception; }, - ] - ] + ], + ], ]); $commandRouter = $container->get(CommandRouter::class); diff --git a/test/Asset/CgiExerciseImpl.php b/test/Asset/CgiExerciseImpl.php index d8c23c96..8f3e7516 100644 --- a/test/Asset/CgiExerciseImpl.php +++ b/test/Asset/CgiExerciseImpl.php @@ -2,25 +2,23 @@ namespace PhpSchool\PhpWorkshopTest\Asset; -use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\ExerciseDispatcher; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; -use Psr\Http\Message\RequestInterface; class CgiExerciseImpl implements ExerciseInterface, CgiExercise { - /** - * @var string - */ - private $name; + private string $name; + private SolutionInterface $solution; + private CgiScenario $scenario; public function __construct(string $name = 'my-exercise') { $this->name = $name; + $this->scenario = new CgiScenario(); } public function getName(): string @@ -33,9 +31,14 @@ public function getDescription(): string return $this->name; } + public function setSolution(SolutionInterface $solution): void + { + $this->solution = $solution; + } + public function getSolution(): SolutionInterface { - // TODO: Implement getSolution() method. + return $this->solution; } public function getProblem(): string @@ -48,17 +51,6 @@ public function tearDown(): void // TODO: Implement tearDown() method. } - /** - * This method should return an array of PSR-7 requests, which will be forwarded to the student's - * solution. - * - * @return RequestInterface[] An array of PSR-7 requests. - */ - public function getRequests(): array - { - return []; // TODO: Implement getRequests() method. - } - public function getType(): ExerciseType { return ExerciseType::CGI(); @@ -69,7 +61,16 @@ public function getRequiredChecks(): array return []; } - public function defineListeners(EventDispatcher $dispatcher): void + public function defineListeners(EventDispatcher $dispatcher): void {} + + public function setScenario(CgiScenario $scenario): void { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CgiScenario + { + return $this->scenario; + } } diff --git a/test/Asset/CgiExerciseInterface.php b/test/Asset/CgiExerciseInterface.php deleted file mode 100644 index 3661d990..00000000 --- a/test/Asset/CgiExerciseInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -name = $name; + $this->scenario = new CliScenario(); } public function getName(): string @@ -50,7 +44,12 @@ public function getSolution(): SolutionInterface public function getProblem(): string { - // TODO: Implement getProblem() method. + return $this->problemFile; + } + + public function setProblem(string $problemFile): void + { + $this->problemFile = $problemFile; } public function tearDown(): void @@ -58,9 +57,14 @@ public function tearDown(): void // TODO: Implement tearDown() method. } - public function getArgs(): array + public function setScenario(CliScenario $scenario): void + { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CliScenario { - return []; // TODO: Implement getArgs() method. + return $this->scenario; } public function getType(): ExerciseType @@ -73,7 +77,6 @@ public function getRequiredChecks(): array return []; } - public function defineListeners(EventDispatcher $dispatcher): void - { - } + + public function defineListeners(EventDispatcher $dispatcher): void {} } diff --git a/test/Asset/CliExerciseInterface.php b/test/Asset/CliExerciseInterface.php deleted file mode 100644 index 486477b8..00000000 --- a/test/Asset/CliExerciseInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -scenario = new CliScenario(); + } + + public function setSeeder(\Closure $seeder): void + { + $this->seeder = $seeder; + } + + public function seed(PDO $db): void + { + $seeder = $this->seeder; + if ($seeder) { + $seeder($db); + } + } + + public function setVerifier(\Closure $verifier): void + { + $this->verifier = $verifier; + } + + public function verify(PDO $db): bool + { + $verifier = $this->verifier; + + if ($verifier) { + return $verifier($db); + } + + return true; + } + + public function getName(): string + { + // TODO: Implement getName() method. + } + + public function getType(): ExerciseType + { + return ExerciseType::CLI(); + } + + public function getProblem(): string + { + // TODO: Implement getProblem() method. + } + + public function defineListeners(EventDispatcher $dispatcher): void + { + // TODO: Implement defineListeners() method. + } + + public function getRequiredChecks(): array + { + return [DatabaseCheck::class]; + } + + public function getDescription(): string + { + // TODO: Implement getDescription() method. + } + + public function tearDown(): void + { + // TODO: Implement tearDown() method. + } + + public function setSolution(SolutionInterface $solution): void + { + $this->solution = $solution; + } + + public function getSolution(): SolutionInterface + { + return $this->solution; + } + + public function setScenario(CliScenario $scenario): void + { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CliScenario + { + return $this->scenario; + } +} diff --git a/test/Asset/DatabaseExerciseInterface.php b/test/Asset/DatabaseExerciseInterface.php deleted file mode 100644 index 4d55b9a5..00000000 --- a/test/Asset/DatabaseExerciseInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -testDir = sprintf( - '%s/%s/%s', - str_replace('\\', '/', sys_get_temp_dir()), - basename(str_replace('\\', '/', get_class($this))), - $this->getName() - ); - - mkdir($this->testDir, 0777, true); $this->check = new CodeExistsCheck((new ParserFactory())->create(ParserFactory::PREFER_PHP7)); - $this->exercise = $this->createMock(ExerciseInterface::class); + } + + public function testCheckMeta(): void + { $this->assertEquals('Code Exists Check', $this->check->getName()); $this->assertEquals(ExerciseInterface::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_BEFORE, $this->check->getPosition()); $this->assertTrue($this->check->canRun(ExerciseType::CGI())); $this->assertTrue($this->check->canRun(ExerciseType::CLI())); - - $this->file = sprintf('%s/submission.php', $this->testDir); - touch($this->file); } public function testSuccess(): void { - file_put_contents($this->file, 'createStudentSolutionDirectory(); + $context->importStudentFileFromString('assertInstanceOf( Success::class, - $this->check->check($this->exercise, new Input('app', ['program' => $this->file])) + $this->check->check($context), ); } public function testFailure(): void { - file_put_contents($this->file, 'createStudentSolutionDirectory(); + $context->importStudentFileFromString('check->check($this->exercise, new Input('app', ['program' => $this->file])); + $failure = $this->check->check($context); $this->assertInstanceOf(Failure::class, $failure); $this->assertEquals('No code was found', $failure->getReason()); } - - public function tearDown(): void - { - unlink($this->file); - rmdir($this->testDir); - } } diff --git a/test/Check/CodeParseCheckTest.php b/test/Check/CodeParseCheckTest.php index 2c313cc8..a8f2ca52 100644 --- a/test/Check/CodeParseCheckTest.php +++ b/test/Check/CodeParseCheckTest.php @@ -7,9 +7,10 @@ use PhpSchool\PhpWorkshop\Check\SimpleCheckInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\Success; +use PhpSchool\PhpWorkshop\Utils\Path; use PHPUnit\Framework\TestCase; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; @@ -17,64 +18,53 @@ class CodeParseCheckTest extends TestCase { use AssertionRenames; - /** - * @var SimpleCheckInterface - */ - private $check; - - /** - * @var string - */ - private $file; + private CodeParseCheck $check; public function setUp(): void { $this->check = new CodeParseCheck((new ParserFactory())->create(ParserFactory::PREFER_PHP7)); + } + + public function testCheckMeta(): void + { $this->assertEquals('Code Parse Check', $this->check->getName()); $this->assertEquals(ExerciseInterface::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_BEFORE, $this->check->getPosition()); $this->assertTrue($this->check->canRun(ExerciseType::CGI())); $this->assertTrue($this->check->canRun(ExerciseType::CLI())); - - $this->file = sprintf('%s/%s/submission.php', str_replace('\\', '/', sys_get_temp_dir()), $this->getName()); - mkdir(dirname($this->file), 0775, true); - touch($this->file); } public function testUnParseableCodeReturnsFailure(): void { - file_put_contents($this->file, 'createStudentSolutionDirectory(); + $context->importStudentFileFromString('check->check( - $this->createMock(ExerciseInterface::class), - new Input('app', ['program' => $this->file]) - ); + $result = $this->check->check($context); $this->assertInstanceOf(Failure::class, $result); $this->assertEquals('Code Parse Check', $result->getCheckName()); $this->assertMatchesRegularExpression( - sprintf('|^File: "%s" could not be parsed\. Error: "|', preg_quote($this->file)), - $result->getReason() + sprintf( + '|^File: "%s" could not be parsed\. Error: "|', + preg_quote( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + ), + ), + $result->getReason(), ); } public function testParseableCodeReturnsSuccess(): void { - file_put_contents($this->file, 'createStudentSolutionDirectory(); + $context->importStudentFileFromString('check->check( - $this->createMock(ExerciseInterface::class), - new Input('app', ['program' => $this->file]) - ); + $result = $this->check->check($context); $this->assertInstanceOf(Success::class, $result); $this->assertEquals('Code Parse Check', $result->getCheckName()); } - - public function tearDown(): void - { - unlink($this->file); - rmdir(dirname($this->file)); - } } diff --git a/test/Check/ComposerCheckTest.php b/test/Check/ComposerCheckTest.php index 45b36931..422b22a5 100644 --- a/test/Check/ComposerCheckTest.php +++ b/test/Check/ComposerCheckTest.php @@ -8,29 +8,25 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\ComposerExerciseCheck; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Result\ComposerFailure; -use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\Success; use PhpSchool\PhpWorkshopTest\Asset\ComposerExercise; use PHPUnit\Framework\TestCase; class ComposerCheckTest extends TestCase { - /** - * @var ComposerCheck - */ - private $check; - - /** - * @var ExerciseInterface - */ - private $exercise; + private ComposerCheck $check; + private ComposerExercise $exercise; public function setUp(): void { $this->check = new ComposerCheck(); $this->exercise = new ComposerExercise(); + } + + public function testCheckMeta(): void + { $this->assertEquals('Composer Dependency Check', $this->check->getName()); $this->assertEquals(ComposerExerciseCheck::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_BEFORE, $this->check->getPosition()); @@ -44,14 +40,13 @@ public function testExceptionIsThrownIfNotValidExercise(): void $exercise = $this->createMock(ExerciseInterface::class); $this->expectException(InvalidArgumentException::class); - $this->check->check($exercise, new Input('app')); + $this->check->check(new TestContext()); } public function testCheckReturnsFailureIfNoComposerFile(): void { $result = $this->check->check( - $this->exercise, - new Input('app', ['program' => 'invalid/solution']) + new TestContext($this->exercise), ); $this->assertInstanceOf(ComposerFailure::class, $result); @@ -62,11 +57,13 @@ public function testCheckReturnsFailureIfNoComposerFile(): void public function testCheckReturnsFailureIfNoComposerLockFile(): void { - $result = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/composer/not-locked/solution.php']) + __DIR__ . '/../res/composer/not-locked/solution.php', ); + $result = $this->check->check($context); + $this->assertInstanceOf(ComposerFailure::class, $result); $this->assertSame('Composer Dependency Check', $result->getCheckName()); $this->assertTrue($result->isMissingComponent()); @@ -75,11 +72,13 @@ public function testCheckReturnsFailureIfNoComposerLockFile(): void public function testCheckReturnsFailureIfNoVendorFolder(): void { - $result = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/composer/no-vendor/solution.php']) + __DIR__ . '/../res/composer/no-vendor/solution.php', ); + $result = $this->check->check($context); + $this->assertInstanceOf(ComposerFailure::class, $result); $this->assertSame('Composer Dependency Check', $result->getCheckName()); $this->assertTrue($result->isMissingComponent()); @@ -88,9 +87,6 @@ public function testCheckReturnsFailureIfNoVendorFolder(): void /** * @dataProvider dependencyProvider - * - * @param string $dependency - * @param string $solutionFile */ public function testCheckReturnsFailureIfDependencyNotRequired(string $dependency, string $solutionFile): void { @@ -99,7 +95,9 @@ public function testCheckReturnsFailureIfDependencyNotRequired(string $dependenc ->method('getRequiredPackages') ->willReturn([$dependency]); - $result = $this->check->check($exercise, new Input('app', ['program' => $solutionFile])); + $context = TestContext::fromExerciseAndStudentSolution($exercise, $solutionFile); + + $result = $this->check->check($context); $this->assertInstanceOf(ComposerFailure::class, $result); $this->assertSame('Composer Dependency Check', $result->getCheckName()); @@ -114,17 +112,19 @@ public function dependencyProvider(): array { return [ ['klein/klein', __DIR__ . '/../res/composer/no-klein/solution.php'], - ['danielstjules/stringy', __DIR__ . '/../res/composer/no-stringy/solution.php'] + ['danielstjules/stringy', __DIR__ . '/../res/composer/no-stringy/solution.php'], ]; } public function testCheckReturnsSuccessIfCorrectLockFile(): void { - $result = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/composer/good-solution/solution.php']) + __DIR__ . '/../res/composer/good-solution/solution.php', ); + $result = $this->check->check($context); + $this->assertInstanceOf(Success::class, $result); $this->assertSame('Composer Dependency Check', $result->getCheckName()); } diff --git a/test/Check/DatabaseCheckTest.php b/test/Check/DatabaseCheckTest.php index da96cd7f..df3cdd5f 100644 --- a/test/Check/DatabaseCheckTest.php +++ b/test/Check/DatabaseCheckTest.php @@ -8,42 +8,30 @@ use PhpSchool\PhpWorkshop\Check\DatabaseCheck; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; use PhpSchool\PhpWorkshop\ExerciseCheck\DatabaseExerciseCheck; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\RunnerManager; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; -use PhpSchool\PhpWorkshopTest\Asset\DatabaseExerciseInterface; +use PhpSchool\PhpWorkshopTest\Asset\DatabaseExercise; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionProperty; use RuntimeException; +use Symfony\Component\Filesystem\Filesystem; class DatabaseCheckTest extends TestCase { - /** - * @var DatabaseCheck - */ - private $check; - - /** - * @var CheckRepository - */ - private $checkRepository; - - /** - * @var ExerciseInterface - */ - private $exercise; - - /** - * @var string - */ - private $dbDir; + private DatabaseCheck $check; + private CheckRepository $checkRepository; + private ExerciseInterface $exercise; + private string $dbDir; public function setUp(): void { @@ -54,13 +42,15 @@ public function setUp(): void $this->checkRepository = $container->get(CheckRepository::class); $this->check = new DatabaseCheck(); - $this->exercise = $this->createMock(DatabaseExerciseInterface::class); - $this->exercise->method('getType')->willReturn(ExerciseType::CLI()); + $this->exercise = new DatabaseExercise(); $this->dbDir = sprintf( '%s/php-school/PhpSchool_PhpWorkshop_Check_DatabaseCheck', - str_replace('\\', '/', realpath(sys_get_temp_dir())) + str_replace('\\', '/', realpath(sys_get_temp_dir())), ); + } + public function testCheckMeta(): void + { $this->assertEquals('Database Verification Check', $this->check->getName()); $this->assertEquals(DatabaseExerciseCheck::class, $this->check->getExerciseInterface()); } @@ -68,8 +58,8 @@ public function setUp(): void private function getRunnerManager(ExerciseInterface $exercise, EventDispatcher $eventDispatcher): MockObject { $runner = $this->getMockBuilder(CliRunner::class) - ->setConstructorArgs([$exercise, $eventDispatcher]) - ->setMethods(['configure', 'getRequiredChecks']) + ->setConstructorArgs([$exercise, $eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $eventDispatcher)]) + ->onlyMethods(['getRequiredChecks']) ->getMock(); $runner @@ -94,7 +84,6 @@ public function testIfDatabaseFolderExistsExceptionIsThrown(): void $this->fail('Exception was not thrown'); } catch (RuntimeException $e) { $this->assertEquals(sprintf('Database directory: "%s" already exists', $this->dbDir), $e->getMessage()); - rmdir($this->dbDir); } } @@ -119,26 +108,9 @@ public function testIfPDOThrowsExceptionItCleansUp(): void //try to run the check as usual $this->check = new DatabaseCheck(); $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(true); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + $this->exercise->setVerifier(fn() => true); $this->checkRepository->registerCheck($this->check); @@ -148,7 +120,7 @@ public function testIfPDOThrowsExceptionItCleansUp(): void $this->getRunnerManager($this->exercise, $eventDispatcher), $results, $eventDispatcher, - $this->checkRepository + $this->checkRepository, ); $dispatcher->verify($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user.php'])); @@ -158,27 +130,10 @@ public function testIfPDOThrowsExceptionItCleansUp(): void public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(true); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + $this->exercise->setVerifier(fn() => true); $this->checkRepository->registerCheck($this->check); $results = new ResultAggregator(); @@ -187,10 +142,9 @@ public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void $this->getRunnerManager($this->exercise, $eventDispatcher), $results, $eventDispatcher, - $this->checkRepository + $this->checkRepository, ); - $dispatcher->verify($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user.php'])); $this->assertTrue($results->isSuccessful()); @@ -198,11 +152,6 @@ public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void public function testRunExercise(): void { - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([]); - $this->checkRepository->registerCheck($this->check); $results = new ResultAggregator(); @@ -211,40 +160,22 @@ public function testRunExercise(): void $this->getRunnerManager($this->exercise, $eventDispatcher), $results, $eventDispatcher, - $this->checkRepository + $this->checkRepository, ); $dispatcher->run( $this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user-solution-alter-db.php']), - $this->createMock(OutputInterface::class) + $this->createMock(OutputInterface::class), ); } public function testFailureIsReturnedIfDatabaseVerificationFails(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(false); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + $this->exercise->setVerifier(fn() => false); $this->checkRepository->registerCheck($this->check); @@ -254,7 +185,7 @@ public function testFailureIsReturnedIfDatabaseVerificationFails(): void $this->getRunnerManager($this->exercise, $eventDispatcher), $results, $eventDispatcher, - $this->checkRepository + $this->checkRepository, ); $dispatcher->verify($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user.php'])); @@ -267,53 +198,32 @@ public function testFailureIsReturnedIfDatabaseVerificationFails(): void public function testAlteringDatabaseInSolutionDoesNotEffectDatabaseInUserSolution(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution-alter-db.php')); - - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->method('getArgs') - ->willReturn([]); - - $this->exercise - ->method('verify') - ->willReturn(true); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('seed') - ->with($this->isInstanceOf(PDO::class)) - ->willReturnCallback(function (PDO $db) { - $db->exec( - 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)' - ); - $stmt = $db->prepare('INSERT into users (name, age, gender) VALUES (:name, :age, :gender)'); - $stmt->execute([':name' => 'Jimi Hendrix', ':age' => 27, ':gender' => 'Male']); - }); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturnCallback(function (PDO $db) { - $users = $db->query('SELECT * FROM users'); - $users = $users->fetchAll(PDO::FETCH_ASSOC); - - $this->assertEquals( - [ - ['id' => 1, 'name' => 'Jimi Hendrix', 'age' => '27', 'gender' => 'Male'], - ['id' => 2, 'name' => 'Kurt Cobain', 'age' => '27', 'gender' => 'Male'], - ], - $users - ); - }); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); + + $this->exercise->setVerifier(function (PDO $db) { + $users = $db->query('SELECT * FROM users'); + $users = $users->fetchAll(PDO::FETCH_ASSOC); + + $this->assertCount(2, $users); + $this->assertEquals( + [ + ['id' => 1, 'name' => 'Jimi Hendrix', 'age' => '27', 'gender' => 'Male'], + ['id' => 2, 'name' => 'Kurt Cobain', 'age' => '27', 'gender' => 'Male'], + ], + $users, + ); + + return true; + }); + + $this->exercise->setSeeder(function (PDO $db) { + $db->exec( + 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)', + ); + $stmt = $db->prepare('INSERT into users (name, age, gender) VALUES (:name, :age, :gender)'); + $stmt->execute([':name' => 'Jimi Hendrix', ':age' => 27, ':gender' => 'Male']); + }); $this->checkRepository->registerCheck($this->check); @@ -323,12 +233,18 @@ public function testAlteringDatabaseInSolutionDoesNotEffectDatabaseInUserSolutio $this->getRunnerManager($this->exercise, $eventDispatcher), $results, $eventDispatcher, - $this->checkRepository + $this->checkRepository, ); $dispatcher->verify( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/database/user-solution-alter-db.php']) + new Input('app', ['program' => __DIR__ . '/../res/database/user-solution-alter-db.php']), ); } + + protected function tearDown(): void + { + $fs = new Filesystem(); + $fs->remove($this->dbDir); + } } diff --git a/test/Check/FileComparisonCheckTest.php b/test/Check/FileComparisonCheckTest.php index 78f61a82..11d76b29 100644 --- a/test/Check/FileComparisonCheckTest.php +++ b/test/Check/FileComparisonCheckTest.php @@ -7,24 +7,24 @@ use PhpSchool\PhpWorkshop\Exception\SolutionFileDoesNotExistException; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\FileComparisonExerciseCheck; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Result\FileComparisonFailure; -use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; use PhpSchool\PhpWorkshopTest\Asset\FileComparisonExercise; -use PhpSchool\PhpWorkshopTest\BaseTest; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\Result\Success; +use PHPUnit\Framework\TestCase; -class FileComparisonCheckTest extends BaseTest +class FileComparisonCheckTest extends TestCase { - /** - * @var FileComparisonCheck - */ - private $check; + private FileComparisonCheck $check; public function setUp(): void { $this->check = new FileComparisonCheck(); + } + + public function testCheckMeta(): void + { $this->assertEquals('File Comparison Check', $this->check->getName()); $this->assertEquals(FileComparisonExerciseCheck::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_AFTER, $this->check->getPosition()); @@ -39,19 +39,19 @@ public function testExceptionIsThrownIfReferenceFileDoesNotExist(): void $this->expectExceptionMessage('File: "some-file.txt" does not exist in solution folder'); $exercise = new FileComparisonExercise(['some-file.txt']); - $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + $context = new TestContext($exercise); - $this->check->check($exercise, new Input('app', ['program' => 'my-solution.php'])); + $this->check->check($context); } public function testFailureIsReturnedIfStudentsFileDoesNotExist(): void { - $referenceFile = $this->getTemporaryFile('solution/some-file.txt', "name,age\nAydin,33\nMichael,29\n"); - $exercise = new FileComparisonExercise(['some-file.txt']); - $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + $context = new TestContext($exercise); + $context->createReferenceSolutionDirectory(); + $context->importReferenceFileFromString("name,age\nAydin,33\nMichael,29\n", 'some-file.txt'); - $failure = $this->check->check($exercise, new Input('app', ['program' => 'my-solution.php'])); + $failure = $this->check->check($context); $this->assertInstanceOf(Failure::class, $failure); $this->assertEquals('File: "some-file.txt" does not exist', $failure->getReason()); @@ -59,15 +59,14 @@ public function testFailureIsReturnedIfStudentsFileDoesNotExist(): void public function testFailureIsReturnedIfStudentFileDosNotMatchReferenceFile(): void { - $referenceFile = $this->getTemporaryFile('solution/some-file.txt', "name,age\nAydin,33\nMichael,29\n"); - $studentFile = $this->getTemporaryFile('student/some-file.txt', "somegibberish"); - $exercise = new FileComparisonExercise(['some-file.txt']); - $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString("somegibberish", 'some-file.txt'); + $context->importReferenceFileFromString("name,age\nAydin,33\nMichael,29\n", 'some-file.txt'); - $failure = $this->check->check($exercise, new Input('app', [ - 'program' => $this->getTemporaryFile('student/my-solution.php') - ])); + $failure = $this->check->check($context); $this->assertInstanceOf(FileComparisonFailure::class, $failure); $this->assertEquals($failure->getFileName(), 'some-file.txt'); @@ -77,37 +76,27 @@ public function testFailureIsReturnedIfStudentFileDosNotMatchReferenceFile(): vo public function testSuccessIsReturnedIfFilesMatch(): void { - $referenceFile = $this->getTemporaryFile('solution/some-file.txt', "name,age\nAydin,33\nMichael,29\n"); - $studentFile = $this->getTemporaryFile('student/some-file.txt', "name,age\nAydin,33\nMichael,29\n"); - $exercise = new FileComparisonExercise(['some-file.txt']); - $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); - - $this->assertInstanceOf( - Success::class, - $this->check->check($exercise, new Input('app', [ - 'program' => $this->getTemporaryFile('student/my-solution.php') - ])) - ); + + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString("name,age\nAydin,33\nMichael,29\n", 'some-file.txt'); + $context->importReferenceFileFromString("name,age\nAydin,33\nMichael,29\n", 'some-file.txt'); + + $this->assertInstanceOf(Success::class, $this->check->check($context)); } public function testFailureIsReturnedIfFileDoNotMatchUsingStrip(): void { - $referenceFile = $this->getTemporaryFile( - 'solution/some-file.txt', - "01:03name,age\n04:05Aydin,33\n17:21Michael,29\n" - ); - $studentFile = $this->getTemporaryFile( - 'student/some-file.txt', - "01:04name,age\n06:76Aydin,34\n99:00Michael,29\n" - ); - $exercise = new FileComparisonExercise(['some-file.txt' => ['strip' => '/\d{2}:\d{2}/']]); - $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString("01:04name,age\n06:76Aydin,34\n99:00Michael,29\n", 'some-file.txt'); + $context->importReferenceFileFromString("01:03name,age\n04:05Aydin,33\n17:21Michael,29\n", 'some-file.txt'); - $failure = $this->check->check($exercise, new Input('app', [ - 'program' => $this->getTemporaryFile('student/my-solution.php') - ])); + $failure = $this->check->check($context); $this->assertInstanceOf(FileComparisonFailure::class, $failure); $this->assertEquals($failure->getFileName(), 'some-file.txt'); diff --git a/test/Check/FileExistsCheckTest.php b/test/Check/FileExistsCheckTest.php index 8afb8c8a..a422bc08 100644 --- a/test/Check/FileExistsCheckTest.php +++ b/test/Check/FileExistsCheckTest.php @@ -4,7 +4,8 @@ use PhpSchool\PhpWorkshop\Check\SimpleCheckInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; +use PhpSchool\PhpWorkshop\Utils\Path; use PHPUnit\Framework\TestCase; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; @@ -13,27 +14,15 @@ class FileExistsCheckTest extends TestCase { - /** - * @var string - */ - private $testDir; - - /** - * @var FileExistsCheck - */ - private $check; - - /** - * @var ExerciseInterface - */ - private $exercise; + private FileExistsCheck $check; public function setUp(): void { - $this->testDir = sprintf('%s/%s', sys_get_temp_dir(), $this->getName()); - mkdir($this->testDir, 0777, true); $this->check = new FileExistsCheck(); - $this->exercise = $this->createMock(ExerciseInterface::class); + } + + public function testCheckMeta(): void + { $this->assertEquals('File Exists Check', $this->check->getName()); $this->assertEquals(ExerciseInterface::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_BEFORE, $this->check->getPosition()); @@ -44,26 +33,28 @@ public function setUp(): void public function testSuccess(): void { - $file = sprintf('%s/test.txt', $this->testDir); - touch($file); + $context = new TestContext(); + $context->createStudentSolutionDirectory(); + $context->importStudentFileFromString('assertInstanceOf( Success::class, - $this->check->check($this->exercise, new Input('app', ['program' => $file])) + $this->check->check($context), ); - unlink($file); } public function testFailure(): void { - $file = sprintf('%s/test.txt', $this->testDir); - $failure = $this->check->check($this->exercise, new Input('app', ['program' => $file])); - $this->assertInstanceOf(Failure::class, $failure); - $this->assertEquals(sprintf('File: "%s" does not exist', $file), $failure->getReason()); - } + $context = new TestContext(); - public function tearDown(): void - { - rmdir($this->testDir); + $failure = $this->check->check($context); + $this->assertInstanceOf(Failure::class, $failure); + $this->assertEquals( + sprintf( + 'File: "%s" does not exist', + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + ), + $failure->getReason(), + ); } } diff --git a/test/Check/FunctionRequirementsCheckTest.php b/test/Check/FunctionRequirementsCheckTest.php index 70a10249..e4693c07 100644 --- a/test/Check/FunctionRequirementsCheckTest.php +++ b/test/Check/FunctionRequirementsCheckTest.php @@ -7,7 +7,7 @@ use PhpParser\ParserFactory; use PhpSchool\PhpWorkshop\Check\SimpleCheckInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshopTest\Asset\FunctionRequirementsExercise; use PHPUnit\Framework\TestCase; use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck; @@ -19,20 +19,9 @@ class FunctionRequirementsCheckTest extends TestCase { - /** - * @var FunctionRequirementsCheck - */ - private $check; - - /** - * @var ExerciseInterface - */ - private $exercise; - - /** - * @var Parser - */ - private $parser; + private FunctionRequirementsCheck $check; + private FunctionRequirementsExercise $exercise; + private Parser $parser; public function setUp(): void { @@ -40,6 +29,10 @@ public function setUp(): void $this->parser = $parserFactory->create(ParserFactory::PREFER_PHP7); $this->check = new FunctionRequirementsCheck($this->parser); $this->exercise = new FunctionRequirementsExercise(); + } + + public function testCheckMeta(): void + { $this->assertEquals('Function Requirements Check', $this->check->getName()); $this->assertEquals(FunctionRequirementsExerciseCheck::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_AFTER, $this->check->getPosition()); @@ -53,25 +46,35 @@ public function testExceptionIsThrownIfNotValidExercise(): void $exercise = $this->createMock(ExerciseInterface::class); $this->expectException(InvalidArgumentException::class); - $this->check->check($exercise, new Input('app')); + $this->check->check(new TestContext($exercise)); } public function testFailureIsReturnedIfCodeCouldNotBeParsed(): void { - $file = __DIR__ . '/../res/function-requirements/fail-invalid-code.php'; - $failure = $this->check->check($this->exercise, new Input('app', ['program' => $file])); - $this->assertInstanceOf(Failure::class, $failure); + $context = TestContext::fromExerciseAndStudentSolution( + $this->exercise, + __DIR__ . '/../res/function-requirements/fail-invalid-code.php', + ); + + $failure = $this->check->check($context); - $message = sprintf('File: "%s" could not be parsed. Error: "Syntax error, unexpected T_ECHO on line 4"', $file); + $this->assertInstanceOf(Failure::class, $failure); + $message = sprintf( + 'File: "%s/fail-invalid-code.php" could not be parsed. Error: "Syntax error, unexpected T_ECHO on line 4"', + $context->getStudentExecutionDirectory(), + ); $this->assertEquals($message, $failure->getReason()); } public function testFailureIsReturnedIfBannedFunctionsAreUsed(): void { - $failure = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/function-requirements/fail-banned-function.php']) + __DIR__ . '/../res/function-requirements/fail-banned-function.php', ); + + $failure = $this->check->check($context); + $this->assertInstanceOf(FunctionRequirementsFailure::class, $failure); $this->assertEquals([['function' => 'file', 'line' => 3]], $failure->getBannedFunctions()); $this->assertEquals([], $failure->getMissingFunctions()); @@ -90,12 +93,14 @@ public function testFailureIsReturnedIfNotAllRequiredFunctionsHaveBeenUsed(): vo ->method('getRequiredFunctions') ->willReturn(['file_get_contents', 'implode']); - $failure = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $exercise, - new Input('app', ['program' => __DIR__ . '/../res/function-requirements/fail-banned-function.php']) + __DIR__ . '/../res/function-requirements/fail-banned-function.php', ); - $this->assertInstanceOf(FunctionRequirementsFailure::class, $failure); + $failure = $this->check->check($context); + + $this->assertInstanceOf(FunctionRequirementsFailure::class, $failure); $this->assertEquals(['file_get_contents', 'implode'], $failure->getMissingFunctions()); $this->assertEquals([], $failure->getBannedFunctions()); } @@ -113,10 +118,13 @@ public function testSuccess(): void ->method('getRequiredFunctions') ->willReturn(['file_get_contents']); - $success = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $exercise, - new Input('app', ['program' => __DIR__ . '/../res/function-requirements/success.php']) + __DIR__ . '/../res/function-requirements/success.php', ); + + $success = $this->check->check($context); + $this->assertInstanceOf(Success::class, $success); } } diff --git a/test/Check/PhpLintCheckTest.php b/test/Check/PhpLintCheckTest.php index 82c3025d..3cc51649 100644 --- a/test/Check/PhpLintCheckTest.php +++ b/test/Check/PhpLintCheckTest.php @@ -4,7 +4,7 @@ use PhpSchool\PhpWorkshop\Check\SimpleCheckInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PHPUnit\Framework\TestCase; use PhpSchool\PhpWorkshop\Check\PhpLintCheck; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; @@ -16,20 +16,17 @@ class PhpLintCheckTest extends TestCase { use AssertionRenames; - /** - * @var PhpLintCheck - */ - private $check; - - /** - * @var ExerciseInterface - */ - private $exercise; + private PhpLintCheck $check; + private ExerciseInterface $exercise; public function setUp(): void { $this->check = new PhpLintCheck(); $this->exercise = $this->createMock(ExerciseInterface::class); + } + + public function testCheckMeta(): void + { $this->assertEquals('PHP Code Check', $this->check->getName()); $this->assertEquals(ExerciseInterface::class, $this->check->getExerciseInterface()); $this->assertEquals(SimpleCheckInterface::CHECK_BEFORE, $this->check->getPosition()); @@ -40,22 +37,32 @@ public function setUp(): void public function testSuccess(): void { + $context = TestContext::fromExerciseAndStudentSolution( + $this->exercise, + __DIR__ . '/../res/lint/pass.php', + ); + + $res = $this->check->check($context); + $this->assertInstanceOf( Success::class, - $this->check->check($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/lint/pass.php'])) + $this->check->check($context), ); } public function testFailure(): void { - $failure = $this->check->check( + $context = TestContext::fromExerciseAndStudentSolution( $this->exercise, - new Input('app', ['program' => __DIR__ . '/../res/lint/fail.php']) + __DIR__ . '/../res/lint/fail.php', ); + + $failure = $this->check->check($context); + $this->assertInstanceOf(Failure::class, $failure); $this->assertMatchesRegularExpression( "/(PHP )?Parse error:\W+syntax error, unexpected end of file, expecting ['\"][,;]['\"] or ['\"][;,]['\"]/", - $failure->getReason() + $failure->getReason(), ); } } diff --git a/test/CodePatcherTest.php b/test/CodePatcherTest.php index 3db0d8a4..edab5a90 100644 --- a/test/CodePatcherTest.php +++ b/test/CodePatcherTest.php @@ -10,7 +10,6 @@ use PhpParser\Node\Stmt\TryCatch; use PhpParser\ParserFactory; use PhpParser\PrettyPrinter\Standard; -use PhpParser\PrettyPrinterAbstract; use PhpSchool\PhpWorkshop\CodePatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Patch; @@ -31,7 +30,7 @@ public function testDefaultPatchIsAppliedIfAvailable(): void (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), new NullLogger(), - $patch + $patch, ); $exercise = $this->createMock(ExerciseInterface::class); @@ -44,7 +43,7 @@ public function testPatcherDoesNotApplyPatchIfNotPatchableExercise(): void $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(ExerciseInterface::class); @@ -60,7 +59,7 @@ public function testPatcher(string $code, Patch $patch, string $expectedResult): $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(PatchableExercise::class); @@ -79,35 +78,35 @@ public function codeProvider(): array 'only-before-insertion' => [ 'withInsertion(new Insertion(Insertion::TYPE_BEFORE, '$before = "here";')), - " [ 'withInsertion(new Insertion(Insertion::TYPE_AFTER, '$after = "here";')), - " [ 'withInsertion(new Insertion(Insertion::TYPE_BEFORE, '$before = "here";')) ->withInsertion(new Insertion(Insertion::TYPE_AFTER, '$after = "here";')), - " [ 'withInsertion(new Insertion(Insertion::TYPE_BEFORE, '$before = "here"')), //no semicolon at the end - " [ 'withInsertion(new Insertion(Insertion::TYPE_BEFORE, ' [ 'withInsertion(new Insertion(Insertion::TYPE_BEFORE, ' [ ' [ ' [ 'withTransformer(new class implements Patch\Transformer { + ->withTransformer(new class () implements Patch\Transformer { public function transform(array $statements): array { return [ new TryCatch( $statements, - [new Catch_([new Name(\Exception::class)], new Variable('e'), [])] - ) + [new Catch_([new Name(\Exception::class)], new Variable('e'), [])], + ), ]; } }), - "create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(PatchableExercise::class); @@ -175,7 +174,7 @@ public function testBeforeInsertionInsertsAfterStrictTypesDeclaration(): void $this->assertEquals( "patch($exercise, $code) + $patcher->patch($exercise, $code), ); } @@ -187,15 +186,15 @@ public function testTransformerWithStrictTypes(): void return [ new TryCatch( $statements, - [new Catch_([new Name(\Exception::class)], new Variable('e'), [])] - ) + [new Catch_([new Name(\Exception::class)], new Variable('e'), [])], + ), ]; }); $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(PatchableExercise::class); @@ -207,7 +206,7 @@ public function testTransformerWithStrictTypes(): void $this->assertEquals( "patch($exercise, $code) + $patcher->patch($exercise, $code), ); } @@ -219,15 +218,15 @@ public function testTransformerWhichAddsStrictTypesDoesNotResultInDoubleStrictTy return [new \PhpParser\Node\Stmt\Declare_([ new DeclareDeclare( new \PhpParser\Node\Identifier('strict_types'), - new LNumber(1) - ) + new LNumber(1), + ), ])]; }); $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(PatchableExercise::class); @@ -239,7 +238,7 @@ public function testTransformerWhichAddsStrictTypesDoesNotResultInDoubleStrictTy $this->assertEquals( "patch($exercise, $code) + $patcher->patch($exercise, $code), ); } @@ -251,8 +250,8 @@ public function testAddingStrictTypesDeclareDoesNotBreakBeforeInsertion(): void return array_merge([new \PhpParser\Node\Stmt\Declare_([ new DeclareDeclare( new \PhpParser\Node\Identifier('strict_types'), - new LNumber(1) - ) + new LNumber(1), + ), ])], $statements); }) ->withInsertion(new Insertion(Insertion::TYPE_BEFORE, '$before = "here";')); @@ -260,7 +259,7 @@ public function testAddingStrictTypesDeclareDoesNotBreakBeforeInsertion(): void $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - new NullLogger() + new NullLogger(), ); $exercise = $this->createMock(PatchableExercise::class); @@ -272,7 +271,7 @@ public function testAddingStrictTypesDeclareDoesNotBreakBeforeInsertion(): void $this->assertEquals( "patch($exercise, $code) + $patcher->patch($exercise, $code), ); } @@ -281,7 +280,7 @@ public function testExceptionIsLoggedIfCodeIsNotParseable(): void $patcher = new CodePatcher( (new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard(), - $logger = $this->createMock(LoggerInterface::class) + $logger = $this->createMock(LoggerInterface::class), ); $exercise = $this->createMock(PatchableExercise::class); @@ -298,12 +297,12 @@ public function testExceptionIsLoggedIfCodeIsNotParseable(): void ->method('critical') ->with( 'Code Insertion could not be parsed: Syntax error, unexpected EOF on line 1', - ['code' => '$before = "here"'] + ['code' => '$before = "here"'], ); $this->assertEquals( "patch($exercise, 'patch($exercise, ' 'Aydin Hassan', '@mikeymike' => 'Michael Woodward', '@shakeyShane' => 'Shane Osbourne', - '@chris3ailey' => 'Chris Bailey' + '@chris3ailey' => 'Chris Bailey', ], [ '@AydinHassan' => 'Aydin Hassan', '@mikeymike' => 'Michael Woodward', ], new StdOutput($color, $this->createMock(Terminal::class)), - $color + $color, ); $command->__invoke(); @@ -47,11 +47,11 @@ public function testWithOnlyCoreContributors(): void '@AydinHassan' => 'Aydin Hassan', '@mikeymike' => 'Michael Woodward', '@shakeyShane' => 'Shane Osbourne', - '@chris3ailey' => 'Chris Bailey' + '@chris3ailey' => 'Chris Bailey', ], [], new StdOutput($color, $this->createMock(Terminal::class)), - $color + $color, ); $command->__invoke(); @@ -68,7 +68,7 @@ public function testWithNoContributors(): void [], [], new StdOutput($color, $this->createMock(Terminal::class)), - $color + $color, ); $command->__invoke(); diff --git a/test/Command/HelpCommandTest.php b/test/Command/HelpCommandTest.php index ec1a607f..968aa31d 100644 --- a/test/Command/HelpCommandTest.php +++ b/test/Command/HelpCommandTest.php @@ -20,7 +20,7 @@ public function testInvoke(): void $command = new HelpCommand( 'learnyouphp', new StdOutput($color, $this->createMock(Terminal::class)), - $color + $color, ); $command->__invoke(); diff --git a/test/Command/PrintCommandTest.php b/test/Command/PrintCommandTest.php index 88be8ed3..292ff920 100644 --- a/test/Command/PrintCommandTest.php +++ b/test/Command/PrintCommandTest.php @@ -3,9 +3,8 @@ namespace PhpSchool\PhpWorkshopTest\Command; use PhpSchool\PhpWorkshop\Command\PrintCommand; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\UserState\UserState; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; use PhpSchool\PhpWorkshop\ExerciseRepository; use PhpSchool\PhpWorkshop\MarkdownRenderer; @@ -18,23 +17,13 @@ public function testExerciseIsPrintedIfAssigned(): void $file = tempnam(sys_get_temp_dir(), 'pws'); file_put_contents($file, '### Exercise 1'); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getProblem') - ->willReturn($file); - - $exercise - ->method('getType') - ->willReturn(ExerciseType::CLI()); - - $exercise - ->method('getName') - ->willReturn('some-exercise'); + $exercise = new CliExerciseImpl(); + $exercise->setProblem($file); $repo = new ExerciseRepository([$exercise]); $state = new UserState(); - $state->setCurrentExercise('some-exercise'); + $state->setCurrentExercise('my-exercise'); $output = $this->createMock(OutputInterface::class); $renderer = $this->createMock(MarkdownRenderer::class); diff --git a/test/Command/RunCommandTest.php b/test/Command/RunCommandTest.php index 7755220a..95745160 100644 --- a/test/Command/RunCommandTest.php +++ b/test/Command/RunCommandTest.php @@ -3,7 +3,6 @@ namespace PhpSchool\PhpWorkshopTest\Command; use Colors\Color; -use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait; use PhpSchool\PhpWorkshopTest\BaseTest; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Command\RunCommand; @@ -13,7 +12,6 @@ use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshop\UserState\UserState; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; -use PHPUnit\Framework\TestCase; class RunCommandTest extends BaseTest { diff --git a/test/CommandDefinitionTest.php b/test/CommandDefinitionTest.php index f6620528..5c3e2b64 100644 --- a/test/CommandDefinitionTest.php +++ b/test/CommandDefinitionTest.php @@ -11,8 +11,7 @@ class CommandDefinitionTest extends TestCase { public function testGettersSettersWithStringArgs(): void { - $callable = function () { - }; + $callable = function () {}; $definition = new CommandDefinition('animal', ['name'], $callable); $this->assertSame($definition->getName(), 'animal'); @@ -27,8 +26,7 @@ public function testGettersSettersWithStringArgs(): void public function testGettersSettersWithObjArgs(): void { - $callable = function () { - }; + $callable = function () {}; $definition = new CommandDefinition('animal', [new CommandArgument('name')], $callable); $this->assertSame($definition->getName(), 'animal'); diff --git a/test/CommandRouterTest.php b/test/CommandRouterTest.php index a7f20964..bf2aa36b 100644 --- a/test/CommandRouterTest.php +++ b/test/CommandRouterTest.php @@ -40,7 +40,7 @@ public function testAddCommandAppendsToExistingCommands(): void [new CommandDefinition('verify', [], $routeCallable)], 'verify', $eventDispatcher, - $c + $c, ); $router->addCommand(new CommandDefinition('run', [], $routeCallable)); @@ -111,8 +111,7 @@ public function testRouteCommandThrowsExceptionIfCommandWithNameNotExist(): void $c = $this->createMock(ContainerInterface::class); $eventDispatcher = $this->createMock(EventDispatcher::class); - $router = new CommandRouter([new CommandDefinition('cmd', [], function () { - }),], 'cmd', $eventDispatcher, $c); + $router = new CommandRouter([new CommandDefinition('cmd', [], function () {}),], 'cmd', $eventDispatcher, $c); $router->route(['app', 'not-a-cmd']); } @@ -124,11 +123,10 @@ public function testRouteCommandThrowsExceptionIfCommandIsMissingAllArguments(): $c = $this->createMock(ContainerInterface::class); $eventDispatcher = $this->createMock(EventDispatcher::class); $router = new CommandRouter( - [new CommandDefinition('verify', ['exercise', 'program'], function () { - }),], + [new CommandDefinition('verify', ['exercise', 'program'], function () {}),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify']); } @@ -141,11 +139,10 @@ public function testRouteCommandThrowsExceptionIfCommandIsMissingArguments(): vo $c = $this->createMock(ContainerInterface::class); $eventDispatcher = $this->createMock(EventDispatcher::class); $router = new CommandRouter( - [new CommandDefinition('verify', ['exercise', 'program'], function () { - }),], + [new CommandDefinition('verify', ['exercise', 'program'], function () {}),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise']); } @@ -171,7 +168,7 @@ public function testRouteCommandWithArgs(): void [new CommandDefinition('verify', ['exercise', 'program'], $mock),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise', 'program.php']); } @@ -187,7 +184,7 @@ public function testExceptionIsThrownIfCallableNotCallableAndNotContainerReferen [new CommandDefinition('verify', ['exercise', 'program'], new \stdClass()),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise', 'program.php']); } @@ -210,7 +207,7 @@ public function testExceptionIsThrownIfCallableNotCallableAndNotExistingContaine [new CommandDefinition('verify', ['exercise', 'program'], 'some.service'),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise', 'program.php']); } @@ -239,7 +236,7 @@ public function testExceptionIsThrownIfContainerEntryNotCallable(): void [new CommandDefinition('verify', ['exercise', 'program'], 'some.service'),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise', 'program.php']); } @@ -279,7 +276,7 @@ public function testCallableFromContainer(): void [new CommandDefinition('verify', ['exercise', 'program'], 'some.service'),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise', 'program.php']); } @@ -319,7 +316,7 @@ public function testCallableFromContainerWithIntegerReturnCode(): void [new CommandDefinition('verify', ['exercise', 'program'], 'some.service'),], 'verify', $eventDispatcher, - $c + $c, ); $res = $router->route(['app', 'verify', 'some-exercise', 'program.php']); $this->assertEquals(10, $res); @@ -347,7 +344,7 @@ public function testRouteCommandSpeltIncorrectlyStillRoutes(): void [new CommandDefinition('verify', ['exercise', 'program'], $mock),], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verifu', 'some-exercise', 'program.php']); } @@ -365,14 +362,14 @@ public function testRouteCommandWithOptionalArgument(): void $this->callback(function (Input $input) { return $input->getAppName() === 'app' && $input->getArgument('exercise') === 'some-exercise'; - }) + }), ], [ $this->callback(function (Input $input) { return $input->getAppName() === 'app' && $input->getArgument('exercise') === 'some-exercise' && $input->getArgument('program') === 'program.php'; - }) + }), ], [ $this->callback(function (Input $input) { @@ -380,8 +377,8 @@ public function testRouteCommandWithOptionalArgument(): void && $input->getArgument('exercise') === 'some-exercise' && $input->getArgument('program') === 'program.php' && $input->getArgument('some-other-arg') === 'some-other-arg-value'; - }) - ] + }), + ], ) ->willReturnOnConsecutiveCalls(1, null, 1); @@ -394,14 +391,14 @@ public function testRouteCommandWithOptionalArgument(): void [ 'exercise', new CommandArgument('program', true), - new CommandArgument('some-other-arg', true) + new CommandArgument('some-other-arg', true), ], - $mock - ) + $mock, + ), ], 'verify', $eventDispatcher, - $c + $c, ); $router->route(['app', 'verify', 'some-exercise']); $router->route(['app', 'verify', 'some-exercise', 'program.php']); diff --git a/test/ContainerAwareTest.php b/test/ContainerAwareTest.php index c54e5573..b3bda6c5 100644 --- a/test/ContainerAwareTest.php +++ b/test/ContainerAwareTest.php @@ -64,9 +64,9 @@ public function assertLoggerHasMessages(array $messages): void [ 'level' => $message['level'], 'message' => $message['message'], - 'context' => $message['context'] + 'context' => $message['context'], ], - $logged + $logged, ); } } diff --git a/test/Event/CgiExecuteEventTest.php b/test/Event/CgiExecuteEventTest.php index 112e0c41..a4cd3d21 100644 --- a/test/Event/CgiExecuteEventTest.php +++ b/test/Event/CgiExecuteEventTest.php @@ -4,6 +4,8 @@ use GuzzleHttp\Psr7\Request; use PhpSchool\PhpWorkshop\Event\CgiExecuteEvent; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -11,8 +13,11 @@ class CgiExecuteEventTest extends TestCase { public function testAddHeader(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $e->addHeaderToRequest('Content-Type', 'text/html'); $this->assertSame( @@ -20,15 +25,18 @@ public function testAddHeader(): void 'Host' => ['some.site'], 'Content-Type' => ['text/html'], ], - $e->getRequest()->getHeaders() + $e->getRequest()->getHeaders(), ); $this->assertNotSame($request, $e->getRequest()); } public function testModifyRequest(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $e->modifyRequest(function (RequestInterface $request) { return $request @@ -40,17 +48,22 @@ public function testModifyRequest(): void 'Host' => ['some.site'], 'Content-Type' => ['text/html'], ], - $e->getRequest()->getHeaders() + $e->getRequest()->getHeaders(), ); $this->assertSame('POST', $e->getRequest()->getMethod()); $this->assertNotSame($request, $e->getRequest()); } - public function testGetRequest(): void + public function testGetters(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $this->assertSame($request, $e->getRequest()); + $this->assertSame($context, $e->getContext()); + $this->assertSame($scenario, $e->getScenario()); } } diff --git a/test/Event/CgiExerciseRunnerEventTest.php b/test/Event/CgiExerciseRunnerEventTest.php new file mode 100644 index 00000000..ac10f1a6 --- /dev/null +++ b/test/Event/CgiExerciseRunnerEventTest.php @@ -0,0 +1,31 @@ + 1]); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); + $this->assertSame($context, $event->getContext()); + $this->assertSame($scenario, $event->getScenario()); + self::assertEquals( + [ + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), + 'number' => 1, + ], + $event->getParameters(), + ); + } +} diff --git a/test/Event/CliExecuteEventTest.php b/test/Event/CliExecuteEventTest.php index a8ea3174..243f2d0a 100644 --- a/test/Event/CliExecuteEventTest.php +++ b/test/Event/CliExecuteEventTest.php @@ -3,15 +3,20 @@ namespace PhpSchool\PhpWorkshopTest\Event; use PhpSchool\PhpWorkshop\Event\CliExecuteEvent; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; +use PhpSchool\PhpWorkshop\Utils\Collection; use PHPUnit\Framework\TestCase; class CliExecuteEventTest extends TestCase { public function testAppendArg(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $e->appendArg('4'); $this->assertEquals([1, 2, 3, 4], $e->getArgs()->getArrayCopy()); @@ -20,19 +25,27 @@ public function testAppendArg(): void public function testPrependArg(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $e->prependArg('4'); $this->assertEquals([4, 1, 2, 3], $e->getArgs()->getArrayCopy()); $this->assertNotSame($arr, $e->getArgs()); } - public function testGetArgs(): void + public function testGetters(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $this->assertSame($arr, $e->getArgs()); + $this->assertSame($context, $e->getContext()); + $this->assertSame($scenario, $e->getScenario()); } } diff --git a/test/Event/CliExerciseRunnerEventTest.php b/test/Event/CliExerciseRunnerEventTest.php new file mode 100644 index 00000000..a32d7012 --- /dev/null +++ b/test/Event/CliExerciseRunnerEventTest.php @@ -0,0 +1,31 @@ + 1]); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); + $this->assertSame($context, $event->getContext()); + $this->assertSame($scenario, $event->getScenario()); + self::assertEquals( + [ + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), + 'number' => 1, + ], + $event->getParameters(), + ); + } +} diff --git a/test/Event/EventDispatcherTest.php b/test/Event/EventDispatcherTest.php index c517fb6a..58999a0f 100644 --- a/test/Event/EventDispatcherTest.php +++ b/test/Event/EventDispatcherTest.php @@ -144,11 +144,9 @@ public function testListenersAndVerifiersAreCalledInOrderOfAttachment(): void public function testRemoveListener(): void { - $listener = function () { - }; + $listener = function () {}; - $listener2 = function () { - }; + $listener2 = function () {}; $this->eventDispatcher->listen('some-event', $listener); $this->eventDispatcher->listen('some-event', $listener2); @@ -168,10 +166,8 @@ public function testRemoveLazyListeners(): void { $container = $this->createMock(ContainerInterface::class); - $myListener = new class { - public function __invoke() - { - } + $myListener = new class () { + public function __invoke() {} }; $container->expects($this->any()) @@ -181,7 +177,7 @@ public function __invoke() $lazy = new LazyContainerListener( $container, - new ContainerListenerHelper('my-listener') + new ContainerListenerHelper('my-listener'), ); $this->eventDispatcher->listen('some-event', $lazy); @@ -197,10 +193,8 @@ public function testRemoveLazyListenersWithAlternateMethod(): void { $container = $this->createMock(ContainerInterface::class); - $myListener = new class { - public function myMethod() - { - } + $myListener = new class () { + public function myMethod() {} }; $container->expects($this->any()) @@ -210,7 +204,7 @@ public function myMethod() $lazy = new LazyContainerListener( $container, - new ContainerListenerHelper('my-listener', 'myMethod') + new ContainerListenerHelper('my-listener', 'myMethod'), ); $this->eventDispatcher->listen('some-event', $lazy); diff --git a/test/Event/ExerciseRunnerEventTest.php b/test/Event/ExerciseRunnerEventTest.php index baa7f4f4..2b952e9e 100644 --- a/test/Event/ExerciseRunnerEventTest.php +++ b/test/Event/ExerciseRunnerEventTest.php @@ -3,27 +3,26 @@ namespace PhpSchool\PhpWorkshopTest\Event; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; -use PhpSchool\PhpWorkshop\Input\Input; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PHPUnit\Framework\TestCase; class ExerciseRunnerEventTest extends TestCase { public function testGetters(): void { - $exercise = new CliExerciseImpl(); - $input = new Input('app'); + $context = new TestContext(); - $event = new ExerciseRunnerEvent('Some Event', $exercise, $input, ['number' => 1]); - self::assertSame($exercise, $event->getExercise()); - self::assertSame($input, $event->getInput()); + $event = new ExerciseRunnerEvent('Some Event', $context, ['number' => 1]); + self::assertSame($context, $event->getContext()); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); self::assertEquals( [ - 'exercise' => $exercise, - 'input' => $input, - 'number' => 1 + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), + 'number' => 1, ], - $event->getParameters() + $event->getParameters(), ); } } diff --git a/test/Exception/MissingArgumentExceptionTest.php b/test/Exception/MissingArgumentExceptionTest.php index a17c7dc5..851c4db6 100644 --- a/test/Exception/MissingArgumentExceptionTest.php +++ b/test/Exception/MissingArgumentExceptionTest.php @@ -12,7 +12,7 @@ public function testException(): void $e = new MissingArgumentException('some-route', ['arg1', 'arg2']); $this->assertEquals( 'Command: "some-route" is missing the following arguments: "arg1", "arg2"', - $e->getMessage() + $e->getMessage(), ); $this->assertSame(['arg1', 'arg2'], $e->getMissingArguments()); diff --git a/test/Exercise/AbstractExerciseTest.php b/test/Exercise/AbstractExerciseTest.php index c500b0f7..4bedbae5 100644 --- a/test/Exercise/AbstractExerciseTest.php +++ b/test/Exercise/AbstractExerciseTest.php @@ -3,9 +3,7 @@ namespace PhpSchool\PhpWorkshopTest\Exercise; use PhpSchool\PhpWorkshop\Event\EventDispatcher; -use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\Solution\SolutionFile; -use PhpSchool\PhpWorkshop\Solution\SolutionInterface; use PhpSchool\PhpWorkshopTest\Asset\AbstractExerciseImpl; use PHPUnit\Framework\TestCase; use ReflectionClass; diff --git a/test/Exercise/Scenario/CgiScenarioTest.php b/test/Exercise/Scenario/CgiScenarioTest.php index 4c0782ca..8cebba7d 100644 --- a/test/Exercise/Scenario/CgiScenarioTest.php +++ b/test/Exercise/Scenario/CgiScenarioTest.php @@ -3,7 +3,6 @@ namespace PhpSchool\PhpWorkshopTest\Exercise\Scenario; use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; -use PhpSchool\PhpWorkshop\Utils\Collection; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -25,15 +24,15 @@ public function testScenario(): void 'file1.txt' => 'content1', 'file2.txt' => 'content2', ], - $scenario->getFiles() + $scenario->getFiles(), ); static::assertEquals( [ $requestOne, - $requestTwo + $requestTwo, ], - $scenario->getExecutions() + $scenario->getExecutions(), ); } } diff --git a/test/Exercise/Scenario/CliScenarioTest.php b/test/Exercise/Scenario/CliScenarioTest.php index 1ea86a9a..50967b7b 100644 --- a/test/Exercise/Scenario/CliScenarioTest.php +++ b/test/Exercise/Scenario/CliScenarioTest.php @@ -21,7 +21,7 @@ public function testScenario(): void 'file1.txt' => 'content1', 'file2.txt' => 'content2', ], - $scenario->getFiles() + $scenario->getFiles(), ); static::assertEquals( @@ -30,9 +30,9 @@ public function testScenario(): void ['arg3', 'arg4'], ], array_map( - fn (Collection $collection) => $collection->getArrayCopy(), - $scenario->getExecutions() - ) + fn(Collection $collection) => $collection->getArrayCopy(), + $scenario->getExecutions(), + ), ); } } diff --git a/test/ExerciseDispatcherTest.php b/test/ExerciseDispatcherTest.php index bab752ea..6a8fa1a4 100644 --- a/test/ExerciseDispatcherTest.php +++ b/test/ExerciseDispatcherTest.php @@ -14,6 +14,7 @@ use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\ExerciseDispatcher; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; use PhpSchool\PhpWorkshop\ExerciseRunner\RunnerManager; use PhpSchool\PhpWorkshop\Input\Input; @@ -28,15 +29,8 @@ class ExerciseDispatcherTest extends TestCase { - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var string - */ - private $file; + private Filesystem $filesystem; + private string $file; public function setUp(): void { @@ -54,7 +48,7 @@ public function testGetEventDispatcher(): void $this->createMock(RunnerManager::class), $results, $eventDispatcher, - new CheckRepository() + new CheckRepository(), ); $this->assertSame($eventDispatcher, $exerciseDispatcher->getEventDispatcher()); @@ -69,7 +63,7 @@ public function testRequireCheckThrowsExceptionIfCheckDoesNotExist(): void $this->createMock(RunnerManager::class), new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository() + new CheckRepository(), ); $exerciseDispatcher->requireCheck('NotACheck'); } @@ -84,7 +78,7 @@ public function testRequireCheckThrowsExceptionIfPositionNotValid(): void $this->createMock(RunnerManager::class), new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $this->expectException(InvalidArgumentException::class); @@ -102,7 +96,7 @@ public function testRequireBeforeCheckIsCorrectlyRegistered(): void $this->createMock(RunnerManager::class), new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $exerciseDispatcher->requireCheck(get_class($check)); @@ -119,7 +113,7 @@ public function testRequireAfterCheckIsCorrectlyRegistered(): void $this->createMock(RunnerManager::class), new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $exerciseDispatcher->requireCheck(get_class($check)); @@ -135,7 +129,7 @@ public function testRequireCheckThrowsExceptionIfCheckIsNotSimpleOrListenable(): $this->createMock(RunnerManager::class), new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $this->expectException(InvalidArgumentException::class); @@ -153,7 +147,7 @@ public function testRequireListenableCheckAttachesToDispatcher(): void $this->createMock(RunnerManager::class), new ResultAggregator(), $eventDispatcher, - new CheckRepository([$check]) + new CheckRepository([$check]), ); $exerciseDispatcher->requireCheck(get_class($check)); @@ -178,13 +172,13 @@ public function testVerifyThrowsExceptionIfCheckDoesNotSupportExerciseType(): vo $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $this->expectException(CheckNotApplicableException::class); $this->expectExceptionMessage('Check: "Some Check" cannot process exercise: "Some Exercise" with type: "CLI"'); - $exerciseDispatcher->verify($exercise, new Input('app')); + $exerciseDispatcher->verify($exercise, new Input('app', ['program' => $this->file])); } public function testVerifyThrowsExceptionIfExerciseDoesNotImplementCorrectInterface(): void @@ -206,13 +200,13 @@ public function testVerifyThrowsExceptionIfExerciseDoesNotImplementCorrectInterf $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $this->expectException(ExerciseNotConfiguredException::class); $this->expectExceptionMessage('Exercise: "Some Exercise" should implement interface: "LolIDoNotExist"'); - $exerciseDispatcher->verify($exercise, new Input('app')); + $exerciseDispatcher->verify($exercise, new Input('app', ['program' => $this->file])); } public function testVerify(): void @@ -224,11 +218,13 @@ public function testVerify(): void $check->method('canRun')->with($exercise->getType())->willReturn(true); $check->method('getPosition')->willReturn(SimpleCheckInterface::CHECK_BEFORE); $check->method('getExerciseInterface')->willReturn(ExerciseInterface::class); - $check->method('check')->with($exercise, $input)->willReturn(new Success('Success!')); + $check->method('check') + ->with($this->isInstanceOf(ExecutionContext::class)) + ->willReturn(new Success('Success!')); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner->method('getRequiredChecks')->willReturn([get_class($check)]); - $runner->method('verify')->with($input)->willReturn(new Success('Success!')); + $runner->method('verify')->willReturn(new Success('Success!')); $runnerManager = $this->createMock(RunnerManager::class); $runnerManager->method('getRunner')->with($exercise)->willReturn($runner); @@ -237,7 +233,7 @@ public function testVerify(): void $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check]) + new CheckRepository([$check]), ); $result = $exerciseDispatcher->verify($exercise, $input); @@ -268,7 +264,7 @@ public function testVerifyOnlyRunsRequiredChecks(): void $check1 ->method('check') - ->with($exercise, $input) + ->with($this->isInstanceOf(ExecutionContext::class)) ->willReturn(new Success('Success!')); $check2 = $this @@ -279,7 +275,7 @@ public function testVerifyOnlyRunsRequiredChecks(): void $check2 ->expects($this->never()) ->method('check') - ->with($exercise, $input); + ->with($this->isInstanceOf(ExecutionContext::class)); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner @@ -289,7 +285,6 @@ public function testVerifyOnlyRunsRequiredChecks(): void $runner ->method('verify') - ->with($input) ->willReturn(new Success('Success!')); $runnerManager = $this->createMock(RunnerManager::class); @@ -302,7 +297,7 @@ public function testVerifyOnlyRunsRequiredChecks(): void $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check1, $check2]) + new CheckRepository([$check1, $check2]), ); $result = $exerciseDispatcher->verify($exercise, $input); @@ -319,18 +314,21 @@ public function testVerifyWithBeforeAndAfterRequiredChecks(): void $check1->method('canRun')->with($exercise->getType())->willReturn(true); $check1->method('getPosition')->willReturn(SimpleCheckInterface::CHECK_BEFORE); $check1->method('getExerciseInterface')->willReturn(ExerciseInterface::class); - $check1->method('check')->with($exercise, $input)->willReturn(new Success('Success!')); + $check1->method('check') + ->with($this->isInstanceOf(ExecutionContext::class)) + ->willReturn(new Success('Success!')); $check2 = $this->createMock(SimpleCheckInterface::class); $check2->method('canRun')->with($exercise->getType())->willReturn(true); $check2->method('getPosition')->willReturn(SimpleCheckInterface::CHECK_AFTER); $check2->method('getExerciseInterface')->willReturn(ExerciseInterface::class); - $check2->method('check')->with($exercise, $input)->willReturn(new Success('Success!')); - + $check2->method('check') + ->with($this->isInstanceOf(ExecutionContext::class)) + ->willReturn(new Success('Success!')); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner->method('getRequiredChecks')->willReturn([get_class($check1), get_class($check2)]); - $runner->method('verify')->with($input)->willReturn(new Success('Success!')); + $runner->method('verify')->willReturn(new Success('Success!')); $runnerManager = $this->createMock(RunnerManager::class); $runnerManager->method('getRunner')->with($exercise)->willReturn($runner); @@ -339,7 +337,7 @@ public function testVerifyWithBeforeAndAfterRequiredChecks(): void $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check1, $check2]) + new CheckRepository([$check1, $check2]), ); $result = $exerciseDispatcher->verify($exercise, $input); @@ -372,7 +370,7 @@ public function testWhenBeforeChecksFailTheyReturnImmediately(): void $check1 ->method('check') - ->with($exercise, $input) + ->with($this->isInstanceOf(ExecutionContext::class)) ->willReturn(new Failure('Failure', 'nope')); $check2 = $this @@ -395,7 +393,7 @@ public function testWhenBeforeChecksFailTheyReturnImmediately(): void $check2 ->expects($this->never()) ->method('check') - ->with($exercise, $input); + ->with($this->isInstanceOf(ExecutionContext::class)); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner @@ -415,7 +413,7 @@ public function testWhenBeforeChecksFailTheyReturnImmediately(): void $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([$check1, $check2]) + new CheckRepository([$check1, $check2]), ); $result = $exerciseDispatcher->verify($exercise, $input); @@ -436,33 +434,33 @@ public function testAllEventsAreDispatched(): void [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.start'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.pre.execute'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.post.execute'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.post.check'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.finish'; - }) - ] + }), + ], ); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner->method('getRequiredChecks')->willReturn([]); - $runner->method('verify')->with($input)->willReturn(new Success('Success!')); + $runner->method('verify')->willReturn(new Success('Success!')); $runnerManager = $this->createMock(RunnerManager::class); $runnerManager->method('getRunner')->with($exercise)->willReturn($runner); @@ -471,7 +469,7 @@ public function testAllEventsAreDispatched(): void $runnerManager, new ResultAggregator(), $eventDispatcher, - new CheckRepository() + new CheckRepository(), ); $exerciseDispatcher->verify($exercise, $input); @@ -490,23 +488,23 @@ public function testVerifyPostExecuteIsStillDispatchedEvenIfRunnerThrowsExceptio [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.start'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.pre.execute'; - }) + }), ], [ $this->callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'verify.post.execute'; - }) - ] + }), + ], ); $runner = $this->createMock(ExerciseRunnerInterface::class); $runner->method('getRequiredChecks')->willReturn([]); - $runner->method('verify')->with($input)->will($this->throwException(new RuntimeException())); + $runner->method('verify')->will($this->throwException(new RuntimeException())); $runnerManager = $this->createMock(RunnerManager::class); $runnerManager->method('getRunner')->with($exercise)->willReturn($runner); @@ -515,7 +513,7 @@ public function testVerifyPostExecuteIsStillDispatchedEvenIfRunnerThrowsExceptio $runnerManager, new ResultAggregator(), $eventDispatcher, - new CheckRepository() + new CheckRepository(), ); $this->expectException(RuntimeException::class); @@ -530,7 +528,7 @@ public function testRun(): void $runner = $this->createMock(ExerciseRunnerInterface::class); $runner->method('getRequiredChecks')->willReturn([]); - $runner->method('run')->with($input, $output)->willReturn(true); + $runner->method('run')->willReturn(true); $runnerManager = $this->createMock(RunnerManager::class); $runnerManager->method('getRunner')->with($exercise)->willReturn($runner); @@ -539,7 +537,7 @@ public function testRun(): void $runnerManager, new ResultAggregator(), new EventDispatcher(new ResultAggregator()), - new CheckRepository([new PhpLintCheck()]) + new CheckRepository([new PhpLintCheck()]), ); $this->assertTrue($exerciseDispatcher->run($exercise, $input, $output)); diff --git a/test/ExerciseRendererTest.php b/test/ExerciseRendererTest.php index de799e51..7457f89c 100644 --- a/test/ExerciseRendererTest.php +++ b/test/ExerciseRendererTest.php @@ -73,7 +73,7 @@ public function testExerciseRendererSetsCurrentExerciseAndRendersExercise(): voi $markdownRenderer = new MarkdownRenderer( new DocParser(Environment::createCommonMarkEnvironment()), - (new CliRendererFactory())->__invoke() + (new CliRendererFactory())->__invoke(), ); $color = new Color(); @@ -86,7 +86,7 @@ public function testExerciseRendererSetsCurrentExerciseAndRendersExercise(): voi $userStateSerializer, $markdownRenderer, $color, - new StdOutput($color, $this->createMock(Terminal::class)) + new StdOutput($color, $this->createMock(Terminal::class)), ); $this->expectOutputString(file_get_contents(__DIR__ . '/res/exercise-help-expected.txt')); diff --git a/test/ExerciseRepositoryTest.php b/test/ExerciseRepositoryTest.php index 92aaa031..75d86ab2 100644 --- a/test/ExerciseRepositoryTest.php +++ b/test/ExerciseRepositoryTest.php @@ -3,12 +3,9 @@ namespace PhpSchool\PhpWorkshopTest; use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseMissingInterface; use PHPUnit\Framework\TestCase; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\ExerciseRepository; class ExerciseRepositoryTest extends TestCase diff --git a/test/ExerciseRunner/CgiRunnerTest.php b/test/ExerciseRunner/CgiRunnerTest.php index df8304a3..5daee748 100644 --- a/test/ExerciseRunner/CgiRunnerTest.php +++ b/test/ExerciseRunner/CgiRunnerTest.php @@ -5,16 +5,19 @@ use Colors\Color; use GuzzleHttp\Psr7\Request; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseImpl; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; use PhpSchool\PhpWorkshop\Check\PhpLintCheck; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; -use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; @@ -22,36 +25,23 @@ use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; -use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseInterface; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; class CgiRunnerTest extends TestCase { use AssertionRenames; - /** @var CgiRunner */ - private $runner; - - /** - * @var CgiExerciseInterface - */ + private CgiRunner $runner; private $exercise; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; + private EventDispatcher $eventDispatcher; public function setUp(): void { - $this->exercise = $this->createMock(CgiExerciseInterface::class); + $this->exercise = new CgiExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher); - - $this->exercise - ->method('getType') - ->willReturn(ExerciseType::CGI()); + $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); $this->assertEquals('CGI Program Runner', $this->runner->getName()); } @@ -71,121 +61,87 @@ public function testRequiredChecks(): void public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/solution-error.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $request = (new Request('GET', 'http://some.site?number=5')); - - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario( + (new CgiScenario())->withExecution((new Request('GET', 'http://some.site?number=5'))), + ); $regex = "/^PHP Code failed to execute\. Error: \"PHP Parse error: syntax error, unexpected end of file in/"; $this->expectException(SolutionExecutionException::class); $this->expectExceptionMessageMatches($regex); - $this->runner->verify(new Input('app', ['program' => ''])); + + $this->runner->verify(new TestContext($this->exercise)); } public function testVerifyReturnsSuccessIfGetSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $request = (new Request('GET', 'http://some.site?number=5')); + $this->exercise->setSolution($solution); + $this->exercise->setScenario( + (new CgiScenario())->withExecution((new Request('GET', 'http://some.site?number=5'))), + ); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/get-solution.php'); + $result = $this->runner->verify($context); - $this->assertInstanceOf( - CgiResult::class, - $this->runner->verify(new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-solution.php')])) - ); + $this->assertInstanceOf(CgiResult::class, $result); + $this->assertTrue($result->isSuccessful()); } public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/post-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('POST', 'http://some.site')) ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write('number=5'); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); - $this->assertInstanceOf( - CgiResult::class, - $res = $this->runner->verify( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/post-solution.php')]) - ) - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/post-solution.php'); + $result = $this->runner->verify($context); - $this->assertTrue($res->isSuccessful()); + $this->assertInstanceOf(CgiResult::class, $result); + $this->assertTrue($result->isSuccessful()); } public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutputWithMultipleParams(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/post-multiple-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('POST', 'http://some.site')) ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write('number=5&start=4'); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); - $result = $this->runner->verify( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/post-multiple-solution.php')]) - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/post-multiple-solution.php'); + $result = $this->runner->verify($context); $this->assertInstanceOf(CgiResult::class, $result); + $this->assertTrue($result->isSuccessful()); } public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); - $failure = $this->runner->verify( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/user-error.php')]) - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/user-error.php'); + $result = $this->runner->verify($context); - $this->assertInstanceOf(CgiResult::class, $failure); - $this->assertCount(1, $failure); + $this->assertInstanceOf(CgiResult::class, $result); + $this->assertCount(1, $result); - $result = iterator_to_array($failure)[0]; + $result = iterator_to_array($result)[0]; $this->assertInstanceOf(Failure::class, $result); $failureMsg = '/^PHP Code failed to execute. Error: "PHP Parse error: syntax error, unexpected end of file'; @@ -196,26 +152,19 @@ public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); - $failure = $this->runner->verify( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-user-wrong.php')]) - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/get-user-wrong.php'); + $result = $this->runner->verify($context); - $this->assertInstanceOf(CgiResult::class, $failure); - $this->assertCount(1, $failure); + $this->assertInstanceOf(CgiResult::class, $result); + $this->assertCount(1, $result); - $result = iterator_to_array($failure)[0]; + $result = iterator_to_array($result)[0]; $this->assertInstanceOf(RequestFailure::class, $result); $this->assertEquals('10', $result->getExpectedOutput()); $this->assertEquals('15', $result->getActualOutput()); @@ -226,42 +175,35 @@ public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput() public function testVerifyReturnsFailureIfSolutionOutputHeadersDoesNotMatchUserOutputHeaders(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution-header.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); - $failure = $this->runner->verify( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-user-header-wrong.php')]) - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/get-user-header-wrong.php'); + $result = $this->runner->verify($context); - $this->assertInstanceOf(CgiResult::class, $failure); - $this->assertCount(1, $failure); + $this->assertInstanceOf(CgiResult::class, $result); + $this->assertCount(1, $result); - $result = iterator_to_array($failure)[0]; + $result = iterator_to_array($result)[0]; $this->assertInstanceOf(RequestFailure::class, $result); $this->assertSame($result->getExpectedOutput(), $result->getActualOutput()); $this->assertEquals( [ 'Pragma' => 'cache', - 'Content-type' => 'text/html; charset=UTF-8' + 'Content-type' => 'text/html; charset=UTF-8', ], - $result->getExpectedHeaders() + $result->getExpectedHeaders(), ); $this->assertEquals( [ 'Pragma' => 'no-cache', - 'Content-type' => 'text/html; charset=UTF-8' + 'Content-type' => 'text/html; charset=UTF-8', ], - $result->getActualHeaders() + $result->getActualHeaders(), ); } @@ -275,13 +217,12 @@ public function testRunPassesOutputAndReturnsSuccessIfAllRequestsAreSuccessful() $this->eventDispatcher->listen( 'cgi.run.student-execute.pre', - new OutputRunInfoListener($output, new RequestRenderer()) + new OutputRunInfoListener($output, new RequestRenderer()), ); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request1, $request2]); + $this->exercise->setScenario( + (new CgiScenario())->withExecution($request1)->withExecution($request2), + ); $exp = "\n\e[1m\e[4mRequest"; $exp .= "\e[0m\e[0m\n\n"; @@ -306,12 +247,10 @@ public function testRunPassesOutputAndReturnsSuccessIfAllRequestsAreSuccessful() $this->expectOutputString($exp); - $success = $this->runner->run( - new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-solution.php')]), - $output - ); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cgi/get-solution.php'); + $result = $this->runner->run($context, $output); - $this->assertTrue($success); + $this->assertTrue($result); } public function testRunPassesOutputAndReturnsFailureIfARequestFails(): void @@ -323,13 +262,10 @@ public function testRunPassesOutputAndReturnsFailureIfARequestFails(): void $this->eventDispatcher->listen( 'cgi.run.student-execute.pre', - new OutputRunInfoListener($output, new RequestRenderer()) + new OutputRunInfoListener($output, new RequestRenderer()), ); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request1]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request1)); $exp = "\n\e[1m\e[4mRequest"; $exp .= "\e[0m\e[0m\n\n"; @@ -345,10 +281,9 @@ public function testRunPassesOutputAndReturnsFailureIfARequestFails(): void $this->expectOutputString($exp); - $success = $this->runner->run( - new Input('app', ['program' => 'not-existing-file.php']), - $output - ); - $this->assertFalse($success); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/non-existent.php'); + $result = $this->runner->run($context, $output); + + $this->assertFalse($result); } } diff --git a/test/ExerciseRunner/CliRunnerTest.php b/test/ExerciseRunner/CliRunnerTest.php index 78bad5e0..9c02ea43 100644 --- a/test/ExerciseRunner/CliRunnerTest.php +++ b/test/ExerciseRunner/CliRunnerTest.php @@ -4,8 +4,13 @@ use Colors\Color; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; @@ -13,45 +18,30 @@ use PhpSchool\PhpWorkshop\Event\CliExecuteEvent; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; -use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshop\Result\Cli\CliResult; use PhpSchool\PhpWorkshop\Result\Cli\GenericFailure; use PhpSchool\PhpWorkshop\Result\Cli\RequestFailure; use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; class CliRunnerTest extends TestCase { use AssertionRenames; - /** @var CliRunner */ - private $runner; - - /** - * @var CliExerciseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $exercise; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; + private CliRunner $runner; + private CliExerciseImpl $exercise; + private EventDispatcher $eventDispatcher; public function setUp(): void { - $this->exercise = $this->createMock(CliExerciseInterface::class); + $this->exercise = new CliExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CliRunner($this->exercise, $this->eventDispatcher); - - $this->exercise - ->method('getType') - ->willReturn(ExerciseType::CLI()); + $this->runner = new CliRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); $this->assertEquals('CLI Program Runner', $this->runner->getName()); } @@ -71,87 +61,59 @@ public function testRequiredChecks(): void public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution-error.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); $regex = "/^PHP Code failed to execute\\. Error: \"PHP Parse error: syntax error, unexpected end of file"; $regex .= ", expecting ['\"][,;]['\"] or ['\"][;,]['\"]/"; $this->expectException(SolutionExecutionException::class); $this->expectExceptionMessageMatches($regex); - $this->runner->verify(new Input('app', ['program' => ''])); + + $this->runner->verify(new TestContext($this->exercise)); } public function testVerifyReturnsSuccessIfSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); - - $this->assertInstanceOf( - CliResult::class, - $res = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php'])) - ); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user.php'); + $result = $this->runner->verify($context); - $this->assertTrue($res->isSuccessful()); + $this->assertInstanceOf(CliResult::class, $result); + $this->assertTrue($result->isSuccessful()); } public function testSuccessWithSingleSetOfArgsForBC(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->assertInstanceOf( - CliResult::class, - $res = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php'])) - ); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user.php'); + $result = $this->runner->verify($context); - $this->assertTrue($res->isSuccessful()); + $this->assertInstanceOf(CliResult::class, $result); + $this->assertTrue($result->isSuccessful()); } public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); - - $failure = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user-error.php'])); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user-error.php'); + $result = $this->runner->verify($context); $failureMsg = '/^PHP Code failed to execute. Error: "PHP Parse error: syntax error, '; $failureMsg .= "unexpected end of file, expecting ['\"][,;]['\"] or ['\"][;,]['\"]/"; - $this->assertInstanceOf(CliResult::class, $failure); - $this->assertCount(1, $failure); + $this->assertInstanceOf(CliResult::class, $result); + $this->assertCount(1, $result); - $result = iterator_to_array($failure)[0]; + $result = iterator_to_array($result)[0]; $this->assertInstanceOf(GenericFailure::class, $result); $this->assertMatchesRegularExpression($failureMsg, $result->getReason()); } @@ -159,22 +121,16 @@ public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); - $failure = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user-wrong.php'])); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user-wrong.php'); + $result = $this->runner->verify($context); - $this->assertInstanceOf(CliResult::class, $failure); - $this->assertCount(1, $failure); + $this->assertInstanceOf(CliResult::class, $result); + $this->assertCount(1, $result); - $result = iterator_to_array($failure)[0]; + $result = iterator_to_array($result)[0]; $this->assertInstanceOf(RequestFailure::class, $result); $this->assertEquals('6', $result->getExpectedOutput()); @@ -189,14 +145,9 @@ public function testRunPassesOutputAndReturnsSuccessIfScriptIsSuccessful(): void $this->eventDispatcher->listen( 'cli.run.student-execute.pre', - new OutputRunInfoListener($output, new RequestRenderer()) + new OutputRunInfoListener($output, new RequestRenderer()), ); - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3], [4, 5, 6]]); - $exp = "\n\e[1m\e[4mArguments\e[0m\e[0m\n"; $exp .= "1, 2, 3\n"; $exp .= "\n\e[1m\e[4mOutput\e[0m\e[0m\n\n"; @@ -210,25 +161,28 @@ public function testRunPassesOutputAndReturnsSuccessIfScriptIsSuccessful(): void $this->expectOutputString($exp); - $success = $this->runner->run(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php']), $output); - $this->assertTrue($success); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])->withExecution([4, 5, 6])); + + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user.php'); + $result = $this->runner->run($context, $output); + + $this->assertTrue($result); } public function testRunPassesOutputAndReturnsFailureIfScriptFails(): void { $output = new StdOutput(new Color(), $this->createMock(Terminal::class)); - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->expectOutputRegex( - "/(PHP )?Parse error:\W+syntax error, unexpected end of file, expecting ['\"][,;]['\"] or ['\"][;,]['\"] /" + "/(PHP )?Parse error:\W+syntax error, unexpected end of file, expecting ['\"][,;]['\"] or ['\"][;,]['\"] /", ); - $success = $this->runner->run(new Input('app', ['program' => __DIR__ . '/../res/cli/user-error.php']), $output); - $this->assertFalse($success); + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user-error.php'); + $result = $this->runner->run($context, $output); + + $this->assertFalse($result); } public function testsArgsAppendedByEventsArePassedToResults(): void @@ -237,26 +191,18 @@ public function testsArgsAppendedByEventsArePassedToResults(): void ['cli.verify.student-execute.pre', 'cli.verify.reference-execute.pre'], function (CliExecuteEvent $e) { $e->appendArg('4'); - } + }, ); $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->assertInstanceOf( - CliResult::class, - $res = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php'])) - ); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + + $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user.php'); + $result = $this->runner->verify($context); - $this->assertTrue($res->isSuccessful()); - $this->assertEquals([1, 2, 3, 4], $res->getResults()[0]->getArgs()->getArrayCopy()); + $this->assertInstanceOf(CliResult::class, $result); + $this->assertTrue($result->isSuccessful()); + $this->assertEquals([1, 2, 3, 4], $result->getResults()[0]->getArgs()->getArrayCopy()); } } diff --git a/test/ExerciseRunner/Context/ExecutionContextTest.php b/test/ExerciseRunner/Context/ExecutionContextTest.php index 016b29af..09a0e291 100644 --- a/test/ExerciseRunner/Context/ExecutionContextTest.php +++ b/test/ExerciseRunner/Context/ExecutionContextTest.php @@ -19,7 +19,7 @@ public function testGetters(): void '/student-dir', '/reference-dir', $exercise, - $input + $input, ); static::assertSame($exercise, $context->getExercise()); @@ -36,7 +36,7 @@ public function testHasStudentSolution(): void '/student-dir', '/reference-dir', $exercise, - $input + $input, ); static::assertTrue($context->hasStudentSolution()); @@ -47,7 +47,7 @@ public function testHasStudentSolution(): void '/student-dir', '/reference-dir', $exercise, - $input + $input, ); static::assertFalse($context->hasStudentSolution()); @@ -61,7 +61,7 @@ public function testGetEntryPoint(): void '/student-dir', '/reference-dir', $exercise, - $input + $input, ); static::assertSame('/student-dir/solution.php', $context->getEntryPoint()); @@ -77,7 +77,7 @@ public function testGetEntryPointThrowsExceptionWhenNoStudentSolution(): void '/student-dir', '/reference-dir', $exercise, - $input + $input, ); $context->getEntryPoint(); diff --git a/test/ExerciseRunner/Context/TestContextTest.php b/test/ExerciseRunner/Context/TestContextTest.php index ac5ee0a9..72ad3d78 100644 --- a/test/ExerciseRunner/Context/TestContextTest.php +++ b/test/ExerciseRunner/Context/TestContextTest.php @@ -3,10 +3,7 @@ namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context; use PhpSchool\PhpWorkshop\Exercise\MockExercise; -use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; -use PhpSchool\PhpWorkshop\Solution\DirectorySolution; -use PhpSchool\PhpWorkshop\Utils\System; use PHPUnit\Framework\TestCase; class TestContextTest extends TestCase diff --git a/test/ExerciseRunner/CustomVerifyingRunnerTest.php b/test/ExerciseRunner/CustomVerifyingRunnerTest.php index 76c79749..33ec59f1 100644 --- a/test/ExerciseRunner/CustomVerifyingRunnerTest.php +++ b/test/ExerciseRunner/CustomVerifyingRunnerTest.php @@ -3,24 +3,17 @@ namespace PhpSchool\PhpWorkshopTest\ExerciseRunner; use Colors\Color; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\ExerciseRunner\CustomVerifyingRunner; -use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshopTest\Asset\CustomVerifyingExerciseImpl; use PHPUnit\Framework\TestCase; class CustomVerifyingRunnerTest extends TestCase { - /** - * @var CustomVerifyingRunner - */ - private $runner; - - /** - * @var CustomVerifyingExerciseImpl - */ - private $exercise; + private CustomVerifyingRunner $runner; + private CustomVerifyingExerciseImpl $exercise; public function setUp(): void { @@ -46,11 +39,13 @@ public function testRunOutputsErrorMessage(): void $this->expectOutputString($exp); - $this->runner->run(new Input('app'), $output); + $this->runner->run(new TestContext(), $output); } public function testVerifyProxiesToExercise(): void { - self::assertEquals($this->exercise->verify(), $this->runner->verify(new Input('app'))); + $result = $this->runner->verify(new TestContext()); + + self::assertEquals($this->exercise->verify(), $result); } } diff --git a/test/ExerciseRunner/EnvironmentManagerTest.php b/test/ExerciseRunner/EnvironmentManagerTest.php new file mode 100644 index 00000000..90f9ea04 --- /dev/null +++ b/test/ExerciseRunner/EnvironmentManagerTest.php @@ -0,0 +1,96 @@ +createStudentSolutionDirectory(); + + $scenario = (new CliScenario()) + ->withFile('file.txt', 'content') + ->withFile('file2.txt', 'content2'); + + $manager = new EnvironmentManager(new Filesystem(), new EventDispatcher(new ResultAggregator())); + $manager->prepareStudent($context, $scenario); + + static::assertStringEqualsFile($context->getStudentExecutionDirectory() . '/file.txt', 'content'); + static::assertStringEqualsFile($context->getStudentExecutionDirectory() . '/file2.txt', 'content2'); + } + + public function testPrepareReferenceCopiesAllScenarioFilesAndSolutionFilesToExecutionDirectory(): void + { + $exercise = new CliExerciseImpl(); + $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); + $exercise->setSolution($solution); + + $context = new TestContext($exercise); + + $scenario = (new CliScenario()) + ->withFile('file.txt', 'content') + ->withFile('file2.txt', 'content2'); + + $manager = new EnvironmentManager(new Filesystem(), new EventDispatcher(new ResultAggregator())); + $manager->prepareReference($context, $scenario); + + static::assertFileEquals($context->getReferenceExecutionDirectory() . '/solution.php', __DIR__ . '/../res/cli/solution.php'); + static::assertStringEqualsFile($context->getReferenceExecutionDirectory() . '/file.txt', 'content'); + static::assertStringEqualsFile($context->getReferenceExecutionDirectory() . '/file2.txt', 'content2'); + } + + /** + * @dataProvider finishEvents + */ + public function testFileAreCleanedUpOnlyWhenFinishEventIsDispatched(string $eventName): void + { + $exercise = new CliExerciseImpl(); + $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); + $exercise->setSolution($solution); + + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + + $scenario = (new CliScenario()) + ->withFile('file.txt', 'content') + ->withFile('file2.txt', 'content2'); + + $eventDispatcher = new EventDispatcher(new ResultAggregator()); + $manager = new EnvironmentManager(new Filesystem(), $eventDispatcher); + $manager->prepareStudent($context, $scenario); + $manager->prepareReference($context, $scenario); + + static::assertFileExists($context->getStudentExecutionDirectory()); + static::assertFileExists($context->getReferenceExecutionDirectory()); + + $eventDispatcher->dispatch(new ExerciseRunnerEvent($eventName, $context)); + + static::assertFileExists($context->getStudentExecutionDirectory()); + static::assertFileNotExists($context->getReferenceExecutionDirectory() . '/file.txt'); + static::assertFileNotExists($context->getReferenceExecutionDirectory() . '/file2.txt'); + static::assertFileNotExists($context->getReferenceExecutionDirectory()); + } + + /** + * @return array + */ + public function finishEvents(): array + { + return [ + ['run.finish'], + ['verify.finish'], + ]; + } +} diff --git a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php index 03daf67e..5010b80c 100644 --- a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php @@ -7,27 +7,21 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CgiRunnerFactory; -use PhpSchool\PhpWorkshop\Utils\RequestRenderer; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseImpl; use PHPUnit\Framework\TestCase; class CgiRunnerFactoryTest extends TestCase { - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @var CgiRunnerFactory - */ - private $factory; + private EventDispatcher $eventDispatcher; + private CgiRunnerFactory $factory; public function setUp(): void { $this->eventDispatcher = $this->createMock(EventDispatcher::class); - $this->factory = new CgiRunnerFactory($this->eventDispatcher); + $this->factory = new CgiRunnerFactory($this->eventDispatcher, new HostProcessFactory(), $this->createMock(EnvironmentManager::class)); } public function testSupports(): void diff --git a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php index 6678104d..ce5fe6f6 100644 --- a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php @@ -7,26 +7,21 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CliRunnerFactory; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; class CliRunnerFactoryTest extends TestCase { - /** - * @var EventDispatcher - */ - private $eventDispatcher; - - /** - * @var CliRunnerFactory - */ - private $factory; + private EventDispatcher $eventDispatcher; + private CliRunnerFactory $factory; public function setUp(): void { $this->eventDispatcher = $this->createMock(EventDispatcher::class); - $this->factory = new CliRunnerFactory($this->eventDispatcher); + $this->factory = new CliRunnerFactory($this->eventDispatcher, new HostProcessFactory(), $this->createMock(EnvironmentManager::class)); } public function testSupports(): void diff --git a/test/Factory/EventDispatcherFactoryTest.php b/test/Factory/EventDispatcherFactoryTest.php index f20bc1f7..52fa7358 100644 --- a/test/Factory/EventDispatcherFactoryTest.php +++ b/test/Factory/EventDispatcherFactoryTest.php @@ -5,7 +5,6 @@ use DI\ContainerBuilder; use PhpSchool\PhpWorkshop\Event\Event; use Psr\Container\ContainerInterface; -use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Factory\EventDispatcherFactory; use PhpSchool\PhpWorkshop\ResultAggregator; @@ -32,11 +31,11 @@ public function testExceptionIsThrownIfEventListenerGroupsNotArray(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - new \stdClass() + new \stdClass(), ); $c->method('has')->with('eventListeners')->willReturn(true); @@ -54,11 +53,11 @@ public function testExceptionIsThrownIfEventsNotArray(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - ['my-group' => new \stdClass()] + ['my-group' => new \stdClass()], ); $c->method('has')->with('eventListeners')->willReturn(true); @@ -73,8 +72,8 @@ public function testExceptionIsThrownIfEventListenersNotArray(): void { $eventConfig = [ 'my-group' => [ - 'someEvent' => new \stdClass() - ] + 'someEvent' => new \stdClass(), + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -82,11 +81,11 @@ public function testExceptionIsThrownIfEventListenersNotArray(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - $eventConfig + $eventConfig, ); $c->method('has')->with('eventListeners')->willReturn(true); @@ -101,8 +100,8 @@ public function testExceptionIsThrownIfListenerNotCallable(): void { $eventConfig = [ 'my-group' => [ - 'someEvent' => [new \stdClass()] - ] + 'someEvent' => [new \stdClass()], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -110,11 +109,11 @@ public function testExceptionIsThrownIfListenerNotCallable(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - $eventConfig + $eventConfig, ); $c->method('has')->with('eventListeners')->willReturn(true); @@ -129,8 +128,8 @@ public function testExceptionIsThrownIfEventsListenerContainerEntryNotExist(): v { $eventConfig = [ 'my-group' => [ - 'someEvent' => [containerListener('nonExistingContainerEntry')()] - ] + 'someEvent' => [containerListener('nonExistingContainerEntry')()], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -138,21 +137,21 @@ public function testExceptionIsThrownIfEventsListenerContainerEntryNotExist(): v $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - $eventConfig + $eventConfig, ); $c->method('has') ->withConsecutive( ['eventListeners'], - ['nonExistingContainerEntry'] + ['nonExistingContainerEntry'], ) ->willReturnOnConsecutiveCalls( true, - false + false, ); $this->expectException(InvalidArgumentException::class); @@ -163,13 +162,12 @@ public function testExceptionIsThrownIfEventsListenerContainerEntryNotExist(): v public function testConfigEventListenersWithAnonymousFunction(): void { - $callback = function () { - }; + $callback = function () {}; $eventConfig = [ 'my-group' => [ - 'someEvent' => [$callback] - ] + 'someEvent' => [$callback], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -177,11 +175,11 @@ public function testConfigEventListenersWithAnonymousFunction(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - $eventConfig + $eventConfig, ); $c->method('has')->with('eventListeners')->willReturn(true); @@ -190,10 +188,10 @@ public function testConfigEventListenersWithAnonymousFunction(): void $this->assertSame( [ 'someEvent' => [ - $callback - ] + $callback, + ], ], - $dispatcher->getListeners() + $dispatcher->getListeners(), ); } @@ -201,8 +199,8 @@ public function testListenerFromContainerIsNotFetchedDuringAttaching(): void { $eventConfig = [ 'my-group' => [ - 'someEvent' => [containerListener('containerEntry')] - ] + 'someEvent' => [containerListener('containerEntry')], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -210,21 +208,21 @@ public function testListenerFromContainerIsNotFetchedDuringAttaching(): void $c->method('get') ->withConsecutive( [ResultAggregator::class], - ['eventListeners'] + ['eventListeners'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), - $eventConfig + $eventConfig, ); $c->method('has') ->withConsecutive( ['eventListeners'], - ['containerEntry'] + ['containerEntry'], ) ->willReturnOnConsecutiveCalls( true, - true + true, ); $dispatcher = (new EventDispatcherFactory())->__invoke($c); @@ -235,8 +233,8 @@ public function testListenerFromContainerIsFetchedWhenEventDispatched(): void { $eventConfig = [ 'my-group' => [ - 'someEvent' => [containerListener('containerEntry')] - ] + 'someEvent' => [containerListener('containerEntry')], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -245,23 +243,22 @@ public function testListenerFromContainerIsFetchedWhenEventDispatched(): void ->withConsecutive( [ResultAggregator::class], ['eventListeners'], - ['containerEntry'] + ['containerEntry'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), $eventConfig, - function () { - } + function () {}, ); $c->method('has') ->withConsecutive( ['eventListeners'], - ['containerEntry'] + ['containerEntry'], ) ->willReturnOnConsecutiveCalls( true, - true + true, ); $dispatcher = (new EventDispatcherFactory())->__invoke($c); @@ -274,8 +271,8 @@ public function testExceptionIsThrownIfMethodDoesNotExistOnContainerEntry(): voi { $eventConfig = [ 'my-group' => [ - 'someEvent' => [containerListener('containerEntry', 'notHere')()] - ] + 'someEvent' => [containerListener('containerEntry', 'notHere')()], + ], ]; $c = $this->createMock(ContainerInterface::class); @@ -284,22 +281,22 @@ public function testExceptionIsThrownIfMethodDoesNotExistOnContainerEntry(): voi ->withConsecutive( [ResultAggregator::class], ['eventListeners'], - ['containerEntry'] + ['containerEntry'], ) ->willReturnOnConsecutiveCalls( new ResultAggregator(), $eventConfig, - new \stdClass() + new \stdClass(), ); $c->method('has') ->withConsecutive( ['eventListeners'], - ['containerEntry'] + ['containerEntry'], ) ->willReturnOnConsecutiveCalls( true, - true + true, ); $this->expectException(InvalidArgumentException::class); diff --git a/test/Factory/MenuFactoryTest.php b/test/Factory/MenuFactoryTest.php index 4ea1ff55..c8e1bd69 100644 --- a/test/Factory/MenuFactoryTest.php +++ b/test/Factory/MenuFactoryTest.php @@ -2,7 +2,6 @@ namespace PhpSchool\PhpWorkshopTest\Factory; -use PhpSchool\CliMenu\MenuItem\SelectableItem; use PhpSchool\PhpWorkshop\Event\EventInterface; use PhpSchool\PhpWorkshop\UserState\Serializer; use PhpSchool\PhpWorkshop\UserState\UserState; @@ -59,7 +58,7 @@ public function testFactoryReturnsInstance(): void 'workshopTitle' => 'TITLE', WorkshopType::class => WorkshopType::STANDARD(), EventDispatcher::class => $this->createMock(EventDispatcher::class), - Terminal::class => $terminal + Terminal::class => $terminal, ]; $container @@ -106,13 +105,13 @@ public function testSelectExercise(): void [ self::callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'exercise.selected'; - }) + }), ], [ self::callback(function ($event) { return $event instanceof EventInterface && $event->getName() === 'exercise.selected.exercise'; - }) - ] + }), + ], ); $exerciseRenderer = $this->createMock(ExerciseRenderer::class); @@ -133,7 +132,7 @@ public function testSelectExercise(): void 'workshopTitle' => 'TITLE', WorkshopType::class => WorkshopType::STANDARD(), EventDispatcher::class => $eventDispatcher, - Terminal::class => $terminal + Terminal::class => $terminal, ]; $container diff --git a/test/FunctionsTest.php b/test/FunctionsTest.php index c36e2e8e..be392134 100644 --- a/test/FunctionsTest.php +++ b/test/FunctionsTest.php @@ -40,8 +40,8 @@ public function camelCaseToKebabCaseProvider(): array ['camelCase', 'camel-case'], [ 'educationIsThePassportToTheFutureForTomorrowBelongsToThoseWhoPrepareForItToday', - 'education-is-the-passport-to-the-future-for-tomorrow-belongs-to-those-who-prepare-for-it-today' - ] + 'education-is-the-passport-to-the-future-for-tomorrow-belongs-to-those-who-prepare-for-it-today', + ], ]; } diff --git a/test/Listener/CodePatchListenerTest.php b/test/Listener/CodePatchListenerTest.php index 503ef535..4819ce05 100644 --- a/test/Listener/CodePatchListenerTest.php +++ b/test/Listener/CodePatchListenerTest.php @@ -5,32 +5,16 @@ use PhpSchool\PhpWorkshop\CodePatcher; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\CodePatchListener; -use PhpSchool\PhpWorkshop\Utils\System; +use PhpSchool\PhpWorkshop\Utils\Path; use PhpSchool\PhpWorkshopTest\Asset\ProvidesSolutionExercise; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\Filesystem\Filesystem; class CodePatchListenerTest extends TestCase { - /** - * @var string - */ - private $file; - - /** - * @var string - */ - private $solution; - - /** - * @var Filesystem - */ - private $filesystem; - /** * @var CodePatcher */ @@ -38,29 +22,19 @@ class CodePatchListenerTest extends TestCase public function setUp(): void { - $this->filesystem = new Filesystem(); $this->codePatcher = $this->createMock(CodePatcher::class); - - $this->file = sprintf('%s/%s/submission.php', System::tempDir(), $this->getName()); - mkdir(dirname($this->file), 0775, true); - touch($this->file); - - $this->solution = sprintf('%s/%s/solution.php', System::tempDir(), $this->getName()); - touch($this->solution); - } - - public function tearDown(): void - { - $this->filesystem->remove(dirname($this->file)); } public function testPatchUpdatesCode(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -68,19 +42,25 @@ public function testPatchUpdatesCode(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT', + ); } public function testRevertAfterPatch(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -88,20 +68,26 @@ public function testRevertAfterPatch(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); $listener->revert($event); - self::assertStringEqualsFile($this->file, 'ORIGINAL CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'ORIGINAL CONTENT', + ); } public function testPatchesProvidedSolution(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = new ProvidesSolutionExercise(); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->exactly(2)) ->method('patch') @@ -109,43 +95,57 @@ public function testPatchesProvidedSolution(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); - self::assertStringEqualsFile($exercise->getSolution()->getEntryPoint()->getAbsolutePath(), 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT', + ); + self::assertStringEqualsFile( + Path::join( + $context->getReferenceExecutionDirectory(), + $exercise->getSolution()->getEntryPoint()->getRelativePath(), + ), + 'MODIFIED CONTENT', + ); } public function testFileIsLoggedWhenPatches(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $this->codePatcher ->expects($this->once()) ->method('patch') ->with($exercise, 'ORIGINAL CONTENT') ->willReturn('MODIFIED CONTENT'); + $path = Path::join($context->getStudentExecutionDirectory(), 'solution.php'); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('debug') - ->with('Patching file: ' . $this->file); + ->with('Patching file: ' . $path); $listener = new CodePatchListener($this->codePatcher, $logger, false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); } public function testRevertDoesNotRevertStudentSubmissionPatchIfInDebugMode(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -153,10 +153,13 @@ public function testRevertDoesNotRevertStudentSubmissionPatchIfInDebugMode(): vo ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), true); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); $listener->revert($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT', + ); } } diff --git a/test/Listener/ConfigureCommandListenerTest.php b/test/Listener/ConfigureCommandListenerTest.php index 3aff44bb..f6291d7e 100644 --- a/test/Listener/ConfigureCommandListenerTest.php +++ b/test/Listener/ConfigureCommandListenerTest.php @@ -18,8 +18,7 @@ class ConfigureCommandListenerTest extends TestCase */ public function testInputIsConfiguredForCorrectCommands(string $commandName): void { - $command = new CommandDefinition($commandName, [], function () { - }); + $command = new CommandDefinition($commandName, [], function () {}); $state = new UserState([], 'Exercise 1'); $exercise = new CliExerciseImpl('Exercise 1'); @@ -45,8 +44,7 @@ public function configurableCommands(): array */ public function testInputIsNotConfiguredForCorrectCommands(string $commandName): void { - $command = new CommandDefinition($commandName, [], function () { - }); + $command = new CommandDefinition($commandName, [], function () {}); $state = new UserState([], 'Exercise 1'); $exercise = new CliExerciseImpl('Exercise 1'); diff --git a/test/Listener/InitialCodeListenerTest.php b/test/Listener/InitialCodeListenerTest.php index 2faf1723..49752db4 100644 --- a/test/Listener/InitialCodeListenerTest.php +++ b/test/Listener/InitialCodeListenerTest.php @@ -32,7 +32,7 @@ public function testExerciseCodeIsCopiedIfExerciseProvidesInitialCode(): void $this->assertFileExists($this->getCurrentWorkingDirectory() . '/init-solution.php'); $this->assertFileEquals( $exercise->getInitialCode()->getFiles()[0]->getAbsolutePath(), - $this->getCurrentWorkingDirectory() . '/init-solution.php' + $this->getCurrentWorkingDirectory() . '/init-solution.php', ); $this->assertLoggerHasMessages( @@ -43,10 +43,10 @@ public function testExerciseCodeIsCopiedIfExerciseProvidesInitialCode(): void 'context' => [ 'exercise' => 'exercise-with-initial-code', 'workingDir' => $this->getCurrentWorkingDirectory(), - 'file' => $exercise->getInitialCode()->getFiles()[0]->getAbsolutePath() - ] - ] - ] + 'file' => $exercise->getInitialCode()->getFiles()[0]->getAbsolutePath(), + ], + ], + ], ); } @@ -71,10 +71,10 @@ public function testExerciseCodeIsNotCopiedIfFileWithSameNameExistsInWorkingDire 'context' => [ 'exercise' => 'exercise-with-initial-code', 'workingDir' => $this->getCurrentWorkingDirectory(), - 'file' => $exercise->getInitialCode()->getFiles()[0]->getAbsolutePath() - ] - ] - ] + 'file' => $exercise->getInitialCode()->getFiles()[0]->getAbsolutePath(), + ], + ], + ], ); } diff --git a/test/Listener/LazyContainerListenerTest.php b/test/Listener/LazyContainerListenerTest.php index beb9bb75..08f9d165 100644 --- a/test/Listener/LazyContainerListenerTest.php +++ b/test/Listener/LazyContainerListenerTest.php @@ -13,10 +13,8 @@ class LazyContainerListenerTest extends TestCase { public function testExceptionIsThrownIfServiceMethodDoesNotExist(): void { - $myListener = new class { - public function __invoke() - { - } + $myListener = new class () { + public function __invoke() {} }; $class = get_class($myListener); @@ -33,7 +31,7 @@ public function __invoke() $lazy = new LazyContainerListener( $container, - new ContainerListenerHelper('my-listener', 'myMethod') + new ContainerListenerHelper('my-listener', 'myMethod'), ); $lazy->__invoke(new Event('some-event')); @@ -41,7 +39,7 @@ public function __invoke() public function testThatUnderlyingListenerIsCalled(): void { - $myListener = new class { + $myListener = new class () { public $called = false; public function __invoke() { @@ -58,7 +56,7 @@ public function __invoke() $lazy = new LazyContainerListener( $container, - new ContainerListenerHelper('my-listener') + new ContainerListenerHelper('my-listener'), ); $lazy->__invoke(new Event('some-event')); @@ -68,10 +66,8 @@ public function __invoke() public function testWrappedReturnsUnderlyingListener(): void { - $myListener = new class { - public function __invoke() - { - } + $myListener = new class () { + public function __invoke() {} }; $container = $this->createMock(ContainerInterface::class); @@ -83,7 +79,7 @@ public function __invoke() $lazy = new LazyContainerListener( $container, - new ContainerListenerHelper('my-listener', '__invoke') + new ContainerListenerHelper('my-listener', '__invoke'), ); $wrapped = $lazy->getWrapped(); diff --git a/test/Listener/PrepareSolutionListenerTest.php b/test/Listener/PrepareSolutionListenerTest.php index a0a0552d..6ad72d2c 100644 --- a/test/Listener/PrepareSolutionListenerTest.php +++ b/test/Listener/PrepareSolutionListenerTest.php @@ -3,166 +3,127 @@ namespace PhpSchool\PhpWorkshopTest\Listener; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; -use PhpSchool\PhpWorkshop\Exercise\CliExercise; -use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessNotFoundException; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; -use ReflectionProperty; -use RuntimeException; -use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\ExecutableFinder; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; class PrepareSolutionListenerTest extends TestCase { use AssertionRenames; - /** - * @var string - */ - private $file; - - /** - * @var PrepareSolutionListener - */ - private $listener; - - /** - * @var Filesystem - */ - private $filesystem; - - public function setUp(): void - { - $this->filesystem = new Filesystem(); - $this->listener = new PrepareSolutionListener(); - $this->file = sprintf('%s/%s/submission.php', str_replace('\\', '/', sys_get_temp_dir()), $this->getName()); - - mkdir(dirname($this->file), 0775, true); - touch($this->file); - } - - /** - * @runInSeparateProcess - */ public function testIfSolutionRequiresComposerButComposerCannotBeLocatedExceptionIsThrown(): void { - $refProp = new ReflectionProperty(PrepareSolutionListener::class, 'composerLocations'); - $refProp->setAccessible(true); - $refProp->setValue($this->listener, []); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); - $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + $finder = $this->createMock(ExecutableFinder::class); + $finder->expects($this->once())->method('find')->with('composer')->willReturn(null); + $solution = $this->createMock(SolutionInterface::class); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Composer could not be located on the system'); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $exercise->setSolution($solution); + + $this->expectException(ProcessNotFoundException::class); + $this->expectExceptionMessage('Could not find executable: "composer"'); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory($finder)))->__invoke($event); } public function testIfSolutionRequiresComposerButVendorDirExistsNothingIsDone(): void { - mkdir(sprintf('%s/vendor', dirname($this->file))); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $context->createReferenceSolutionDirectory(); - $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + mkdir(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); + $solution = $this->createMock(SolutionInterface::class); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); + $exercise->setSolution($solution); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); //check for non existence of lock file, composer generates this when updating if it doesn't exist - $this->assertFileDoesNotExist(sprintf('%s/composer.lock', dirname($this->file))); + $this->assertFileDoesNotExist(sprintf('%s/composer.lock', $context->getReferenceExecutionDirectory())); } public function testIfSolutionRequiresComposerComposerInstallIsExecuted(): void { - $this->assertFileDoesNotExist(sprintf('%s/vendor', dirname($this->file))); - file_put_contents(sprintf('%s/composer.json', dirname($this->file)), json_encode([ - 'require' => [ - 'phpunit/phpunit' => '~5.0' - ], - ])); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $context->createReferenceSolutionDirectory(); + $context->importReferenceFileFromString( + json_encode([ + 'require' => [ + 'phpunit/phpunit' => '~5.0', + ], + ]), + 'composer.json', + ); $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); - $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); + $exercise->setSolution($solution); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } public function testExceptionIsThrownIfDependenciesCannotBeResolved(): void { + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $this->expectException(\PhpSchool\PhpWorkshop\Exception\RuntimeException::class); $this->expectExceptionMessage('Composer dependencies could not be installed'); - $this->assertFileDoesNotExist(sprintf('%s/vendor', dirname($this->file))); - file_put_contents(sprintf('%s/composer.json', dirname($this->file)), json_encode([ - 'require' => [ - 'phpunit/phpunit' => '1.0' - ], - ])); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + + $context->createReferenceSolutionDirectory(); + $context->importReferenceFileFromString( + json_encode([ + 'require' => [ + 'phpunit/phpunit' => '1.0', + ], + ]), + 'composer.json', + ); $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + $exercise->setSolution($solution); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); - - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); - } - - public function tearDown(): void - { - $this->filesystem->remove(dirname($this->file)); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } } diff --git a/test/Listener/RealPathListenerTest.php b/test/Listener/RealPathListenerTest.php index 255d5e24..a4b71916 100644 --- a/test/Listener/RealPathListenerTest.php +++ b/test/Listener/RealPathListenerTest.php @@ -2,33 +2,32 @@ namespace PhpSchool\PhpWorkshopTest\Listener; -use PhpSchool\PhpWorkshop\Event\Event; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Listener\RealPathListener; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; -use PHPUnit\Framework\TestCase; +use PhpSchool\PhpWorkshopTest\BaseTest; -class RealPathListenerTest extends TestCase +class RealPathListenerTest extends BaseTest { public function testInputArgumentIsReplacesWithAbsolutePathIfFileExists(): void { - $current = getcwd(); - - $tempDirectory = sprintf('%s/%s', realpath(sys_get_temp_dir()), $this->getName()); - mkdir($tempDirectory, 0777, true); - chdir($tempDirectory); - touch('test-file.php'); - $exercise = new CliExerciseImpl(); $input = new Input('app', ['program' => 'test-file.php']); $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); - $this->assertEquals(sprintf('%s/test-file.php', $tempDirectory), $input->getArgument('program')); + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + $context->importStudentFileFromString('', 'test-file.php'); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); + + $this->assertEquals(sprintf('%s/test-file.php', $context->getStudentExecutionDirectory()), $input->getArgument('program')); - unlink('test-file.php'); - rmdir($tempDirectory); chdir($current); } @@ -37,18 +36,37 @@ public function testInputArgumentIsLeftUnchangedIfFileDoesNotExist(): void $exercise = new CliExerciseImpl(); $input = new Input('app', ['program' => 'test-file.php']); $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); + + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); $this->assertEquals('test-file.php', $input->getArgument('program')); + + chdir($current); } public function testInputIsUnchangedIfNoProgramArgument(): void { $exercise = new CliExerciseImpl(); $input = new Input('app', ['some-arg' => 'some-value']); + $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); + + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); $this->assertEquals('some-value', $input->getArgument('some-arg')); + + chdir($current); } } diff --git a/test/Listener/SelfCheckListenerTest.php b/test/Listener/SelfCheckListenerTest.php index d27cee7c..3e118d4f 100644 --- a/test/Listener/SelfCheckListenerTest.php +++ b/test/Listener/SelfCheckListenerTest.php @@ -2,9 +2,9 @@ namespace PhpSchool\PhpWorkshopTest\Listener; -use PhpSchool\PhpWorkshop\Event\Event; +use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; -use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\SelfCheckListener; use PhpSchool\PhpWorkshop\Result\Success; use PhpSchool\PhpWorkshop\ResultAggregator; @@ -16,14 +16,13 @@ class SelfCheckListenerTest extends TestCase public function testSelfCheck(): void { $exercise = $this->createMock(SelfCheckExerciseInterface::class); - $input = new Input('app', ['program' => 'some-file.php']); - $event = new Event('event', compact('exercise', 'input')); + $context = new TestContext($exercise); + $event = new ExerciseRunnerEvent('event', $context); $success = new Success('Success'); $exercise ->expects($this->once()) ->method('check') - ->with($input) ->willReturn($success); $results = new ResultAggregator(); @@ -37,8 +36,8 @@ public function testSelfCheck(): void public function testExerciseWithOutSelfCheck(): void { $exercise = $this->createMock(ExerciseInterface::class); - $input = new Input('app', ['program' => 'some-file.php']); - $event = new Event('event', compact('exercise', 'input')); + $context = new TestContext($exercise); + $event = new ExerciseRunnerEvent('event', $context); $results = new ResultAggregator(); $listener = new SelfCheckListener($results); diff --git a/test/Logger/LoggerTest.php b/test/Logger/LoggerTest.php index a29cd95f..57738669 100644 --- a/test/Logger/LoggerTest.php +++ b/test/Logger/LoggerTest.php @@ -43,7 +43,7 @@ public function testLoggerCreatesFileWhenMessageIsLogged(): void $this->assertMatchesRegularExpression( $match, - file_get_contents($expectedFileName) + file_get_contents($expectedFileName), ); } @@ -63,7 +63,7 @@ public function testLoggerAppendsToFileWhenSecondMessageIsLogged(): void $this->assertMatchesRegularExpression( $match, - file_get_contents($expectedFileName) + file_get_contents($expectedFileName), ); } @@ -84,7 +84,7 @@ public function testLoggerAppendsToFileWhenItAlreadyExists(): void $this->assertMatchesRegularExpression( $match, - file_get_contents($expectedFileName) + file_get_contents($expectedFileName), ); } @@ -101,7 +101,7 @@ public function testLoggerWithContextIsEncoded(): void $match .= 'Context\: {"exercise":"my-exercise"}/'; $this->assertMatchesRegularExpression( $match, - file_get_contents($expectedFileName) + file_get_contents($expectedFileName), ); } } diff --git a/test/Markdown/Parser/HandleBarParserTest.php b/test/Markdown/Parser/HandleBarParserTest.php index 28c70257..2798c1fa 100644 --- a/test/Markdown/Parser/HandleBarParserTest.php +++ b/test/Markdown/Parser/HandleBarParserTest.php @@ -46,7 +46,7 @@ public function noShorthandsProvider(): array ['{{cloud wut}}'], ['{{ cloud wut}}'], ['{{ cloud wut }}'], - ['{{ cloud wut }}'] + ['{{ cloud wut }}'], ]; } @@ -69,7 +69,7 @@ public function testWithShorthand(string $content, bool $expectedParseResult, ar true, true, true, - ['appendChild'] + ['appendChild'], ); $container->expects($this->once()) ->method('appendChild') @@ -83,7 +83,7 @@ public function testWithShorthand(string $content, bool $expectedParseResult, ar $context->expects($this->never())->method('getContainer'); } - $shorthand = new class implements ShorthandInterface { + $shorthand = new class () implements ShorthandInterface { public $args = []; public function __invoke(array $callArgs): array { @@ -127,7 +127,7 @@ public function parseProvider(): array [ '{{ test "argument 1" \'a really long argument two\' arg3}}', true, - ['argument 1', 'a really long argument two', 'arg3'] + ['argument 1', 'a really long argument two', 'arg3'], ], ]; } @@ -147,7 +147,7 @@ public function testParsingWithContextShorthand(): void true, true, true, - ['appendChild'] + ['appendChild'], ); $container->expects($this->once()) ->method('appendChild') diff --git a/test/MarkdownRendererTest.php b/test/MarkdownRendererTest.php index e97caefa..94888f97 100644 --- a/test/MarkdownRendererTest.php +++ b/test/MarkdownRendererTest.php @@ -2,7 +2,6 @@ namespace PhpSchool\PhpWorkshopTest; -use PhpSchool\CliMdRenderer\CliRenderer; use PhpSchool\CliMdRenderer\CliRendererFactory; use League\CommonMark\DocParser; use League\CommonMark\Environment; diff --git a/test/MockLogger.php b/test/MockLogger.php index 2c908414..41c22fbd 100644 --- a/test/MockLogger.php +++ b/test/MockLogger.php @@ -19,7 +19,7 @@ public function log($level, $message, array $context = []): void $this->messages[] = [ 'level' => $level, 'message' => $message, - 'context' => $context + 'context' => $context, ]; } diff --git a/test/Patch/ForceStrictTypesTest.php b/test/Patch/ForceStrictTypesTest.php index 001ee9ac..30678bb6 100644 --- a/test/Patch/ForceStrictTypesTest.php +++ b/test/Patch/ForceStrictTypesTest.php @@ -19,7 +19,7 @@ public function testStrictTypesDeclareIsAppended(): void self::assertSame( "declare (strict_types=1);\necho 'Hello World';", - (new Standard())->prettyPrint($ast) + (new Standard())->prettyPrint($ast), ); } @@ -33,7 +33,7 @@ public function testStrictTypesDeclareIsNotAppendedIfItAlreadyExists(): void self::assertSame( "declare (strict_types=1);\necho 'Hello World';", - (new Standard())->prettyPrint($ast) + (new Standard())->prettyPrint($ast), ); } } diff --git a/test/Patch/WrapInTryCatchTest.php b/test/Patch/WrapInTryCatchTest.php index a25773d2..97687713 100644 --- a/test/Patch/WrapInTryCatchTest.php +++ b/test/Patch/WrapInTryCatchTest.php @@ -21,7 +21,7 @@ public function testStatementsAreWrappedInTryCatch(): void self::assertSame( "try {\n echo 'Hello World';\n} catch (Exception \$e) {\n echo \$e->getMessage();\n}", - (new Standard())->prettyPrint($ast) + (new Standard())->prettyPrint($ast), ); } @@ -35,7 +35,7 @@ public function testStatementsAreWrappedInTryCatchWithCustomExceptionClass(): vo self::assertSame( "try {\n echo 'Hello World';\n} catch (RuntimeException \$e) {\n echo \$e->getMessage();\n}", - (new Standard())->prettyPrint($ast) + (new Standard())->prettyPrint($ast), ); } @@ -49,7 +49,7 @@ public function testStatementsAreWrappedInTryCatchWithStatements(): void self::assertSame( "try {\n echo 'Hello World';\n} catch (RuntimeException \$e) {\n echo 'You caught me!';\n}", - (new Standard())->prettyPrint($ast) + (new Standard())->prettyPrint($ast), ); } } diff --git a/test/PatchTest.php b/test/PatchTest.php index d85e6d87..54a588ce 100644 --- a/test/PatchTest.php +++ b/test/PatchTest.php @@ -34,7 +34,7 @@ public function testWithTransformerWithClosure(): void public function testWithTransformerWithTransformer(): void { $patch = new Patch(); - $transformer = new class implements Patch\Transformer { + $transformer = new class () implements Patch\Transformer { public function transform(array $ast): array { return $ast; @@ -49,7 +49,7 @@ public function transform(array $ast): array public function testWithTransformerMultiple(): void { - $transformer1 = new class implements Patch\Transformer { + $transformer1 = new class () implements Patch\Transformer { public function transform(array $ast): array { return $ast; diff --git a/test/Result/Cgi/RequestFailureTest.php b/test/Result/Cgi/RequestFailureTest.php index d475021e..11ab359c 100644 --- a/test/Result/Cgi/RequestFailureTest.php +++ b/test/Result/Cgi/RequestFailureTest.php @@ -23,7 +23,7 @@ public function testWhenOnlyOutputDifferent(): void 'Expected Output', 'Actual Output', [], - [] + [], ); $this->assertEquals('Expected Output', $requestFailure->getExpectedOutput()); @@ -41,7 +41,7 @@ public function testWhenOnlyHeadersDifferent(): void 'Output', 'Output', ['header1' => 'some-value'], - ['header2' => 'some-value'] + ['header2' => 'some-value'], ); $this->assertEquals(['header1' => 'some-value'], $requestFailure->getExpectedHeaders()); @@ -59,7 +59,7 @@ public function testWhenOutputAndHeadersDifferent(): void 'Expected Output', 'Actual Output', ['header1' => 'some-value'], - ['header2' => 'some-value'] + ['header2' => 'some-value'], ); $this->assertTrue($requestFailure->headersDifferent()); diff --git a/test/Result/ComposerFailureTest.php b/test/Result/ComposerFailureTest.php index 6131b9ec..bfc659c5 100644 --- a/test/Result/ComposerFailureTest.php +++ b/test/Result/ComposerFailureTest.php @@ -44,9 +44,9 @@ public function testWithMissingComponent(): void 'is_missing_component' => true, 'is_missing_packages' => false, 'missing_component' => 'composer.json', - 'missing_packages' => [] + 'missing_packages' => [], ], - $failure->toArray() + $failure->toArray(), ); } @@ -72,9 +72,9 @@ public function testWithMissingPackages(): void 'is_missing_component' => false, 'is_missing_packages' => true, 'missing_component' => null, - 'missing_packages' => ['some/package'] + 'missing_packages' => ['some/package'], ], - $failure->toArray() + $failure->toArray(), ); } } diff --git a/test/Result/FailureTest.php b/test/Result/FailureTest.php index 262425c8..670324a0 100644 --- a/test/Result/FailureTest.php +++ b/test/Result/FailureTest.php @@ -67,7 +67,7 @@ public function testFailureFromCodeParseException(): void $this->assertInstanceOf(ResultInterface::class, $failure); $this->assertEquals( 'File: "exercise.php" could not be parsed. Error: "Something went wrong yo on unknown line"', - $failure->getReason() + $failure->getReason(), ); $this->assertEquals('Some Check', $failure->getCheckName()); } diff --git a/test/ResultAggregatorTest.php b/test/ResultAggregatorTest.php index 0d2a710a..ce6f73a5 100644 --- a/test/ResultAggregatorTest.php +++ b/test/ResultAggregatorTest.php @@ -59,7 +59,7 @@ public function testIterator(): void { $results = [ new Success('Some Check'), - new Failure('Some Check', 'nope') + new Failure('Some Check', 'nope'), ]; $resultAggregator = new ResultAggregator(); diff --git a/test/ResultRenderer/AbstractResultRendererTest.php b/test/ResultRenderer/AbstractResultRendererTest.php index 0718e4fc..2b0f8df4 100644 --- a/test/ResultRenderer/AbstractResultRendererTest.php +++ b/test/ResultRenderer/AbstractResultRendererTest.php @@ -53,7 +53,7 @@ protected function getRenderer(): ResultsRenderer $terminal, $exerciseRepo, new KeyLighter(), - $this->getResultRendererFactory() + $this->getResultRendererFactory(), ); } diff --git a/test/ResultRenderer/Cgi/RequestFailureRendererTest.php b/test/ResultRenderer/Cgi/RequestFailureRendererTest.php index a72d9ee0..e4c1d16a 100644 --- a/test/ResultRenderer/Cgi/RequestFailureRendererTest.php +++ b/test/ResultRenderer/Cgi/RequestFailureRendererTest.php @@ -16,7 +16,7 @@ public function testRenderWhenOnlyHeadersDifferent(): void 'OUTPUT', 'OUTPUT', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $renderer = new RequestFailureRenderer($failure); @@ -35,7 +35,7 @@ public function testRenderWhenOnlyOutputDifferent(): void 'EXPECTED OUTPUT', 'ACTUAL OUTPUT', ['header1' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $renderer = new RequestFailureRenderer($failure); @@ -53,7 +53,7 @@ public function testRenderWhenOutputAndHeadersDifferent(): void 'EXPECTED OUTPUT', 'ACTUAL OUTPUT', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $renderer = new RequestFailureRenderer($failure); diff --git a/test/ResultRenderer/CgiResultRendererTest.php b/test/ResultRenderer/CgiResultRendererTest.php index 9b12870e..4aebb8b2 100644 --- a/test/ResultRenderer/CgiResultRendererTest.php +++ b/test/ResultRenderer/CgiResultRendererTest.php @@ -32,7 +32,7 @@ public function testRenderWithFailedRequest(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $failure = new RequestFailure( @@ -40,7 +40,7 @@ function (RequestFailure $failure) use ($failureRenderer) { 'EXPECTED OUTPUT', 'ACTUAL OUTPUT', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $result = new CgiResult([$failure]); $renderer = new CgiResultRenderer($result, new RequestRenderer()); @@ -70,7 +70,7 @@ public function testMultipleFailedRequests(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $failure1 = new RequestFailure( @@ -78,7 +78,7 @@ function (RequestFailure $failure) use ($failureRenderer) { 'EXPECTED OUTPUT 1', 'ACTUAL OUTPUT 1', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $failure2 = new RequestFailure( @@ -86,7 +86,7 @@ function (RequestFailure $failure) use ($failureRenderer) { 'EXPECTED OUTPUT 2', 'ACTUAL OUTPUT 2', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $result = new CgiResult([$failure1, $failure2]); $renderer = new CgiResultRenderer($result, new RequestRenderer()); @@ -128,7 +128,7 @@ public function testRenderWithFailedRequestAndSuccess(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $failure = new RequestFailure( @@ -136,7 +136,7 @@ function (RequestFailure $failure) use ($failureRenderer) { 'EXPECTED OUTPUT', 'ACTUAL OUTPUT', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $result = new CgiResult([$failure, new Success($this->request())]); $renderer = new CgiResultRenderer($result, new RequestRenderer()); @@ -166,7 +166,7 @@ public function testRenderWithFailedRequestAndGenericFailure(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $genericFailureRenderer = $this->createMock(FailureRenderer::class); @@ -177,7 +177,7 @@ function (RequestFailure $failure) use ($failureRenderer) { FailureRenderer::class, function (GenericFailure $failure) use ($genericFailureRenderer) { return $genericFailureRenderer; - } + }, ); $failure = new RequestFailure( @@ -185,7 +185,7 @@ function (GenericFailure $failure) use ($genericFailureRenderer) { 'EXPECTED OUTPUT', 'ACTUAL OUTPUT', ['header1' => 'val', 'header2' => 'val'], - ['header1' => 'val'] + ['header1' => 'val'], ); $codeExecutionFailure = new GenericFailure($this->request(), 'Code Execution Failure'); diff --git a/test/ResultRenderer/CliResultRendererTest.php b/test/ResultRenderer/CliResultRendererTest.php index 17849fe7..ed36ef42 100644 --- a/test/ResultRenderer/CliResultRendererTest.php +++ b/test/ResultRenderer/CliResultRendererTest.php @@ -29,13 +29,13 @@ public function testRenderWithFailedRequest(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $failure = new RequestFailure( new ArrayObject(), 'EXPECTED OUTPUT', - 'ACTUAL OUTPUT' + 'ACTUAL OUTPUT', ); $result = new CliResult([$failure]); $renderer = new CliResultRenderer($result); @@ -59,13 +59,13 @@ public function testRenderWithFailedRequestWithMultipleArgs(): void RequestFailureRenderer::class, function (RequestFailure $failure) use ($failureRenderer) { return $failureRenderer; - } + }, ); $failure = new RequestFailure( new ArrayObject(['one', 'two', 'three']), 'EXPECTED OUTPUT', - 'ACTUAL OUTPUT' + 'ACTUAL OUTPUT', ); $result = new CliResult([$failure]); $renderer = new CliResultRenderer($result); diff --git a/test/ResultRenderer/ComposerFailureRendererTest.php b/test/ResultRenderer/ComposerFailureRendererTest.php index 64655f73..bf7b1191 100644 --- a/test/ResultRenderer/ComposerFailureRendererTest.php +++ b/test/ResultRenderer/ComposerFailureRendererTest.php @@ -17,7 +17,7 @@ public function testRenderWithMissingFiles(string $file, string $message): void { $failure = new ComposerFailure( $this->createMock(CheckInterface::class), - $file + $file, ); $renderer = new ComposerFailureRenderer($failure); @@ -40,7 +40,7 @@ public function testRenderWithMissingPackages(): void $failure = new ComposerFailure( $this->createMock(CheckInterface::class), null, - ['some/package'] + ['some/package'], ); $renderer = new ComposerFailureRenderer($failure); @@ -54,7 +54,7 @@ public function testRenderWithMultipleMissingPackages(): void $failure = new ComposerFailure( $this->createMock(CheckInterface::class), null, - ['some/package', 'some-other/package'] + ['some/package', 'some-other/package'], ); $renderer = new ComposerFailureRenderer($failure); diff --git a/test/ResultRenderer/FailureRendererTest.php b/test/ResultRenderer/FailureRendererTest.php index 9884f07f..306fb485 100644 --- a/test/ResultRenderer/FailureRendererTest.php +++ b/test/ResultRenderer/FailureRendererTest.php @@ -2,7 +2,6 @@ namespace PhpSchool\PhpWorkshopTest\ResultRenderer; -use PhpSchool\PhpWorkshop\Check\CheckInterface; use PhpSchool\PhpWorkshop\Result\Failure; use PhpSchool\PhpWorkshop\ResultRenderer\FailureRenderer; diff --git a/test/ResultRenderer/FileComparisonFailureRendererTest.php b/test/ResultRenderer/FileComparisonFailureRendererTest.php index d7607863..9747d226 100644 --- a/test/ResultRenderer/FileComparisonFailureRendererTest.php +++ b/test/ResultRenderer/FileComparisonFailureRendererTest.php @@ -16,7 +16,7 @@ public function testRender(): void $this->createMock(CheckInterface::class), 'some-file.text', 'EXPECTED OUTPUT', - 'ACTUAL OUTPUT' + 'ACTUAL OUTPUT', ); $renderer = new FileComparisonFailureRenderer($failure); diff --git a/test/ResultRenderer/FunctionRequirementsFailureRendererTest.php b/test/ResultRenderer/FunctionRequirementsFailureRendererTest.php index bda80c2f..4151f45c 100644 --- a/test/ResultRenderer/FunctionRequirementsFailureRendererTest.php +++ b/test/ResultRenderer/FunctionRequirementsFailureRendererTest.php @@ -13,7 +13,7 @@ public function testRenderer(): void $failure = new FunctionRequirementsFailure( $this->createMock(CheckInterface::class), [['function' => 'file', 'line' => 3], ['function' => 'explode', 'line' => 5]], - ['implode'] + ['implode'], ); $renderer = new FunctionRequirementsFailureRenderer($failure); diff --git a/test/ResultRenderer/ResultsRendererTest.php b/test/ResultRenderer/ResultsRendererTest.php index c3cc1636..cb84b8eb 100644 --- a/test/ResultRenderer/ResultsRendererTest.php +++ b/test/ResultRenderer/ResultsRendererTest.php @@ -11,7 +11,6 @@ use PhpSchool\PhpWorkshopTest\BaseTest; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; -use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\ExerciseRepository; use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory; use PhpSchool\PhpWorkshop\Output\StdOutput; @@ -21,7 +20,6 @@ use PhpSchool\PhpWorkshop\ResultRenderer\FailureRenderer; use PhpSchool\PhpWorkshop\ResultRenderer\ResultsRenderer; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; -use PHPUnit\Framework\TestCase; use function PhpSchool\PhpWorkshop\camel_case_to_kebab_case; @@ -44,7 +42,7 @@ public function testRenderIndividualResult(): void $terminal, new ExerciseRepository([]), new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); @@ -66,7 +64,7 @@ public function testLineBreak(): void $terminal, new ExerciseRepository([]), new KeyLighter(), - new ResultRendererFactory() + new ResultRendererFactory(), ); $this->assertSame("\e[33m──────────\e[0m", $renderer->lineBreak()); @@ -91,7 +89,7 @@ public function testRenderSuccess(): void $terminal, $exerciseRepo, new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -104,7 +102,7 @@ public function testRenderSuccess(): void $resultSet, $this->createMock(ExerciseInterface::class), new UserState(['exercise1']), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } @@ -132,7 +130,7 @@ public function testRenderSuccessWithSolution(): void $terminal, $exerciseRepo, new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -145,7 +143,7 @@ public function testRenderSuccessWithSolution(): void $resultSet, $exercise, new UserState(['exercise1']), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } @@ -181,7 +179,7 @@ public function testRenderSuccessWithPhpSolutionFileIsSyntaxHighlighted(): void $terminal, $exerciseRepo, $syntaxHighlighter, - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -194,7 +192,7 @@ public function testRenderSuccessWithPhpSolutionFileIsSyntaxHighlighted(): void $resultSet, $exercise, new UserState(['exercise1']), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } @@ -205,12 +203,12 @@ public function testRenderSuccessAndFailure(): void $resultRendererFactory = new ResultRendererFactory(); $resultRendererFactory->registerRenderer(Failure::class, FailureRenderer::class, function (Failure $failure) { - $renderer = $this->createMock(FailureRenderer::class); - $renderer - ->method('render') - ->with($this->isInstanceOf(ResultsRenderer::class)) - ->willReturn($failure->getReason() . "\n"); - return $renderer; + $renderer = $this->createMock(FailureRenderer::class); + $renderer + ->method('render') + ->with($this->isInstanceOf(ResultsRenderer::class)) + ->willReturn($failure->getReason() . "\n"); + return $renderer; }); $terminal = $this->createMock(Terminal::class); @@ -225,7 +223,7 @@ public function testRenderSuccessAndFailure(): void $terminal, $exerciseRepo, new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -239,7 +237,7 @@ public function testRenderSuccessAndFailure(): void $resultSet, $this->createMock(ExerciseInterface::class), new UserState(), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } @@ -270,7 +268,7 @@ public function testAllSuccessResultsAreHoistedToTheTop(): void $terminal, $exerciseRepo, new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -285,7 +283,7 @@ public function testAllSuccessResultsAreHoistedToTheTop(): void $resultSet, $this->createMock(ExerciseInterface::class), new UserState(), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } @@ -315,7 +313,7 @@ public function testRenderAllFailures(): void $terminal, $exerciseRepo, new KeyLighter(), - $resultRendererFactory + $resultRendererFactory, ); $resultSet = new ResultAggregator(); @@ -328,7 +326,7 @@ public function testRenderAllFailures(): void $resultSet, $this->createMock(ExerciseInterface::class), new UserState(), - new StdOutput($color, $terminal) + new StdOutput($color, $terminal), ); } diff --git a/test/Solution/InTempSolutionMapperTest.php b/test/Solution/InTempSolutionMapperTest.php deleted file mode 100644 index 945ddd11..00000000 --- a/test/Solution/InTempSolutionMapperTest.php +++ /dev/null @@ -1,96 +0,0 @@ -remove(System::tempDir('php-school')); - - parent::tearDown(); - } - - public function testFileMapping(): void - { - $filePath = $this->getTemporaryFile('test.file'); - - $mappedFile = InTempSolutionMapper::mapFile($filePath); - - self::assertFileExists($mappedFile); - self::assertNotSame($filePath, $mappedFile); - self::assertStringContainsString(System::tempDir(), $mappedFile); - } - - public function testDirectoryMapping(): void - { - $this->getTemporaryFile('test.file'); - $this->getTemporaryFile('innerDir/test.file'); - - $mappedDir = InTempSolutionMapper::mapDirectory($this->getTemporaryDirectory()); - - self::assertDirectoryExists($mappedDir); - self::assertDirectoryExists(Path::join($mappedDir, 'innerDir')); - self::assertFileExists(Path::join($mappedDir, 'test.file')); - self::assertFileExists(Path::join($mappedDir, 'innerDir', 'test.file')); - self::assertNotSame($this->getTemporaryDirectory(), $mappedDir); - self::assertStringContainsString(System::tempDir(), $mappedDir); - } - - public function testMappingIsDeterministicTempDir(): void - { - $filePath = $this->getTemporaryFile('test.file'); - - $dirName = bin2hex(random_bytes(10)); - $tempDir = Path::join($this->getTemporaryDirectory(), $dirName); - mkdir($tempDir); - - $fileHash = md5($filePath); - $dirHash = md5($tempDir); - - self::assertSame( - InTempSolutionMapper::mapFile($filePath), - Path::join(System::tempDir(), $fileHash, 'test.file') - ); - - self::assertNotSame( - InTempSolutionMapper::mapDirectory($this->getTemporaryDirectory()), - System::tempDir(Path::join('php-school', $dirHash, dirname($dirName))) - ); - } - - public function testContentsAreNotOverwroteIfExists(): void - { - $filePath = $this->getTemporaryFile('test.file', 'Old contents'); - - $dirName = bin2hex(random_bytes(10)); - $tempDir = Path::join($this->getTemporaryDirectory(), $dirName); - - $this->getTemporaryFile(Path::join($dirName, 'test.file'), 'Old contents'); - - $tempFilePath = System::tempDir(Path::join('php-school', md5($filePath), 'test.file')); - $tempDirPath = System::tempDir(Path::join('php-school', md5($tempDir), $dirName)); - - mkdir(dirName($tempFilePath), 0777, true); - file_put_contents($tempFilePath, 'Fresh contents'); - mkdir($tempDirPath, 0777, true); - file_put_contents(Path::join($tempDirPath, 'test.file'), 'Fresh contents'); - - // These calls will invoke the copying of of dir/files to temp - InTempSolutionMapper::mapFile($filePath); - InTempSolutionMapper::mapDirectory($tempDir); - - self::assertSame('Old contents', file_get_contents($filePath)); - self::assertSame('Fresh contents', file_get_contents($tempFilePath)); - self::assertSame('Old contents', file_get_contents(Path::join($tempDir, 'test.file'))); - self::assertSame('Fresh contents', file_get_contents(Path::join($tempDirPath, 'test.file'))); - } -} diff --git a/test/UserState/LocalJsonSerializerTest.php b/test/UserState/LocalJsonSerializerTest.php index b7a2743f..da840159 100644 --- a/test/UserState/LocalJsonSerializerTest.php +++ b/test/UserState/LocalJsonSerializerTest.php @@ -2,13 +2,12 @@ namespace PhpSchool\PhpWorkshopTest\UserState; -use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRepository; use PhpSchool\PhpWorkshop\UserState\LocalJsonSerializer; use PhpSchool\PhpWorkshop\UserState\UserState; use PhpSchool\PhpWorkshop\Utils\Path; use PhpSchool\PhpWorkshop\Utils\System; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PhpSchool\PhpWorkshopTest\BaseTest; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; @@ -16,25 +15,10 @@ class LocalJsonSerializerTest extends BaseTest { use AssertionRenames; - /** - * @var string - */ - private $tmpDir; - - /** - * @var string - */ - private $tmpFile; - - /** - * @var string - */ - private $workshopName = 'My Workshop'; - - /** - * @var ExerciseRepository - */ - private $exerciseRepository; + private string $tmpDir; + private string $tmpFile; + private string $workshopName = 'My Workshop'; + private ExerciseRepository $exerciseRepository; public function setUp(): void { @@ -59,7 +43,7 @@ public function testSerializeEmptySate(): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = new UserState(); @@ -68,7 +52,7 @@ public function testSerializeEmptySate(): void 'My Workshop' => [ 'completed_exercises' => [], 'current_exercise' => null, - ] + ], ]); $serializer->serialize($state); @@ -80,7 +64,7 @@ public function testSerialize(): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = new UserState(['exercise1'], 'exercise2'); @@ -90,7 +74,7 @@ public function testSerialize(): void 'My Workshop' => [ 'completed_exercises' => ['exercise1'], 'current_exercise' => 'exercise2', - ] + ], ]); $serializer->serialize($state); @@ -103,7 +87,7 @@ public function testDeserializeNonExistingFile(): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = $serializer->deSerialize(); @@ -117,7 +101,7 @@ public function testDeserializeEmptyFile(): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = $serializer->deSerialize(); $this->assertFalse($state->isAssignedExercise()); @@ -130,7 +114,7 @@ public function testDeserializeNonValidJson(): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = $serializer->deSerialize(); $this->assertFalse($state->isAssignedExercise()); @@ -146,14 +130,14 @@ public function testDeserialize(array $data, array $expected): void $serializer = new LocalJsonSerializer( $this->getTemporaryDirectory(), $this->workshopName, - $this->exerciseRepository + $this->exerciseRepository, ); $state = $serializer->deSerialize(); $this->assertEquals($expected['completed_exercises'], $state->getCompletedExercises()); $this->assertEquals( $expected['current_exercise'], - $state->isAssignedExercise() ? $state->getCurrentExercise() : null + $state->isAssignedExercise() ? $state->getCurrentExercise() : null, ); } @@ -162,51 +146,47 @@ public function deserializerProvider(): array return [ 'empty-array' => [ [], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'no-data-should-return-defaults' => [ ['My Workshop' => []], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'no-current-exercise-set' => [ ['My Workshop' => ['completed_exercises' => []]], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'completed-exercise-not-array' => [ ['My Workshop' => ['completed_exercises' => null, 'current_exercise' => null]], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'invalid-completed-exercise' => [ ['My Workshop' => ['completed_exercises' => [null], 'current_exercise' => null]], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'completed-exercises-no-current-exercise' => [ ['My Workshop' => ['completed_exercises' => ['exercise1']]], - ['completed_exercises' => [], 'current_exercise' => null] + ['completed_exercises' => [], 'current_exercise' => null], ], 'completed-exercise-invalid-current-exercise' => [ ['My Workshop' => ['completed_exercises' => ['exercise1'], 'current_exercise' => new \stdClass()]], - ['completed_exercises' => ['exercise1'], 'current_exercise' => null] + ['completed_exercises' => ['exercise1'], 'current_exercise' => null], ], 'completed-exercise-current-null' => [ ['My Workshop' => ['completed_exercises' => ['exercise1'], 'current_exercise' => null]], - ['completed_exercises' => ['exercise1'], 'current_exercise' => null] + ['completed_exercises' => ['exercise1'], 'current_exercise' => null], ], 'completed-exercise-with-current' => [ ['My Workshop' => ['completed_exercises' => ['exercise1'], 'current_exercise' => 'exercise2']], - ['completed_exercises' => ['exercise1'], 'current_exercise' => 'exercise2'] - ] + ['completed_exercises' => ['exercise1'], 'current_exercise' => 'exercise2'], + ], ]; } public function testOldDataWillBeMigratedWhenInCorrectWorkshop(): void { - $exercise1 = $this->createMock(CliExerciseInterface::class); - $exercise2 = $this->createMock(CliExerciseInterface::class); - $exercise1->method('getType')->willReturn(ExerciseType::CLI()); - $exercise2->method('getType')->willReturn(ExerciseType::CLI()); - $exercise1->method('getName')->willReturn('Exercise 1'); - $exercise2->method('getName')->willReturn('Exercise 2'); + $exercise1 = new CliExerciseImpl('Exercise 1'); + $exercise2 = new CliExerciseImpl('Exercise 2'); $oldData = [ 'current_exercise' => 'Exercise 3', @@ -220,8 +200,8 @@ public function testOldDataWillBeMigratedWhenInCorrectWorkshop(): void $this->workshopName, new ExerciseRepository([ $exercise1, - $exercise2 - ]) + $exercise2, + ]), ); $state = $serializer->deSerialize(); @@ -240,22 +220,18 @@ public function testOldDataWillBeMigratedWhenInCorrectWorkshop(): void $this->assertFileExists(Path::join($this->getTemporaryDirectory(), '.phpschool-save.json')); $this->assertEquals( $expected, - json_decode(file_get_contents($this->getTemporaryFile('.phpschool-save.json')), true) + json_decode(file_get_contents($this->getTemporaryFile('.phpschool-save.json')), true), ); } public function testOldDataWillNotBeMigratedWhenNotInCorrectWorkshop(): void { - $exercise1 = $this->createMock(CliExerciseInterface::class); - $exercise2 = $this->createMock(CliExerciseInterface::class); - $exercise1->method('getType')->willReturn(ExerciseType::CLI()); - $exercise2->method('getType')->willReturn(ExerciseType::CLI()); - $exercise1->method('getName')->willReturn('Exercise 1'); - $exercise2->method('getName')->willReturn('Exercise 2'); + $exercise1 = new CliExerciseImpl('Exercise 1'); + $exercise2 = new CliExerciseImpl('Exercise 2'); $exercises = [ $exercise1, - $exercise2 + $exercise2, ]; $repo = new ExerciseRepository($exercises); @@ -279,12 +255,8 @@ public function testOldDataWillNotBeMigratedWhenNotInCorrectWorkshop(): void public function testOldDataWillNotBeMigratedWhenNotInCorrectWorkshopWithOtherWorkshop(): void { - $exercise1 = $this->createMock(CliExerciseInterface::class); - $exercise2 = $this->createMock(CliExerciseInterface::class); - $exercise1->method('getType')->willReturn(ExerciseType::CLI()); - $exercise2->method('getType')->willReturn(ExerciseType::CLI()); - $exercise1->method('getName')->willReturn('Exercise 1'); - $exercise2->method('getName')->willReturn('Exercise 2'); + $exercise1 = new CliExerciseImpl('Exercise 1'); + $exercise2 = new CliExerciseImpl('Exercise 2'); $oldData = [ 'current_exercise' => 'Exercise 3', @@ -306,8 +278,8 @@ public function testOldDataWillNotBeMigratedWhenNotInCorrectWorkshopWithOtherWor $this->workshopName, new ExerciseRepository([ $exercise1, - $exercise2 - ]) + $exercise2, + ]), ); $state = $serializer->deSerialize(); diff --git a/test/Utils/ArrayObjectTest.php b/test/Utils/ArrayObjectTest.php index 803477e7..15e85dde 100644 --- a/test/Utils/ArrayObjectTest.php +++ b/test/Utils/ArrayObjectTest.php @@ -181,7 +181,7 @@ public function testKstort() $arrayObject = new ArrayObject([ 'z' => 'more test data', 'a' => 'test data', - 't' => 'yup moar test data' + 't' => 'yup moar test data', ]); $expected = [ diff --git a/test/Utils/PathTest.php b/test/Utils/PathTest.php index d75eb811..37d965e7 100644 --- a/test/Utils/PathTest.php +++ b/test/Utils/PathTest.php @@ -11,47 +11,47 @@ public function testJoin(): void { $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path', 'some-folder/file.txt') + Path::join('/some/path', 'some-folder/file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path/', 'some-folder/file.txt') + Path::join('/some/path/', 'some-folder/file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path', '/some-folder/file.txt') + Path::join('/some/path', '/some-folder/file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path/', '/some-folder/file.txt') + Path::join('/some/path/', '/some-folder/file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path//', '//some-folder/file.txt') + Path::join('/some/path//', '//some-folder/file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path/', 'some-folder', 'file.txt') + Path::join('/some/path/', 'some-folder', 'file.txt'), ); $this->assertEquals( '/some/path/some-folder/file.txt', - Path::join('/some/path/', '/some-folder/', '/file.txt') + Path::join('/some/path/', '/some-folder/', '/file.txt'), ); $this->assertEquals( '/some/path', - Path::join('/some/path/') + Path::join('/some/path/'), ); $this->assertEquals( '/some/path', - Path::join('/some/path/', '') + Path::join('/some/path/', ''), ); } } diff --git a/test/Utils/RequestRendererTest.php b/test/Utils/RequestRendererTest.php index 86c81926..04cca526 100644 --- a/test/Utils/RequestRendererTest.php +++ b/test/Utils/RequestRendererTest.php @@ -35,7 +35,7 @@ public function testWriteRequestWithPostBodyJson(): void ->withHeader('Content-Type', 'application/json'); $request->getBody()->write( - json_encode(['data' => 'test', 'other_data' => 'test2']) + json_encode(['data' => 'test', 'other_data' => 'test2']), ); $expected = "URL: /endpoint\n"; @@ -55,7 +55,7 @@ public function testWriteRequestWithPostBodyUrlEncoded(): void ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write( - http_build_query(['data' => 'test', 'other_data' => 'test2']) + http_build_query(['data' => 'test', 'other_data' => 'test2']), ); $expected = "URL: /endpoint\n"; diff --git a/test/Utils/StringUtilsTest.php b/test/Utils/StringUtilsTest.php index 362e4147..99e32217 100644 --- a/test/Utils/StringUtilsTest.php +++ b/test/Utils/StringUtilsTest.php @@ -44,7 +44,7 @@ public function pluraliseMultipleProvider(): array return [ [ 'Property "%s" should not have changed type', - 'Properties "propOne" & "propTwo" should not have changed type' + 'Properties "propOne" & "propTwo" should not have changed type', ], ['Property "%s" was not promoted', 'Properties "propOne" & "propTwo" were not promoted'], ['Property "%s" was missing', 'Properties "propOne" & "propTwo" were missing'], diff --git a/vendor-bin/php-cs-fixer/composer.json b/vendor-bin/php-cs-fixer/composer.json new file mode 100644 index 00000000..8e2f14be --- /dev/null +++ b/vendor-bin/php-cs-fixer/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.56" + } +} diff --git a/vendor-bin/php-cs-fixer/composer.lock b/vendor-bin/php-cs-fixer/composer.lock new file mode 100644 index 00000000..edaef88f --- /dev/null +++ b/vendor-bin/php-cs-fixer/composer.lock @@ -0,0 +1,1827 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "42a90527cc8998493cafbc20093018a7", + "packages": [], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-19T10:26:25+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.56.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "69c6168ae8bc96dc656c7f6c7271120a68ae5903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/69c6168ae8bc96dc656c7f6c7271120a68ae5903", + "reference": "69c6168ae8bc96dc656c7f6c7271120a68ae5903", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", + "infection/infection": "^0.27.11", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", + "phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz RumiƄski", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.56.1" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2024-05-10T11:31:15+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ab83243ecc233de5655b76f577711de9f842e712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ab83243ecc233de5655b76f577711de9f842e712", + "reference": "ab83243ecc233de5655b76f577711de9f842e712", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:30:33+00:00" + }, + { + "name": "symfony/console", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c981e0e9380ce9f146416bde3150c79197ce9986" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c981e0e9380ce9f146416bde3150c79197ce9986", + "reference": "c981e0e9380ce9f146416bde3150c79197ce9986", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9", + "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/cc168be6fbdcdf3401f50ae863ee3818ed4338f5", + "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/4d58f0f4fe95a30d7b538d71197135483560b97c", + "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-28T11:44:19+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa", + "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/process", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "3839e56b94dd1dbd13235d27504e66baf23faba0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/3839e56b94dd1dbd13235d27504e66baf23faba0", + "reference": "3839e56b94dd1dbd13235d27504e66baf23faba0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/string", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +}