From 11a15f9bec5051144766a02cbcce4eb522499ced Mon Sep 17 00:00:00 2001 From: Aydin Hassan Date: Thu, 9 May 2024 08:15:44 +0200 Subject: [PATCH] Update events to use context --- app/config.php | 15 +- src/Event/CgiExecuteEvent.php | 10 +- src/Event/CgiExerciseRunnerEvent.php | 20 +++ src/Event/CliExecuteEvent.php | 24 +-- src/Event/CliExerciseRunnerEvent.php | 20 +++ src/Event/EventDispatcher.php | 4 - src/Event/ExerciseRunnerEvent.php | 33 ++-- src/ExerciseCheck/SelfCheck.php | 5 +- src/ExerciseDispatcher.php | 14 +- src/ExerciseRunner/CgiRunner.php | 40 +++-- src/ExerciseRunner/CliRunner.php | 47 +++--- src/ExerciseRunner/Context/TestContext.php | 23 +++ src/Listener/CodePatchListener.php | 10 +- src/Listener/PrepareSolutionListener.php | 58 +++---- src/Listener/SelfCheckListener.php | 21 +-- src/TestUtils/WorkshopExerciseTest.php | 6 +- test/Event/CgiExecuteEventTest.php | 22 ++- test/Event/CgiExerciseRunnerEventTest.php | 18 +- test/Event/CliExecuteEventTest.php | 29 +++- test/Event/CliExerciseRunnerEventTest.php | 19 ++- test/Event/ExerciseRunnerEventTest.php | 16 +- .../ExerciseRunner/EnvironmentManagerTest.php | 2 +- test/Listener/CodePatchListenerTest.php | 114 +++++++------ test/Listener/PrepareSolutionListenerTest.php | 155 +++++++----------- test/Listener/RealPathListenerTest.php | 49 ++++-- test/Listener/SelfCheckListenerTest.php | 12 +- 26 files changed, 436 insertions(+), 350 deletions(-) diff --git a/app/config.php b/app/config.php index 25ae0367..e88cf3cc 100644 --- a/app/config.php +++ b/app/config.php @@ -267,7 +267,11 @@ InitialCodeListener::class => function (ContainerInterface $c) { return new InitialCodeListener($c->get('currentWorkingDirectory'), $c->get(LoggerInterface::class)); }, - PrepareSolutionListener::class => create(), + PrepareSolutionListener::class => function (ContainerInterface $c) { + return new PrepareSolutionListener( + $c->get(ProcessFactory::class) + ); + }, CodePatchListener::class => function (ContainerInterface $c) { return new CodePatchListener( $c->get(CodePatcher::class), @@ -472,13 +476,13 @@ function (CgiResult $result) use ($c) { ], ], 'prepare-solution' => [ - 'cli.verify.start' => [ + 'cli.verify.reference-execute.pre' => [ containerListener(PrepareSolutionListener::class), ], 'cli.run.start' => [ containerListener(PrepareSolutionListener::class), ], - 'cgi.verify.start' => [ + 'cgi.verify.reference-execute.pre' => [ containerListener(PrepareSolutionListener::class), ], 'cgi.run.start' => [ @@ -486,7 +490,10 @@ function (CgiResult $result) use ($c) { ], ], 'code-patcher' => [ - 'verify.pre.execute' => [ + 'cli.verify.start' => [ + containerListener(CodePatchListener::class, 'patch'), + ], + 'cgi.verify.start' => [ containerListener(CodePatchListener::class, 'patch'), ], 'verify.post.execute' => [ diff --git a/src/Event/CgiExecuteEvent.php b/src/Event/CgiExecuteEvent.php index 2a79076b..67c0fa9f 100644 --- a/src/Event/CgiExecuteEvent.php +++ b/src/Event/CgiExecuteEvent.php @@ -5,6 +5,8 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use Psr\Http\Message\RequestInterface; @@ -19,17 +21,17 @@ class CgiExecuteEvent extends CgiExerciseRunnerEvent /** * @param string $name The event name. * @param RequestInterface $request The request that will be performed. - * @param array $parameters The event parameters. + * @param array $parameters The event parameters. */ public function __construct( string $name, - ExerciseInterface $exercise, - Input $input, + ExecutionContext $context, + CgiScenario $scenario, RequestInterface $request, array $parameters = [] ) { $parameters['request'] = $request; - parent::__construct($name, $exercise, $input, $parameters); + parent::__construct($name, $context, $scenario, $parameters); $this->request = $request; } diff --git a/src/Event/CgiExerciseRunnerEvent.php b/src/Event/CgiExerciseRunnerEvent.php index 9b283b97..de8f8362 100644 --- a/src/Event/CgiExerciseRunnerEvent.php +++ b/src/Event/CgiExerciseRunnerEvent.php @@ -3,9 +3,29 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; + use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; class CgiExerciseRunnerEvent extends ExerciseRunnerEvent { + private CgiScenario $scenario; + + /** + * @param array $parameters + */ + public function __construct( + string $name, + ExecutionContext $context, + CgiScenario $scenario, + array $parameters = [] + ) { + $this->scenario = $scenario; + parent::__construct($name, $context, $parameters); + } + + public function getScenario(): CgiScenario + { + return $this->scenario; + } } diff --git a/src/Event/CliExecuteEvent.php b/src/Event/CliExecuteEvent.php index d05bef17..ed855b06 100644 --- a/src/Event/CliExecuteEvent.php +++ b/src/Event/CliExecuteEvent.php @@ -5,8 +5,10 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; -use PhpSchool\PhpWorkshop\Utils\ArrayObject; +use PhpSchool\PhpWorkshop\Utils\Collection; /** * An event to represent events which occur throughout the verification and running process in @@ -15,24 +17,24 @@ class CliExecuteEvent extends CliExerciseRunnerEvent { /** - * @var ArrayObject + * @var Collection */ - private ArrayObject $args; + private Collection $args; /** * @param string $name The event name. - * @param ArrayObject $args The arguments that should be/have been passed to the program. - * @param array $parameters The event parameters. + * @param Collection $args The arguments that should be/have been passed to the program. + * @param array $parameters The event parameters. */ public function __construct( string $name, - ExerciseInterface $exercise, - Input $input, - ArrayObject $args, + ExecutionContext $context, + CliScenario $scenario, + Collection $args, array $parameters = [] ) { $parameters['args'] = $args; - parent::__construct($name, $exercise, $input, $parameters); + parent::__construct($name, $context, $scenario, $parameters); $this->args = $args; } @@ -59,9 +61,9 @@ public function appendArg(string $arg): void /** * Get the arguments to be passed to the program. * - * @return ArrayObject + * @return Collection */ - public function getArgs(): ArrayObject + public function getArgs(): Collection { return $this->args; } diff --git a/src/Event/CliExerciseRunnerEvent.php b/src/Event/CliExerciseRunnerEvent.php index 1f2992ca..de635742 100644 --- a/src/Event/CliExerciseRunnerEvent.php +++ b/src/Event/CliExerciseRunnerEvent.php @@ -3,9 +3,29 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; class CliExerciseRunnerEvent extends ExerciseRunnerEvent { + private CliScenario $scenario; + + /** + * @param array $parameters + */ + public function __construct( + string $name, + ExecutionContext $context, + CliScenario $scenario, + array $parameters = [] + ) { + $this->scenario = $scenario; + parent::__construct($name, $context, $parameters); + } + + public function getScenario(): CliScenario + { + return $this->scenario; + } } diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php index 11b1102c..25a00ac2 100644 --- a/src/Event/EventDispatcher.php +++ b/src/Event/EventDispatcher.php @@ -17,10 +17,6 @@ class EventDispatcher * @var array> */ private array $listeners = []; - - /** - * @var ResultAggregator - */ private ResultAggregator $resultAggregator; public function __construct(ResultAggregator $resultAggregator) diff --git a/src/Event/ExerciseRunnerEvent.php b/src/Event/ExerciseRunnerEvent.php index 0dc9ce4a..7aca6ee9 100644 --- a/src/Event/ExerciseRunnerEvent.php +++ b/src/Event/ExerciseRunnerEvent.php @@ -5,6 +5,7 @@ namespace PhpSchool\PhpWorkshop\Event; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; /** @@ -12,30 +13,24 @@ */ class ExerciseRunnerEvent extends Event { - /** - * @var ExerciseInterface - */ - private $exercise; - - /** - * @var Input - */ - private $input; + private ExecutionContext $context; /** * @param string $name - * @param ExerciseInterface $exercise - * @param Input $input - * @param array $parameters + * @param array $parameters */ - public function __construct(string $name, ExerciseInterface $exercise, Input $input, array $parameters = []) + public function __construct(string $name, ExecutionContext $context, array $parameters = []) { - $parameters['input'] = $input; - $parameters['exercise'] = $exercise; + $this->context = $context; + + $parameters['input'] = $context->getInput(); + $parameters['exercise'] = $context->getExercise(); parent::__construct($name, $parameters); + } - $this->exercise = $exercise; - $this->input = $input; + public function getContext(): ExecutionContext + { + return $this->context; } /** @@ -43,7 +38,7 @@ public function __construct(string $name, ExerciseInterface $exercise, Input $in */ public function getInput(): Input { - return $this->input; + return $this->context->getInput(); } /** @@ -51,6 +46,6 @@ public function getInput(): Input */ public function getExercise(): ExerciseInterface { - return $this->exercise; + return $this->context->getExercise(); } } diff --git a/src/ExerciseCheck/SelfCheck.php b/src/ExerciseCheck/SelfCheck.php index 857cf850..7063aa22 100644 --- a/src/ExerciseCheck/SelfCheck.php +++ b/src/ExerciseCheck/SelfCheck.php @@ -4,6 +4,7 @@ namespace PhpSchool\PhpWorkshop\ExerciseCheck; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Result\ResultInterface; @@ -21,8 +22,8 @@ interface SelfCheck * The method is passed the absolute file path to the student's solution and should return a result * object which indicates the success or not of the check. * - * @param Input $input The command line arguments passed to the command. + * @param ExecutionContext $context The current execution context. * @return ResultInterface The result of the check. */ - public function check(Input $input): ResultInterface; + public function check(ExecutionContext $context): ResultInterface; } diff --git a/src/ExerciseDispatcher.php b/src/ExerciseDispatcher.php index a9004aa5..1ff52300 100644 --- a/src/ExerciseDispatcher.php +++ b/src/ExerciseDispatcher.php @@ -117,7 +117,7 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega $this->requireCheck($requiredCheck); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.start', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.start', $context)); $this->validateChecks($this->checksToRunBefore, $exercise); $this->validateChecks($this->checksToRunAfter, $exercise); @@ -130,22 +130,22 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega } } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.pre.execute', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.pre.execute', $context)); try { $this->results->add($runner->verify($context)); } finally { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.execute', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.execute', $context)); } foreach ($this->checksToRunAfter as $check) { $this->results->add($check->check($context)); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.check', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.post.check', $context)); $exercise->tearDown(); - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.finish', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.finish', $context)); return $this->results; } @@ -173,14 +173,14 @@ public function run(ExerciseInterface $exercise, Input $input, OutputInterface $ throw CouldNotRunException::fromFailure($result); } - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.start', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.start', $context)); try { $exitStatus = $this->runnerManager ->getRunner($exercise) ->run($context, $output); } finally { - $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.finish', $exercise, $input)); + $this->eventDispatcher->dispatch(new ExerciseRunnerEvent('run.finish', $context)); } return $exitStatus; diff --git a/src/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index b207844e..b9677584 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -18,6 +18,7 @@ use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; @@ -111,30 +112,31 @@ public function verify(ExecutionContext $context): ResultInterface $this->environmentManager->prepareStudent($context, $scenario); $this->environmentManager->prepareReference($context, $scenario); - $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.start', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.start', $context, $scenario)); $result = new CgiResult( array_map( - function (RequestInterface $request) use ($context) { - return $this->doVerify($request, $context); + function (RequestInterface $request) use ($context, $scenario) { + return $this->doVerify($context, $scenario, $request); }, $scenario->getExecutions() ) ); - $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.finish', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.finish', $context, $scenario)); return $result; } - private function doVerify(RequestInterface $request, ExecutionContext $context): CgiResultInterface + private function doVerify(ExecutionContext $context, CgiScenario $scenario, RequestInterface $request): CgiResultInterface { try { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.verify.reference-execute.pre', $this->exercise, $context->getInput(), $request) + new CgiExecuteEvent('cgi.verify.reference-execute.pre', $context, $scenario, $request) ); $solutionResponse = $this->executePhpFile( $context, + $scenario, $context->getReferenceExecutionDirectory(), $this->exercise->getSolution()->getEntryPoint()->getRelativePath(), $event->getRequest(), @@ -144,8 +146,8 @@ private function doVerify(RequestInterface $request, ExecutionContext $context): $this->eventDispatcher->dispatch( new CgiExecuteEvent( 'cgi.verify.reference-execute.fail', - $this->exercise, - $context->getInput(), + $context, + $scenario, $request, ['exception' => $e] ) @@ -156,10 +158,11 @@ private function doVerify(RequestInterface $request, ExecutionContext $context): try { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.verify.student-execute.pre', $this->exercise, $context->getInput(), $request) + new CgiExecuteEvent('cgi.verify.student-execute.pre', $context, $scenario, $request) ); $userResponse = $this->executePhpFile( $context, + $scenario, $context->getStudentExecutionDirectory(), $context->getEntryPoint(), $event->getRequest(), @@ -169,8 +172,8 @@ private function doVerify(RequestInterface $request, ExecutionContext $context): $this->eventDispatcher->dispatch( new CgiExecuteEvent( 'cgi.verify.student-execute.fail', - $this->exercise, - $context->getInput(), + $context, + $scenario, $request, ['exception' => $e] ) @@ -211,6 +214,7 @@ private function getHeaders(ResponseInterface $response): array */ private function executePhpFile( ExecutionContext $context, + CgiScenario $scenario, string $workingDirectory, string $fileName, RequestInterface $request, @@ -220,7 +224,7 @@ private function executePhpFile( $process->start(); $this->eventDispatcher->dispatch( - new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $this->exercise, $context->getInput(), $request) + new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $context, $scenario, $request) ); $process->wait(); @@ -299,13 +303,13 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $this->environmentManager->prepareStudent($context, $scenario); - $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $context, $scenario)); $success = true; foreach ($scenario->getExecutions() as $i => $request) { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student-execute.pre', $this->exercise, $context->getInput(), $request) + new CgiExecuteEvent('cgi.run.student-execute.pre', $context, $scenario, $request) ); $process = $this->getPhpProcess( $context->getStudentExecutionDirectory(), @@ -317,8 +321,8 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $this->eventDispatcher->dispatch( new CgiExecuteEvent( 'cgi.run.student.executing', - $this->exercise, - $context->getInput(), + $context, + $scenario, $request, ['output' => $output] ) @@ -335,11 +339,11 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $output->lineBreak(); $this->eventDispatcher->dispatch( - new CgiExecuteEvent('cgi.run.student-execute.post', $this->exercise, $context->getInput(), $request) + new CgiExecuteEvent('cgi.run.student-execute.post', $context, $scenario, $request) ); } - $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.finish', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.finish', $context, $scenario)); return $success; } } diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index 96440e8d..3d3b4088 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -17,6 +17,7 @@ use PhpSchool\PhpWorkshop\Exception\SolutionExecutionException; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; @@ -108,33 +109,35 @@ public function verify(ExecutionContext $context): ResultInterface $this->environmentManager->prepareStudent($context, $scenario); $this->environmentManager->prepareReference($context, $scenario); - $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.start', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.start', $context, $scenario)); $result = new CliResult( array_map( - function (Collection $args) use ($context) { - return $this->doVerify($context, $args); + function (Collection $args) use ($context, $scenario) { + return $this->doVerify($context, $scenario, $args); }, $scenario->getExecutions() ) ); - $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.finish', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.finish', $context, $scenario)); + return $result; } /** * @param Collection $args */ - private function doVerify(ExecutionContext $context, Collection $args): CliResultInterface + private function doVerify(ExecutionContext $context, CliScenario $scenario, Collection $args): CliResultInterface { try { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.verify.reference-execute.pre', $this->exercise, $context->getInput(), $args) + new CliExecuteEvent('cli.verify.reference-execute.pre', $context, $scenario, $args) ); $solutionOutput = $this->executePhpFile( $context, + $scenario, $context->getReferenceExecutionDirectory(), $this->exercise->getSolution()->getEntryPoint()->getRelativePath(), $event->getArgs(), @@ -144,8 +147,8 @@ private function doVerify(ExecutionContext $context, Collection $args): CliResul $this->eventDispatcher->dispatch( new CliExecuteEvent( 'cli.verify.reference-execute.fail', - $this->exercise, - $context->getInput(), + $context, + $scenario, $args, ['exception' => $e] ) @@ -156,10 +159,11 @@ private function doVerify(ExecutionContext $context, Collection $args): CliResul try { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.verify.student-execute.pre', $this->exercise, $context->getInput(), $args) + new CliExecuteEvent('cli.verify.student-execute.pre', $context, $scenario, $args) ); $userOutput = $this->executePhpFile( $context, + $scenario, $context->getStudentExecutionDirectory(), $context->getEntryPoint(), $event->getArgs(), @@ -169,8 +173,8 @@ private function doVerify(ExecutionContext $context, Collection $args): CliResul $this->eventDispatcher->dispatch( new CliExecuteEvent( 'cli.verify.student-execute.fail', - $this->exercise, - $context->getInput(), + $context, + $scenario, $args, ['exception' => $e] ) @@ -206,13 +210,13 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $this->environmentManager->prepareStudent($context, $scenario); - $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $context, $scenario)); $success = true; foreach ($scenario->getExecutions() as $i => $args) { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student-execute.pre', $this->exercise, $context->getInput(), $args) + new CliExecuteEvent('cli.run.student-execute.pre', $context, $scenario, $args) ); $args = $event->getArgs(); @@ -225,7 +229,7 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $process->start(); $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student.executing', $this->exercise, $context->getInput(), $args, ['output' => $output]) + new CliExecuteEvent('cli.run.student.executing', $context, $scenario, $args, ['output' => $output]) ); $process->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); @@ -239,24 +243,25 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $output->lineBreak(); $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student-execute.post', $this->exercise, $context->getInput(), $args) + new CliExecuteEvent('cli.run.student-execute.post', $context, $scenario, $args) ); } - $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.finish', $this->exercise, $context->getInput())); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.finish', $context, $scenario)); + return $success; } /** - * @param ArrayObject $args + * @param Collection $args */ - private function executePhpFile(ExecutionContext $context, string $workingDirectory, string $fileName, ArrayObject $args, string $type): string + private function executePhpFile(ExecutionContext $context, CliScenario $scenario, string $workingDirectory, string $fileName, Collection $args, string $type): string { $process = $this->getPhpProcess($workingDirectory, $fileName, $args); $process->start(); $this->eventDispatcher->dispatch( - new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $this->exercise, $context->getInput(), $args) + new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $context, $scenario, $args) ); $process->wait(); @@ -268,9 +273,9 @@ private function executePhpFile(ExecutionContext $context, string $workingDirect } /** - * @param ArrayObject $args + * @param Collection $args */ - private function getPhpProcess(string $workingDirectory, string $fileName, ArrayObject $args): Process + private function getPhpProcess(string $workingDirectory, string $fileName, Collection $args): Process { return $this->processFactory->create( new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, []) diff --git a/src/ExerciseRunner/Context/TestContext.php b/src/ExerciseRunner/Context/TestContext.php index d5ceb0ba..6f271517 100644 --- a/src/ExerciseRunner/Context/TestContext.php +++ b/src/ExerciseRunner/Context/TestContext.php @@ -2,12 +2,16 @@ namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\RuntimeException; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\ExerciseScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Input\Input; +use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; use PhpSchool\PhpWorkshop\Utils\System; use Symfony\Component\Filesystem\Filesystem; @@ -16,6 +20,7 @@ class TestContext extends ExecutionContext { private Filesystem $filesystem; + private EnvironmentManager $environmentManager; private ExerciseInterface $exercise; private bool $studentSolutionDirWasCreated = false; private bool $referenceSolutionDirWasCreated = false; @@ -28,6 +33,7 @@ public function __construct( $this->exercise = $exercise ?? new MockExercise(); $this->filesystem = new Filesystem(); + $this->environmentManager = new EnvironmentManager($this->filesystem, new EventDispatcher(new ResultAggregator())); if ($studentDirectory === null) { $studentDirectory = System::randomTempDir(); @@ -53,6 +59,23 @@ public function createReferenceSolutionDirectory(): void $this->referenceSolutionDirWasCreated = true; } + public function importReferenceSolution(): void + { + if (!$this->referenceSolutionDirWasCreated) { + throw new RuntimeException( + sprintf('Reference execution directory not created. Call %s::createReferenceSolutionDirectory() first.', self::class) + ); + } + + $scenario = new class extends ExerciseScenario { + }; + if ($this->exercise instanceof CliExercise || $this->exercise instanceof CgiExercise) { + $scenario = $this->exercise->defineTestScenario(); + } + + $this->environmentManager->prepareReference($this, $scenario); + } + public function importStudentFileFromString(string $content, string $filename = 'solution.php'): void { if (!$this->studentSolutionDirWasCreated) { diff --git a/src/Listener/CodePatchListener.php b/src/Listener/CodePatchListener.php index 3cbcf787..afca93cd 100644 --- a/src/Listener/CodePatchListener.php +++ b/src/Listener/CodePatchListener.php @@ -8,6 +8,7 @@ use PhpSchool\PhpWorkshop\Event\EventInterface; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; +use PhpSchool\PhpWorkshop\Utils\Path; use Psr\Log\LoggerInterface; use RuntimeException; @@ -53,11 +54,14 @@ public function __construct(CodePatcher $codePatcher, LoggerInterface $logger, b */ public function patch(ExerciseRunnerEvent $event): void { - $files = [$event->getInput()->getArgument('program')]; + $files = [$event->getContext()->getEntryPoint()]; $exercise = $event->getExercise(); if ($exercise instanceof ProvidesSolution) { - $files[] = $exercise->getSolution()->getEntryPoint()->getAbsolutePath(); + $files[] = Path::join( + $event->getContext()->getReferenceExecutionDirectory(), + $exercise->getSolution()->getEntryPoint()->getRelativePath() + ); } foreach (array_filter($files) as $fileName) { @@ -83,7 +87,7 @@ public function revert(EventInterface $event): void //if we're in debug mode leave the students patch for debugging if ($event instanceof ExerciseRunnerEvent && $this->debugMode) { - unset($this->originalCode[$event->getInput()->getArgument('program')]); + unset($this->originalCode[$event->getContext()->getEntryPoint()]); } foreach ($this->originalCode as $fileName => $contents) { diff --git a/src/Listener/PrepareSolutionListener.php b/src/Listener/PrepareSolutionListener.php index ecac7e59..4528955e 100644 --- a/src/Listener/PrepareSolutionListener.php +++ b/src/Listener/PrepareSolutionListener.php @@ -7,24 +7,18 @@ use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\Exception\RuntimeException; -use Symfony\Component\Process\Process; +use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessInput; /** * Listener to install composer deps for an exercise solution */ class PrepareSolutionListener { - /** - * Locations for composer executable - * - * @var array - */ - private static $composerLocations = [ - 'composer', - 'composer.phar', - '/usr/local/bin/composer', - __DIR__ . '/../../vendor/bin/composer', - ]; + public function __construct(private ProcessFactory $processFactory) + { + $this->processFactory = $processFactory; + } /** * @param ExerciseRunnerEvent $event @@ -39,36 +33,22 @@ public function __invoke(ExerciseRunnerEvent $event): void $solution = $exercise->getSolution(); - if ($solution->hasComposerFile()) { - //prepare composer deps - //only install if composer.lock file not available - - if (!file_exists(sprintf('%s/vendor', $solution->getBaseDirectory()))) { - $process = new Process( - [self::locateComposer(), 'install', '--no-interaction'], - $solution->getBaseDirectory() - ); - - try { - $process->mustRun(); - } catch (\Symfony\Component\Process\Exception\RuntimeException $e) { - throw new RuntimeException('Composer dependencies could not be installed', 0, $e); - } - } + if (!$solution->hasComposerFile()) { + return; } - } - /** - * @return string - */ - public static function locateComposer(): string - { - foreach (self::$composerLocations as $location) { - if (file_exists($location) && is_executable($location)) { - return $location; + //prepare composer deps + //only install if vendor folder not available + if (!file_exists(sprintf('%s/vendor', $event->getContext()->getReferenceExecutionDirectory()))) { + $process = $this->processFactory->create( + new ProcessInput('composer', ['install', '--no-interaction'], $event->getContext()->getReferenceExecutionDirectory(), []) + ); + + try { + $process->mustRun(); + } catch (\Symfony\Component\Process\Exception\RuntimeException $e) { + throw new RuntimeException('Composer dependencies could not be installed', 0, $e); } } - - throw new RuntimeException('Composer could not be located on the system'); } } diff --git a/src/Listener/SelfCheckListener.php b/src/Listener/SelfCheckListener.php index b70b0951..fc1ca971 100644 --- a/src/Listener/SelfCheckListener.php +++ b/src/Listener/SelfCheckListener.php @@ -5,6 +5,7 @@ namespace PhpSchool\PhpWorkshop\Listener; use PhpSchool\PhpWorkshop\Event\Event; +use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\ResultAggregator; @@ -14,30 +15,18 @@ */ class SelfCheckListener { - /** - * @var ResultAggregator - */ - private $results; - - /** - * @param ResultAggregator $results - */ - public function __construct(ResultAggregator $results) + public function __construct(private ResultAggregator $results) { - $this->results = $results; } - /** - * @param Event $event - */ - public function __invoke(Event $event): void + public function __invoke(ExerciseRunnerEvent $event): void { - $exercise = $event->getParameter('exercise'); + $exercise = $event->getContext()->getExercise(); if ($exercise instanceof SelfCheck) { /** @var Input $input */ $input = $event->getParameter('input'); - $this->results->add($exercise->check($input)); + $this->results->add($exercise->check($event->getContext())); } } } diff --git a/src/TestUtils/WorkshopExerciseTest.php b/src/TestUtils/WorkshopExerciseTest.php index b98a0930..db3c2823 100644 --- a/src/TestUtils/WorkshopExerciseTest.php +++ b/src/TestUtils/WorkshopExerciseTest.php @@ -29,6 +29,7 @@ use Psr\Container\ContainerInterface; use PhpSchool\PhpWorkshop\Input\Input; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; use function PhpSchool\PhpWorkshop\collect; @@ -114,8 +115,11 @@ public function runExercise(string $submissionFile): void private function installDeps(ExerciseInterface $exercise, string $directory): void { if (file_exists("$directory/composer.json") && !file_exists("$directory/vendor")) { + $execFinder = new ExecutableFinder(); + $execFinder->addSuffix('.phar'); + $process = new Process( - [PrepareSolutionListener::locateComposer(), 'install', '--no-interaction'], + [$execFinder->find('composer'), 'install', '--no-interaction'], $directory ); $process->mustRun(); diff --git a/test/Event/CgiExecuteEventTest.php b/test/Event/CgiExecuteEventTest.php index a885716b..53ca8450 100644 --- a/test/Event/CgiExecuteEventTest.php +++ b/test/Event/CgiExecuteEventTest.php @@ -5,6 +5,9 @@ use GuzzleHttp\Psr7\Request; use PhpSchool\PhpWorkshop\Event\CgiExecuteEvent; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -13,8 +16,11 @@ class CgiExecuteEventTest extends TestCase { public function testAddHeader(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $e->addHeaderToRequest('Content-Type', 'text/html'); $this->assertSame( @@ -29,8 +35,11 @@ public function testAddHeader(): void public function testModifyRequest(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $e->modifyRequest(function (RequestInterface $request) { return $request @@ -48,11 +57,16 @@ public function testModifyRequest(): void $this->assertNotSame($request, $e->getRequest()); } - public function testGetRequest(): void + public function testGetters(): void { + $context = new TestContext(); + $scenario = new CgiScenario(); + $request = new Request('GET', 'https://some.site'); - $e = new CgiExecuteEvent('event', new MockExercise(), new Input('test'), $request); + $e = new CgiExecuteEvent('event', $context, $scenario, $request); $this->assertSame($request, $e->getRequest()); + $this->assertSame($context, $e->getContext()); + $this->assertSame($scenario, $e->getScenario()); } } diff --git a/test/Event/CgiExerciseRunnerEventTest.php b/test/Event/CgiExerciseRunnerEventTest.php index 8f326dfb..2e8caa72 100644 --- a/test/Event/CgiExerciseRunnerEventTest.php +++ b/test/Event/CgiExerciseRunnerEventTest.php @@ -4,6 +4,8 @@ use PhpSchool\PhpWorkshop\Event\CgiExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; @@ -12,16 +14,18 @@ class CgiExerciseRunnerEventTest extends TestCase { public function testGetters(): void { - $exercise = new MockExercise(); - $input = new Input('app'); + $context = new TestContext(); + $scenario = new CgiScenario(); - $event = new CgiExerciseRunnerEvent('Some Event', $exercise, $input, ['number' => 1]); - self::assertSame($exercise, $event->getExercise()); - self::assertSame($input, $event->getInput()); + $event = new CgiExerciseRunnerEvent('Some Event', $context, $scenario, ['number' => 1]); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); + $this->assertSame($context, $event->getContext()); + $this->assertSame($scenario, $event->getScenario()); self::assertEquals( [ - 'exercise' => $exercise, - 'input' => $input, + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), 'number' => 1 ], $event->getParameters() diff --git a/test/Event/CliExecuteEventTest.php b/test/Event/CliExecuteEventTest.php index 63712642..59bf9de3 100644 --- a/test/Event/CliExecuteEventTest.php +++ b/test/Event/CliExecuteEventTest.php @@ -4,16 +4,23 @@ use PhpSchool\PhpWorkshop\Event\CliExecuteEvent; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Utils\ArrayObject; +use PhpSchool\PhpWorkshop\Utils\Collection; use PHPUnit\Framework\TestCase; class CliExecuteEventTest extends TestCase { public function testAppendArg(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $e->appendArg('4'); $this->assertEquals([1, 2, 3, 4], $e->getArgs()->getArrayCopy()); @@ -22,19 +29,27 @@ public function testAppendArg(): void public function testPrependArg(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $e->prependArg('4'); $this->assertEquals([4, 1, 2, 3], $e->getArgs()->getArrayCopy()); $this->assertNotSame($arr, $e->getArgs()); } - public function testGetArgs(): void + public function testGetters(): void { - $arr = new ArrayObject([1, 2, 3]); - $e = new CliExecuteEvent('event', new MockExercise(), new Input('test'), $arr); + $context = new TestContext(); + $scenario = new CliScenario(); + + $arr = new Collection([1, 2, 3]); + $e = new CliExecuteEvent('event', $context, $scenario, $arr); $this->assertSame($arr, $e->getArgs()); + $this->assertSame($context, $e->getContext()); + $this->assertSame($scenario, $e->getScenario()); } } diff --git a/test/Event/CliExerciseRunnerEventTest.php b/test/Event/CliExerciseRunnerEventTest.php index 0c122586..d3f7c706 100644 --- a/test/Event/CliExerciseRunnerEventTest.php +++ b/test/Event/CliExerciseRunnerEventTest.php @@ -4,6 +4,9 @@ use PhpSchool\PhpWorkshop\Event\CliExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\MockExercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; @@ -12,16 +15,18 @@ class CliExerciseRunnerEventTest extends TestCase { public function testGetters(): void { - $exercise = new MockExercise(); - $input = new Input('app'); + $context = new TestContext(); + $scenario = new CliScenario(); - $event = new CliExerciseRunnerEvent('Some Event', $exercise, $input, ['number' => 1]); - self::assertSame($exercise, $event->getExercise()); - self::assertSame($input, $event->getInput()); + $event = new CliExerciseRunnerEvent('Some Event', $context, $scenario, ['number' => 1]); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); + $this->assertSame($context, $event->getContext()); + $this->assertSame($scenario, $event->getScenario()); self::assertEquals( [ - 'exercise' => $exercise, - 'input' => $input, + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), 'number' => 1 ], $event->getParameters() diff --git a/test/Event/ExerciseRunnerEventTest.php b/test/Event/ExerciseRunnerEventTest.php index baa7f4f4..4095b769 100644 --- a/test/Event/ExerciseRunnerEventTest.php +++ b/test/Event/ExerciseRunnerEventTest.php @@ -3,6 +3,8 @@ namespace PhpSchool\PhpWorkshopTest\Event; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; @@ -11,16 +13,16 @@ class ExerciseRunnerEventTest extends TestCase { public function testGetters(): void { - $exercise = new CliExerciseImpl(); - $input = new Input('app'); + $context = new TestContext(); - $event = new ExerciseRunnerEvent('Some Event', $exercise, $input, ['number' => 1]); - self::assertSame($exercise, $event->getExercise()); - self::assertSame($input, $event->getInput()); + $event = new ExerciseRunnerEvent('Some Event', $context, ['number' => 1]); + self::assertSame($context, $event->getContext()); + self::assertSame($context->getExercise(), $event->getExercise()); + self::assertSame($context->getInput(), $event->getInput()); self::assertEquals( [ - 'exercise' => $exercise, - 'input' => $input, + 'exercise' => $context->getExercise(), + 'input' => $context->getInput(), 'number' => 1 ], $event->getParameters() diff --git a/test/ExerciseRunner/EnvironmentManagerTest.php b/test/ExerciseRunner/EnvironmentManagerTest.php index 8ab31e09..71aa37f9 100644 --- a/test/ExerciseRunner/EnvironmentManagerTest.php +++ b/test/ExerciseRunner/EnvironmentManagerTest.php @@ -78,7 +78,7 @@ public function testFileAreCleanedUpOnlyWhenFinishEventIsDispatched(string $even static::assertFileExists($context->getStudentExecutionDirectory()); static::assertFileExists($context->getReferenceExecutionDirectory()); - $eventDispatcher->dispatch(new ExerciseRunnerEvent($eventName, $exercise, new Input('app', ['program' => '']))); + $eventDispatcher->dispatch(new ExerciseRunnerEvent($eventName, $context)); static::assertFileExists($context->getStudentExecutionDirectory()); static::assertFileNotExists($context->getReferenceExecutionDirectory() . '/file.txt'); diff --git a/test/Listener/CodePatchListenerTest.php b/test/Listener/CodePatchListenerTest.php index 503ef535..5374a2ba 100644 --- a/test/Listener/CodePatchListenerTest.php +++ b/test/Listener/CodePatchListenerTest.php @@ -5,8 +5,10 @@ use PhpSchool\PhpWorkshop\CodePatcher; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Listener\CodePatchListener; +use PhpSchool\PhpWorkshop\Utils\Path; use PhpSchool\PhpWorkshop\Utils\System; use PhpSchool\PhpWorkshopTest\Asset\ProvidesSolutionExercise; use PHPUnit\Framework\TestCase; @@ -16,21 +18,6 @@ class CodePatchListenerTest extends TestCase { - /** - * @var string - */ - private $file; - - /** - * @var string - */ - private $solution; - - /** - * @var Filesystem - */ - private $filesystem; - /** * @var CodePatcher */ @@ -38,29 +25,19 @@ class CodePatchListenerTest extends TestCase public function setUp(): void { - $this->filesystem = new Filesystem(); $this->codePatcher = $this->createMock(CodePatcher::class); - - $this->file = sprintf('%s/%s/submission.php', System::tempDir(), $this->getName()); - mkdir(dirname($this->file), 0775, true); - touch($this->file); - - $this->solution = sprintf('%s/%s/solution.php', System::tempDir(), $this->getName()); - touch($this->solution); - } - - public function tearDown(): void - { - $this->filesystem->remove(dirname($this->file)); } public function testPatchUpdatesCode(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -68,19 +45,25 @@ public function testPatchUpdatesCode(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT' + ); } public function testRevertAfterPatch(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -88,20 +71,26 @@ public function testRevertAfterPatch(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); $listener->revert($event); - self::assertStringEqualsFile($this->file, 'ORIGINAL CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'ORIGINAL CONTENT' + ); } public function testPatchesProvidedSolution(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = new ProvidesSolutionExercise(); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $context->importReferenceSolution(); + $this->codePatcher ->expects($this->exactly(2)) ->method('patch') @@ -109,43 +98,57 @@ public function testPatchesProvidedSolution(): void ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); - self::assertStringEqualsFile($exercise->getSolution()->getEntryPoint()->getAbsolutePath(), 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT' + ); + self::assertStringEqualsFile( + Path::join( + $context->getReferenceExecutionDirectory(), + $exercise->getSolution()->getEntryPoint()->getRelativePath(), + ), + 'MODIFIED CONTENT' + ); } public function testFileIsLoggedWhenPatches(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $this->codePatcher ->expects($this->once()) ->method('patch') ->with($exercise, 'ORIGINAL CONTENT') ->willReturn('MODIFIED CONTENT'); + $path = Path::join($context->getStudentExecutionDirectory(), 'solution.php'); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('debug') - ->with('Patching file: ' . $this->file); + ->with('Patching file: ' . $path); $listener = new CodePatchListener($this->codePatcher, $logger, false); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); } public function testRevertDoesNotRevertStudentSubmissionPatchIfInDebugMode(): void { - file_put_contents($this->file, 'ORIGINAL CONTENT'); - - $input = new Input('app', ['program' => $this->file]); $exercise = $this->createMock(ExerciseInterface::class); + $context = new TestContext($exercise); + $context->createStudentSolutionDirectory(); + $context->createReferenceSolutionDirectory(); + $context->importStudentFileFromString('ORIGINAL CONTENT'); + $this->codePatcher ->expects($this->once()) ->method('patch') @@ -153,10 +156,13 @@ public function testRevertDoesNotRevertStudentSubmissionPatchIfInDebugMode(): vo ->willReturn('MODIFIED CONTENT'); $listener = new CodePatchListener($this->codePatcher, new NullLogger(), true); - $event = new ExerciseRunnerEvent('event', $exercise, $input); + $event = new ExerciseRunnerEvent('event', $context); $listener->patch($event); $listener->revert($event); - self::assertStringEqualsFile($this->file, 'MODIFIED CONTENT'); + self::assertStringEqualsFile( + Path::join($context->getStudentExecutionDirectory(), 'solution.php'), + 'MODIFIED CONTENT' + ); } } diff --git a/test/Listener/PrepareSolutionListenerTest.php b/test/Listener/PrepareSolutionListenerTest.php index a0a0552d..c497cd9a 100644 --- a/test/Listener/PrepareSolutionListenerTest.php +++ b/test/Listener/PrepareSolutionListenerTest.php @@ -5,164 +5,131 @@ use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\CliExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener; +use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\ProcessNotFoundException; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; -use PhpSchool\PhpWorkshopTest\Asset\CliExerciseInterface; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; use ReflectionProperty; use RuntimeException; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\ExecutableFinder; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; class PrepareSolutionListenerTest extends TestCase { use AssertionRenames; - /** - * @var string - */ - private $file; - - /** - * @var PrepareSolutionListener - */ - private $listener; - - /** - * @var Filesystem - */ - private $filesystem; - - public function setUp(): void - { - $this->filesystem = new Filesystem(); - $this->listener = new PrepareSolutionListener(); - $this->file = sprintf('%s/%s/submission.php', str_replace('\\', '/', sys_get_temp_dir()), $this->getName()); - - mkdir(dirname($this->file), 0775, true); - touch($this->file); - } - - /** - * @runInSeparateProcess - */ public function testIfSolutionRequiresComposerButComposerCannotBeLocatedExceptionIsThrown(): void { - $refProp = new ReflectionProperty(PrepareSolutionListener::class, 'composerLocations'); - $refProp->setAccessible(true); - $refProp->setValue($this->listener, []); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); - $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + $finder = $this->createMock(ExecutableFinder::class); + $finder->expects($this->once())->method('find')->with('composer')->willReturn(null); + $solution = $this->createMock(SolutionInterface::class); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Composer could not be located on the system'); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $exercise->setSolution($solution); + + $this->expectException(ProcessNotFoundException::class); + $this->expectExceptionMessage('Could not find executable: "composer"'); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory($finder)))->__invoke($event); } public function testIfSolutionRequiresComposerButVendorDirExistsNothingIsDone(): void { - mkdir(sprintf('%s/vendor', dirname($this->file))); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $context->createReferenceSolutionDirectory(); - $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + mkdir(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); + $solution = $this->createMock(SolutionInterface::class); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); + $exercise->setSolution($solution); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); //check for non existence of lock file, composer generates this when updating if it doesn't exist - $this->assertFileDoesNotExist(sprintf('%s/composer.lock', dirname($this->file))); + $this->assertFileDoesNotExist(sprintf('%s/composer.lock', $context->getReferenceExecutionDirectory())); } public function testIfSolutionRequiresComposerComposerInstallIsExecuted(): void { - $this->assertFileDoesNotExist(sprintf('%s/vendor', dirname($this->file))); - file_put_contents(sprintf('%s/composer.json', dirname($this->file)), json_encode([ - 'require' => [ - 'phpunit/phpunit' => '~5.0' - ], - ])); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $context->createReferenceSolutionDirectory(); + $context->importReferenceFileFromString( + json_encode([ + 'require' => [ + 'phpunit/phpunit' => '~5.0' + ], + ]), + 'composer.json' + ); $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); - $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); + $exercise->setSolution($solution); - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } public function testExceptionIsThrownIfDependenciesCannotBeResolved(): void { + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + $this->expectException(\PhpSchool\PhpWorkshop\Exception\RuntimeException::class); $this->expectExceptionMessage('Composer dependencies could not be installed'); - $this->assertFileDoesNotExist(sprintf('%s/vendor', dirname($this->file))); - file_put_contents(sprintf('%s/composer.json', dirname($this->file)), json_encode([ - 'require' => [ - 'phpunit/phpunit' => '1.0' - ], - ])); + $exercise = new CliExerciseImpl(); + $context = new TestContext($exercise); + + $context->createReferenceSolutionDirectory(); + $context->importReferenceFileFromString( + json_encode([ + 'require' => [ + 'phpunit/phpunit' => '1.0' + ], + ]), + 'composer.json' + ); $solution = $this->createMock(SolutionInterface::class); - $exercise = $this->createMock(CliExerciseInterface::class); - $exercise - ->method('getSolution') - ->willReturn($solution); + $exercise->setSolution($solution); $solution ->expects($this->once()) ->method('hasComposerFile') ->willReturn(true); - $solution - ->method('getBaseDirectory') - ->willReturn(dirname($this->file)); - - $event = new ExerciseRunnerEvent('event', $exercise, new Input('app')); - $this->listener->__invoke($event); + $event = new ExerciseRunnerEvent('event', $context); + (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); - $this->assertFileExists(sprintf('%s/vendor', dirname($this->file))); - } - - public function tearDown(): void - { - $this->filesystem->remove(dirname($this->file)); + $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } } diff --git a/test/Listener/RealPathListenerTest.php b/test/Listener/RealPathListenerTest.php index 255d5e24..a22ce8d0 100644 --- a/test/Listener/RealPathListenerTest.php +++ b/test/Listener/RealPathListenerTest.php @@ -4,31 +4,33 @@ use PhpSchool\PhpWorkshop\Event\Event; use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Listener\RealPathListener; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; +use PhpSchool\PhpWorkshopTest\BaseTest; use PHPUnit\Framework\TestCase; -class RealPathListenerTest extends TestCase +class RealPathListenerTest extends BaseTest { public function testInputArgumentIsReplacesWithAbsolutePathIfFileExists(): void { - $current = getcwd(); - - $tempDirectory = sprintf('%s/%s', realpath(sys_get_temp_dir()), $this->getName()); - mkdir($tempDirectory, 0777, true); - chdir($tempDirectory); - touch('test-file.php'); - $exercise = new CliExerciseImpl(); $input = new Input('app', ['program' => 'test-file.php']); $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); - $this->assertEquals(sprintf('%s/test-file.php', $tempDirectory), $input->getArgument('program')); + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + $context->importStudentFileFromString('', 'test-file.php'); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); + + $this->assertEquals(sprintf('%s/test-file.php', $context->getStudentExecutionDirectory()), $input->getArgument('program')); - unlink('test-file.php'); - rmdir($tempDirectory); chdir($current); } @@ -37,18 +39,37 @@ public function testInputArgumentIsLeftUnchangedIfFileDoesNotExist(): void $exercise = new CliExerciseImpl(); $input = new Input('app', ['program' => 'test-file.php']); $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); + + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); $this->assertEquals('test-file.php', $input->getArgument('program')); + + chdir($current); } public function testInputIsUnchangedIfNoProgramArgument(): void { $exercise = new CliExerciseImpl(); $input = new Input('app', ['some-arg' => 'some-value']); + $listener = new RealPathListener(); - $listener->__invoke(new ExerciseRunnerEvent('some.event', $exercise, $input)); + + $context = new TestContext(input: $input); + $context->createStudentSolutionDirectory(); + + $current = getcwd(); + chdir($context->getStudentExecutionDirectory()); + + $listener->__invoke(new ExerciseRunnerEvent('some.event', $context)); $this->assertEquals('some-value', $input->getArgument('some-arg')); + + chdir($current); } } diff --git a/test/Listener/SelfCheckListenerTest.php b/test/Listener/SelfCheckListenerTest.php index d27cee7c..0742d0d1 100644 --- a/test/Listener/SelfCheckListenerTest.php +++ b/test/Listener/SelfCheckListenerTest.php @@ -2,8 +2,9 @@ namespace PhpSchool\PhpWorkshopTest\Listener; -use PhpSchool\PhpWorkshop\Event\Event; +use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Listener\SelfCheckListener; use PhpSchool\PhpWorkshop\Result\Success; @@ -16,14 +17,13 @@ class SelfCheckListenerTest extends TestCase public function testSelfCheck(): void { $exercise = $this->createMock(SelfCheckExerciseInterface::class); - $input = new Input('app', ['program' => 'some-file.php']); - $event = new Event('event', compact('exercise', 'input')); + $context = new TestContext($exercise); + $event = new ExerciseRunnerEvent('event', $context); $success = new Success('Success'); $exercise ->expects($this->once()) ->method('check') - ->with($input) ->willReturn($success); $results = new ResultAggregator(); @@ -37,8 +37,8 @@ public function testSelfCheck(): void public function testExerciseWithOutSelfCheck(): void { $exercise = $this->createMock(ExerciseInterface::class); - $input = new Input('app', ['program' => 'some-file.php']); - $event = new Event('event', compact('exercise', 'input')); + $context = new TestContext($exercise); + $event = new ExerciseRunnerEvent('event', $context); $results = new ResultAggregator(); $listener = new SelfCheckListener($results);