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() + ); + } +}