Skip to content

Commit 897f8f6

Browse files
authored
Merge pull request #189 from php-school/exercise-testing-utils
Exercise testing utils
2 parents 84196ec + 82f3b31 commit 897f8f6

7 files changed

+261
-9
lines changed

phpstan.neon

+7
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ parameters:
77
-
88
message: '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:__toString\(\)#'
99
path: src/Check/FunctionRequirementsCheck.php
10+
11+
-
12+
message: '#Class PHPUnit\\Framework\\TestCase not found#'
13+
path: src/TestUtils/WorkshopExerciseTest.php
14+
15+
excludes_analyse:
16+
- src/TestUtils/WorkshopExerciseTest.php

src/Application.php

+15-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpSchool\PhpWorkshop\Exception\MissingArgumentException;
1212
use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory;
1313
use PhpSchool\PhpWorkshop\Output\OutputInterface;
14+
use Psr\Container\ContainerInterface;
1415
use RuntimeException;
1516

1617
use function class_exists;
@@ -160,13 +161,7 @@ public function setBgColour(string $colour): void
160161
$this->bgColour = $colour;
161162
}
162163

163-
/**
164-
* Executes the framework, invoking the specified command.
165-
* The return value is the exit code. 0 for success, anything else is a failure.
166-
*
167-
* @return int The exit code
168-
*/
169-
public function run(): int
164+
public function configure(): ContainerInterface
170165
{
171166
$container = $this->getContainer();
172167

@@ -197,6 +192,19 @@ public function run(): int
197192
}
198193
}
199194

195+
return $container;
196+
}
197+
198+
/**
199+
* Executes the framework, invoking the specified command.
200+
* The return value is the exit code. 0 for success, anything else is a failure.
201+
*
202+
* @return int The exit code
203+
*/
204+
public function run(): int
205+
{
206+
$container = $this->configure();
207+
200208
try {
201209
$exitCode = $container->get(CommandRouter::class)->route();
202210
} catch (MissingArgumentException $e) {

src/ExerciseRepository.php

+19
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,25 @@ public function findByName(string $name): ExerciseInterface
8080
throw new InvalidArgumentException(sprintf('Exercise with name: "%s" does not exist', $name));
8181
}
8282

83+
/**
84+
* Find an exercise by it's class name. If it does not exist
85+
* an `InvalidArgumentException` exception is thrown.
86+
*
87+
* @param class-string $className
88+
* @return ExerciseInterface
89+
* @throws InvalidArgumentException
90+
*/
91+
public function findByClassName(string $className): ExerciseInterface
92+
{
93+
foreach ($this->exercises as $exercise) {
94+
if ($className === get_class($exercise)) {
95+
return $exercise;
96+
}
97+
}
98+
99+
throw new InvalidArgumentException(sprintf('Exercise with name: "%s" does not exist', $className));
100+
}
101+
83102
/**
84103
* Get the names of each exercise as an array.
85104
*
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PhpWorkshop\TestUtils;
6+
7+
use PhpSchool\PhpWorkshop\Application;
8+
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
9+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
10+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
11+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
12+
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
13+
use PhpSchool\PhpWorkshop\ExerciseRepository;
14+
use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult;
15+
use PhpSchool\PhpWorkshop\Result\Cli\CliResult;
16+
use PhpSchool\PhpWorkshop\Result\Failure;
17+
use PhpSchool\PhpWorkshop\Result\FailureInterface;
18+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
19+
use PhpSchool\PhpWorkshop\ResultAggregator;
20+
use PhpSchool\PhpWorkshop\Utils\Collection;
21+
use PHPUnit\Framework\TestCase;
22+
use Psr\Container\ContainerInterface;
23+
use PhpSchool\PhpWorkshop\Input\Input;
24+
25+
abstract class WorkshopExerciseTest extends TestCase
26+
{
27+
/**
28+
* @var Application
29+
*/
30+
protected $app;
31+
32+
/**
33+
* @var ContainerInterface
34+
*/
35+
protected $container;
36+
37+
/**
38+
* @var ResultAggregator
39+
*/
40+
protected $results;
41+
42+
public function setUp(): void
43+
{
44+
$this->app = $this->getApplication();
45+
$this->container = $this->app->configure();
46+
}
47+
48+
/**
49+
* @return class-string
50+
*/
51+
abstract public function getExerciseClass(): string;
52+
53+
abstract public function getApplication(): Application;
54+
55+
private function getExercise(): ExerciseInterface
56+
{
57+
return $this->container->get(ExerciseRepository::class)
58+
->findByClassName($this->getExerciseClass());
59+
}
60+
61+
public function runExercise(string $submissionFile)
62+
{
63+
$exercise = $this->getExercise();
64+
65+
$submissionFileAbsolute = sprintf(
66+
'%s/test/solutions/%s/%s',
67+
rtrim($this->container->get('basePath'), '/'),
68+
AbstractExercise::normaliseName($exercise->getName()),
69+
$submissionFile
70+
);
71+
72+
if (!file_exists($submissionFileAbsolute)) {
73+
throw new InvalidArgumentException(
74+
sprintf(
75+
'Submission file "%s" does not exist in "%s"',
76+
$submissionFile,
77+
dirname($submissionFileAbsolute)
78+
)
79+
);
80+
}
81+
82+
$input = new Input($this->container->get('appName'), [
83+
'program' => $submissionFileAbsolute
84+
]);
85+
86+
$this->results = $this->container->get(ExerciseDispatcher::class)
87+
->verify($exercise, $input);
88+
}
89+
90+
public function assertVerifyWasSuccessful(): void
91+
{
92+
$failures = (new Collection($this->results->getIterator()->getArrayCopy()))
93+
->filter(function (ResultInterface $result) {
94+
return $result instanceof FailureInterface;
95+
})
96+
->map(function (Failure $failure) {
97+
return $failure->getReason();
98+
})
99+
->implode(', ');
100+
101+
102+
$this->assertTrue($this->results->isSuccessful(), $failures);
103+
}
104+
105+
public function assertVerifyWasNotSuccessful(): void
106+
{
107+
$this->assertFalse($this->results->isSuccessful());
108+
}
109+
110+
public function assertResultCount(int $count): void
111+
{
112+
$this->assertCount($count, $this->results);
113+
}
114+
115+
public function assertResultsHasFailure(string $resultClass, string $reason): void
116+
{
117+
$failures = (new Collection($this->results->getIterator()->getArrayCopy()))
118+
->filter(function (ResultInterface $result) {
119+
return $result instanceof Failure;
120+
})
121+
->filter(function (Failure $failure) use ($reason) {
122+
return $failure->getReason() === $reason;
123+
});
124+
125+
$this->assertCount(1, $failures, "No failure with reason: '$reason'");
126+
}
127+
128+
public function assertOutputWasIncorrect(): void
129+
{
130+
$exerciseType = $this->getExercise()->getType();
131+
132+
if ($exerciseType->equals(ExerciseType::CLI())) {
133+
$results = (new Collection($this->results->getIterator()->getArrayCopy()))
134+
->filter(function (ResultInterface $result) {
135+
return $result instanceof CliResult;
136+
});
137+
138+
$this->assertCount(1, $results);
139+
}
140+
141+
if ($exerciseType->equals(ExerciseType::CGI())) {
142+
$results = (new Collection($this->results->getIterator()->getArrayCopy()))
143+
->filter(function (ResultInterface $result) {
144+
return $result instanceof CgiResult;
145+
});
146+
147+
$this->assertCount(1, $results);
148+
}
149+
150+
$outputResults = $results->values()->get(0);
151+
152+
$this->assertFalse($outputResults->isSuccessful());
153+
}
154+
}

src/Utils/ArrayObject.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ public function keys(): self
107107
return new static(array_keys($this->array));
108108
}
109109

110+
/**
111+
* @return static
112+
*/
113+
public function values(): self
114+
{
115+
return new static(array_values($this->array));
116+
}
117+
118+
/**
119+
* @return T|mixed
120+
*/
121+
public function first()
122+
{
123+
return $this->get(0);
124+
}
125+
110126
/**
111127
* Implode each item together using the provided glue.
112128
*
@@ -143,11 +159,11 @@ public function append($value): self
143159
/**
144160
* Get an item at the given key.
145161
*
146-
* @param string $key
162+
* @param string|int $key
147163
* @param mixed $default
148164
* @return T|mixed
149165
*/
150-
public function get(string $key, $default = null)
166+
public function get($key, $default = null)
151167
{
152168
return $this->array[$key] ?? $default;
153169
}

test/ExerciseRepositoryTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ public function testFindByNameThrowsExceptionIfNotFound(): void
4545
$repo->findByName('exercise1');
4646
}
4747

48+
public function testFindByClassName(): void
49+
{
50+
$exercises = [
51+
new CliExerciseImpl('Exercise 1'),
52+
];
53+
54+
$repo = new ExerciseRepository($exercises);
55+
$this->assertSame($exercises[0], $repo->findByClassName(CliExerciseImpl::class));
56+
}
57+
58+
public function testFindByClassNameThrowsExceptionIfNotFound(): void
59+
{
60+
$this->expectException(InvalidArgumentException::class);
61+
$this->expectExceptionMessage(sprintf('Exercise with name: "%s" does not exist', CliExerciseImpl::class));
62+
63+
$repo = new ExerciseRepository([]);
64+
$repo->findByClassName(CliExerciseImpl::class);
65+
}
66+
4867
public function testGetAllNames(): void
4968
{
5069
$exercises = [

test/Util/ArrayObjectTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,33 @@ public function testIsEmpty(): void
134134
$arrayObject = new ArrayObject();
135135
self::assertTrue($arrayObject->isEmpty());
136136
}
137+
138+
public function testFilter(): void
139+
{
140+
$arrayObject = new ArrayObject([1, 2, 3]);
141+
$new = $arrayObject->filter(function ($elem) {
142+
return $elem > 1;
143+
});
144+
145+
$this->assertNotSame($arrayObject, $new);
146+
$this->assertEquals([1 => 2, 2 => 3], $new->getArrayCopy());
147+
}
148+
149+
public function testValues(): void
150+
{
151+
$arrayObject = new ArrayObject([1, 2, 3]);
152+
$new = $arrayObject->filter(function ($elem) {
153+
return $elem > 1;
154+
});
155+
156+
$this->assertNotSame($arrayObject, $new);
157+
$this->assertEquals([0 => 2, 1 => 3], $new->values()->getArrayCopy());
158+
}
159+
160+
public function testFirst(): void
161+
{
162+
$arrayObject = new ArrayObject([10, 11, 12]);
163+
164+
$this->assertEquals(10, $arrayObject->first());
165+
}
137166
}

0 commit comments

Comments
 (0)