diff --git a/app/config.php b/app/config.php index 9a7b532f..ae923075 100644 --- a/app/config.php +++ b/app/config.php @@ -3,10 +3,13 @@ declare(strict_types=1); use Colors\Color; +use PhpSchool\PhpWorkshop\Check\FileComparisonCheck; use PhpSchool\PhpWorkshop\Listener\InitialCodeListener; use PhpSchool\PhpWorkshop\Listener\TearDownListener; use PhpSchool\PhpWorkshop\Logger\ConsoleLogger; use PhpSchool\PhpWorkshop\Logger\Logger; +use PhpSchool\PhpWorkshop\Result\FileComparisonFailure; +use PhpSchool\PhpWorkshop\ResultRenderer\FileComparisonFailureRenderer; use Psr\Log\LoggerInterface; use function DI\create; use function DI\factory; @@ -122,6 +125,7 @@ $c->get(ComposerCheck::class), $c->get(FunctionRequirementsCheck::class), $c->get(DatabaseCheck::class), + $c->get(FileComparisonCheck::class) ]); }, CommandRouter::class => function (ContainerInterface $c) { @@ -261,6 +265,7 @@ }, DatabaseCheck::class => create(), ComposerCheck::class => create(), + FileComparisonCheck::class => create(), //Utils Filesystem::class => create(), @@ -332,6 +337,7 @@ function (CgiResult $result) use ($c) { $factory->registerRenderer(CliRequestFailure::class, CliRequestFailureRenderer::class); $factory->registerRenderer(ComparisonFailure::class, ComparisonFailureRenderer::class); + $factory->registerRenderer(FileComparisonFailure::class, FileComparisonFailureRenderer::class); return $factory; }, diff --git a/src/Check/FileComparisonCheck.php b/src/Check/FileComparisonCheck.php new file mode 100644 index 00000000..3e2fc963 --- /dev/null +++ b/src/Check/FileComparisonCheck.php @@ -0,0 +1,93 @@ +getFilesToCompare() as $file) { + $studentFile = Path::join(dirname($input->getRequiredArgument('program')), $file); + $referenceFile = Path::join($exercise->getSolution()->getBaseDirectory(), $file); + + if (!file_exists($referenceFile)) { + throw SolutionFileDoesNotExistException::fromExpectedFile($file); + } + + if (!file_exists($studentFile)) { + return Failure::fromCheckAndReason($this, sprintf('File: "%s" does not exist', $file)); + } + + $actual = (string) file_get_contents($studentFile); + $expected = (string) file_get_contents($referenceFile); + + if ($expected !== $actual) { + return new FileComparisonFailure($this, $file, $expected, $actual); + } + } + + return Success::fromCheck($this); + } + + /** + * This check can run on any exercise type. + * + * @param ExerciseType $exerciseType + * @return bool + */ + public function canRun(ExerciseType $exerciseType): bool + { + return in_array($exerciseType->getValue(), [ExerciseType::CGI, ExerciseType::CLI], true); + } + + public function getExerciseInterface(): string + { + return FileComparisonExerciseCheck::class; + } + + /** + * This check must run after executing the solution because the files will not exist otherwise. + */ + public function getPosition(): string + { + return SimpleCheckInterface::CHECK_AFTER; + } +} diff --git a/src/Exception/SolutionFileDoesNotExistException.php b/src/Exception/SolutionFileDoesNotExistException.php new file mode 100644 index 00000000..5d79c8e3 --- /dev/null +++ b/src/Exception/SolutionFileDoesNotExistException.php @@ -0,0 +1,20 @@ + + */ + public function getFilesToCompare(): array; +} diff --git a/src/Result/FileComparisonFailure.php b/src/Result/FileComparisonFailure.php new file mode 100644 index 00000000..b486f5a4 --- /dev/null +++ b/src/Result/FileComparisonFailure.php @@ -0,0 +1,74 @@ +check = $check; + $this->fileName = $fileName; + $this->expectedValue = $expectedValue; + $this->actualValue = $actualValue; + } + + /** + * Get the name of the file to be verified + * + * @return string + */ + public function getFileName(): string + { + return $this->fileName; + } + + /** + * Get the expected value. + * + * @return string + */ + public function getExpectedValue(): string + { + return $this->expectedValue; + } + + /** + * Get the actual value. + * + * @return string + */ + public function getActualValue(): string + { + return $this->actualValue; + } +} diff --git a/src/ResultRenderer/FileComparisonFailureRenderer.php b/src/ResultRenderer/FileComparisonFailureRenderer.php new file mode 100644 index 00000000..09914cba --- /dev/null +++ b/src/ResultRenderer/FileComparisonFailureRenderer.php @@ -0,0 +1,62 @@ +result = $result; + } + + /** + * Print the actual and expected output. + * + * @param ResultsRenderer $renderer + * @return string + */ + public function render(ResultsRenderer $renderer): string + { + return sprintf( + " %s%s\n%s\n\n %s%s\n%s\n", + $renderer->style('YOUR OUTPUT FOR: ', ['bold', 'yellow']), + $renderer->style($this->result->getFileName(), ['bold', 'green']), + $this->indent($renderer->style(sprintf('"%s"', $this->result->getActualValue()), 'red')), + $renderer->style('EXPECTED OUTPUT FOR: ', ['bold', 'yellow']), + $renderer->style($this->result->getFileName(), ['bold', 'green']), + $this->indent($renderer->style(sprintf('"%s"', $this->result->getExpectedValue()), 'green')) + ); + } + + /** + * @param string $string + * @return string + */ + private function indent(string $string): string + { + return implode( + "\n", + array_map( + function ($line) { + return sprintf(' %s', $line); + }, + explode("\n", $string) + ) + ); + } +} diff --git a/test/Asset/FileComparisonExercise.php b/test/Asset/FileComparisonExercise.php new file mode 100644 index 00000000..b4ecd9c1 --- /dev/null +++ b/test/Asset/FileComparisonExercise.php @@ -0,0 +1,78 @@ + + */ + private $files; + + /** + * @var SolutionInterface + */ + private $solution; + + public function __construct(array $files) + { + $this->files = $files; + } + + public function getName(): string + { + // TODO: Implement getName() method. + } + + public function getDescription(): string + { + // TODO: Implement getDescription() method. + } + + public function setSolution(SolutionInterface $solution): void + { + $this->solution = $solution; + } + + public function getSolution(): SolutionInterface + { + return $this->solution; + } + + public function getProblem(): string + { + // TODO: Implement getProblem() method. + } + + public function tearDown(): void + { + // TODO: Implement tearDown() method. + } + + public function getArgs(): array + { + return []; // TODO: Implement getArgs() method. + } + + public function getType(): ExerciseType + { + return ExerciseType::CLI(); + } + + public function configure(ExerciseDispatcher $dispatcher): void + { + $dispatcher->requireCheck(ComposerCheck::class); + } + + public function getFilesToCompare(): array + { + return $this->files; + } +} diff --git a/test/Asset/FunctionRequirementsExercise.php b/test/Asset/FunctionRequirementsExercise.php index 25322fa7..927d6f56 100644 --- a/test/Asset/FunctionRequirementsExercise.php +++ b/test/Asset/FunctionRequirementsExercise.php @@ -40,14 +40,6 @@ public function getArgs(): array return []; // TODO: Implement getArgs() method. } - public function getRequiredPackages(): array - { - return [ - 'klein/klein', - 'danielstjules/stringy' - ]; - } - public function getType(): ExerciseType { return ExerciseType::CLI(); diff --git a/test/Check/FileComparisonCheckTest.php b/test/Check/FileComparisonCheckTest.php new file mode 100644 index 00000000..9c50926a --- /dev/null +++ b/test/Check/FileComparisonCheckTest.php @@ -0,0 +1,98 @@ +check = new FileComparisonCheck(); + $this->assertEquals('File Comparison Check', $this->check->getName()); + $this->assertEquals(FileComparisonExerciseCheck::class, $this->check->getExerciseInterface()); + $this->assertEquals(SimpleCheckInterface::CHECK_AFTER, $this->check->getPosition()); + + $this->assertTrue($this->check->canRun(ExerciseType::CGI())); + $this->assertTrue($this->check->canRun(ExerciseType::CLI())); + } + + public function testExceptionIsThrownIfReferenceFileDoesNotExist(): void + { + $this->expectException(SolutionFileDoesNotExistException::class); + $this->expectExceptionMessage('File: "some-file.txt" does not exist in solution folder'); + + $exercise = new FileComparisonExercise(['some-file.txt']); + $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + + $this->check->check($exercise, new Input('app', ['program' => 'my-solution.php'])); + } + + public function testFailureIsReturnedIfStudentsFileDoesNotExist(): void + { + $file = $this->getTemporaryFile('solution/some-file.txt'); + file_put_contents($file, "name,age\nAydin,33\nMichael,29\n"); + + $exercise = new FileComparisonExercise(['some-file.txt']); + $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + + $failure = $this->check->check($exercise, new Input('app', ['program' => 'my-solution.php'])); + + $this->assertInstanceOf(Failure::class, $failure); + $this->assertEquals('File: "some-file.txt" does not exist', $failure->getReason()); + } + + public function testFailureIsReturnedIfStudentFileDosNotMatchReferenceFile(): void + { + $file = $this->getTemporaryFile('solution/some-file.txt'); + file_put_contents($file, "name,age\nAydin,33\nMichael,29\n"); + + $studentSolution = $this->getTemporaryFile('student/my-solution.php'); + $studentFile = $this->getTemporaryFile('student/some-file.txt'); + file_put_contents($studentFile, "somegibberish"); + + $exercise = new FileComparisonExercise(['some-file.txt']); + $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + + $failure = $this->check->check($exercise, new Input('app', ['program' => $studentSolution])); + + $this->assertInstanceOf(FileComparisonFailure::class, $failure); + $this->assertEquals($failure->getFileName(), 'some-file.txt'); + $this->assertEquals($failure->getExpectedValue(), "name,age\nAydin,33\nMichael,29\n"); + $this->assertEquals($failure->getActualValue(), "somegibberish"); + } + + public function testSuccessIsReturnedIfFilesMatch(): void + { + $file = $this->getTemporaryFile('solution/some-file.txt'); + file_put_contents($file, "name,age\nAydin,33\nMichael,29\n"); + + $studentSolution = $this->getTemporaryFile('student/my-solution.php'); + $studentFile = $this->getTemporaryFile('student/some-file.txt'); + file_put_contents($studentFile, "name,age\nAydin,33\nMichael,29\n"); + + $exercise = new FileComparisonExercise(['some-file.txt']); + $exercise->setSolution(new SingleFileSolution($this->getTemporaryFile('solution/solution.php'))); + + $this->assertInstanceOf( + Success::class, + $this->check->check($exercise, new Input('app', ['program' => $studentSolution])) + ); + } +} diff --git a/test/Exception/SolutionFileDoesNotExistExceptionTest.php b/test/Exception/SolutionFileDoesNotExistExceptionTest.php new file mode 100644 index 00000000..ce2c61eb --- /dev/null +++ b/test/Exception/SolutionFileDoesNotExistExceptionTest.php @@ -0,0 +1,15 @@ +assertEquals('File: "some-file.csv" does not exist in solution folder', $e->getMessage()); + } +} diff --git a/test/Result/FileComparisonFailureTest.php b/test/Result/FileComparisonFailureTest.php new file mode 100644 index 00000000..df938fc2 --- /dev/null +++ b/test/Result/FileComparisonFailureTest.php @@ -0,0 +1,25 @@ +createMock(CheckInterface::class); + $check + ->method('getName') + ->willReturn('Some Check'); + + $failure = new FileComparisonFailure($check, 'users.txt', 'Expected Output', 'Actual Output'); + $this->assertEquals('Expected Output', $failure->getExpectedValue()); + $this->assertEquals('Actual Output', $failure->getActualValue()); + $this->assertEquals('users.txt', $failure->getFileName()); + } +} diff --git a/test/ResultRenderer/FileComparisonFailureRendererTest.php b/test/ResultRenderer/FileComparisonFailureRendererTest.php new file mode 100644 index 00000000..d7607863 --- /dev/null +++ b/test/ResultRenderer/FileComparisonFailureRendererTest.php @@ -0,0 +1,30 @@ +createMock(CheckInterface::class), + 'some-file.text', + 'EXPECTED OUTPUT', + 'ACTUAL OUTPUT' + ); + $renderer = new FileComparisonFailureRenderer($failure); + + $expected = " \e[33m\e[1mYOUR OUTPUT FOR: \e[0m\e[0m\e[32m\e[1msome-file.text\e[0m\e[0m\n"; + $expected .= " \e[31m\"ACTUAL OUTPUT\"\e[0m\n\n"; + $expected .= " \e[33m\e[1mEXPECTED OUTPUT FOR: \e[0m\e[0m\e[32m\e[1msome-file.text\e[0m\e[0m\n"; + $expected .= " \e[32m\"EXPECTED OUTPUT\"\e[0m\n"; + + $this->assertEquals($expected, $renderer->render($this->getRenderer())); + } +}