diff --git a/phpstan.neon b/phpstan.neon index 71ba3712..2849b8d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,6 @@ parameters: treatPhpDocTypesAsCertain: false ignoreErrors: - - - message: '#PhpSchool\\PhpWorkshop\\ExerciseRunner\\CliRunner\:\:preserveOldArgFormat\(\) should return#' - path: src/ExerciseRunner/CliRunner.php - message: '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:__toString\(\)#' path: src/Check/FunctionRequirementsCheck.php diff --git a/src/Exercise/CgiExercise.php b/src/Exercise/CgiExercise.php index 1244712a..6f556af9 100644 --- a/src/Exercise/CgiExercise.php +++ b/src/Exercise/CgiExercise.php @@ -4,6 +4,7 @@ namespace PhpSchool\PhpWorkshop\Exercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; use Psr\Http\Message\RequestInterface; /** @@ -12,10 +13,15 @@ interface CgiExercise extends ProvidesSolution { /** - * This method should return an array of PSR-7 requests, which will be forwarded to the student's - * solution. + * This method should return an instance of CgiScenario which contains PSR-7 requests, + * which will be forwarded to the student's solution. * - * @return array An array of PSR-7 requests. + * Use like so: + * + * ``` + * return (new CgiScenario()) + * ->withExecution($request1) + * ``` */ - public function getRequests(): array; + public function defineTestScenario(): CgiScenario; } diff --git a/src/Exercise/CliExercise.php b/src/Exercise/CliExercise.php index 80a287f9..05449b10 100644 --- a/src/Exercise/CliExercise.php +++ b/src/Exercise/CliExercise.php @@ -4,16 +4,24 @@ namespace PhpSchool\PhpWorkshop\Exercise; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; + /** * This interface describes the additional methods a CLI type exercise should implement. */ interface CliExercise extends ProvidesSolution { /** - * This method should return an array of an array of strings. - * Each set of arguments will be passed to the students solution as command line arguments. + * This method should return an instance of CliScenario which contains sets of arguments, + * which will be passed to the students solution as command line arguments. + * + * Use like so: * - * @return array> An array of string arguments. + * ``` + * return (new CliScenario()) + * ->withExecution(['arg1', 'arg2']) + * ->withExecution(['round2-arg1', 'round2-arg2']) + * ``` */ - public function getArgs(): array; + public function defineTestScenario(): CliScenario; } diff --git a/src/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index bd5f456b..288a692d 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -104,13 +104,16 @@ public function getRequiredChecks(): array */ public function verify(Input $input): ResultInterface { + $scenario = $this->exercise->defineTestScenario(); + $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() + $scenario->getExecutions() ) ); $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.finish', $this->exercise, $input)); @@ -283,9 +286,12 @@ private function getPhpProcess(string $workingDirectory, string $fileName, Reque */ public function run(Input $input, OutputInterface $output): bool { + $scenario = $this->exercise->defineTestScenario(); + $this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $this->exercise, $input)); + $success = true; - foreach ($this->exercise->getRequests() as $i => $request) { + foreach ($scenario->getExecutions() as $i => $request) { /** @var CgiExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( new CgiExecuteEvent('cgi.run.student-execute.pre', $this->exercise, $input, $request) diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index 3cd71760..3a7b23a1 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -101,13 +101,15 @@ public function getRequiredChecks(): array */ public function verify(Input $input): ResultInterface { + $scenario = $this->exercise->defineTestScenario(); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.start', $this->exercise, $input)); $result = new CliResult( array_map( - function (array $args) use ($input) { - return $this->doVerify($args, $input); + function (Collection $args) use ($input) { + return $this->doVerify($input, $args); }, - $this->preserveOldArgFormat($this->exercise->getArgs()) + $scenario->getExecutions() ) ); $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.finish', $this->exercise, $input)); @@ -115,32 +117,12 @@ function (array $args) use ($input) { } /** - * BC - getArgs only returned 1 set of args in v1 instead of multiple sets of args in v2 - * - * @param array>|array $args - * @return array> - */ - private function preserveOldArgFormat(array $args): array - { - if (isset($args[0]) && !is_array($args[0])) { - $args = [$args]; - } elseif (count($args) === 0) { - $args = [[]]; - } - - return $args; - } - - /** - * @param array $args * @param Input $input + * @param Collection $args * @return CliResultInterface */ - private function doVerify(array $args, Input $input): CliResultInterface + private function doVerify(Input $input, Collection $args): CliResultInterface { - //arrays are not pass-by-ref - $args = new ArrayObject($args); - try { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( @@ -213,12 +195,15 @@ private function doVerify(array $args, Input $input): CliResultInterface */ public function run(Input $input, OutputInterface $output): bool { + $scenario = $this->exercise->defineTestScenario(); + $this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $this->exercise, $input)); + $success = true; - foreach ($this->preserveOldArgFormat($this->exercise->getArgs()) as $i => $args) { + foreach ($scenario->getExecutions() as $i => $args) { /** @var CliExecuteEvent $event */ $event = $this->eventDispatcher->dispatch( - new CliExecuteEvent('cli.run.student-execute.pre', $this->exercise, $input, new ArrayObject($args)) + new CliExecuteEvent('cli.run.student-execute.pre', $this->exercise, $input, $args) ); $args = $event->getArgs(); diff --git a/test/Asset/CgiExerciseImpl.php b/test/Asset/CgiExerciseImpl.php index d8c23c96..43ebda58 100644 --- a/test/Asset/CgiExerciseImpl.php +++ b/test/Asset/CgiExerciseImpl.php @@ -2,25 +2,25 @@ namespace PhpSchool\PhpWorkshopTest\Asset; -use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\CgiExercise; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; use Psr\Http\Message\RequestInterface; class CgiExerciseImpl implements ExerciseInterface, CgiExercise { - /** - * @var string - */ - private $name; + private string $name; + private SolutionInterface $solution; + private CgiScenario $scenario; public function __construct(string $name = 'my-exercise') { $this->name = $name; + $this->scenario = new CgiScenario(); } public function getName(): string @@ -33,9 +33,14 @@ public function getDescription(): string return $this->name; } + public function setSolution(SolutionInterface $solution): void + { + $this->solution = $solution; + } + public function getSolution(): SolutionInterface { - // TODO: Implement getSolution() method. + return $this->solution; } public function getProblem(): string @@ -48,17 +53,6 @@ public function tearDown(): void // TODO: Implement tearDown() method. } - /** - * This method should return an array of PSR-7 requests, which will be forwarded to the student's - * solution. - * - * @return RequestInterface[] An array of PSR-7 requests. - */ - public function getRequests(): array - { - return []; // TODO: Implement getRequests() method. - } - public function getType(): ExerciseType { return ExerciseType::CGI(); @@ -72,4 +66,14 @@ public function getRequiredChecks(): array public function defineListeners(EventDispatcher $dispatcher): void { } + + public function setScenario(CgiScenario $scenario): void + { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CgiScenario + { + return $this->scenario; + } } diff --git a/test/Asset/CliExerciseImpl.php b/test/Asset/CliExerciseImpl.php index 0fcc9e32..cdd627cd 100644 --- a/test/Asset/CliExerciseImpl.php +++ b/test/Asset/CliExerciseImpl.php @@ -2,30 +2,25 @@ 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; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; class CliExerciseImpl implements ExerciseInterface, CliExercise { - /** - * @var string - */ - private $name; - - /** - * @var SolutionInterface - */ - private $solution; + private string $name; + private SolutionInterface $solution; + private CliScenario $scenario; public function __construct(string $name = 'my-exercise') { $this->name = $name; + $this->scenario = new CliScenario(); } public function getName(): string @@ -58,9 +53,14 @@ public function tearDown(): void // TODO: Implement tearDown() method. } - public function getArgs(): array + public function setScenario(CliScenario $scenario): void + { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CliScenario { - return []; // TODO: Implement getArgs() method. + return $this->scenario; } public function getType(): ExerciseType diff --git a/test/Asset/DatabaseExercise.php b/test/Asset/DatabaseExercise.php new file mode 100644 index 00000000..d0ad538e --- /dev/null +++ b/test/Asset/DatabaseExercise.php @@ -0,0 +1,110 @@ +scenario = new CliScenario(); + } + + public function setSeeder(\Closure $seeder): void + { + $this->seeder = $seeder; + } + + public function seed(PDO $db): void + { + $seeder = $this->seeder; + if ($seeder) { + $seeder($db); + } + } + + public function setVerifier(\Closure $verifier): void + { + $this->verifier = $verifier; + } + + public function verify(PDO $db): bool + { + $verifier = $this->verifier; + + if ($verifier) { + return $verifier($db); + } + + return true; + } + + public function getName(): string + { + // TODO: Implement getName() method. + } + + public function getType(): ExerciseType + { + return ExerciseType::CLI(); + } + + public function getProblem(): string + { + // TODO: Implement getProblem() method. + } + + public function defineListeners(EventDispatcher $dispatcher): void + { + // TODO: Implement defineListeners() method. + } + + public function getRequiredChecks(): array + { + return [DatabaseCheck::class]; + } + + public function getDescription(): string + { + // TODO: Implement getDescription() method. + } + + public function tearDown(): void + { + // TODO: Implement tearDown() method. + } + + public function setSolution(SolutionInterface $solution): void + { + $this->solution = $solution; + } + + public function getSolution(): SolutionInterface + { + return $this->solution; + } + + public function setScenario(CliScenario $scenario): void + { + $this->scenario = $scenario; + } + + public function defineTestScenario(): CliScenario + { + return $this->scenario; + } +} diff --git a/test/Check/DatabaseCheckTest.php b/test/Check/DatabaseCheckTest.php index 70a40256..35d5152b 100644 --- a/test/Check/DatabaseCheckTest.php +++ b/test/Check/DatabaseCheckTest.php @@ -9,6 +9,7 @@ use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface; use PhpSchool\PhpWorkshop\Exercise\ExerciseType; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; use PhpSchool\PhpWorkshop\ExerciseCheck\DatabaseExerciseCheck; use PhpSchool\PhpWorkshop\ExerciseDispatcher; use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; @@ -18,6 +19,7 @@ use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\ResultAggregator; use PhpSchool\PhpWorkshop\Solution\SingleFileSolution; +use PhpSchool\PhpWorkshopTest\Asset\DatabaseExercise; use PhpSchool\PhpWorkshopTest\Asset\DatabaseExerciseInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -55,8 +57,7 @@ public function setUp(): void $this->checkRepository = $container->get(CheckRepository::class); $this->check = new DatabaseCheck(); - $this->exercise = $this->createMock(DatabaseExerciseInterface::class); - $this->exercise->method('getType')->willReturn(ExerciseType::CLI()); + $this->exercise = new DatabaseExercise(); $this->dbDir = sprintf( '%s/php-school/PhpSchool_PhpWorkshop_Check_DatabaseCheck', str_replace('\\', '/', realpath(sys_get_temp_dir())) @@ -120,26 +121,9 @@ public function testIfPDOThrowsExceptionItCleansUp(): void //try to run the check as usual $this->check = new DatabaseCheck(); $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(true); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + $this->exercise->setVerifier(fn () => true); $this->checkRepository->registerCheck($this->check); @@ -159,26 +143,8 @@ public function testIfPDOThrowsExceptionItCleansUp(): void public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(true); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->checkRepository->registerCheck($this->check); @@ -199,11 +165,6 @@ public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void public function testRunExercise(): void { - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([]); - $this->checkRepository->registerCheck($this->check); $results = new ResultAggregator(); @@ -225,27 +186,9 @@ public function testRunExercise(): void public function testFailureIsReturnedIfDatabaseVerificationFails(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php')); - - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturn(false); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); + $this->exercise->setVerifier(fn () => false); $this->checkRepository->registerCheck($this->check); @@ -268,53 +211,32 @@ public function testFailureIsReturnedIfDatabaseVerificationFails(): void public function testAlteringDatabaseInSolutionDoesNotEffectDatabaseInUserSolution(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution-alter-db.php')); - - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->method('getArgs') - ->willReturn([]); - - $this->exercise - ->method('verify') - ->willReturn(true); - - $this->exercise - ->expects($this->once()) - ->method('getRequiredChecks') - ->willReturn([DatabaseCheck::class]); - - $this->exercise - ->expects($this->once()) - ->method('seed') - ->with($this->isInstanceOf(PDO::class)) - ->willReturnCallback(function (PDO $db) { - $db->exec( - 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)' - ); - $stmt = $db->prepare('INSERT into users (name, age, gender) VALUES (:name, :age, :gender)'); - $stmt->execute([':name' => 'Jimi Hendrix', ':age' => 27, ':gender' => 'Male']); - }); - - $this->exercise - ->expects($this->once()) - ->method('verify') - ->with($this->isInstanceOf(PDO::class)) - ->willReturnCallback(function (PDO $db) { - $users = $db->query('SELECT * FROM users'); - $users = $users->fetchAll(PDO::FETCH_ASSOC); - - $this->assertEquals( - [ - ['id' => 1, 'name' => 'Jimi Hendrix', 'age' => '27', 'gender' => 'Male'], - ['id' => 2, 'name' => 'Kurt Cobain', 'age' => '27', 'gender' => 'Male'], - ], - $users - ); - }); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); + + $this->exercise->setVerifier(function (PDO $db) { + $users = $db->query('SELECT * FROM users'); + $users = $users->fetchAll(PDO::FETCH_ASSOC); + + $this->assertCount(2, $users); + $this->assertEquals( + [ + ['id' => 1, 'name' => 'Jimi Hendrix', 'age' => '27', 'gender' => 'Male'], + ['id' => 2, 'name' => 'Kurt Cobain', 'age' => '27', 'gender' => 'Male'], + ], + $users + ); + + return true; + }); + + $this->exercise->setSeeder(function (PDO $db) { + $db->exec( + 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)' + ); + $stmt = $db->prepare('INSERT into users (name, age, gender) VALUES (:name, :age, :gender)'); + $stmt->execute([':name' => 'Jimi Hendrix', ':age' => 27, ':gender' => 'Male']); + }); $this->checkRepository->registerCheck($this->check); diff --git a/test/ExerciseRunner/CgiRunnerTest.php b/test/ExerciseRunner/CgiRunnerTest.php index aade9085..28298e67 100644 --- a/test/ExerciseRunner/CgiRunnerTest.php +++ b/test/ExerciseRunner/CgiRunnerTest.php @@ -5,8 +5,11 @@ use Colors\Color; use GuzzleHttp\Psr7\Request; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseImpl; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; @@ -33,22 +36,15 @@ class CgiRunnerTest extends TestCase use AssertionRenames; private CgiRunner $runner; - /** - * @var CgiExerciseInterface&MockObject - */ - private $exercise; + private CgiExerciseImpl $exercise; private EventDispatcher $eventDispatcher; public function setUp(): void { - $this->exercise = $this->createMock(CgiExerciseInterface::class); + $this->exercise = new CgiExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory()); - $this->exercise - ->method('getType') - ->willReturn(ExerciseType::CGI()); - $this->assertEquals('CGI Program Runner', $this->runner->getName()); } @@ -67,17 +63,10 @@ public function testRequiredChecks(): void public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/solution-error.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $request = (new Request('GET', 'http://some.site?number=5')); - - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario( + (new CgiScenario())->withExecution((new Request('GET', 'http://some.site?number=5'))) + ); $regex = "/^PHP Code failed to execute\. Error: \"PHP Parse error: syntax error, unexpected end of file in/"; $this->expectException(SolutionExecutionException::class); @@ -88,18 +77,13 @@ public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void public function testVerifyReturnsSuccessIfGetSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); + $this->exercise->setScenario( + (new CgiScenario())->withExecution((new Request('GET', 'http://some.site?number=5'))) + ); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); - $this->assertInstanceOf( CgiResult::class, $this->runner->verify(new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-solution.php')])) @@ -109,20 +93,14 @@ public function testVerifyReturnsSuccessIfGetSolutionOutputMatchesUserOutput(): public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/post-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('POST', 'http://some.site')) ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write('number=5'); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); $this->assertInstanceOf( CgiResult::class, @@ -137,20 +115,14 @@ public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutput(): public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutputWithMultipleParams(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/post-multiple-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('POST', 'http://some.site')) ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); $request->getBody()->write('number=5&start=4'); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); $result = $this->runner->verify( new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/post-multiple-solution.php')]) @@ -162,17 +134,11 @@ public function testVerifyReturnsSuccessIfPostSolutionOutputMatchesUserOutputWit public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); $failure = $this->runner->verify( new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/user-error.php')]) @@ -192,17 +158,11 @@ public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); $failure = $this->runner->verify( new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-user-wrong.php')]) @@ -222,17 +182,11 @@ public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput() public function testVerifyReturnsFailureIfSolutionOutputHeadersDoesNotMatchUserOutputHeaders(): void { $solution = SingleFileSolution::fromFile(__DIR__ . '/../res/cgi/get-solution-header.php'); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); + $this->exercise->setSolution($solution); $request = (new Request('GET', 'http://some.site?number=5')); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request)); $failure = $this->runner->verify( new Input('app', ['program' => realpath(__DIR__ . '/../res/cgi/get-user-header-wrong.php')]) @@ -274,10 +228,9 @@ public function testRunPassesOutputAndReturnsSuccessIfAllRequestsAreSuccessful() new OutputRunInfoListener($output, new RequestRenderer()) ); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request1, $request2]); + $this->exercise->setScenario( + (new CgiScenario())->withExecution($request1)->withExecution($request2) + ); $exp = "\n\e[1m\e[4mRequest"; $exp .= "\e[0m\e[0m\n\n"; @@ -322,10 +275,7 @@ public function testRunPassesOutputAndReturnsFailureIfARequestFails(): void new OutputRunInfoListener($output, new RequestRenderer()) ); - $this->exercise - ->expects($this->once()) - ->method('getRequests') - ->willReturn([$request1]); + $this->exercise->setScenario((new CgiScenario())->withExecution($request1)); $exp = "\n\e[1m\e[4mRequest"; $exp .= "\e[0m\e[0m\n\n"; diff --git a/test/ExerciseRunner/CliRunnerTest.php b/test/ExerciseRunner/CliRunnerTest.php index ecbf0bc6..e9af3f3d 100644 --- a/test/ExerciseRunner/CliRunnerTest.php +++ b/test/ExerciseRunner/CliRunnerTest.php @@ -4,9 +4,12 @@ use Colors\Color; use PhpSchool\PhpWorkshop\Check\CodeExistsCheck; +use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario; +use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; use PhpSchool\PhpWorkshop\Process\HostProcessFactory; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; +use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; use PhpSchool\PhpWorkshop\Check\FileExistsCheck; @@ -33,22 +36,15 @@ class CliRunnerTest extends TestCase use AssertionRenames; private CliRunner $runner; - /** - * @var CliExerciseInterface&MockObject - */ - private CliExerciseInterface $exercise; + private CliExerciseImpl $exercise; private EventDispatcher $eventDispatcher; public function setUp(): void { - $this->exercise = $this->createMock(CliExerciseInterface::class); + $this->exercise = new CliExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); $this->runner = new CliRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory()); - $this->exercise - ->method('getType') - ->willReturn(ExerciseType::CLI()); - $this->assertEquals('CLI Program Runner', $this->runner->getName()); } @@ -67,15 +63,9 @@ public function testRequiredChecks(): void public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution-error.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); $regex = "/^PHP Code failed to execute\\. Error: \"PHP Parse error: syntax error, unexpected end of file"; $regex .= ", expecting ['\"][,;]['\"] or ['\"][;,]['\"]/"; @@ -87,15 +77,8 @@ public function testVerifyThrowsExceptionIfSolutionFailsExecution(): void public function testVerifyReturnsSuccessIfSolutionOutputMatchesUserOutput(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->assertInstanceOf( CliResult::class, @@ -108,15 +91,8 @@ public function testVerifyReturnsSuccessIfSolutionOutputMatchesUserOutput(): voi public function testSuccessWithSingleSetOfArgsForBC(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->assertInstanceOf( CliResult::class, @@ -129,15 +105,8 @@ public function testSuccessWithSingleSetOfArgsForBC(): void public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution()); $failure = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user-error.php'])); @@ -155,15 +124,8 @@ public function testVerifyReturnsFailureIfUserSolutionFailsToExecute(): void public function testVerifyReturnsFailureIfSolutionOutputDoesNotMatchUserOutput(): void { $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $failure = $this->runner->verify(new Input('app', ['program' => __DIR__ . '/../res/cli/user-wrong.php'])); @@ -188,11 +150,6 @@ public function testRunPassesOutputAndReturnsSuccessIfScriptIsSuccessful(): void new OutputRunInfoListener($output, new RequestRenderer()) ); - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3], [4, 5, 6]]); - $exp = "\n\e[1m\e[4mArguments\e[0m\e[0m\n"; $exp .= "1, 2, 3\n"; $exp .= "\n\e[1m\e[4mOutput\e[0m\e[0m\n\n"; @@ -206,18 +163,22 @@ public function testRunPassesOutputAndReturnsSuccessIfScriptIsSuccessful(): void $this->expectOutputString($exp); - $success = $this->runner->run(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php']), $output); - $this->assertTrue($success); + $this->exercise->setScenario( + (new CliScenario()) + ->withExecution([1, 2, 3]) + ->withExecution([4, 5, 6]) + ); + + + $result = $this->runner->run(new Input('app', ['program' => __DIR__ . '/../res/cli/user.php']), $output); + + $this->assertTrue($result); } public function testRunPassesOutputAndReturnsFailureIfScriptFails(): void { $output = new StdOutput(new Color(), $this->createMock(Terminal::class)); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([[1, 2, 3]]); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->expectOutputRegex( "/(PHP )?Parse error:\W+syntax error, unexpected end of file, expecting ['\"][,;]['\"] or ['\"][;,]['\"] /" @@ -237,15 +198,8 @@ function (CliExecuteEvent $e) { ); $solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/cli/solution.php')); - $this->exercise - ->expects($this->once()) - ->method('getSolution') - ->willReturn($solution); - - $this->exercise - ->expects($this->once()) - ->method('getArgs') - ->willReturn([1, 2, 3]); + $this->exercise->setSolution($solution); + $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $this->assertInstanceOf( CliResult::class,