Skip to content

Exercise testing utils #189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 15 additions & 7 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions src/ExerciseRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
154 changes: 154 additions & 0 deletions src/TestUtils/WorkshopExerciseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshop\TestUtils;

use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
use PhpSchool\PhpWorkshop\ExerciseRepository;
use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult;
use PhpSchool\PhpWorkshop\Result\Cli\CliResult;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\FailureInterface;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\ResultAggregator;
use PhpSchool\PhpWorkshop\Utils\Collection;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use PhpSchool\PhpWorkshop\Input\Input;

abstract class WorkshopExerciseTest extends TestCase
{
/**
* @var Application
*/
protected $app;

/**
* @var ContainerInterface
*/
protected $container;

/**
* @var ResultAggregator
*/
protected $results;

public function setUp(): void
{
$this->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());
}
}
20 changes: 18 additions & 2 deletions src/Utils/ArrayObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
Expand Down
19 changes: 19 additions & 0 deletions test/ExerciseRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
29 changes: 29 additions & 0 deletions test/Util/ArrayObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}