From be53db0937ea579d0cc089e72a04ba8f726dc520 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Tue, 7 May 2024 22:34:17 +0200 Subject: [PATCH 1/6] Ignore line length checks --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6e6275a3..1b092da9 100644 --- a/composer.json +++ b/composer.json @@ -63,8 +63,8 @@ ], "unit-tests": "phpunit", "cs" : [ - "phpcs src --standard=PSR12 --encoding=UTF-8", - "phpcs test --standard=PSR12 --encoding=UTF-8" + "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", From 29eeaf76fefed02c4109954b46c1332f21218f37 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 5 May 2024 15:40:40 +0200 Subject: [PATCH 2/6] Context --- src/Exercise/MockExercise.php | 28 +++ .../Context/ExecutionContext.php | 56 ++++++ .../Context/ExecutionContextFactory.php | 20 +++ src/ExerciseRunner/Context/NoEntryPoint.php | 13 ++ .../Context/StaticExecutionContextFactory.php | 20 +++ src/ExerciseRunner/Context/TestContext.php | 115 +++++++++++++ src/Utils/System.php | 5 + .../Context/ExecutionContextFactoryTest.php | 31 ++++ .../Context/ExecutionContextTest.php | 84 +++++++++ .../Context/NoEntryPointTest.php | 15 ++ .../StaticExecutionContextFactoryTest.php | 24 +++ .../Context/TestContextTest.php | 160 ++++++++++++++++++ test/Utils/SystemTest.php | 5 + 13 files changed, 576 insertions(+) create mode 100644 src/Exercise/MockExercise.php create mode 100644 src/ExerciseRunner/Context/ExecutionContext.php create mode 100644 src/ExerciseRunner/Context/ExecutionContextFactory.php create mode 100644 src/ExerciseRunner/Context/NoEntryPoint.php create mode 100644 src/ExerciseRunner/Context/StaticExecutionContextFactory.php create mode 100644 src/ExerciseRunner/Context/TestContext.php create mode 100644 test/ExerciseRunner/Context/ExecutionContextFactoryTest.php create mode 100644 test/ExerciseRunner/Context/ExecutionContextTest.php create mode 100644 test/ExerciseRunner/Context/NoEntryPointTest.php create mode 100644 test/ExerciseRunner/Context/StaticExecutionContextFactoryTest.php create mode 100644 test/ExerciseRunner/Context/TestContextTest.php diff --git a/src/Exercise/MockExercise.php b/src/Exercise/MockExercise.php new file mode 100644 index 00000000..a8268785 --- /dev/null +++ b/src/Exercise/MockExercise.php @@ -0,0 +1,28 @@ +exercise; + } + + public function getInput(): Input + { + return $this->input; + } + + public function hasStudentSolution(): bool + { + return $this->input->hasArgument('program'); + } + + public function getEntryPoint(): string + { + if (!$this->hasStudentSolution()) { + throw new NoEntryPoint(); + } + + return Path::join( + $this->studentExecutionDirectory, + basename($this->input->getRequiredArgument('program')) + ); + } + + public function getStudentExecutionDirectory(): string + { + return $this->studentExecutionDirectory; + } + + public function getReferenceExecutionDirectory(): string + { + return $this->referenceExecutionDirectory; + } +} diff --git a/src/ExerciseRunner/Context/ExecutionContextFactory.php b/src/ExerciseRunner/Context/ExecutionContextFactory.php new file mode 100644 index 00000000..ddaa785f --- /dev/null +++ b/src/ExerciseRunner/Context/ExecutionContextFactory.php @@ -0,0 +1,20 @@ +getRequiredArgument('program')), + System::randomTempDir(), + $exercise, + $input + ); + } +} diff --git a/src/ExerciseRunner/Context/NoEntryPoint.php b/src/ExerciseRunner/Context/NoEntryPoint.php new file mode 100644 index 00000000..2480feb4 --- /dev/null +++ b/src/ExerciseRunner/Context/NoEntryPoint.php @@ -0,0 +1,13 @@ +context; + } +} diff --git a/src/ExerciseRunner/Context/TestContext.php b/src/ExerciseRunner/Context/TestContext.php new file mode 100644 index 00000000..f3b8a62b --- /dev/null +++ b/src/ExerciseRunner/Context/TestContext.php @@ -0,0 +1,115 @@ +exercise = $exercise ?? new MockExercise(); + + $this->filesystem = new Filesystem(); + + parent::__construct( + System::randomTempDir(), + System::randomTempDir(), + $this->exercise, + $input ? $input : new Input('test', ['program' => 'solution.php']), + ); + } + + public function importStudentFileFromString(string $content, string $filename = 'solution.php'): void + { + if (!$this->filesystem->exists($this->getStudentExecutionDirectory())) { + throw new RuntimeException( + sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class) + ); + } + + file_put_contents(Path::join($this->getStudentExecutionDirectory(), $filename), $content); + } + + public function importStudentSolution(string $file): void + { + if (!$this->filesystem->exists($this->getStudentExecutionDirectory())) { + throw new RuntimeException( + sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class) + ); + } + + copy($file, Path::join($this->getStudentExecutionDirectory(), 'solution.php')); + } + + public function importStudentSolutionFolder(string $folder): void + { + if (!$this->filesystem->exists($this->getStudentExecutionDirectory())) { + throw new RuntimeException( + sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class) + ); + } + + $this->filesystem->mirror($folder, $this->getStudentExecutionDirectory()); + } + + public function importReferenceFileFromString(string $content, string $filename = 'solution.php'): void + { + if (!$this->filesystem->exists($this->getReferenceExecutionDirectory())) { + throw new RuntimeException( + sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class) + ); + } + + file_put_contents(Path::join($this->getReferenceExecutionDirectory(), $filename), $content); + } + + public function importReferenceSolution(SolutionInterface $solution): void + { + if (!$this->filesystem->exists($this->getReferenceExecutionDirectory())) { + throw new RuntimeException( + sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class) + ); + } + + foreach ($solution->getFiles() as $file) { + $this->filesystem->copy( + $file->getAbsolutePath(), + Path::join($this->getReferenceExecutionDirectory(), $file->getRelativePath()) + ); + } + } + + public static function withDirectories(Input $input = null, ExerciseInterface $exercise = null): self + { + $self = new self($exercise, $input); + + $self->filesystem->mkdir($self->getStudentExecutionDirectory()); + $self->filesystem->mkdir($self->getReferenceExecutionDirectory()); + + return $self; + } + + public static function withoutDirectories(Input $input = null, ExerciseInterface $exercise = null): self + { + return new self($exercise, $input); + } + + public function __destruct() + { + $this->filesystem->remove($this->getStudentExecutionDirectory()); + $this->filesystem->remove($this->getReferenceExecutionDirectory()); + } +} diff --git a/src/Utils/System.php b/src/Utils/System.php index 5dc80128..eb8da511 100644 --- a/src/Utils/System.php +++ b/src/Utils/System.php @@ -23,4 +23,9 @@ public static function tempDir(string $path = ''): string { return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', $path); } + + public static function randomTempDir(): string + { + return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', bin2hex(random_bytes(4))); + } } diff --git a/test/ExerciseRunner/Context/ExecutionContextFactoryTest.php b/test/ExerciseRunner/Context/ExecutionContextFactoryTest.php new file mode 100644 index 00000000..761b0194 --- /dev/null +++ b/test/ExerciseRunner/Context/ExecutionContextFactoryTest.php @@ -0,0 +1,31 @@ + $temporaryDirectory . '/solution.php']); + $exercise = new MockExercise(); + + $context = $factory->fromInputAndExercise($input, $exercise); + + //check that student execution directory uses the parent directory of the program from the input + static::assertSame($temporaryDirectory, $context->getStudentExecutionDirectory()); + static::assertSame($temporaryDirectory . '/solution.php', $context->getEntryPoint()); + + //check that reference execution directory is a random temporary directory + static::assertTrue(str_starts_with($context->getReferenceExecutionDirectory(), System::tempDir())); + } +} diff --git a/test/ExerciseRunner/Context/ExecutionContextTest.php b/test/ExerciseRunner/Context/ExecutionContextTest.php new file mode 100644 index 00000000..de3d2db6 --- /dev/null +++ b/test/ExerciseRunner/Context/ExecutionContextTest.php @@ -0,0 +1,84 @@ + 'solution.php']); + $context = new ExecutionContext( + '/student-dir', + '/reference-dir', + $exercise, + $input + ); + + static::assertSame($exercise, $context->getExercise()); + static::assertSame($input, $context->getInput()); + static::assertSame('/student-dir', $context->getStudentExecutionDirectory()); + static::assertSame('/reference-dir', $context->getReferenceExecutionDirectory()); + } + + public function testHasStudentSolution(): void + { + $exercise = new MockExercise(); + $input = new Input('test', ['program' => 'solution.php']); + $context = new ExecutionContext( + '/student-dir', + '/reference-dir', + $exercise, + $input + ); + + static::assertTrue($context->hasStudentSolution()); + + $exercise = new MockExercise(); + $input = new Input('test'); + $context = new ExecutionContext( + '/student-dir', + '/reference-dir', + $exercise, + $input + ); + + static::assertFalse($context->hasStudentSolution()); + } + + public function testGetEntryPoint(): void + { + $exercise = new MockExercise(); + $input = new Input('test', ['program' => 'solution.php']); + $context = new ExecutionContext( + '/student-dir', + '/reference-dir', + $exercise, + $input + ); + + static::assertSame('/student-dir/solution.php', $context->getEntryPoint()); + } + + public function testGetEntryPointThrowsExceptionWhenNoStudentSolution(): void + { + static::expectException(NoEntryPoint::class); + + $exercise = new MockExercise(); + $input = new Input('test'); + $context = new ExecutionContext( + '/student-dir', + '/reference-dir', + $exercise, + $input + ); + + $context->getEntryPoint(); + } +} diff --git a/test/ExerciseRunner/Context/NoEntryPointTest.php b/test/ExerciseRunner/Context/NoEntryPointTest.php new file mode 100644 index 00000000..324119ff --- /dev/null +++ b/test/ExerciseRunner/Context/NoEntryPointTest.php @@ -0,0 +1,15 @@ +getMessage()); + } +} diff --git a/test/ExerciseRunner/Context/StaticExecutionContextFactoryTest.php b/test/ExerciseRunner/Context/StaticExecutionContextFactoryTest.php new file mode 100644 index 00000000..15f37f64 --- /dev/null +++ b/test/ExerciseRunner/Context/StaticExecutionContextFactoryTest.php @@ -0,0 +1,24 @@ + 'solution.php']), + new MockExercise() + ); + + $factory = new StaticExecutionContextFactory($context); + + static::assertSame($context, $factory->fromInputAndExercise(new Input('test', []), new MockExercise())); + } +} diff --git a/test/ExerciseRunner/Context/TestContextTest.php b/test/ExerciseRunner/Context/TestContextTest.php new file mode 100644 index 00000000..4a8144de --- /dev/null +++ b/test/ExerciseRunner/Context/TestContextTest.php @@ -0,0 +1,160 @@ +getStudentExecutionDirectory()); + static::assertFileNotExists($context->getReferenceExecutionDirectory()); + } + + public function testWithDirectoriesCreatesExecutionDirectories(): void + { + $context = TestContext::withDirectories(); + + static::assertFileExists($context->getStudentExecutionDirectory()); + static::assertFileExists($context->getReferenceExecutionDirectory()); + } + + public function testImportStudentSolutionFileFromStingThrowsExceptionIfExecutionDirectoryDoesNotExist(): void + { + $this->expectException(\RuntimeException::class); + + $context = TestContext::withoutDirectories(); + $context->importStudentFileFromString('importStudentFileFromString('getStudentExecutionDirectory() . '/solution.php'); + static::assertEquals('getStudentExecutionDirectory() . '/solution.php')); + } + + public function testImportStudentSolutionFileFromStingWithCustomPathCreatesFileInExecutionDirectory(): void + { + $context = TestContext::withDirectories(); + + $context->importStudentFileFromString('getStudentExecutionDirectory() . '/some-file.php'); + static::assertEquals('getStudentExecutionDirectory() . '/some-file.php')); + } + + public function testImportStudentSolutionThrowsExceptionIfExecutionDirectoryDoesNotExist(): void + { + $this->expectException(\RuntimeException::class); + + $context = TestContext::withoutDirectories(); + $context->importStudentSolution('path/to/solution.php'); + } + + public function testImportStudentSolutionCopiesSolutionToExecutionDirectory(): void + { + $context = TestContext::withDirectories(); + + $context->importStudentSolution(__FILE__); + + static::assertFileExists($context->getStudentExecutionDirectory() . '/solution.php'); + static::assertFileEquals(__FILE__, $context->getStudentExecutionDirectory() . '/solution.php'); + } + + public function testImportStudentSolutionFolderThrowsExceptionIfExecutionDirectoryDoesNotExist(): void + { + $this->expectException(\RuntimeException::class); + + $context = TestContext::withoutDirectories(); + $context->importStudentSolutionFolder('path/to/solution'); + } + + public function testImportStudentSolutionFolderCopiesSolutionToExecutionDirectory(): void + { + $context = TestContext::withDirectories(); + + $context->importStudentSolutionFolder(__DIR__); + + static::assertFileExists($context->getStudentExecutionDirectory()); + static::assertCount($this->getFileCountInThisDirectory(), scandir($context->getStudentExecutionDirectory())); + } + + public function testImportReferenceSolutionFileFromStingThrowsExceptionIfExecutionDirectoryDoesNotExist(): void + { + $this->expectException(\RuntimeException::class); + + $context = TestContext::withoutDirectories(); + $context->importReferenceFileFromString('importReferenceFileFromString('getReferenceExecutionDirectory() . '/solution.php'); + static::assertEquals('getReferenceExecutionDirectory() . '/solution.php')); + } + + public function testImportReferenceSolutionFileFromStingWithCustomPathCreatesFileInExecutionDirectory(): void + { + $context = TestContext::withDirectories(); + + $context->importReferenceFileFromString('getReferenceExecutionDirectory() . '/some-file.php'); + static::assertEquals('getReferenceExecutionDirectory() . '/some-file.php')); + } + + + public function testImportReferenceSolutionFolderThrowsExceptionIfExecutionDirectoryDoesNotExist(): void + { + $this->expectException(\RuntimeException::class); + + $context = TestContext::withoutDirectories(); + $context->importReferenceSolution(DirectorySolution::fromDirectory('path/to/solution')); + } + + public function testImportReferenceSolutionFolderCopiesSolutionToExecutionDirectory(): void + { + $context = TestContext::withDirectories(); + + $context->importReferenceSolution(DirectorySolution::fromDirectory(__DIR__, [], basename(__FILE__))); + + static::assertFileExists($context->getStudentExecutionDirectory()); + static::assertCount($this->getFileCountInThisDirectory(), scandir($context->getReferenceExecutionDirectory())); + } + + public function testDestructCleansUpExecutionDirectories(): void + { + $context = TestContext::withDirectories(); + + $studentExecutionDirectory = $context->getStudentExecutionDirectory(); + $referenceExecutionDirectory = $context->getReferenceExecutionDirectory(); + + static::assertFileExists($studentExecutionDirectory); + static::assertFileExists($referenceExecutionDirectory); + + unset($context); + + static::assertFileNotExists($studentExecutionDirectory); + static::assertFileNotExists($referenceExecutionDirectory); + } + + private function getFileCountInThisDirectory(): int + { + return count(scandir(__DIR__)); + } +} diff --git a/test/Utils/SystemTest.php b/test/Utils/SystemTest.php index 96759cd6..69fd2a0a 100644 --- a/test/Utils/SystemTest.php +++ b/test/Utils/SystemTest.php @@ -33,4 +33,9 @@ public function testTempDirWithPath(): void $expect = sprintf('%s/php-school/%s', realpath(sys_get_temp_dir()), 'test'); self::assertSame($expect, System::tempDir('test')); } + + public function testRandomTempDir(): void + { + self::assertTrue(str_starts_with(System::randomTempDir(), realpath(sys_get_temp_dir()) . '/php-school')); + } } From e6639d869a739adc59b81655a515bd620438f004 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 5 May 2024 18:46:37 +0200 Subject: [PATCH 3/6] Introduce exercise scenario objects --- src/Exercise/Scenario/CgiScenario.php | 28 ++++++++++++++++ src/Exercise/Scenario/CliScenario.php | 31 +++++++++++++++++ src/Exercise/Scenario/ExerciseScenario.php | 26 +++++++++++++++ test/Exercise/Scenario/CgiScenarioTest.php | 39 ++++++++++++++++++++++ test/Exercise/Scenario/CliScenarioTest.php | 38 +++++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 src/Exercise/Scenario/CgiScenario.php create mode 100644 src/Exercise/Scenario/CliScenario.php create mode 100644 src/Exercise/Scenario/ExerciseScenario.php create mode 100644 test/Exercise/Scenario/CgiScenarioTest.php create mode 100644 test/Exercise/Scenario/CliScenarioTest.php diff --git a/src/Exercise/Scenario/CgiScenario.php b/src/Exercise/Scenario/CgiScenario.php new file mode 100644 index 00000000..30dd1ad4 --- /dev/null +++ b/src/Exercise/Scenario/CgiScenario.php @@ -0,0 +1,28 @@ + + */ + private array $executions = []; + + public function withExecution(RequestInterface $request): self + { + $this->executions[] = $request; + + return $this; + } + + /** + * @return array + */ + public function getExecutions(): array + { + return $this->executions; + } +} diff --git a/src/Exercise/Scenario/CliScenario.php b/src/Exercise/Scenario/CliScenario.php new file mode 100644 index 00000000..88b57d09 --- /dev/null +++ b/src/Exercise/Scenario/CliScenario.php @@ -0,0 +1,31 @@ +> + */ + private array $executions = []; + + /** + * @param array $args + */ + public function withExecution(array $args = []): static + { + $this->executions[] = new Collection($args); + + return $this; + } + + /** + * @return array> + */ + public function getExecutions(): array + { + return $this->executions; + } +} diff --git a/src/Exercise/Scenario/ExerciseScenario.php b/src/Exercise/Scenario/ExerciseScenario.php new file mode 100644 index 00000000..4d6a595a --- /dev/null +++ b/src/Exercise/Scenario/ExerciseScenario.php @@ -0,0 +1,26 @@ + + */ + private array $files = []; + + public function withFile(string $relativeFileName, string $content): static + { + $this->files[$relativeFileName] = $content; + + return $this; + } + + /** + * @return array + */ + public function getFiles(): array + { + return $this->files; + } +} diff --git a/test/Exercise/Scenario/CgiScenarioTest.php b/test/Exercise/Scenario/CgiScenarioTest.php new file mode 100644 index 00000000..4c0782ca --- /dev/null +++ b/test/Exercise/Scenario/CgiScenarioTest.php @@ -0,0 +1,39 @@ +createMock(RequestInterface::class); + $requestTwo = $this->createMock(RequestInterface::class); + + $scenario = (new CgiScenario()) + ->withFile('file1.txt', 'content1') + ->withFile('file2.txt', 'content2') + ->withExecution($requestOne) + ->withExecution($requestTwo); + + static::assertEquals( + [ + 'file1.txt' => 'content1', + 'file2.txt' => 'content2', + ], + $scenario->getFiles() + ); + + static::assertEquals( + [ + $requestOne, + $requestTwo + ], + $scenario->getExecutions() + ); + } +} diff --git a/test/Exercise/Scenario/CliScenarioTest.php b/test/Exercise/Scenario/CliScenarioTest.php new file mode 100644 index 00000000..1ea86a9a --- /dev/null +++ b/test/Exercise/Scenario/CliScenarioTest.php @@ -0,0 +1,38 @@ +withFile('file1.txt', 'content1') + ->withFile('file2.txt', 'content2') + ->withExecution(['arg1', 'arg2']) + ->withExecution(['arg3', 'arg4']); + + static::assertEquals( + [ + 'file1.txt' => 'content1', + 'file2.txt' => 'content2', + ], + $scenario->getFiles() + ); + + static::assertEquals( + [ + ['arg1', 'arg2'], + ['arg3', 'arg4'], + ], + array_map( + fn (Collection $collection) => $collection->getArrayCopy(), + $scenario->getExecutions() + ) + ); + } +} From 300070cd78ad3dfcb4bc0571278142c8b3ba4d86 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 5 May 2024 21:15:43 +0200 Subject: [PATCH 4/6] Seperate listeners and checks --- src/Exercise/AbstractExercise.php | 14 ++++++---- src/Exercise/ExerciseInterface.php | 15 ++++++---- src/ExerciseDispatcher.php | 8 +++--- test/Asset/CgiExerciseImpl.php | 9 +++++- test/Asset/CliExerciseImpl.php | 9 +++++- test/Asset/CliExerciseMissingInterface.php | 7 +++++ test/Asset/ComposerExercise.php | 9 ++++-- test/Asset/ExerciseWithInitialCode.php | 14 +++++++--- test/Asset/FileComparisonExercise.php | 14 +++++++--- test/Asset/FunctionRequirementsExercise.php | 17 +++++++---- test/Asset/PatchableExercise.php | 10 +++++-- test/Asset/ProvidesSolutionExercise.php | 16 +++++++---- test/Check/DatabaseCheckTest.php | 31 ++++++--------------- test/Exercise/AbstractExerciseTest.php | 7 +++-- 14 files changed, 115 insertions(+), 65 deletions(-) diff --git a/src/Exercise/AbstractExercise.php b/src/Exercise/AbstractExercise.php index ff790419..87a0b241 100644 --- a/src/Exercise/AbstractExercise.php +++ b/src/Exercise/AbstractExercise.php @@ -4,6 +4,8 @@ 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; @@ -78,12 +80,14 @@ public static function normaliseName(string $name): string } /** - * This method is implemented as empty by default, if you want to add additional checks or listen - * to events, you should override this method. - * - * @param ExerciseDispatcher $dispatcher + * @return list */ - public function configure(ExerciseDispatcher $dispatcher): void + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void { } } diff --git a/src/Exercise/ExerciseInterface.php b/src/Exercise/ExerciseInterface.php index 2a969359..10443374 100644 --- a/src/Exercise/ExerciseInterface.php +++ b/src/Exercise/ExerciseInterface.php @@ -4,6 +4,7 @@ namespace PhpSchool\PhpWorkshop\Exercise; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\ExerciseDispatcher; /** @@ -34,14 +35,16 @@ public function getType(): ExerciseType; public function getProblem(): string; /** - * This is where the exercise specifies the extra checks it may require. It is also - * possible to grab the event dispatcher from the exercise dispatcher and listen to any - * events. This method is automatically invoked just before verifying/running an student's solution - * to an exercise. + * Subscribe to events triggered throughout the verification process + */ + public function defineListeners(EventDispatcher $dispatcher): void; + + /** + * This is where the exercise specifies the extra checks it may require. * - * @param ExerciseDispatcher $dispatcher + * @return array */ - public function configure(ExerciseDispatcher $dispatcher): void; + public function getRequiredChecks(): array; /** * A short description of the exercise. diff --git a/src/ExerciseDispatcher.php b/src/ExerciseDispatcher.php index 6d9348a6..112fd68b 100644 --- a/src/ExerciseDispatcher.php +++ b/src/ExerciseDispatcher.php @@ -129,11 +129,11 @@ public function requireCheck(string $requiredCheck): void */ public function verify(ExerciseInterface $exercise, Input $input): ResultAggregator { - $exercise->configure($this); - $runner = $this->runnerManager->getRunner($exercise); - foreach ($runner->getRequiredChecks() as $requiredCheck) { + $exercise->defineListeners($this->eventDispatcher); + + foreach ([...$runner->getRequiredChecks(), ...$exercise->getRequiredChecks()] as $requiredCheck) { $this->requireCheck($requiredCheck); } @@ -181,7 +181,7 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega */ public function run(ExerciseInterface $exercise, Input $input, OutputInterface $output): bool { - $exercise->configure($this); + $exercise->defineListeners($this->eventDispatcher); /** @var PhpLintCheck $lint */ $lint = $this->checkRepository->getByClass(PhpLintCheck::class); diff --git a/test/Asset/CgiExerciseImpl.php b/test/Asset/CgiExerciseImpl.php index 45c78005..d8c23c96 100644 --- a/test/Asset/CgiExerciseImpl.php +++ b/test/Asset/CgiExerciseImpl.php @@ -2,6 +2,8 @@ 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; @@ -62,7 +64,12 @@ public function getType(): ExerciseType return ExerciseType::CGI(); } - public function configure(ExerciseDispatcher $dispatcher): void + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void { } } diff --git a/test/Asset/CliExerciseImpl.php b/test/Asset/CliExerciseImpl.php index df157189..0fcc9e32 100644 --- a/test/Asset/CliExerciseImpl.php +++ b/test/Asset/CliExerciseImpl.php @@ -2,6 +2,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; @@ -66,7 +68,12 @@ public function getType(): ExerciseType return ExerciseType::CLI(); } - public function configure(ExerciseDispatcher $dispatcher): void + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void { } } diff --git a/test/Asset/CliExerciseMissingInterface.php b/test/Asset/CliExerciseMissingInterface.php index 5edd2200..21c8d482 100644 --- a/test/Asset/CliExerciseMissingInterface.php +++ b/test/Asset/CliExerciseMissingInterface.php @@ -2,6 +2,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\AbstractExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; @@ -31,4 +33,9 @@ public function getType(): ExerciseType { return ExerciseType::CLI(); } + + public function getRequiredChecks(): array + { + return []; + } } diff --git a/test/Asset/ComposerExercise.php b/test/Asset/ComposerExercise.php index 622a7de8..e7365893 100644 --- a/test/Asset/ComposerExercise.php +++ b/test/Asset/ComposerExercise.php @@ -3,6 +3,7 @@ namespace PhpSchool\PhpWorkshopTest\Asset; use PhpSchool\PhpWorkshop\Check\ComposerCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\ComposerExerciseCheck; @@ -53,8 +54,12 @@ public function getType(): ExerciseType return ExerciseType::CLI(); } - public function configure(ExerciseDispatcher $dispatcher): void + public function getRequiredChecks(): array + { + return [ComposerCheck::class]; + } + + public function defineListeners(EventDispatcher $dispatcher): void { - $dispatcher->requireCheck(ComposerCheck::class); } } diff --git a/test/Asset/ExerciseWithInitialCode.php b/test/Asset/ExerciseWithInitialCode.php index 398b3785..49282605 100644 --- a/test/Asset/ExerciseWithInitialCode.php +++ b/test/Asset/ExerciseWithInitialCode.php @@ -2,6 +2,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\Exercise\ProvidesInitialCode; @@ -41,13 +43,17 @@ public function getType(): ExerciseType // TODO: Implement getType() method. } - public function configure(ExerciseDispatcher $dispatcher): void + public function getInitialCode(): SolutionInterface { - // TODO: Implement configure() method. + return SingleFileSolution::fromFile(__DIR__ . '/initial-code/init-solution.php'); } - public function getInitialCode(): SolutionInterface + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void { - return SingleFileSolution::fromFile(__DIR__ . '/initial-code/init-solution.php'); } } diff --git a/test/Asset/FileComparisonExercise.php b/test/Asset/FileComparisonExercise.php index b4ecd9c1..8627a37c 100644 --- a/test/Asset/FileComparisonExercise.php +++ b/test/Asset/FileComparisonExercise.php @@ -3,6 +3,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; use PhpSchool\PhpWorkshop\Check\ComposerCheck; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\FileComparisonExerciseCheck; @@ -66,13 +68,17 @@ public function getType(): ExerciseType return ExerciseType::CLI(); } - public function configure(ExerciseDispatcher $dispatcher): void + public function getFilesToCompare(): array { - $dispatcher->requireCheck(ComposerCheck::class); + return $this->files; } - public function getFilesToCompare(): array + public function getRequiredChecks(): array + { + return [FileComparisonCheck::class]; + } + + public function defineListeners(EventDispatcher $dispatcher): void { - return $this->files; } } diff --git a/test/Asset/FunctionRequirementsExercise.php b/test/Asset/FunctionRequirementsExercise.php index 927d6f56..4e243a3f 100644 --- a/test/Asset/FunctionRequirementsExercise.php +++ b/test/Asset/FunctionRequirementsExercise.php @@ -3,6 +3,9 @@ namespace PhpSchool\PhpWorkshopTest\Asset; use PhpSchool\PhpWorkshop\Check\ComposerCheck; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck; @@ -45,11 +48,6 @@ public function getType(): ExerciseType return ExerciseType::CLI(); } - public function configure(ExerciseDispatcher $dispatcher): void - { - $dispatcher->requireCheck(ComposerCheck::class); - } - /** * @return string[] */ @@ -65,4 +63,13 @@ public function getBannedFunctions(): array { return ['file']; } + + public function getRequiredChecks(): array + { + return [FunctionRequirementsCheck::class]; + } + + public function defineListeners(EventDispatcher $dispatcher): void + { + } } diff --git a/test/Asset/PatchableExercise.php b/test/Asset/PatchableExercise.php index 51a9d601..c232113e 100644 --- a/test/Asset/PatchableExercise.php +++ b/test/Asset/PatchableExercise.php @@ -2,6 +2,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable; @@ -45,8 +47,12 @@ public function getType(): ExerciseType // TODO: Implement getType() method. } - public function configure(ExerciseDispatcher $dispatcher): void + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void { - // TODO: Implement configure() method. } } diff --git a/test/Asset/ProvidesSolutionExercise.php b/test/Asset/ProvidesSolutionExercise.php index b86f8dd9..503e1bf8 100644 --- a/test/Asset/ProvidesSolutionExercise.php +++ b/test/Asset/ProvidesSolutionExercise.php @@ -4,6 +4,8 @@ namespace PhpSchool\PhpWorkshopTest\Asset; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; @@ -28,11 +30,6 @@ public function getProblem(): string // TODO: Implement getProblem() method. } - public function configure(ExerciseDispatcher $dispatcher): void - { - // TODO: Implement configure() method. - } - public function getDescription(): string { // TODO: Implement getDescription() method. @@ -47,4 +44,13 @@ public function getSolution(): SolutionInterface { return SingleFileSolution::fromFile(__DIR__ . '/provided-solution/solution.php'); } + + public function getRequiredChecks(): array + { + return []; + } + + public function defineListeners(EventDispatcher $dispatcher): void + { + } } diff --git a/test/Check/DatabaseCheckTest.php b/test/Check/DatabaseCheckTest.php index 14629e90..da96cd7f 100644 --- a/test/Check/DatabaseCheckTest.php +++ b/test/Check/DatabaseCheckTest.php @@ -131,10 +131,8 @@ public function testIfPDOThrowsExceptionItCleansUp(): void $this->exercise ->expects($this->once()) - ->method('configure') - ->willReturnCallback(function (ExerciseDispatcher $dispatcher) { - $dispatcher->requireCheck(DatabaseCheck::class); - }); + ->method('getRequiredChecks') + ->willReturn([DatabaseCheck::class]); $this->exercise ->expects($this->once()) @@ -172,10 +170,8 @@ public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void $this->exercise ->expects($this->once()) - ->method('configure') - ->willReturnCallback(function (ExerciseDispatcher $dispatcher) { - $dispatcher->requireCheck(DatabaseCheck::class); - }); + ->method('getRequiredChecks') + ->willReturn([DatabaseCheck::class]); $this->exercise ->expects($this->once()) @@ -207,13 +203,6 @@ public function testRunExercise(): void ->method('getArgs') ->willReturn([]); - $this->exercise - ->expects($this->once()) - ->method('configure') - ->willReturnCallback(function (ExerciseDispatcher $dispatcher) { - $dispatcher->requireCheck(DatabaseCheck::class); - }); - $this->checkRepository->registerCheck($this->check); $results = new ResultAggregator(); @@ -248,10 +237,8 @@ public function testFailureIsReturnedIfDatabaseVerificationFails(): void $this->exercise ->expects($this->once()) - ->method('configure') - ->willReturnCallback(function (ExerciseDispatcher $dispatcher) { - $dispatcher->requireCheck(DatabaseCheck::class); - }); + ->method('getRequiredChecks') + ->willReturn([DatabaseCheck::class]); $this->exercise ->expects($this->once()) @@ -296,10 +283,8 @@ public function testAlteringDatabaseInSolutionDoesNotEffectDatabaseInUserSolutio $this->exercise ->expects($this->once()) - ->method('configure') - ->willReturnCallback(function (ExerciseDispatcher $dispatcher) { - $dispatcher->requireCheck(DatabaseCheck::class); - }); + ->method('getRequiredChecks') + ->willReturn([DatabaseCheck::class]); $this->exercise ->expects($this->once()) diff --git a/test/Exercise/AbstractExerciseTest.php b/test/Exercise/AbstractExerciseTest.php index 6b2d722c..c500b0f7 100644 --- a/test/Exercise/AbstractExerciseTest.php +++ b/test/Exercise/AbstractExerciseTest.php @@ -2,6 +2,7 @@ namespace PhpSchool\PhpWorkshopTest\Exercise; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\Solution\SolutionFile; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; @@ -65,11 +66,11 @@ public function problemProvider(): array ]; } - public function testConfigureDoesNothing(): void + public function testDefineListenersDoesNothing(): void { - $dispatcher = $this->createMock(ExerciseDispatcher::class); + $dispatcher = $this->createMock(EventDispatcher::class); $exercise = new AbstractExerciseImpl('Array We Go'); - $this->assertNull($exercise->configure($dispatcher)); + $this->assertNull($exercise->defineListeners($dispatcher)); } } From 441b2bda462e17a02f759ebf0673a4aa5f6824f0 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 5 May 2024 21:56:09 +0200 Subject: [PATCH 5/6] Use new process factory --- app/config.php | 10 +- src/ExerciseRunner/CgiRunner.php | 91 +++++------- src/ExerciseRunner/CliRunner.php | 131 ++++++------------ .../Factory/CgiRunnerFactory.php | 16 +-- .../Factory/CliRunnerFactory.php | 16 +-- src/ExerciseRunner/RunnerManager.php | 2 +- test/Check/DatabaseCheckTest.php | 5 +- test/ExerciseRunner/CgiRunnerTest.php | 16 +-- test/ExerciseRunner/CliRunnerTest.php | 18 +-- .../Factory/CgiRunnerFactoryTest.php | 14 +- .../Factory/CliRunnerFactoryTest.php | 14 +- 11 files changed, 116 insertions(+), 217 deletions(-) diff --git a/app/config.php b/app/config.php index ab0aa126..39cea60d 100644 --- a/app/config.php +++ b/app/config.php @@ -71,6 +71,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; @@ -187,12 +189,16 @@ //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))); + $manager->addFactory(new CgiRunnerFactory($c->get(EventDispatcher::class), $c->get(ProcessFactory::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')); diff --git a/src/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index e29e3348..692e7891 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -19,6 +19,8 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; 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; @@ -32,31 +34,18 @@ use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; +use function PHPStan\dumpType; + /** * The `CGI` runner. This runner executes solutions as if they were behind a web-server. They populate the `$_SERVER`, * `$_GET` & `$_POST` super globals with information based of the request objects returned from the exercise. */ 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,26 +57,13 @@ 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 + private CgiExercise $exercise, + private EventDispatcher $eventDispatcher, + private ProcessFactory $processFactory ) { - $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; } /** @@ -172,7 +148,7 @@ private function getHeaders(ResponseInterface $response): array */ private function executePhpFile(string $fileName, RequestInterface $request, string $type): ResponseInterface { - $process = $this->getProcess($fileName, $request); + $process = $this->getPhpProcess(dirname($fileName), basename($fileName), $request); $process->start(); $this->eventDispatcher->dispatch(new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $request)); @@ -196,47 +172,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); + $processInput = new ProcessInput( + 'php-cgi', + [ + '-dalways_populate_raw_post_data=-1', + '-dhtml_errors=0', + '-dexpose_php=0', + ], + $workingDirectory, + $env, + $content + ); - return $env; + return $this->processFactory->create($processInput); } /** @@ -297,7 +264,11 @@ public function run(Input $input, OutputInterface $output): bool $event = $this->eventDispatcher->dispatch( new CgiExecuteEvent('cgi.run.student-execute.pre', $request) ); - $process = $this->getProcess($input->getRequiredArgument('program'), $event->getRequest()); + $process = $this->getPhpProcess( + dirname($input->getRequiredArgument('program')), + $input->getRequiredArgument('program'), + $event->getRequest() + ); $process->start(); $this->eventDispatcher->dispatch( diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index 4750405e..b56ff1d2 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -18,6 +18,8 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; 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; @@ -25,6 +27,7 @@ use PhpSchool\PhpWorkshop\Result\Cli\ResultInterface as CliResultInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; use PhpSchool\PhpWorkshop\Utils\ArrayObject; +use PhpSchool\PhpWorkshop\Utils\Collection; use RuntimeException; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -39,25 +42,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,24 +55,13 @@ 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 + ) { } /** @@ -105,59 +82,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. @@ -272,7 +196,12 @@ public function run(Input $input, OutputInterface $output): bool $args = $event->getArgs(); - $process = $this->getPhpProcess($input->getRequiredArgument('program'), $args); + $process = $this->getPhpProcess( + dirname($input->getRequiredArgument('program')), + $input->getRequiredArgument('program'), + $args + ); + $process->start(); $this->eventDispatcher->dispatch( new CliExecuteEvent('cli.run.student.executing', $args, ['output' => $output]) @@ -296,4 +225,32 @@ public function run(Input $input, OutputInterface $output): bool $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.finish', $this->exercise, $input)); return $success; } + + /** + * @param ArrayObject $args + */ + private function executePhpFile(string $fileName, ArrayObject $args, string $type): string + { + $process = $this->getPhpProcess(dirname($fileName), $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 ArrayObject $args + */ + private function getPhpProcess(string $workingDirectory, string $fileName, ArrayObject $args): Process + { + return $this->processFactory->create( + new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, []) + ); + } } diff --git a/src/ExerciseRunner/Factory/CgiRunnerFactory.php b/src/ExerciseRunner/Factory/CgiRunnerFactory.php index a7917039..3b0deed3 100644 --- a/src/ExerciseRunner/Factory/CgiRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CgiRunnerFactory.php @@ -12,6 +12,7 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; /** @@ -22,19 +23,10 @@ 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) + public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory) { - $this->eventDispatcher = $eventDispatcher; } /** @@ -66,6 +58,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); } } diff --git a/src/ExerciseRunner/Factory/CliRunnerFactory.php b/src/ExerciseRunner/Factory/CliRunnerFactory.php index 9694d642..9b6eec2e 100644 --- a/src/ExerciseRunner/Factory/CliRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CliRunnerFactory.php @@ -12,6 +12,7 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; /** * Factory class for `CliRunner` @@ -21,19 +22,10 @@ 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) + public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory) { - $this->eventDispatcher = $eventDispatcher; } /** @@ -65,6 +57,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); } } diff --git a/src/ExerciseRunner/RunnerManager.php b/src/ExerciseRunner/RunnerManager.php index 149022e0..d1b96f9a 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 diff --git a/test/Check/DatabaseCheckTest.php b/test/Check/DatabaseCheckTest.php index da96cd7f..70a40256 100644 --- a/test/Check/DatabaseCheckTest.php +++ b/test/Check/DatabaseCheckTest.php @@ -15,6 +15,7 @@ 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; @@ -68,8 +69,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()]) + ->onlyMethods(['getRequiredChecks']) ->getMock(); $runner diff --git a/test/ExerciseRunner/CgiRunnerTest.php b/test/ExerciseRunner/CgiRunnerTest.php index df8304a3..aade9085 100644 --- a/test/ExerciseRunner/CgiRunnerTest.php +++ b/test/ExerciseRunner/CgiRunnerTest.php @@ -6,6 +6,7 @@ use GuzzleHttp\Psr7\Request; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; @@ -23,6 +24,7 @@ use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; @@ -30,24 +32,18 @@ class CgiRunnerTest extends TestCase { use AssertionRenames; - /** @var CgiRunner */ - private $runner; - + private CgiRunner $runner; /** - * @var CgiExerciseInterface + * @var CgiExerciseInterface&MockObject */ private $exercise; - - /** - * @var EventDispatcher - */ - private $eventDispatcher; + private EventDispatcher $eventDispatcher; public function setUp(): void { $this->exercise = $this->createMock(CgiExerciseInterface::class); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher); + $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory()); $this->exercise ->method('getType') diff --git a/test/ExerciseRunner/CliRunnerTest.php b/test/ExerciseRunner/CliRunnerTest.php index 78bad5e0..ecbf0bc6 100644 --- a/test/ExerciseRunner/CliRunnerTest.php +++ b/test/ExerciseRunner/CliRunnerTest.php @@ -5,6 +5,7 @@ use Colors\Color; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; @@ -23,6 +24,7 @@ use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; @@ -30,24 +32,18 @@ class CliRunnerTest extends TestCase { use AssertionRenames; - /** @var CliRunner */ - private $runner; - - /** - * @var CliExerciseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $exercise; - + private CliRunner $runner; /** - * @var EventDispatcher + * @var CliExerciseInterface&MockObject */ - private $eventDispatcher; + private CliExerciseInterface $exercise; + private EventDispatcher $eventDispatcher; public function setUp(): void { $this->exercise = $this->createMock(CliExerciseInterface::class); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CliRunner($this->exercise, $this->eventDispatcher); + $this->runner = new CliRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory()); $this->exercise ->method('getType') diff --git a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php index 03daf67e..9b334eae 100644 --- a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php @@ -8,26 +8,20 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CgiRunnerFactory; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; 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()); } public function testSupports(): void diff --git a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php index 6678104d..8fc5eb58 100644 --- a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php @@ -8,25 +8,19 @@ use PhpSchool\PhpWorkshop\Exercise\ExerciseType; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; 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()); } public function testSupports(): void From 4f58bfa72e5f18d4f39d685ac05569d2fe521bb2 Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Sun, 5 May 2024 22:49:38 +0200 Subject: [PATCH 6/6] Reorganise event hiearchy --- src/Event/CgiExecuteEvent.php | 20 ++-- src/Event/CgiExerciseRunnerEvent.php | 11 ++ src/Event/CliExecuteEvent.php | 17 ++- src/Event/CliExerciseRunnerEvent.php | 11 ++ src/Event/Event.php | 11 +- src/Event/EventDispatcher.php | 13 +-- src/Event/EventInterface.php | 4 +- src/ExerciseRunner/CgiRunner.php | 130 ++++++++++++++-------- src/ExerciseRunner/CliRunner.php | 57 +++++++--- test/Event/CgiExecuteEventTest.php | 8 +- test/Event/CgiExerciseRunnerEventTest.php | 30 +++++ test/Event/CliExecuteEventTest.php | 8 +- test/Event/CliExerciseRunnerEventTest.php | 30 +++++ 13 files changed, 248 insertions(+), 102 deletions(-) create mode 100644 src/Event/CgiExerciseRunnerEvent.php create mode 100644 src/Event/CliExerciseRunnerEvent.php create mode 100644 test/Event/CgiExerciseRunnerEventTest.php create mode 100644 test/Event/CliExerciseRunnerEventTest.php diff --git a/src/Event/CgiExecuteEvent.php b/src/Event/CgiExecuteEvent.php index e72551b6..2a79076b 100644 --- a/src/Event/CgiExecuteEvent.php +++ b/src/Event/CgiExecuteEvent.php @@ -4,28 +4,32 @@ namespace PhpSchool\PhpWorkshop\Event; +use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Input\Input; 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. */ - public function __construct(string $name, RequestInterface $request, array $parameters = []) - { + public function __construct( + string $name, + ExerciseInterface $exercise, + Input $input, + RequestInterface $request, + array $parameters = [] + ) { $parameters['request'] = $request; - parent::__construct($name, $parameters); + parent::__construct($name, $exercise, $input, $parameters); $this->request = $request; } diff --git a/src/Event/CgiExerciseRunnerEvent.php b/src/Event/CgiExerciseRunnerEvent.php new file mode 100644 index 00000000..9b283b97 --- /dev/null +++ b/src/Event/CgiExerciseRunnerEvent.php @@ -0,0 +1,11 @@ + */ - private $args; + private ArrayObject $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. */ - public function __construct(string $name, ArrayObject $args, array $parameters = []) - { + public function __construct( + string $name, + ExerciseInterface $exercise, + Input $input, + ArrayObject $args, + array $parameters = [] + ) { $parameters['args'] = $args; - parent::__construct($name, $parameters); + parent::__construct($name, $exercise, $input, $parameters); $this->args = $args; } diff --git a/src/Event/CliExerciseRunnerEvent.php b/src/Event/CliExerciseRunnerEvent.php new file mode 100644 index 00000000..1f2992ca --- /dev/null +++ b/src/Event/CliExerciseRunnerEvent.php @@ -0,0 +1,11 @@ + */ - 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..11b1102c 100644 --- a/src/Event/EventDispatcher.php +++ b/src/Event/EventDispatcher.php @@ -16,16 +16,13 @@ class EventDispatcher /** * @var array> */ - private $listeners = []; + private array $listeners = []; /** * @var ResultAggregator */ - private $resultAggregator; + private ResultAggregator $resultAggregator; - /** - * @param ResultAggregator $resultAggregator - */ public function __construct(ResultAggregator $resultAggregator) { $this->resultAggregator = $resultAggregator; @@ -33,9 +30,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 +97,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/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index 692e7891..bd5f456b 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -10,6 +10,7 @@ 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; @@ -85,33 +86,84 @@ 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 Input $input The command line arguments passed to the command. + * @return CgiResult The result of the check. */ - private function checkRequest(RequestInterface $request, string $fileName): CgiResultInterface + public function verify(Input $input): ResultInterface + { + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.start', $this->exercise, $input)); + $result = new CgiResult( + array_map( + function (RequestInterface $request) use ($input) { + return $this->doVerify($request, $input); + }, + $this->exercise->getRequests() + ) + ); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.finish', $this->exercise, $input)); + return $result; + } + + private function doVerify(RequestInterface $request, Input $input): CgiResultInterface { try { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.verify.reference-execute.pre', $request) + new CgiExecuteEvent('cgi.verify.reference-execute.pre', $this->exercise, $input, $request) ); $solutionResponse = $this->executePhpFile( + $input, $this->exercise->getSolution()->getEntryPoint()->getAbsolutePath(), $event->getRequest(), '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', + $this->exercise, + $input, + $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', $this->exercise, $input, $request) + ); + $userResponse = $this->executePhpFile( + $input, + $input->getRequiredArgument('program'), + $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', + $this->exercise, + $input, + $request, + ['exception' => $e] + ) + ); return GenericFailure::fromRequestAndCodeExecutionFailure($request, $e); } @@ -146,12 +198,18 @@ private function getHeaders(ResponseInterface $response): array * @param string $type * @return ResponseInterface */ - private function executePhpFile(string $fileName, RequestInterface $request, string $type): ResponseInterface - { + private function executePhpFile( + Input $input, + string $fileName, + RequestInterface $request, + string $type + ): ResponseInterface { $process = $this->getPhpProcess(dirname($fileName), basename($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), $this->exercise, $input, $request) + ); $process->wait(); if (!$process->isSuccessful()) { @@ -206,38 +264,6 @@ private function getPhpProcess(string $workingDirectory, string $fileName, Reque return $this->processFactory->create($processInput); } - /** - * 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() - ) - ); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.verify.finish', $this->exercise, $input)); - return $result; - } - /** * Runs a student's 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 @@ -257,12 +283,12 @@ function (RequestInterface $request) use ($input) { */ public function run(Input $input, OutputInterface $output): bool { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.run.start', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $this->exercise, $input)); $success = true; foreach ($this->exercise->getRequests() 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', $this->exercise, $input, $request) ); $process = $this->getPhpProcess( dirname($input->getRequiredArgument('program')), @@ -272,7 +298,13 @@ public function run(Input $input, OutputInterface $output): bool $process->start(); $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student.executing', $request, ['output' => $output]) + new CgiExecuteEvent( + 'cgi.run.student.executing', + $this->exercise, + $input, + $request, + ['output' => $output] + ) ); $process->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); @@ -286,10 +318,10 @@ 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', $this->exercise, $input, $request) ); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cgi.run.finish', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.finish', $this->exercise, $input)); return $success; } } diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index b56ff1d2..3cd71760 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -9,6 +9,7 @@ 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; @@ -100,7 +101,7 @@ public function getRequiredChecks(): array */ public function verify(Input $input): ResultInterface { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.verify.start', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.start', $this->exercise, $input)); $result = new CliResult( array_map( function (array $args) use ($input) { @@ -109,7 +110,7 @@ function (array $args) use ($input) { $this->preserveOldArgFormat($this->exercise->getArgs()) ) ); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.verify.finish', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.finish', $this->exercise, $input)); return $result; } @@ -142,23 +143,49 @@ private function doVerify(array $args, Input $input): CliResultInterface 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', $this->exercise, $input, $args) + ); $solutionOutput = $this->executePhpFile( + $input, $this->exercise->getSolution()->getEntryPoint()->getAbsolutePath(), $event->getArgs(), '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', + $this->exercise, + $input, + $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', $this->exercise, $input, $args) + ); + $userOutput = $this->executePhpFile( + $input, + $input->getRequiredArgument('program'), + $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', + $this->exercise, + $input, + $args, + ['exception' => $e] + ) + ); return GenericFailure::fromArgsAndCodeExecutionFailure($args, $e); } if ($solutionOutput === $userOutput) { @@ -186,12 +213,12 @@ private function doVerify(array $args, Input $input): CliResultInterface */ public function run(Input $input, OutputInterface $output): bool { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.start', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $this->exercise, $input)); $success = true; foreach ($this->preserveOldArgFormat($this->exercise->getArgs()) 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', $this->exercise, $input, new ArrayObject($args)) ); $args = $event->getArgs(); @@ -204,7 +231,7 @@ public function run(Input $input, OutputInterface $output): bool $process->start(); $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student.executing', $args, ['output' => $output]) + new CliExecuteEvent('cli.run.student.executing', $this->exercise, $input, $args, ['output' => $output]) ); $process->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); @@ -218,23 +245,25 @@ 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', $this->exercise, $input, $args) ); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.finish', $this->exercise, $input)); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.finish', $this->exercise, $input)); return $success; } /** * @param ArrayObject $args */ - private function executePhpFile(string $fileName, ArrayObject $args, string $type): string + private function executePhpFile(Input $input, string $fileName, ArrayObject $args, string $type): string { $process = $this->getPhpProcess(dirname($fileName), $fileName, $args); $process->start(); - $this->eventDispatcher->dispatch(new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $args)); + $this->eventDispatcher->dispatch( + new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $this->exercise, $input, $args) + ); $process->wait(); if (!$process->isSuccessful()) { diff --git a/test/Event/CgiExecuteEventTest.php b/test/Event/CgiExecuteEventTest.php index 112e0c41..a885716b 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\MockExercise; +use PhpSchool\PhpWorkshop\Input\Input; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -12,7 +14,7 @@ class CgiExecuteEventTest extends TestCase public function testAddHeader(): void { $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); $e->addHeaderToRequest('Content-Type', 'text/html'); $this->assertSame( @@ -28,7 +30,7 @@ public function testAddHeader(): void public function testModifyRequest(): void { $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); $e->modifyRequest(function (RequestInterface $request) { return $request @@ -49,7 +51,7 @@ public function testModifyRequest(): void public function testGetRequest(): void { $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', $request); + $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); $this->assertSame($request, $e->getRequest()); } diff --git a/test/Event/CgiExerciseRunnerEventTest.php b/test/Event/CgiExerciseRunnerEventTest.php new file mode 100644 index 00000000..8f326dfb --- /dev/null +++ b/test/Event/CgiExerciseRunnerEventTest.php @@ -0,0 +1,30 @@ + 1]); + self::assertSame($exercise, $event->getExercise()); + self::assertSame($input, $event->getInput()); + self::assertEquals( + [ + 'exercise' => $exercise, + 'input' => $input, + 'number' => 1 + ], + $event->getParameters() + ); + } +} diff --git a/test/Event/CliExecuteEventTest.php b/test/Event/CliExecuteEventTest.php index a8ea3174..63712642 100644 --- a/test/Event/CliExecuteEventTest.php +++ b/test/Event/CliExecuteEventTest.php @@ -3,6 +3,8 @@ namespace PhpSchool\PhpWorkshopTest\Event; use PhpSchool\PhpWorkshop\Event\CliExecuteEvent; +use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Utils\ArrayObject; use PHPUnit\Framework\TestCase; @@ -11,7 +13,7 @@ class CliExecuteEventTest extends TestCase public function testAppendArg(): void { $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); $e->appendArg('4'); $this->assertEquals([1, 2, 3, 4], $e->getArgs()->getArrayCopy()); @@ -21,7 +23,7 @@ public function testAppendArg(): void public function testPrependArg(): void { $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); $e->prependArg('4'); $this->assertEquals([4, 1, 2, 3], $e->getArgs()->getArrayCopy()); @@ -31,7 +33,7 @@ public function testPrependArg(): void public function testGetArgs(): void { $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', $arr); + $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); $this->assertSame($arr, $e->getArgs()); } diff --git a/test/Event/CliExerciseRunnerEventTest.php b/test/Event/CliExerciseRunnerEventTest.php new file mode 100644 index 00000000..0c122586 --- /dev/null +++ b/test/Event/CliExerciseRunnerEventTest.php @@ -0,0 +1,30 @@ + 1]); + self::assertSame($exercise, $event->getExercise()); + self::assertSame($input, $event->getInput()); + self::assertEquals( + [ + 'exercise' => $exercise, + 'input' => $input, + 'number' => 1 + ], + $event->getParameters() + ); + } +}

AltStyle によって変換されたページ (->オリジナル) /