diff --git a/.docker/runtime/Dockerfile b/.docker/runtime/Dockerfile
new file mode 100644
index 0000000..dc8e93f
--- /dev/null
+++ b/.docker/runtime/Dockerfile
@@ -0,0 +1,14 @@
+FROM php:8.3-alpine as build
+
+RUN apk add --no-cache $PHPIZE_DEPS && \
+ apk add --no-cache linux-headers
+
+RUN docker-php-ext-install sockets
+
+FROM php:8.3-alpine as final
+
+COPY --from=build /usr/local/lib/php /usr/local/lib/php
+COPY --from=build /usr/local/etc/php /usr/local/etc/php
+
+RUN apk add git
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
\ No newline at end of file
diff --git a/.docker/runtime/docker-compose.yml b/.docker/runtime/docker-compose.yml
new file mode 100644
index 0000000..b38f58a
--- /dev/null
+++ b/.docker/runtime/docker-compose.yml
@@ -0,0 +1,14 @@
+services:
+ runtime:
+ image: php8appreciate-runtime
+ build:
+ context: .
+ dockerfile: Dockerfile
+
+ user: ${UID}:${GID}
+
+ volumes:
+ - type: bind
+ source: ${SOLUTION}
+ target: '/solution'
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8075bce..9a24134 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,8 +32,9 @@ jobs:
- name: Run phpunit tests
run: |
mkdir -p build/logs
- vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml
-
+ PROCESS_FACTORY=host vendor/bin/phpunit
+ PROCESS_FACTORY=docker vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml
+
- name: Run phpcs
run: composer cs
diff --git a/app/config.php b/app/config.php
index d4bb344..35e687d 100644
--- a/app/config.php
+++ b/app/config.php
@@ -28,22 +28,21 @@
HelloWorld::class => create(HelloWorld::class),
HttpJsonApi::class => create(HttpJsonApi::class),
MyFirstIo::class => factory(function (ContainerInterface $c) {
- return new MyFirstIo($c->get(Filesystem::class), FakerFactory::create());
+ return new MyFirstIo(FakerFactory::create());
}),
FilteredLs::class => factory(function (ContainerInterface $c) {
return new FilteredLs($c->get(Filesystem::class));
}),
ConcernedAboutSeparation::class => factory(function (ContainerInterface $c) {
return new ConcernedAboutSeparation(
- $c->get(Filesystem::class),
$c->get(Parser::class)
);
}),
ArrayWeGo::class => factory(function (ContainerInterface $c) {
- return new ArrayWeGo($c->get(Filesystem::class), FakerFactory::create());
+ return new ArrayWeGo(FakerFactory::create());
}),
ExceptionalCoding::class => factory(function (ContainerInterface $c) {
- return new ExceptionalCoding($c->get(Filesystem::class), FakerFactory::create());
+ return new ExceptionalCoding(FakerFactory::create());
}),
DatabaseRead::class => factory(function (ContainerInterface $c) {
return new DatabaseRead(FakerFactory::create());
diff --git a/composer.json b/composer.json
index a77c12e..f09b583 100644
--- a/composer.json
+++ b/composer.json
@@ -13,8 +13,9 @@
],
"require" : {
"php" : ">=8.0",
+ "ext-pdo": "*",
"ext-pdo_sqlite": "*",
- "php-school/php-workshop": "dev-master",
+ "php-school/php-workshop": "dev-docker-fixes",
"ext-sockets": "*"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 4ed65f1..0196760 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c8283c629d2b2713b55e47ae1c37648f",
+ "content-hash": "6c6ac0e6333aeeee959f1080ff6b2a90",
"packages": [
{
"name": "beberlei/assert",
@@ -734,21 +734,21 @@
},
{
"name": "nikic/php-parser",
- "version": "v4.18.0",
+ "version": "v4.19.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999"
+ "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999",
- "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b",
+ "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "php": ">=7.0"
+ "php": ">=7.1"
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
@@ -784,9 +784,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1"
},
- "time": "2023-12-10T21:03:43+00:00"
+ "time": "2024-03-17T08:10:35+00:00"
},
{
"name": "php-di/invoker",
@@ -1126,16 +1126,16 @@
},
{
"name": "php-school/php-workshop",
- "version": "dev-master",
+ "version": "dev-docker-fixes",
"source": {
"type": "git",
"url": "https://github.com/php-school/php-workshop.git",
- "reference": "11911b2157e80480a8cc272b56f417f5ba4903bf"
+ "reference": "73febf8c62d982cd74896df9266522dc2a5153b5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-school/php-workshop/zipball/11911b2157e80480a8cc272b56f417f5ba4903bf",
- "reference": "11911b2157e80480a8cc272b56f417f5ba4903bf",
+ "url": "https://api.github.com/repos/php-school/php-workshop/zipball/73febf8c62d982cd74896df9266522dc2a5153b5",
+ "reference": "73febf8c62d982cd74896df9266522dc2a5153b5",
"shasum": ""
},
"require": {
@@ -1158,15 +1158,20 @@
"symfony/process": "^4.0 | ^5.0 | ^6.0"
},
"require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8",
"composer/composer": "^2.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^8.5",
- "squizlabs/php_codesniffer": "^3.7",
"yoast/phpunit-polyfills": "^0.2.0"
},
- "default-branch": true,
"type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
"autoload": {
"files": [
"src/Event/functions.php",
@@ -1204,9 +1209,9 @@
],
"support": {
"issues": "https://github.com/php-school/php-workshop/issues",
- "source": "https://github.com/php-school/php-workshop/tree/master"
+ "source": "https://github.com/php-school/php-workshop/tree/docker-fixes"
},
- "time": "2024-03-10T13:04:10+00:00"
+ "time": "2024-09-04T11:22:50+00:00"
},
{
"name": "php-school/terminal",
@@ -1367,20 +1372,20 @@
},
{
"name": "psr/http-factory",
- "version": "1.0.2",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
- "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
- "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
- "php": ">=7.0.0",
+ "php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
@@ -1404,7 +1409,7 @@
"homepage": "https://www.php-fig.org/"
}
],
- "description": "Common interfaces for PSR-7 HTTP message factories",
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
@@ -1416,9 +1421,9 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ "source": "https://github.com/php-fig/http-factory"
},
- "time": "2023-04-10T20:10:41+00:00"
+ "time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
@@ -1569,16 +1574,16 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.4.0",
+ "version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
@@ -1587,7 +1592,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.4-dev"
+ "dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -1616,7 +1621,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
@@ -1632,26 +1637,27 @@
"type": "tidelift"
}
],
- "time": "2023-05-23T14:45:45+00:00"
+ "time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v6.4.3",
+ "version": "v6.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb"
+ "reference": "78dde75f8f6dbbca4ec436a4b0087f7af02076d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb",
- "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/78dde75f8f6dbbca4ec436a4b0087f7af02076d4",
+ "reference": "78dde75f8f6dbbca4ec436a4b0087f7af02076d4",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-mbstring": "~1.8"
+ "symfony/polyfill-mbstring": "~1.8",
+ "symfony/process": "^5.4|^6.4"
},
"type": "library",
"autoload": {
@@ -1679,7 +1685,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v6.4.3"
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.7"
},
"funding": [
{
@@ -1695,7 +1701,7 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T14:51:35+00:00"
+ "time": "2024-04-18T09:22:46+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -1858,16 +1864,16 @@
},
{
"name": "symfony/process",
- "version": "v6.4.4",
+ "version": "v6.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "710e27879e9be3395de2b98da3f52a946039f297"
+ "reference": "cdb1c81c145fd5aa9b0038bab694035020943381"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297",
- "reference": "710e27879e9be3395de2b98da3f52a946039f297",
+ "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381",
+ "reference": "cdb1c81c145fd5aa9b0038bab694035020943381",
"shasum": ""
},
"require": {
@@ -1899,7 +1905,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.4"
+ "source": "https://github.com/symfony/process/tree/v6.4.7"
},
"funding": [
{
@@ -1915,7 +1921,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-20T12:31:00+00:00"
+ "time": "2024-04-18T09:22:46+00:00"
}
],
"packages-dev": [
@@ -2168,16 +2174,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.10.60",
+ "version": "1.11.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe"
+ "reference": "0d5d4294a70deb7547db655c47685d680e39cfec"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
- "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec",
+ "reference": "0d5d4294a70deb7547db655c47685d680e39cfec",
"shasum": ""
},
"require": {
@@ -2220,13 +2226,9 @@
{
"url": "https://github.com/phpstan",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
- "type": "tidelift"
}
],
- "time": "2024-03-07T13:30:19+00:00"
+ "time": "2024-05-24T13:23:04+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -2549,16 +2551,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.17",
+ "version": "9.6.19",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd"
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd",
- "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
"shasum": ""
},
"require": {
@@ -2632,7 +2634,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.17"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
},
"funding": [
{
@@ -2648,7 +2650,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-23T13:14:51+00:00"
+ "time": "2024-04-05T04:35:58+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -3452,16 +3454,16 @@
},
{
"name": "sebastian/resource-operations",
- "version": "3.0.3",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"shasum": ""
},
"require": {
@@ -3473,7 +3475,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -3494,8 +3496,7 @@
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": {
- "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
},
"funding": [
{
@@ -3503,7 +3504,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:45:17+00:00"
+ "time": "2024-03-14T16:00:52+00:00"
},
{
"name": "sebastian/type",
@@ -3616,16 +3617,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.9.0",
+ "version": "3.10.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b"
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877",
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877",
"shasum": ""
},
"require": {
@@ -3692,7 +3693,7 @@
"type": "open_collective"
}
],
- "time": "2024-02-16T15:06:51+00:00"
+ "time": "2024-05-22T21:24:41+00:00"
},
{
"name": "theseer/tokenizer",
@@ -3754,6 +3755,7 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.0",
+ "ext-pdo": "*",
"ext-pdo_sqlite": "*",
"ext-sockets": "*"
},
diff --git a/phpunit.xml b/phpunit.xml
index 4029e8b..aeee4bb 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -10,4 +10,7 @@
./test
./test/solutions
+
+
+
diff --git a/src/Exercise/ArrayWeGo.php b/src/Exercise/ArrayWeGo.php
index 53924d2..26172a5 100644
--- a/src/Exercise/ArrayWeGo.php
+++ b/src/Exercise/ArrayWeGo.php
@@ -8,23 +8,16 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
-use PhpSchool\PhpWorkshop\ExerciseCheck\StdOutExerciseCheck;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
use Symfony\Component\Filesystem\Filesystem;
class ArrayWeGo extends AbstractExercise implements ExerciseInterface, FunctionRequirementsExerciseCheck, CliExercise
{
- use TemporaryDirectoryTrait;
-
- private Filesystem $filesystem;
- private Generator $faker;
-
- public function __construct(Filesystem $filesystem, Generator $faker)
+ public function __construct(private Generator $faker)
{
- $this->filesystem = $filesystem;
- $this->faker = $faker;
}
public function getName(): string
@@ -37,31 +30,29 @@ public function getDescription(): string
return 'Filter an array of file paths and map to SplFile objects';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- $this->filesystem->mkdir($this->getTemporaryPath());
-
$fileCount = rand(2, 10);
- $realFiles = rand(1, $fileCount - 1);
+ $realFileCount = rand(1, $fileCount - 1);
$files = [];
+ $realFiles = [];
foreach (range(1, $fileCount) as $index) {
- $file = sprintf('%s/%s.txt', $this->getTemporaryPath(), $this->faker->uuid());
- if ($index <= $realFiles) {
- $this->filesystem->touch($file);
+ $file = $this->faker->uuid() . ".txt";
+ if ($index <= $realFileCount) {
+ $realFiles[] = $file;
}
$files[] = $file;
}
- return [$files];
- }
+ $scenario = (new CliScenario())
+ ->withExecution($files);
- public function tearDown(): void
- {
- $this->filesystem->remove($this->getTemporaryPath());
+ foreach ($realFiles as $realFile) {
+ $scenario->withFile($realFile, '');
+ }
+
+ return $scenario;
}
/**
@@ -85,8 +76,8 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CLI);
}
- public function configure(ExerciseDispatcher $dispatcher): void
+ public function getRequiredChecks(): array
{
- $dispatcher->requireCheck(FunctionRequirementsCheck::class);
+ return [FunctionRequirementsCheck::class];
}
}
diff --git a/src/Exercise/BabySteps.php b/src/Exercise/BabySteps.php
index c0a1b6e..1b608ea 100644
--- a/src/Exercise/BabySteps.php
+++ b/src/Exercise/BabySteps.php
@@ -6,7 +6,7 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseCheck\StdOutExerciseCheck;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
class BabySteps extends AbstractExercise implements ExerciseInterface, CliExercise
{
@@ -20,10 +20,7 @@ public function getDescription(): string
return 'Simple Addition';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
$numArgs = rand(0, 10);
@@ -32,7 +29,8 @@ public function getArgs(): array
$args[] = (string) rand(0, 100);
}
- return [$args];
+ return (new CliScenario())
+ ->withExecution($args);
}
public function getType(): ExerciseType
diff --git a/src/Exercise/ConcernedAboutSeparation.php b/src/Exercise/ConcernedAboutSeparation.php
index 985e5a7..21facc3 100644
--- a/src/Exercise/ConcernedAboutSeparation.php
+++ b/src/Exercise/ConcernedAboutSeparation.php
@@ -8,28 +8,20 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
-use PhpSchool\PhpWorkshop\Input\Input;
+use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\Result\Success;
use PhpSchool\PhpWorkshop\Solution\DirectorySolution;
use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
-use Symfony\Component\Filesystem\Filesystem;
use PhpParser\Node\Expr\Include_;
class ConcernedAboutSeparation extends AbstractExercise implements ExerciseInterface, CliExercise, SelfCheck
{
- use TemporaryDirectoryTrait;
-
- private Filesystem $filesystem;
- private Parser $parser;
-
- public function __construct(Filesystem $filesystem, Parser $parser)
+ public function __construct(private Parser $parser)
{
- $this->filesystem = $filesystem;
- $this->parser = $parser;
}
public function getName(): string
@@ -42,13 +34,8 @@ public function getDescription(): string
return 'Separate code and utilise files and classes';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- $folder = $this->getTemporaryPath();
-
$files = [
"learnyouphp.dat",
"learnyouphp.txt",
@@ -71,18 +58,20 @@ public function getArgs(): array
"dat",
];
- $this->filesystem->mkdir($folder);
- array_walk($files, function ($file) use ($folder) {
- $this->filesystem->dumpFile(sprintf('%s/%s', $folder, $file), '');
- });
-
$ext = '';
while ($ext === '') {
$index = array_rand($files);
$ext = pathinfo($files[$index], PATHINFO_EXTENSION);
}
- return [[$folder, $ext]];
+ $scenario = (new CliScenario())
+ ->withExecution(['files', $ext]);
+
+ array_walk($files, function (string $file) use ($scenario) {
+ $scenario->withFile('files/' . $file, '');
+ });
+
+ return $scenario;
}
public function getSolution(): SolutionInterface
@@ -90,14 +79,9 @@ public function getSolution(): SolutionInterface
return DirectorySolution::fromDirectory(__DIR__ . '/../../exercises/concerned-about-separation/solution');
}
- public function tearDown(): void
- {
- $this->filesystem->remove($this->getTemporaryPath());
- }
-
- public function check(Input $input): ResultInterface
+ public function check(ExecutionContext $context): ResultInterface
{
- $statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
+ $statements = $this->parser->parse((string) file_get_contents($context->getEntryPoint()));
if (null === $statements) {
return Failure::fromNameAndReason($this->getName(), 'No code was found');
diff --git a/src/Exercise/DatabaseRead.php b/src/Exercise/DatabaseRead.php
index 36fc3b6..063f0af 100644
--- a/src/Exercise/DatabaseRead.php
+++ b/src/Exercise/DatabaseRead.php
@@ -5,29 +5,22 @@
use Faker\Generator;
use PDO;
use PhpSchool\PhpWorkshop\Check\DatabaseCheck;
-use PhpSchool\PhpWorkshop\Check\ListenableCheckInterface;
-use PhpSchool\PhpWorkshop\Event\Event;
-use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
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 Symfony\Component\Filesystem\Filesystem;
class DatabaseRead extends AbstractExercise implements ExerciseInterface, DatabaseExerciseCheck, CliExercise
{
- private Generator $faker;
-
/**
* @var array{id: int, name: string}
*/
private array $randomRecord;
- public function __construct(Generator $faker)
+ public function __construct(private Generator $faker)
{
- $this->faker = $faker;
}
public function getName(): string
@@ -40,12 +33,10 @@ public function getDescription(): string
return 'Read an SQL databases contents';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- return [[$this->randomRecord['name']]];
+ return (new CliScenario())
+ ->withExecution([$this->randomRecord['name']]);
}
public function seed(PDO $db): void
@@ -77,7 +68,7 @@ public function verify(PDO $db): bool
$sql = 'SELECT name FROM users WHERE id = :id';
$stmt = $db->prepare($sql);
- if ($stmt == false) {
+ if (!$stmt) {
return false;
}
@@ -92,8 +83,8 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CLI);
}
- public function configure(ExerciseDispatcher $dispatcher): void
+ public function getRequiredChecks(): array
{
- $dispatcher->requireCheck(DatabaseCheck::class);
+ return [DatabaseCheck::class];
}
}
diff --git a/src/Exercise/DependencyHeaven.php b/src/Exercise/DependencyHeaven.php
index b53a74f..2c0240e 100644
--- a/src/Exercise/DependencyHeaven.php
+++ b/src/Exercise/DependencyHeaven.php
@@ -9,6 +9,7 @@
use PhpSchool\PhpWorkshop\Exercise\CgiExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario;
use PhpSchool\PhpWorkshop\ExerciseCheck\ComposerExerciseCheck;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
use PhpSchool\PhpWorkshop\Solution\DirectorySolution;
@@ -42,20 +43,15 @@ public function getSolution(): SolutionInterface
return DirectorySolution::fromDirectory(__DIR__ . '/../../exercises/dependency-heaven/solution');
}
- /**
- * @inheritdoc
- */
- public function getRequests(): array
+ public function defineTestScenario(): CgiScenario
{
- $requests = [];
-
+ $scenario = new CgiScenario();
for ($i = 0; $i < rand(2, 5); $i++) {
- $requests[] = $this->newApiRequest('/reverse');
- $requests[] = $this->newApiRequest('/snake');
- $requests[] = $this->newApiRequest('/titleize');
+ $scenario->withExecution($this->newApiRequest('/reverse'));
+ $scenario->withExecution($this->newApiRequest('/snake'));
+ $scenario->withExecution($this->newApiRequest('/titleize'));
}
-
- return $requests;
+ return $scenario;
}
private function newApiRequest(string $endpoint): RequestInterface
@@ -88,8 +84,8 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CGI);
}
- public function configure(ExerciseDispatcher $dispatcher): void
+ public function getRequiredChecks(): array
{
- $dispatcher->requireCheck(ComposerCheck::class);
+ return [ComposerCheck::class];
}
}
diff --git a/src/Exercise/ExceptionalCoding.php b/src/Exercise/ExceptionalCoding.php
index d8ade58..afea832 100644
--- a/src/Exercise/ExceptionalCoding.php
+++ b/src/Exercise/ExceptionalCoding.php
@@ -8,25 +8,16 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use Symfony\Component\Filesystem\Filesystem;
class ExceptionalCoding extends AbstractExercise implements
ExerciseInterface,
CliExercise,
FunctionRequirementsExerciseCheck
{
- use TemporaryDirectoryTrait;
-
- private Filesystem $filesystem;
- private Generator $faker;
-
- public function __construct(Filesystem $filesystem, Generator $faker)
+ public function __construct(private Generator $faker)
{
- $this->filesystem = $filesystem;
- $this->faker = $faker;
}
public function getName(): string
@@ -39,31 +30,29 @@ public function getDescription(): string
return "Introduction to Exceptions";
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- $this->filesystem->mkdir($this->getTemporaryPath());
-
$fileCount = rand(2, 10);
- $realFiles = rand(1, $fileCount - 1);
+ $realFileCount = rand(1, $fileCount - 1);
$files = [];
+ $realFiles = [];
foreach (range(1, $fileCount) as $index) {
- $file = sprintf('%s/%s.txt', $this->getTemporaryPath(), $this->faker->uuid());
- if ($index <= $realFiles) {
- $this->filesystem->touch($file);
+ $file = $this->faker->uuid() . ".txt";
+ if ($index <= $realFileCount) {
+ $realFiles[] = $file;
}
$files[] = $file;
}
- return [$files];
- }
+ $scenario = (new CliScenario())
+ ->withExecution($files);
- public function tearDown(): void
- {
- $this->filesystem->remove($this->getTemporaryPath());
+ foreach ($realFiles as $realFile) {
+ $scenario->withFile($realFile, '');
+ }
+
+ return $scenario;
}
/**
@@ -87,8 +76,8 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CLI);
}
- public function configure(ExerciseDispatcher $dispatcher): void
+ public function getRequiredChecks(): array
{
- $dispatcher->requireCheck(FunctionRequirementsCheck::class);
+ return [FunctionRequirementsCheck::class];
}
}
diff --git a/src/Exercise/FilteredLs.php b/src/Exercise/FilteredLs.php
index 9a1bc1b..005280a 100644
--- a/src/Exercise/FilteredLs.php
+++ b/src/Exercise/FilteredLs.php
@@ -6,20 +6,10 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
-use Symfony\Component\Filesystem\Filesystem;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
class FilteredLs extends AbstractExercise implements ExerciseInterface, CliExercise
{
- use TemporaryDirectoryTrait;
-
- private Filesystem $filesystem;
-
- public function __construct(Filesystem $filesystem)
- {
- $this->filesystem = $filesystem;
- }
-
public function getName(): string
{
return 'Filtered LS';
@@ -30,13 +20,8 @@ public function getDescription(): string
return 'Read files in a folder and filter by a given extension';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- $folder = $this->getTemporaryPath();
-
$files = [
"learnyouphp.dat",
"learnyouphp.txt",
@@ -59,23 +44,20 @@ public function getArgs(): array
"dat",
];
- $this->filesystem->mkdir($folder);
- array_walk($files, function ($file) use ($folder) {
- $this->filesystem->dumpFile(sprintf('%s/%s', $folder, $file), '');
- });
-
$ext = '';
while ($ext === '') {
$index = array_rand($files);
$ext = pathinfo($files[$index], PATHINFO_EXTENSION);
}
- return [[$folder, $ext]];
- }
+ $scenario = (new CliScenario())
+ ->withExecution(['files', $ext]);
- public function tearDown(): void
- {
- $this->filesystem->remove($this->getTemporaryPath());
+ array_walk($files, function (string $file) use ($scenario) {
+ $scenario->withFile('files/' . $file, '');
+ });
+
+ return $scenario;
}
public function getType(): ExerciseType
diff --git a/src/Exercise/HelloWorld.php b/src/Exercise/HelloWorld.php
index c94c575..bbd7435 100644
--- a/src/Exercise/HelloWorld.php
+++ b/src/Exercise/HelloWorld.php
@@ -6,6 +6,7 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
class HelloWorld extends AbstractExercise implements ExerciseInterface, CliExercise
{
@@ -19,12 +20,9 @@ public function getDescription(): string
return 'Simple Hello World exercise';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- return [[]];
+ return (new CliScenario())->withExecution();
}
public function getType(): ExerciseType
diff --git a/src/Exercise/HttpJsonApi.php b/src/Exercise/HttpJsonApi.php
index 34bdffb..8167372 100644
--- a/src/Exercise/HttpJsonApi.php
+++ b/src/Exercise/HttpJsonApi.php
@@ -7,6 +7,7 @@
use PhpSchool\PhpWorkshop\Exercise\CgiExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CgiScenario;
use Psr\Http\Message\RequestInterface;
class HttpJsonApi extends AbstractExercise implements ExerciseInterface, CgiExercise
@@ -21,16 +22,17 @@ public function getDescription(): string
return 'HTTP JSON API - Servers JSON when it receives a GET request';
}
- /**
- * @inheritdoc
- */
- public function getRequests(): array
+ public function defineTestScenario(): CgiScenario
{
$url = 'http://www.time.com/api/%s?iso=%s';
- return [
- (new Request('GET', sprintf($url, 'parsetime', urlencode((new \DateTime())->format(DATE_ISO8601))))),
- (new Request('GET', sprintf($url, 'unixtime', urlencode((new \DateTime())->format(DATE_ISO8601)))))
- ];
+
+ return (new CgiScenario())
+ ->withExecution(
+ new Request('GET', sprintf($url, 'parsetime', urlencode((new \DateTime())->format(DATE_ISO8601))))
+ )
+ ->withExecution(
+ new Request('GET', sprintf($url, 'unixtime', urlencode((new \DateTime())->format(DATE_ISO8601))))
+ );
}
public function getType(): ExerciseType
diff --git a/src/Exercise/MyFirstIo.php b/src/Exercise/MyFirstIo.php
index a40f82c..bed0590 100644
--- a/src/Exercise/MyFirstIo.php
+++ b/src/Exercise/MyFirstIo.php
@@ -8,6 +8,7 @@
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
@@ -18,15 +19,8 @@ class MyFirstIo extends AbstractExercise implements
CliExercise,
FunctionRequirementsExerciseCheck
{
- use TemporaryDirectoryTrait;
-
- private Filesystem $filesystem;
- private Generator $faker;
-
- public function __construct(Filesystem $filesystem, Generator $faker)
+ public function __construct(private Generator $faker)
{
- $this->filesystem = $filesystem;
- $this->faker = $faker;
}
public function getName(): string
@@ -39,21 +33,14 @@ public function getDescription(): string
return 'Read a file from the file system';
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- $path = $this->getTemporaryPath();
+ $filename = bin2hex(random_bytes(4)) . '.txt';
$paragraphs = implode("\n\n", (array) $this->faker->paragraphs(rand(5, 50)));
- $this->filesystem->dumpFile($path, $paragraphs);
-
- return [[$path]];
- }
- public function tearDown(): void
- {
- $this->filesystem->remove($this->getTemporaryPath());
+ return (new CliScenario())
+ ->withExecution([$filename])
+ ->withFile($filename, $paragraphs);
}
/**
@@ -77,8 +64,8 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CLI);
}
- public function configure(ExerciseDispatcher $dispatcher): void
+ public function getRequiredChecks(): array
{
- $dispatcher->requireCheck(FunctionRequirementsCheck::class);
+ return [FunctionRequirementsCheck::class];
}
}
diff --git a/src/Exercise/TimeServer.php b/src/Exercise/TimeServer.php
index df2be90..ad5ee12 100644
--- a/src/Exercise/TimeServer.php
+++ b/src/Exercise/TimeServer.php
@@ -3,12 +3,13 @@
namespace PhpSchool\LearnYouPhp\Exercise;
use PhpSchool\PhpWorkshop\Event\CliExecuteEvent;
+use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Exception\RuntimeException;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
+use PhpSchool\PhpWorkshop\Exercise\Scenario\CliScenario;
use PhpSchool\PhpWorkshop\Output\OutputInterface;
use PhpSchool\PhpWorkshop\Result\ComparisonFailure;
use PhpSchool\PhpWorkshop\Result\Failure;
@@ -27,75 +28,103 @@ public function getDescription(): string
return 'Build a Time Server!';
}
- public function configure(ExerciseDispatcher $exerciseDispatcher): void
+ public function defineListeners(EventDispatcher $eventDispatcher): void
{
- $eventDispatcher = $exerciseDispatcher->getEventDispatcher();
+ $referencePort = $this->getRandomPort();
+ $studentPort = $this->getRandomPort();
+
+ $eventDispatcher->listen(
+ 'cli.verify.reference-execute.pre',
+ function (CliExecuteEvent $event) use ($referencePort) {
+ $event->appendArg('0.0.0.0');
+ $event->appendArg((string) $referencePort);
+ $event->getScenario()->exposePort($referencePort);
+ }
+ );
+ $eventDispatcher->listen(
+ ['cli.verify.student-execute.pre', 'cli.run.student-execute.pre'],
+ function (CliExecuteEvent $event) use ($studentPort) {
+ $event->appendArg('0.0.0.0');
+ $event->appendArg((string) $studentPort);
+ $event->getScenario()->exposePort($studentPort);
+ }
+ );
- $appendArgsListener = function (CliExecuteEvent $event) {
- $event->appendArg('127.0.0.1');
- $event->appendArg($this->getRandomPort());
- };
+ $eventDispatcher->listen(
+ 'cli.verify.reference.executing',
+ function (CliExecuteEvent $event) use ($referencePort) {
+ //wait for server to boot
+ sleep(1);
- $eventDispatcher->listen('cli.verify.reference-execute.pre', $appendArgsListener);
- $eventDispatcher->listen('cli.verify.student-execute.pre', $appendArgsListener);
- $eventDispatcher->listen('cli.run.student-execute.pre', $appendArgsListener);
+ $socket = $this->createSocket();
+ @socket_connect($socket, '0.0.0.0', $referencePort);
+ @socket_read($socket, 2048, PHP_NORMAL_READ);
- $eventDispatcher->listen('cli.verify.reference.executing', function (CliExecuteEvent $event) {
- $args = $event->getArgs()->getArrayCopy();
+ socket_close($socket);
- //wait for server to boot
- usleep(100000);
+ //wait for shutdown
+ usleep(100000);
+ }
+ );
- $socket = $this->createSocket();
- socket_connect($socket, $args[0], (int) $args[1]);
- socket_read($socket, 2048, PHP_NORMAL_READ);
+ $eventDispatcher->insertVerifier(
+ 'cli.verify.student.executing',
+ function (CliExecuteEvent $event) use ($studentPort) {
+ //wait for server to boot
+ sleep(1);
- //wait for shutdown
- usleep(100000);
- });
+ $socket = $this->createSocket();
- $eventDispatcher->insertVerifier('cli.verify.student.executing', function (CliExecuteEvent $event) {
- $args = $event->getArgs()->getArrayCopy();
+ $result = @socket_connect($socket, '0.0.0.0', $studentPort);
- //wait for server to boot
- usleep(100000);
+ if (!$result) {
+ $error = "Client returns an error (number %d): Connection refused ";
+ $error .= "while trying to join tcp://0.0.0.0:%d.";
- $socket = $this->createSocket();
- $connectResult = @socket_connect($socket, $args[0], (int) $args[1]);
-
- if (!$connectResult) {
- return Failure::fromNameAndReason($this->getName(), sprintf(
- "Client returns an error (number %d): Connection refused while trying to join tcp://127.0.0.1:%d.",
- socket_last_error($socket),
- $args[1]
- ));
- }
+ return Failure::fromNameAndReason($this->getName(), sprintf(
+ $error,
+ socket_last_error($socket),
+ $studentPort
+ ));
+ }
- $out = (string) socket_read($socket, 2048, PHP_NORMAL_READ);
+ $out = (string) socket_read($socket, 2048, PHP_NORMAL_READ);
- //wait for shutdown
- usleep(100000);
+ socket_close($socket);
- $date = new \DateTime();
+ //wait for shutdown
+ usleep(100000);
- //match the current date but any seconds
- //since we can't mock time in PHP easily
- if (!preg_match(sprintf('/^%s:([0-5][0-9]|60)\n$/', $date->format('Y-m-d H:i')), $out)) {
- return ComparisonFailure::fromNameAndValues($this->getName(), $date->format("Y-m-d H:i:s\n"), $out);
+ $date = new \DateTime();
+
+ //match the current date but any seconds
+ //since we can't mock time in PHP easily
+ if (!preg_match(sprintf('/^%s:([0-5][0-9]|60)\n$/', $date->format('Y-m-d H:i')), $out)) {
+ return ComparisonFailure::fromNameAndValues($this->getName(), $date->format("Y-m-d H:i:s\n"), $out);
+ }
+ return new Success($this->getName());
}
- return new Success($this->getName());
- });
+ );
- $eventDispatcher->listen('cli.run.student.executing', function (CliExecuteEvent $event) {
+ $eventDispatcher->listen('cli.run.student.executing', function (CliExecuteEvent $event) use ($studentPort) {
/** @var OutputInterface $output */
$output = $event->getParameter('output');
- $args = $event->getArgs()->getArrayCopy();
//wait for server to boot
- usleep(100000);
+ sleep(1);
$socket = $this->createSocket();
- socket_connect($socket, $args[0], (int) $args[1]);
+ try {
+ $connectResult = @socket_connect($socket, '0.0.0.0', $studentPort);
+ } catch (\ErrorException $e) {
+ $output->write('Cannot connect');
+ return;
+ }
+
+ if (false === $connectResult) {
+ $output->write('Cannot connect');
+ return;
+ }
$out = (string) socket_read($socket, 2048, PHP_NORMAL_READ);
//wait for shutdown
@@ -105,9 +134,18 @@ public function configure(ExerciseDispatcher $exerciseDispatcher): void
});
}
- private function getRandomPort(): string
+ private function getRandomPort(): int
{
- return (string) mt_rand(1025, 65535);
+ $sock = socket_create_listen(0);
+
+ if ($sock === false) {
+ throw new RuntimeException('Cannot create socket');
+ }
+
+ socket_getsockname($sock, $addr, $port);
+ socket_close($sock);
+
+ return $port;
}
public function getType(): ExerciseType
@@ -115,12 +153,10 @@ public function getType(): ExerciseType
return new ExerciseType(ExerciseType::CLI);
}
- /**
- * @inheritdoc
- */
- public function getArgs(): array
+ public function defineTestScenario(): CliScenario
{
- return [];
+ return (new CliScenario())
+ ->withExecution();
}
private function createSocket(): Socket
@@ -131,6 +167,8 @@ private function createSocket(): Socket
throw new RuntimeException('Cannot create socket');
}
+ socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => 5, "usec" => 0]);
+
return $socket;
}
}
diff --git a/test/Exercise/ArrayWeGoTest.php b/test/Exercise/ArrayWeGoTest.php
index 8653942..82a58da 100644
--- a/test/Exercise/ArrayWeGoTest.php
+++ b/test/Exercise/ArrayWeGoTest.php
@@ -3,35 +3,28 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
use Faker\Factory;
-use Faker\Generator;
use PhpSchool\LearnYouPhp\Exercise\ArrayWeGo;
-use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Filesystem\Filesystem;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\Result\FunctionRequirementsFailure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
-class ArrayWeGoTest extends TestCase
+class ArrayWeGoTest extends WorkshopExerciseTest
{
- /**
- * @var Generator
- */
- private $faker;
-
- /**
- * @var Filesystem
- */
- private $filesystem;
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
- public function setUp(): void
+ public function getExerciseClass(): string
{
- $this->faker = Factory::create();
- $this->filesystem = new Filesystem();
+ return ArrayWeGo::class;
}
- public function testArrWeGoExercise(): void
+ public function testExerciseMeta(): void
{
- $e = new ArrayWeGo($this->filesystem, $this->faker);
+ $e = new ArrayWeGo(Factory::create());
$this->assertEquals('Array We Go!', $e->getName());
$this->assertEquals('Filter an array of file paths and map to SplFile objects', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
@@ -39,69 +32,46 @@ public function testArrWeGoExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetArgsCreateAtLeastOneExistingFile(): void
+ public function testWithNoCode(): void
{
- $e = new ArrayWeGo($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
+ $this->runExercise('solution-no-code.php');
- $existingFiles = array_filter($args, 'file_exists');
+ $this->assertVerifyWasNotSuccessful();
- foreach ($existingFiles as $file) {
- $this->assertFileExists($file);
- }
-
- $this->assertGreaterThanOrEqual(1, count($existingFiles));
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
}
- public function testGetArgsHasAtLeastOneNonExistingFile(): void
+ public function testWithIncorrectOutput(): void
{
- $e = new ArrayWeGo($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
-
- $nonExistingFiles = array_filter($args, function ($arg) {
- return !file_exists($arg);
- });
+ $this->runExercise('solution-wrong-output.php');
- foreach ($nonExistingFiles as $file) {
- $this->assertFileDoesNotExist($file);
- }
+ $this->assertVerifyWasNotSuccessful();
- $this->assertGreaterThanOrEqual(1, count($nonExistingFiles));
+ $this->assertOutputWasIncorrect();
}
- public function testTearDownRemovesFile(): void
+ public function testFailureWhenNotUsingRequiredFunctions(): void
{
- $e = new ArrayWeGo($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
+ $this->runExercise('no-required-functions.php');
- $existingFiles = array_filter($args, 'file_exists');
+ $this->assertVerifyWasNotSuccessful();
- $this->assertFileExists($existingFiles[0]);
+ $this->assertOutputWasCorrect();
- $e->tearDown();
+ $this->assertResultsHasFailureAndMatches(
+ FunctionRequirementsFailure::class,
+ function (FunctionRequirementsFailure $failure) {
+ self::assertEquals(['array_shift', 'array_filter', 'array_map'], $failure->getMissingFunctions());
- $this->assertFileDoesNotExist($existingFiles[0]);
+ return true;
+ }
+ );
}
- public function testFunctionRequirements(): void
+ public function testWithCorrectSolution(): void
{
- $e = new ArrayWeGo($this->filesystem, $this->faker);
- $this->assertEquals(['array_shift', 'array_filter', 'array_map'], $e->getRequiredFunctions());
- $this->assertEquals(['basename'], $e->getBannedFunctions());
- }
-
- public function testConfigure(): void
- {
- $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dispatcher
- ->expects($this->once())
- ->method('requireCheck')
- ->with(FunctionRequirementsCheck::class);
+ $this->runExercise('solution-correct.php');
- $e = new ArrayWeGo($this->filesystem, $this->faker);
- $e->configure($dispatcher);
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/BabyStepsTest.php b/test/Exercise/BabyStepsTest.php
index 3e33e79..0e586ff 100644
--- a/test/Exercise/BabyStepsTest.php
+++ b/test/Exercise/BabyStepsTest.php
@@ -2,29 +2,56 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
use PHPUnit\Framework\TestCase;
use PhpSchool\LearnYouPhp\Exercise\BabySteps;
-class BabyStepsTest extends TestCase
+class BabyStepsTest extends WorkshopExerciseTest
{
- public function testBabyStepsExercise(): void
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return BabySteps::class;
+ }
+
+ public function testExerciseMeta(): void
{
$e = new BabySteps();
$this->assertEquals('Baby Steps', $e->getName());
$this->assertEquals('Simple Addition', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
+ $this->assertFileExists(realpath($e->getProblem()));
+ }
- //sometime we don't get any args as number of args is random
- //we need some args for code-coverage, so just try again
- do {
- $args = $e->getArgs()[0];
- } while (empty($args));
+ public function testWithNoCode(): void
+ {
+ $this->runExercise('solution-no-code.php');
- foreach ($args as $arg) {
- $this->assertIsNumeric($arg);
- }
+ $this->assertVerifyWasNotSuccessful();
- $this->assertFileExists(realpath($e->getProblem()));
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
+ }
+
+ public function testWithIncorrectOutput(): void
+ {
+ $this->runExercise('solution-wrong-output.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertOutputWasIncorrect();
+ }
+
+ public function testWithCorrectSolution(): void
+ {
+ $this->runExercise('solution-correct.php');
+
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/ConcernedAboutSeparationTest.php b/test/Exercise/ConcernedAboutSeparationTest.php
index 69616ed..be50d9b 100644
--- a/test/Exercise/ConcernedAboutSeparationTest.php
+++ b/test/Exercise/ConcernedAboutSeparationTest.php
@@ -2,37 +2,28 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
-use PhpParser\Parser;
use PhpParser\ParserFactory;
use PhpSchool\LearnYouPhp\Exercise\ConcernedAboutSeparation;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Input\Input;
use PhpSchool\PhpWorkshop\Result\Failure;
-use PhpSchool\PhpWorkshop\Result\Success;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Filesystem\Filesystem;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
-class ConcernedAboutSeparationTest extends TestCase
+class ConcernedAboutSeparationTest extends WorkshopExerciseTest
{
- /**
- * @var Filesystem
- */
- private $filesystem;
-
- /**
- * @var Parser
- */
- private $parser;
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
- public function setUp(): void
+ public function getExerciseClass(): string
{
- $this->filesystem = new Filesystem();
- $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
+ return ConcernedAboutSeparation::class;
}
- public function testConcernedAboutSeparationExercise(): void
+ public function testExerciseMeta(): void
{
- $e = new ConcernedAboutSeparation($this->filesystem, $this->parser);
+ $e = new ConcernedAboutSeparation((new ParserFactory())->create(ParserFactory::PREFER_PHP7));
$this->assertEquals('Concerned about Separation?', $e->getName());
$this->assertEquals('Separate code and utilise files and classes', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
@@ -40,74 +31,31 @@ public function testConcernedAboutSeparationExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetArgsCreatesFilesAndReturnsRandomExt(): void
+ public function testWithNoCode(): void
{
- $e = new ConcernedAboutSeparation($this->filesystem, $this->parser);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
-
- $files = [
- "learnyouphp.dat",
- "learnyouphp.txt",
- "learnyouphp.sql",
- "api.html",
- "README.md",
- "CHANGELOG.md",
- "LICENCE.md",
- "md",
- "data.json",
- "data.dat",
- "words.dat",
- "w00t.dat",
- "w00t.txt",
- "wrrrrongdat",
- "dat",
- ];
-
- array_walk($files, function ($file) use ($path) {
- $this->assertFileExists(sprintf('%s/%s', $path, $file));
- });
+ $this->runExercise('solution-no-code.php');
- $extensions = array_unique(array_map(function ($file) {
- return pathinfo($file, PATHINFO_EXTENSION);
- }, $files));
+ $this->assertVerifyWasNotSuccessful();
- $this->assertContains($args[1], $extensions);
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
}
- public function testTearDownRemovesFile(): void
+ public function testFailureWhenNotUsingInclude(): void
{
- $e = new ConcernedAboutSeparation($this->filesystem, $this->parser);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
+ $this->runExercise('no-include.php');
- $e->tearDown();
+ $this->assertVerifyWasNotSuccessful();
- $this->assertFileDoesNotExist($path);
- }
-
- public function testCheckReturnsFailureIfNoIncludeFoundInSolution(): void
- {
- $e = new ConcernedAboutSeparation($this->filesystem, $this->parser);
- $failure = $e->check(
- new Input('learnyouphp', ['program' => __DIR__ . '/../res/concerned-about-separation/no-include.php'])
+ $this->assertResultsHasFailure(
+ Failure::class,
+ 'No require statement found'
);
-
- $this->assertInstanceOf(Failure::class, $failure);
- $this->assertEquals('No require statement found', $failure->getReason());
- $this->assertEquals('Concerned about Separation?', $failure->getCheckName());
}
- public function testCheckReturnsSuccessIfIncludeFound(): void
+ public function testWithCorrectSolution(): void
{
- $e = new ConcernedAboutSeparation($this->filesystem, $this->parser);
- $success = $e->check(
- new Input('learnyouphp', ['program' => __DIR__ . '/../res/concerned-about-separation/include.php'])
- );
+ $this->runExercise('correct/solution.php', self::DIRECTORY_SOLUTION);
- $this->assertInstanceOf(Success::class, $success);
- $this->assertEquals('Concerned about Separation?', $success->getCheckName());
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/DatabaseReadTest.php b/test/Exercise/DatabaseReadTest.php
index 152a222..d571c57 100644
--- a/test/Exercise/DatabaseReadTest.php
+++ b/test/Exercise/DatabaseReadTest.php
@@ -6,22 +6,39 @@
use Faker\Generator;
use PDO;
use PhpSchool\LearnYouPhp\Exercise\DatabaseRead;
-use PhpSchool\PhpWorkshop\Check\DatabaseCheck;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
-class DatabaseReadTest extends TestCase
+class DatabaseReadTest extends WorkshopExerciseTest
{
- /**
- * @var Generator
- */
- private $faker;
+ private Generator $faker;
public function setUp(): void
{
$this->faker = Factory::create();
+ parent::setUp();
+ }
+
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return DatabaseRead::class;
+ }
+
+ public function testExerciseMeta(): void
+ {
+ $e = new DatabaseRead($this->faker);
+ $this->assertEquals('Database Read', $e->getName());
+ $this->assertEquals('Read an SQL databases contents', $e->getDescription());
+ $this->assertEquals(ExerciseType::CLI, $e->getType());
+
+ $this->assertFileExists(realpath($e->getProblem()));
}
public function testDatabaseExercise(): void
@@ -42,13 +59,13 @@ public function testSeedAddsRandomUsersToDatabaseAndStoresRandomIdAndName(): voi
$e->seed($db);
- $args = $e->getArgs()[0];
+ $scenario = $e->defineTestScenario();
$stmt = $db->query('SELECT * FROM users;');
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$this->assertTrue(count($users) >= 5);
$this->assertIsArray($users);
- $this->assertContains($args[0], array_column($users, 'name'));
+ $this->assertContains($scenario->getExecutions()[0]->get(0), array_column($users, 'name'));
}
public function testVerifyReturnsTrueIfRecordExistsWithNameUsingStoredId(): void
@@ -69,18 +86,37 @@ public function testVerifyReturnsTrueIfRecordExistsWithNameUsingStoredId(): void
$this->assertTrue($e->verify($db));
}
- public function testConfigure(): void
+ public function testWithNoCode(): void
{
- $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->runExercise('solution-no-code.php');
- $dispatcher
- ->expects($this->once())
- ->method('requireCheck')
- ->with(DatabaseCheck::class);
+ $this->assertVerifyWasNotSuccessful();
- $e = new DatabaseRead($this->faker);
- $e->configure($dispatcher);
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
+ }
+
+ public function testWithIncorrectOutput(): void
+ {
+ $this->runExercise('solution-wrong-output.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertOutputWasIncorrect();
+ }
+
+ public function testFailureWhenNameIsNotUpdated(): void
+ {
+ $this->runExercise('solution-wrong-update.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertResultsHasFailure(Failure::class, 'Database verification failed');
+ }
+
+ public function testWithCorrectSolution(): void
+ {
+ $this->runExercise('solution-correct.php');
+
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/DependencyHeavenTest.php b/test/Exercise/DependencyHeavenTest.php
index 35efa56..eab3404 100644
--- a/test/Exercise/DependencyHeavenTest.php
+++ b/test/Exercise/DependencyHeavenTest.php
@@ -20,10 +20,7 @@
class DependencyHeavenTest extends WorkshopExerciseTest
{
- /**
- * @var Generator
- */
- private $faker;
+ private Generator $faker;
public function setUp(): void
{
@@ -42,58 +39,6 @@ public function testDependencyHeavenExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetRequiredPackages(): void
- {
- $this->assertSame(
- ['league/route', 'laminas/laminas-diactoros', 'laminas/laminas-httphandlerrunner', 'symfony/string'],
- (new DependencyHeaven($this->faker))->getRequiredPackages()
- );
- }
-
- public function testGetRequests(): void
- {
- $e = new DependencyHeaven($this->faker);
-
- $requests = $e->getRequests();
-
- foreach ($requests as $request) {
- $this->assertInstanceOf(RequestInterface::class, $request);
- $this->assertSame('POST', $request->getMethod());
- $this->assertSame(['application/x-www-form-urlencoded'], $request->getHeader('Content-Type'));
- $this->assertNotEmpty($request->getBody());
- }
- }
-
- public function testGetRequestsReturnsMultipleRequestsForEachEndpoint(): void
- {
- $e = new DependencyHeaven($this->faker);
-
- $endPoints = array_map(function (RequestInterface $request) {
- return $request->getUri()->getPath();
- }, $e->getRequests());
-
- $counts = array_count_values($endPoints);
- foreach (['/reverse', '/snake', '/titleize'] as $endPoint) {
- $this->assertTrue(isset($counts[$endPoint]));
- $this->assertGreaterThan(1, $counts[$endPoint]);
- }
- }
-
- public function testConfigure(): void
- {
- $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dispatcher
- ->expects($this->once())
- ->method('requireCheck')
- ->with(ComposerCheck::class);
-
- $e = new DependencyHeaven($this->faker);
- $e->configure($dispatcher);
- }
-
public function getExerciseClass(): string
{
return DependencyHeaven::class;
@@ -106,7 +51,7 @@ public function getApplication(): Application
public function testWithNoComposerFile(): void
{
- $this->runExercise('no-composer/solution.php');
+ $this->runExercise('no-composer/solution.php', self::DIRECTORY_SOLUTION);
$this->assertVerifyWasNotSuccessful();
$this->assertResultsHasFailureAndMatches(
@@ -119,7 +64,7 @@ function (ComposerFailure $failure) {
public function testWithNoCode(): void
{
- $this->runExercise('no-code/solution.php');
+ $this->runExercise('no-code/solution.php', self::DIRECTORY_SOLUTION);
$this->assertVerifyWasNotSuccessful();
@@ -128,7 +73,7 @@ public function testWithNoCode(): void
public function testWithWrongEndpoint(): void
{
- $this->runExercise('wrong-endpoint/solution.php');
+ $this->runExercise('wrong-endpoint/solution.php', self::DIRECTORY_SOLUTION);
$this->assertVerifyWasNotSuccessful();
@@ -158,7 +103,7 @@ public function testWithWrongEndpoint(): void
public function testWithCorrectSolution(): void
{
- $this->runExercise('correct-solution/solution.php');
+ $this->runExercise('correct-solution/solution.php', self::DIRECTORY_SOLUTION);
$this->assertVerifyWasSuccessful();
}
diff --git a/test/Exercise/ExceptionalCodingTest.php b/test/Exercise/ExceptionalCodingTest.php
index 39ad564..38fc9fe 100644
--- a/test/Exercise/ExceptionalCodingTest.php
+++ b/test/Exercise/ExceptionalCodingTest.php
@@ -5,33 +5,36 @@
use Faker\Factory;
use Faker\Generator;
use PhpSchool\LearnYouPhp\Exercise\ExceptionalCoding;
-use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Filesystem\Filesystem;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\Result\FunctionRequirementsFailure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
-class ExceptionalCodingTest extends TestCase
+class ExceptionalCodingTest extends WorkshopExerciseTest
{
- /**
- * @var Generator
- */
- private $faker;
+ private Generator $faker;
- /**
- * @var Filesystem
- */
- private $filesystem;
public function setUp(): void
{
$this->faker = Factory::create();
- $this->filesystem = new Filesystem();
+ parent::setUp();
}
- public function testArrWeGoExercise(): void
+ public function getApplication(): Application
{
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return ExceptionalCoding::class;
+ }
+
+ public function testExerciseMeta(): void
+ {
+ $e = new ExceptionalCoding($this->faker);
$this->assertEquals('Exceptional Coding', $e->getName());
$this->assertEquals('Introduction to Exceptions', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
@@ -39,69 +42,46 @@ public function testArrWeGoExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetArgsCreateAtleastOneExistingFile(): void
+ public function testWithNoCode(): void
{
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
+ $this->runExercise('solution-no-code.php');
- $existingFiles = array_filter($args, 'file_exists');
+ $this->assertVerifyWasNotSuccessful();
- foreach ($existingFiles as $file) {
- $this->assertFileExists($file);
- }
-
- $this->assertGreaterThanOrEqual(1, count($existingFiles));
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
}
- public function testGetArgsHasAtleastOneNonExistingFile(): void
+ public function testWithIncorrectOutput(): void
{
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
-
- $nonExistingFiles = array_filter($args, function ($arg) {
- return !file_exists($arg);
- });
+ $this->runExercise('solution-wrong-output.php');
- foreach ($nonExistingFiles as $file) {
- $this->assertFileDoesNotExist($file);
- }
+ $this->assertVerifyWasNotSuccessful();
- $this->assertGreaterThanOrEqual(1, count($nonExistingFiles));
+ $this->assertOutputWasIncorrect();
}
- public function testTearDownRemovesFile(): void
+ public function testFailureWhenNotUsingRequiredFunctions(): void
{
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
-
- $existingFiles = array_filter($args, 'file_exists');
+ $this->runExercise('solution-banned-functions.php');
- $this->assertFileExists($existingFiles[0]);
+ $this->assertVerifyWasNotSuccessful();
- $e->tearDown();
+ $this->assertOutputWasCorrect();
- $this->assertFileDoesNotExist($existingFiles[0]);
- }
+ $this->assertResultsHasFailureAndMatches(
+ FunctionRequirementsFailure::class,
+ function (FunctionRequirementsFailure $failure) {
+ self::assertEquals([['function' => 'file_exists', 'line' => 7]], $failure->getBannedFunctions());
- public function testFunctionRequirements(): void
- {
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
- $this->assertEquals([], $e->getRequiredFunctions());
- $this->assertEquals(['array_filter', 'file_exists'], $e->getBannedFunctions());
+ return true;
+ }
+ );
}
- public function testConfigure(): void
+ public function testWithCorrectSolution(): void
{
- $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dispatcher
- ->expects($this->once())
- ->method('requireCheck')
- ->with(FunctionRequirementsCheck::class);
+ $this->runExercise('solution-correct.php');
- $e = new ExceptionalCoding($this->filesystem, $this->faker);
- $e->configure($dispatcher);
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/FilteredLsTest.php b/test/Exercise/FilteredLsTest.php
index fee3b33..80df6d0 100644
--- a/test/Exercise/FilteredLsTest.php
+++ b/test/Exercise/FilteredLsTest.php
@@ -2,26 +2,27 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
use PhpSchool\LearnYouPhp\Exercise\FilteredLs;
-use Symfony\Component\Filesystem\Filesystem;
-class FilteredLsTest extends TestCase
+class FilteredLsTest extends WorkshopExerciseTest
{
- /**
- * @var Filesystem
- */
- private $filesystem;
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
- public function setUp(): void
+ public function getExerciseClass(): string
{
- $this->filesystem = new Filesystem();
+ return FilteredLs::class;
}
- public function testFilteredLsExercise(): void
+ public function testExerciseMeta(): void
{
- $e = new FilteredLs($this->filesystem);
+ $e = new FilteredLs();
$this->assertEquals('Filtered LS', $e->getName());
$this->assertEquals('Read files in a folder and filter by a given extension', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
@@ -29,51 +30,28 @@ public function testFilteredLsExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetArgsCreatesFilesAndReturnsRandomExt(): void
+ public function testWithNoCode(): void
{
- $e = new FilteredLs($this->filesystem);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
+ $this->runExercise('solution-no-code.php');
+
+ $this->assertVerifyWasNotSuccessful();
- $files = [
- "learnyouphp.dat",
- "learnyouphp.txt",
- "learnyouphp.sql",
- "api.html",
- "README.md",
- "CHANGELOG.md",
- "LICENCE.md",
- "md",
- "data.json",
- "data.dat",
- "words.dat",
- "w00t.dat",
- "w00t.txt",
- "wrrrrongdat",
- "dat",
- ];
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
+ }
- array_walk($files, function ($file) use ($path) {
- $this->assertFileExists(sprintf('%s/%s', $path, $file));
- });
+ public function testWithIncorrectOutput(): void
+ {
+ $this->runExercise('solution-wrong-output.php');
- $extensions = array_unique(array_map(function ($file) {
- return pathinfo($file, PATHINFO_EXTENSION);
- }, $files));
+ $this->assertVerifyWasNotSuccessful();
- $this->assertContains($args[1], $extensions);
+ $this->assertOutputWasIncorrect();
}
- public function testTearDownRemovesFile(): void
+ public function testWithCorrectSolution(): void
{
- $e = new FilteredLs($this->filesystem);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
-
- $e->tearDown();
+ $this->runExercise('solution-correct.php');
- $this->assertFileDoesNotExist($path);
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/HelloWorldTest.php b/test/Exercise/HelloWorldTest.php
index b5e57e7..ad85d90 100644
--- a/test/Exercise/HelloWorldTest.php
+++ b/test/Exercise/HelloWorldTest.php
@@ -2,22 +2,55 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
use PhpSchool\LearnYouPhp\Exercise\HelloWorld;
-class HelloWorldTest extends TestCase
+class HelloWorldTest extends WorkshopExerciseTest
{
- public function testHelloWorldExercise(): void
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return HelloWorld::class;
+ }
+
+ public function testExerciseMeta(): void
{
$e = new HelloWorld();
$this->assertEquals('Hello World', $e->getName());
$this->assertEquals('Simple Hello World exercise', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
+ $this->assertFileExists(realpath($e->getProblem()));
+ }
- $this->assertEquals([], $e->getArgs()[0]);
+ public function testWithNoCode(): void
+ {
+ $this->runExercise('solution-no-code.php');
- $this->assertFileExists(realpath($e->getProblem()));
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
+ }
+
+ public function testWithIncorrectOutput(): void
+ {
+ $this->runExercise('solution-wrong-output.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertOutputWasIncorrect();
+ }
+
+ public function testWithCorrectSolution(): void
+ {
+ $this->runExercise('solution-correct.php');
+
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/HttpJsonApiTest.php b/test/Exercise/HttpJsonApiTest.php
index b83dded..d3d2584 100644
--- a/test/Exercise/HttpJsonApiTest.php
+++ b/test/Exercise/HttpJsonApiTest.php
@@ -3,34 +3,56 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
use PhpSchool\LearnYouPhp\Exercise\HttpJsonApi;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
use Psr\Http\Message\RequestInterface;
-class HttpJsonApiTest extends TestCase
+class HttpJsonApiTest extends WorkshopExerciseTest
{
- public function testHttpJsonApiExercise(): void
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return HttpJsonApi::class;
+ }
+
+ public function testExerciseMeta(): void
{
$e = new HttpJsonApi();
$this->assertEquals('HTTP JSON API', $e->getName());
$this->assertEquals('HTTP JSON API - Servers JSON when it receives a GET request', $e->getDescription());
$this->assertEquals(ExerciseType::CGI, $e->getType());
- $requests = $e->getRequests();
- $request1 = $requests[0];
- $request2 = $requests[1];
+ $this->assertFileExists(realpath($e->getProblem()));
+ }
- $this->assertInstanceOf(RequestInterface::class, $request1);
- $this->assertInstanceOf(RequestInterface::class, $request2);
+ public function testWithNoCode(): void
+ {
+ $this->runExercise('solution-no-code.php');
- $this->assertSame('GET', $request1->getMethod());
- $this->assertSame('GET', $request2->getMethod());
- $this->assertSame('www.time.com', $request1->getUri()->getHost());
- $this->assertSame('www.time.com', $request2->getUri()->getHost());
- $this->assertSame('/api/parsetime', $request1->getUri()->getPath());
- $this->assertSame('/api/unixtime', $request2->getUri()->getPath());
+ $this->assertVerifyWasNotSuccessful();
- $this->assertFileExists(realpath($e->getProblem()));
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
+ }
+
+ public function testWithIncorrectOutput(): void
+ {
+ $this->runExercise('solution-wrong-output.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertOutputWasIncorrect();
+ }
+
+ public function testWithCorrectSolution(): void
+ {
+ $this->runExercise('solution-correct.php');
+
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/MyFirstIoTest.php b/test/Exercise/MyFirstIoTest.php
index ab2a4d9..14b0804 100644
--- a/test/Exercise/MyFirstIoTest.php
+++ b/test/Exercise/MyFirstIoTest.php
@@ -4,35 +4,36 @@
use Faker\Factory;
use Faker\Generator;
-use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\Result\Failure;
+use PhpSchool\PhpWorkshop\Result\FunctionRequirementsFailure;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
use PhpSchool\LearnYouPhp\Exercise\MyFirstIo;
-use Symfony\Component\Filesystem\Filesystem;
-class MyFirstIoTest extends TestCase
+class MyFirstIoTest extends WorkshopExerciseTest
{
- /**
- * @var Generator
- */
- private $faker;
-
- /**
- * @var Filesystem
- */
- private $filesystem;
+ private Generator $faker;
public function setUp(): void
{
$this->faker = Factory::create();
- $this->filesystem = new Filesystem();
+ parent::setUp();
+ }
+
+ public function getApplication(): Application
+ {
+ return require __DIR__ . '/../../app/bootstrap.php';
+ }
+
+ public function getExerciseClass(): string
+ {
+ return MyFirstIo::class;
}
- public function testMyFirstIoExercise(): void
+ public function testExerciseMeta(): void
{
- $e = new MyFirstIo($this->filesystem, $this->faker);
+ $e = new MyFirstIo($this->faker);
$this->assertEquals('My First IO', $e->getName());
$this->assertEquals('Read a file from the file system', $e->getDescription());
$this->assertEquals(ExerciseType::CLI, $e->getType());
@@ -40,55 +41,47 @@ public function testMyFirstIoExercise(): void
$this->assertFileExists(realpath($e->getProblem()));
}
- public function testGetArgsCreatesFileWithRandomContentFromFake(): void
+ public function testWithNoCode(): void
{
- $e = new MyFirstIo($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
+ $this->runExercise('solution-no-code.php');
- $content1 = file_get_contents($path);
- unlink($path);
+ $this->assertVerifyWasNotSuccessful();
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
-
- $content2 = file_get_contents($path);
- $this->assertNotEquals($content1, $content2);
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
}
- public function testTearDownRemovesFile(): void
+ public function testWithIncorrectOutput(): void
{
- $e = new MyFirstIo($this->filesystem, $this->faker);
- $args = $e->getArgs()[0];
- $path = $args[0];
- $this->assertFileExists($path);
+ $this->runExercise('solution-wrong-output.php');
- $e->tearDown();
+ $this->assertVerifyWasNotSuccessful();
- $this->assertFileDoesNotExist($path);
+ $this->assertOutputWasIncorrect();
}
- public function testFunctionRequirements(): void
+ public function testFailureWhenNotUsingRequiredFunctions(): void
{
- $e = new MyFirstIo($this->filesystem, $this->faker);
- $this->assertEquals(['file_get_contents'], $e->getRequiredFunctions());
- $this->assertEquals(['file'], $e->getBannedFunctions());
+ $this->runExercise('wrong-function-requirements.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertOutputWasCorrect();
+
+ $this->assertResultsHasFailureAndMatches(
+ FunctionRequirementsFailure::class,
+ function (FunctionRequirementsFailure $failure) {
+ self::assertEquals(['file_get_contents'], $failure->getMissingFunctions());
+ self::assertEquals([['function' => 'file', 'line' => 3]], $failure->getBannedFunctions());
+
+ return true;
+ }
+ );
}
- public function testConfigure(): void
+ public function testWithCorrectSolution(): void
{
- $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dispatcher
- ->expects($this->once())
- ->method('requireCheck')
- ->with(FunctionRequirementsCheck::class);
+ $this->runExercise('solution-correct.php');
- $e = new MyFirstIo($this->filesystem, $this->faker);
- $e->configure($dispatcher);
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/Exercise/TimeServerTest.php b/test/Exercise/TimeServerTest.php
index ac8e733..ea976d6 100644
--- a/test/Exercise/TimeServerTest.php
+++ b/test/Exercise/TimeServerTest.php
@@ -2,127 +2,86 @@
namespace PhpSchool\LearnYouPhpTest\Exercise;
-use Colors\Color;
use PhpSchool\LearnYouPhp\Exercise\TimeServer;
-use PhpSchool\PhpWorkshop\Check\CheckRepository;
-use PhpSchool\PhpWorkshop\Check\PhpLintCheck;
-use PhpSchool\PhpWorkshop\Event\EventDispatcher;
+use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
-use PhpSchool\PhpWorkshop\ExerciseDispatcher;
-use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner;
-use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CliRunnerFactory;
-use PhpSchool\PhpWorkshop\ExerciseRunner\RunnerManager;
use PhpSchool\PhpWorkshop\Input\Input;
-use PhpSchool\PhpWorkshop\Output\StdOutput;
use PhpSchool\PhpWorkshop\Result\ComparisonFailure;
use PhpSchool\PhpWorkshop\Result\Failure;
-use PhpSchool\PhpWorkshop\Result\Success;
-use PhpSchool\PhpWorkshop\ResultAggregator;
-use PhpSchool\Terminal\Terminal;
-use PHPUnit\Framework\TestCase;
+use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
-class TimeServerTest extends TestCase
+class TimeServerTest extends WorkshopExerciseTest
{
- /**
- * @var TimeServer
- */
- private $exercise;
-
- /**
- * @var ExerciseDispatcher
- */
- private $exerciseDispatcher;
-
- public function setUp(): void
+ public function getApplication(): Application
{
- $results = new ResultAggregator();
- $eventDispatcher = new EventDispatcher($results);
-
- $this->exercise = new TimeServer();
- $runner = new CliRunner($this->exercise, $eventDispatcher);
-
- $r = new \ReflectionClass($runner);
- $rp = $r->getProperty('requiredChecks');
- $rp->setAccessible(true);
- $rp->setValue($runner, []);
-
- $runnerFactory = $this->createPartialMock(CliRunnerFactory::class, ['create']);
- $runnerFactory->method('create')->willReturn($runner);
- $runnerManager = new RunnerManager();
- $runnerManager->addFactory($runnerFactory);
- $this->exerciseDispatcher = new ExerciseDispatcher(
- $runnerManager,
- $results,
- $eventDispatcher,
- new CheckRepository([new PhpLintCheck()])
- );
+ return require __DIR__ . '/../../app/bootstrap.php';
}
- public function testGetters(): void
+ public function getExerciseClass(): string
{
- $this->assertEquals('Time Server', $this->exercise->getName());
- $this->assertEquals('Build a Time Server!', $this->exercise->getDescription());
- $this->assertEquals(ExerciseType::CLI, $this->exercise->getType());
-
- $this->assertFileExists(realpath($this->exercise->getProblem()));
+ return TimeServer::class;
}
- public function testFailureIsReturnedIfCannotConnect(): void
+ public function testExerciseMeta(): void
{
- $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/no-server.php']);
- $results = $this->exerciseDispatcher->verify($this->exercise, $input);
- $this->assertCount(2, $results);
-
- $failure = iterator_to_array($results)[0];
- $this->assertInstanceOf(Failure::class, $failure);
-
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
- $reason = '/^Client returns an error \(number \d+\): No connection could be made because';
- $reason .= ' the target machine actively refused it\.\r\n';
- $reason .= ' while trying to join tcp:\/\/127\.0\.0\.1:\d+\.$/';
- } else {
- $reason = '/^Client returns an error \(number \d+\): Connection refused';
- $reason .= ' while trying to join tcp:\/\/127\.0\.0\.1:\d+\.$/';
- }
-
- $this->assertMatchesRegularExpression($reason, $failure->getReason());
- $this->assertEquals('Time Server', $failure->getCheckName());
+ $e = new TimeServer();
+
+ $this->assertEquals('Time Server', $e->getName());
+ $this->assertEquals('Build a Time Server!', $e->getDescription());
+ $this->assertEquals(ExerciseType::CLI, $e->getType());
+
+ $this->assertFileExists(realpath($e->getProblem()));
}
- public function testFailureIsReturnedIfOutputWasNotCorrect(): void
+ public function testWithNoCode(): void
{
- $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution-wrong.php']);
- $results = $this->exerciseDispatcher->verify($this->exercise, $input);
+ $this->runExercise('solution-no-code.php');
- $this->assertCount(2, $results);
- $failure = iterator_to_array($results)[0];
+ $this->assertVerifyWasNotSuccessful();
- $this->assertInstanceOf(ComparisonFailure::class, $failure);
- $this->assertNotEquals($failure->getExpectedValue(), $failure->getActualValue());
- $this->assertEquals('Time Server', $failure->getCheckName());
+ $this->assertResultsHasFailure(Failure::class, 'No code was found');
}
- public function testSuccessIsReturnedIfOutputIsCorrect(): void
+ public function testFailureWhenCannotConnect(): void
{
- $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution.php']);
- $results = $this->exerciseDispatcher->verify($this->exercise, $input);
+ $this->runExercise('solution-no-server.php');
- $this->assertCount(2, $results);
- $success = iterator_to_array($results)[0];
- $this->assertInstanceOf(Success::class, $success);
+ $this->assertVerifyWasNotSuccessful();
+
+ $reason = '/^Client returns an error \(number \d+\): Connection refused';
+ $reason .= ' while trying to join tcp:\/\/0\.0\.0\.0:\d+\.$/';
+
+ $this->assertResultsHasFailureAndMatches(Failure::class, function (Failure $failure) use ($reason) {
+ $this->assertMatchesRegularExpression($reason, $failure->getReason());
+
+ return true;
+ });
}
- public function testRun(): void
+ public function testWithIncorrectOutput(): void
{
- $color = new Color();
- $color->setForceStyle(true);
- $output = new StdOutput($color, $terminal = $this->createMock(Terminal::class));
+ $this->runExercise('solution-wrong-output.php');
+
+ $this->assertVerifyWasNotSuccessful();
+
+ $this->assertResultsHasFailureAndMatches(ComparisonFailure::class, function (ComparisonFailure $failure) {
+ static::assertMatchesRegularExpression(
+ '/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\\n$/',
+ $failure->getExpectedValue()
+ );
+ static::assertMatchesRegularExpression(
+ '/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}\\n$/',
+ $failure->getActualValue()
+ );
+
+ return true;
+ });
+ }
- $outputRegEx = '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}';
- $outputRegEx .= "\n/";
- $this->expectOutputRegex($outputRegEx);
+ public function testWithCorrectSolution(): void
+ {
+ $this->runExercise('solution-correct.php');
- $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution.php']);
- $this->exerciseDispatcher->run($this->exercise, $input, $output);
+ $this->assertVerifyWasSuccessful();
}
}
diff --git a/test/res/concerned-about-separation/include.php b/test/res/concerned-about-separation/include.php
deleted file mode 100644
index 3df0360..0000000
--- a/test/res/concerned-about-separation/include.php
+++ /dev/null
@@ -1,3 +0,0 @@
-getBasename());
+}
diff --git a/test/res/time-server/no-server.php b/test/solutions/array-we-go/solution-no-code.php
similarity index 100%
rename from test/res/time-server/no-server.php
rename to test/solutions/array-we-go/solution-no-code.php
diff --git a/test/solutions/array-we-go/solution-wrong-output.php b/test/solutions/array-we-go/solution-wrong-output.php
new file mode 100644
index 0000000..1978d73
--- /dev/null
+++ b/test/solutions/array-we-go/solution-wrong-output.php
@@ -0,0 +1,12 @@
+getBasename());
+}
diff --git a/test/solutions/baby-steps/solution-correct.php b/test/solutions/baby-steps/solution-correct.php
new file mode 100644
index 0000000..a95d415
--- /dev/null
+++ b/test/solutions/baby-steps/solution-correct.php
@@ -0,0 +1,8 @@
+getFiles($argv[1], $argv[2]));
\ No newline at end of file
diff --git a/test/solutions/concerned-about-separation/no-include.php b/test/solutions/concerned-about-separation/no-include.php
new file mode 100644
index 0000000..9dd8a50
--- /dev/null
+++ b/test/solutions/concerned-about-separation/no-include.php
@@ -0,0 +1,23 @@
+getFiles($argv[1], $argv[2]));
\ No newline at end of file
diff --git a/test/solutions/concerned-about-separation/solution-no-code.php b/test/solutions/concerned-about-separation/solution-no-code.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/test/solutions/concerned-about-separation/solution-no-code.php
@@ -0,0 +1 @@
+query('SELECT * FROM users WHERE age > 30');
+foreach ($users as $user) {
+ echo "User: {$user['name']} Age: {$user['age']} Sex: {$user['gender']}\n";
+}
+$nameToUpdate = $argv[2];
+$stmt = $db->prepare('UPDATE users SET name = :newName WHERE name = :oldName');
+$stmt->execute([':newName' => 'David Attenborough', ':oldName' => $nameToUpdate]);
\ No newline at end of file
diff --git a/test/solutions/database-read/solution-no-code.php b/test/solutions/database-read/solution-no-code.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/test/solutions/database-read/solution-no-code.php
@@ -0,0 +1 @@
+query('SELECT * FROM users WHERE age > 30');
+foreach ($users as $user) {
+ echo "User: {$user['name']}\n";
+}
+$nameToUpdate = $argv[2];
+$stmt = $db->prepare('UPDATE users SET name = :newName WHERE name = :oldName');
+$stmt->execute([':newName' => 'David Attenborough', ':oldName' => $nameToUpdate]);
\ No newline at end of file
diff --git a/test/solutions/database-read/solution-wrong-update.php b/test/solutions/database-read/solution-wrong-update.php
new file mode 100644
index 0000000..f66de34
--- /dev/null
+++ b/test/solutions/database-read/solution-wrong-update.php
@@ -0,0 +1,10 @@
+query('SELECT * FROM users WHERE age > 30');
+foreach ($users as $user) {
+ echo "User: {$user['name']} Age: {$user['age']} Sex: {$user['gender']}\n";
+}
+$nameToUpdate = $argv[2];
+$stmt = $db->prepare('UPDATE users SET name = :newName WHERE name = :oldName');
+$stmt->execute([':newName' => 'Wrong name', ':oldName' => $nameToUpdate]);
\ No newline at end of file
diff --git a/test/solutions/dependency-heaven/no-composer/solution.php b/test/solutions/dependency-heaven/no-composer/solution.php
index a814366..5c881ba 100644
--- a/test/solutions/dependency-heaven/no-composer/solution.php
+++ b/test/solutions/dependency-heaven/no-composer/solution.php
@@ -1 +1,3 @@
-getBasename());
+ } catch (RuntimeException $e) {
+ echo sprintf("Unable to open file at path '%s'\n", $filePath);
+ }
+}
diff --git a/test/solutions/exceptional-coding/solution-no-code.php b/test/solutions/exceptional-coding/solution-no-code.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/test/solutions/exceptional-coding/solution-no-code.php
@@ -0,0 +1 @@
+getExtension() === $argv[2]) {
+ echo $file->getFilename() . "\n";
+ }
+}
diff --git a/test/solutions/filtered-ls/solution-no-code.php b/test/solutions/filtered-ls/solution-no-code.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/test/solutions/filtered-ls/solution-no-code.php
@@ -0,0 +1 @@
+getFilename() . "\n";
+}
diff --git a/test/solutions/hello-world/solution-correct.php b/test/solutions/hello-world/solution-correct.php
new file mode 100644
index 0000000..76b91b1
--- /dev/null
+++ b/test/solutions/hello-world/solution-correct.php
@@ -0,0 +1,3 @@
+ $date->format('H'),
+ "minute" => $date->format('i'),
+ "second" => $date->format('s')
+ ]);
+ exit;
+ }
+}
+
+if ($urlParts['path'] === '/api/unixtime') {
+ if (isset($_GET['iso'])) {
+ $date = new \DateTime($_GET['iso']);
+ echo json_encode(["unixtime" => $date->format('U')]);
+ exit;
+ }
+}
diff --git a/test/solutions/http-json-api/solution-no-code.php b/test/solutions/http-json-api/solution-no-code.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/test/solutions/http-json-api/solution-no-code.php
@@ -0,0 +1 @@
+ $date->format('H'),
+ "minutes" => $date->format('i'),
+ "seconds" => $date->format('s')
+ ]);
+ exit;
+ }
+}
+
+if ($urlParts['path'] === '/api/unixtime') {
+ if (isset($_GET['iso'])) {
+ $date = new \DateTime($_GET['iso']);
+ echo json_encode(["unixtime" => $date->format('U')]);
+ exit;
+ }
+}
diff --git a/test/solutions/my-first-io/solution-correct.php b/test/solutions/my-first-io/solution-correct.php
new file mode 100644
index 0000000..ef663a5
--- /dev/null
+++ b/test/solutions/my-first-io/solution-correct.php
@@ -0,0 +1,4 @@
+