diff --git a/phpstan.neon b/phpstan.neon index ae54b621..087f077e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,10 @@ parameters: - message: '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:__toString\(\)#' path: src/Check/FunctionRequirementsCheck.php + + - + message: '#Class PHPUnit\\Framework\\TestCase not found#' + path: src/TestUtils/WorkshopExerciseTest.php + + excludes_analyse: + - src/TestUtils/WorkshopExerciseTest.php diff --git a/src/Application.php b/src/Application.php index 7cdc923e..247cad99 100644 --- a/src/Application.php +++ b/src/Application.php @@ -11,6 +11,7 @@ use PhpSchool\PhpWorkshop\Exception\MissingArgumentException; use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory; use PhpSchool\PhpWorkshop\Output\OutputInterface; +use Psr\Container\ContainerInterface; use RuntimeException; use function class_exists; @@ -160,13 +161,7 @@ public function setBgColour(string $colour): void $this->bgColour = $colour; } - /** - * Executes the framework, invoking the specified command. - * The return value is the exit code. 0 for success, anything else is a failure. - * - * @return int The exit code - */ - public function run(): int + public function configure(): ContainerInterface { $container = $this->getContainer(); @@ -197,6 +192,19 @@ public function run(): int } } + return $container; + } + + /** + * Executes the framework, invoking the specified command. + * The return value is the exit code. 0 for success, anything else is a failure. + * + * @return int The exit code + */ + public function run(): int + { + $container = $this->configure(); + try { $exitCode = $container->get(CommandRouter::class)->route(); } catch (MissingArgumentException $e) { diff --git a/src/ExerciseRepository.php b/src/ExerciseRepository.php index 5314999a..caf73bc0 100644 --- a/src/ExerciseRepository.php +++ b/src/ExerciseRepository.php @@ -80,6 +80,25 @@ public function findByName(string $name): ExerciseInterface throw new InvalidArgumentException(sprintf('Exercise with name: "%s" does not exist', $name)); } + /** + * Find an exercise by it's class name. If it does not exist + * an `InvalidArgumentException` exception is thrown. + * + * @param class-string $className + * @return ExerciseInterface + * @throws InvalidArgumentException + */ + public function findByClassName(string $className): ExerciseInterface + { + foreach ($this->exercises as $exercise) { + if ($className === get_class($exercise)) { + return $exercise; + } + } + + throw new InvalidArgumentException(sprintf('Exercise with name: "%s" does not exist', $className)); + } + /** * Get the names of each exercise as an array. * diff --git a/src/TestUtils/WorkshopExerciseTest.php b/src/TestUtils/WorkshopExerciseTest.php new file mode 100644 index 00000000..ee34b58a --- /dev/null +++ b/src/TestUtils/WorkshopExerciseTest.php @@ -0,0 +1,154 @@ +app = $this->getApplication(); + $this->container = $this->app->configure(); + } + + /** + * @return class-string + */ + abstract public function getExerciseClass(): string; + + abstract public function getApplication(): Application; + + private function getExercise(): ExerciseInterface + { + return $this->container->get(ExerciseRepository::class) + ->findByClassName($this->getExerciseClass()); + } + + public function runExercise(string $submissionFile) + { + $exercise = $this->getExercise(); + + $submissionFileAbsolute = sprintf( + '%s/test/solutions/%s/%s', + rtrim($this->container->get('basePath'), '/'), + AbstractExercise::normaliseName($exercise->getName()), + $submissionFile + ); + + if (!file_exists($submissionFileAbsolute)) { + throw new InvalidArgumentException( + sprintf( + 'Submission file "%s" does not exist in "%s"', + $submissionFile, + dirname($submissionFileAbsolute) + ) + ); + } + + $input = new Input($this->container->get('appName'), [ + 'program' => $submissionFileAbsolute + ]); + + $this->results = $this->container->get(ExerciseDispatcher::class) + ->verify($exercise, $input); + } + + public function assertVerifyWasSuccessful(): void + { + $failures = (new Collection($this->results->getIterator()->getArrayCopy())) + ->filter(function (ResultInterface $result) { + return $result instanceof FailureInterface; + }) + ->map(function (Failure $failure) { + return $failure->getReason(); + }) + ->implode(', '); + + + $this->assertTrue($this->results->isSuccessful(), $failures); + } + + public function assertVerifyWasNotSuccessful(): void + { + $this->assertFalse($this->results->isSuccessful()); + } + + public function assertResultCount(int $count): void + { + $this->assertCount($count, $this->results); + } + + public function assertResultsHasFailure(string $resultClass, string $reason): void + { + $failures = (new Collection($this->results->getIterator()->getArrayCopy())) + ->filter(function (ResultInterface $result) { + return $result instanceof Failure; + }) + ->filter(function (Failure $failure) use ($reason) { + return $failure->getReason() === $reason; + }); + + $this->assertCount(1, $failures, "No failure with reason: '$reason'"); + } + + public function assertOutputWasIncorrect(): void + { + $exerciseType = $this->getExercise()->getType(); + + if ($exerciseType->equals(ExerciseType::CLI())) { + $results = (new Collection($this->results->getIterator()->getArrayCopy())) + ->filter(function (ResultInterface $result) { + return $result instanceof CliResult; + }); + + $this->assertCount(1, $results); + } + + if ($exerciseType->equals(ExerciseType::CGI())) { + $results = (new Collection($this->results->getIterator()->getArrayCopy())) + ->filter(function (ResultInterface $result) { + return $result instanceof CgiResult; + }); + + $this->assertCount(1, $results); + } + + $outputResults = $results->values()->get(0); + + $this->assertFalse($outputResults->isSuccessful()); + } +} diff --git a/src/Utils/ArrayObject.php b/src/Utils/ArrayObject.php index 26acd2c6..849a01c9 100644 --- a/src/Utils/ArrayObject.php +++ b/src/Utils/ArrayObject.php @@ -107,6 +107,22 @@ public function keys(): self return new static(array_keys($this->array)); } + /** + * @return static + */ + public function values(): self + { + return new static(array_values($this->array)); + } + + /** + * @return T|mixed + */ + public function first() + { + return $this->get(0); + } + /** * Implode each item together using the provided glue. * @@ -143,11 +159,11 @@ public function append($value): self /** * Get an item at the given key. * - * @param string $key + * @param string|int $key * @param mixed $default * @return T|mixed */ - public function get(string $key, $default = null) + public function get($key, $default = null) { return $this->array[$key] ?? $default; } diff --git a/test/ExerciseRepositoryTest.php b/test/ExerciseRepositoryTest.php index 21a72922..92aaa031 100644 --- a/test/ExerciseRepositoryTest.php +++ b/test/ExerciseRepositoryTest.php @@ -45,6 +45,25 @@ public function testFindByNameThrowsExceptionIfNotFound(): void $repo->findByName('exercise1'); } + public function testFindByClassName(): void + { + $exercises = [ + new CliExerciseImpl('Exercise 1'), + ]; + + $repo = new ExerciseRepository($exercises); + $this->assertSame($exercises[0], $repo->findByClassName(CliExerciseImpl::class)); + } + + public function testFindByClassNameThrowsExceptionIfNotFound(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Exercise with name: "%s" does not exist', CliExerciseImpl::class)); + + $repo = new ExerciseRepository([]); + $repo->findByClassName(CliExerciseImpl::class); + } + public function testGetAllNames(): void { $exercises = [ diff --git a/test/Util/ArrayObjectTest.php b/test/Util/ArrayObjectTest.php index 41e23a3e..eb7a8c94 100644 --- a/test/Util/ArrayObjectTest.php +++ b/test/Util/ArrayObjectTest.php @@ -134,4 +134,33 @@ public function testIsEmpty(): void $arrayObject = new ArrayObject(); self::assertTrue($arrayObject->isEmpty()); } + + public function testFilter(): void + { + $arrayObject = new ArrayObject([1, 2, 3]); + $new = $arrayObject->filter(function ($elem) { + return $elem > 1; + }); + + $this->assertNotSame($arrayObject, $new); + $this->assertEquals([1 => 2, 2 => 3], $new->getArrayCopy()); + } + + public function testValues(): void + { + $arrayObject = new ArrayObject([1, 2, 3]); + $new = $arrayObject->filter(function ($elem) { + return $elem > 1; + }); + + $this->assertNotSame($arrayObject, $new); + $this->assertEquals([0 => 2, 1 => 3], $new->values()->getArrayCopy()); + } + + public function testFirst(): void + { + $arrayObject = new ArrayObject([10, 11, 12]); + + $this->assertEquals(10, $arrayObject->first()); + } }