Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ parameters:
- stubs/Colors/Color.stub
- stubs/Kadet/KeyLighter.stub
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
# public helper for downstream workshops, not used within the framework itself
identifier: trait.unused
path: src/Exercise/TemporaryDirectoryTrait.php
136 changes: 59 additions & 77 deletions src/Check/DatabaseCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
namespace PhpSchool\PhpWorkshop\Check;

use PDO;
use PDOException;
use PhpSchool\PhpWorkshop\Event\CgiExerciseRunnerEvent;
use PhpSchool\PhpWorkshop\Event\CliExecuteEvent;
use PhpSchool\PhpWorkshop\Event\CliExerciseRunnerEvent;
use PhpSchool\PhpWorkshop\Event\Event;
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent;
use PhpSchool\PhpWorkshop\ExerciseCheck\DatabaseExerciseCheck;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\Success;
use RuntimeException;
use PhpSchool\PhpWorkshop\Utils\Path;
use PhpSchool\PhpWorkshop\Utils\System;
use Symfony\Component\Filesystem\Filesystem;

/**
* This check sets up a database and a `PDO` object. It prepends the database DSN as a CLI argument to the student's
Expand All @@ -23,24 +26,12 @@
*/
class DatabaseCheck implements ListenableCheckInterface
{
use TemporaryDirectoryTrait;
private Filesystem $filesystem;
private ?string $dbContent = null;

private string $databaseDirectory;
private string $userDatabasePath;
private string $solutionDatabasePath;
private string $userDsn;
private string $solutionDsn;

/**
* Setup paths and DSN's.
*/
public function __construct()
public function __construct(?Filesystem $filesystem = null)
{
$this->databaseDirectory = $this->getTemporaryPath();
$this->userDatabasePath = sprintf('%s/user-db.sqlite', $this->databaseDirectory);
$this->solutionDatabasePath = sprintf('%s/solution-db.sqlite', $this->databaseDirectory);
$this->solutionDsn = sprintf('sqlite:%s', $this->solutionDatabasePath);
$this->userDsn = sprintf('sqlite:%s', $this->userDatabasePath);
$this->filesystem = $filesystem ?? new Filesystem();
}

/**
Expand All @@ -64,78 +55,69 @@ public function getExerciseInterface(): string
*/
public function attach(EventDispatcher $eventDispatcher): void
{
if (file_exists($this->databaseDirectory)) {
throw new RuntimeException(
sprintf('Database directory: "%s" already exists', $this->databaseDirectory),
);
}

mkdir($this->databaseDirectory, 0777, true);

try {
$db = new PDO($this->userDsn);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
rmdir($this->databaseDirectory);
throw $e;
}

$eventDispatcher->listen('verify.start', function (Event $e) use ($db) {
/** @var DatabaseExerciseCheck $exercise */
$exercise = $e->getParameter('exercise');
$exercise->seed($db);
//make a copy - so solution can modify without effecting database user has access to
copy($this->userDatabasePath, $this->solutionDatabasePath);
});
$eventDispatcher->listen(['verify.start', 'run.start'], function (Event $e) {
$path = System::randomTempPath('sqlite');

$eventDispatcher->listen('run.start', function (Event $e) use ($db) {
/** @var DatabaseExerciseCheck $exercise */
$exercise = $e->getParameter('exercise');
$exercise->seed($db);
});
$this->filesystem->touch($path);

try {
$db = $this->getPDO($path);

/** @var DatabaseExerciseCheck $exercise */
$exercise = $e->getParameter('exercise');
$exercise->seed($db);

$eventDispatcher->listen('cli.verify.reference-execute.pre', function (CliExecuteEvent $e) {
$e->prependArg($this->solutionDsn);
$this->dbContent = (string) file_get_contents($path);
} finally {
unset($db);

$this->filesystem->remove($path);
}
});

$eventDispatcher->listen(
['cli.verify.student-execute.pre', 'cli.run.student-execute.pre'],
function (CliExecuteEvent $e) {
$e->prependArg($this->userDsn);
['cli.verify.prepare', 'cgi.verify.prepare', 'cli.run.prepare', 'cgi.run.prepare'],
function (CliExerciseRunnerEvent|CgiExerciseRunnerEvent $e) {
$e->getScenario()->withFile('db.sqlite', (string) $this->dbContent);

$this->dbContent = null;
},
);

$eventDispatcher->insertVerifier('verify.finish', function (Event $e) use ($db) {
/** @var DatabaseExerciseCheck $exercise */
$exercise = $e->getParameter('exercise');
$verifyResult = $exercise->verify($db);
$eventDispatcher->listen(
'cli.verify.reference-execute.pre',
fn(CliExecuteEvent $e) => $e->prependArg('sqlite:db.sqlite'),
);

if (false === $verifyResult) {
return Failure::fromNameAndReason($this->getName(), 'Database verification failed');
}
$eventDispatcher->listen(
['cli.verify.student-execute.pre', 'cli.run.student-execute.pre'],
fn(CliExecuteEvent $e) => $e->prependArg('sqlite:db.sqlite'),
);

return new Success('Database Verification Check');
});
$eventDispatcher->insertVerifier('verify.finish', function (ExerciseRunnerEvent $e) {
$db = $this->getPDO(Path::join($e->getContext()->getStudentExecutionDirectory(), 'db.sqlite'));

$eventDispatcher->listen(
[
'cli.verify.reference-execute.fail',
'verify.finish',
'run.finish',
],
function () use ($db) {
try {
/** @var DatabaseExerciseCheck $exercise */
$exercise = $e->getParameter('exercise');
$verifyResult = $exercise->verify($db);

if (false === $verifyResult) {
return Failure::fromNameAndReason($this->getName(), 'Database verification failed');
}

return new Success('Database Verification Check');
} finally {
unset($db);
$this->unlink($this->userDatabasePath);
$this->unlink($this->solutionDatabasePath);
rmdir($this->databaseDirectory);
},
);
}
});
}

private function unlink(string $file): void
private function getPDO(string $path): PDO
{
if (file_exists($file)) {
unlink($file);
}
$db = new PDO('sqlite:' . $path);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

return $db;
}
}
4 changes: 4 additions & 0 deletions src/ExerciseRunner/CgiRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public function verify(ExecutionContext $context): ResultInterface
{
$scenario = $this->exercise->defineTestScenario();

$this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.prepare', $context, $scenario));

$this->environmentManager->prepareStudent($context, $scenario);
$this->environmentManager->prepareReference($context, $scenario);

Expand Down Expand Up @@ -295,6 +297,8 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
{
$scenario = $this->exercise->defineTestScenario();

$this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.prepare', $context, $scenario));

$this->environmentManager->prepareStudent($context, $scenario);

$this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.run.start', $context, $scenario));
Expand Down
4 changes: 4 additions & 0 deletions src/ExerciseRunner/CliRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public function verify(ExecutionContext $context): ResultInterface
{
$scenario = $this->exercise->defineTestScenario();

$this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.prepare', $context, $scenario));

$this->environmentManager->prepareStudent($context, $scenario);
$this->environmentManager->prepareReference($context, $scenario);

Expand Down Expand Up @@ -203,6 +205,8 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
{
$scenario = $this->exercise->defineTestScenario();

$this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.prepare', $context, $scenario));

$this->environmentManager->prepareStudent($context, $scenario);

$this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.run.start', $context, $scenario));
Expand Down
5 changes: 5 additions & 0 deletions src/Utils/System.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public static function realpath(string $path): string
return $realpath;
}

public static function randomTempPath(string $ext): string
{
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', bin2hex(random_bytes(4)) . '.' . $ext);
}

public static function tempDir(string $path = ''): string
{
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', $path);
Expand Down
53 changes: 0 additions & 53 deletions test/Check/DatabaseCheckTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
use PhpSchool\PhpWorkshopTest\Asset\DatabaseExercise;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;

class DatabaseCheckTest extends TestCase
Expand Down Expand Up @@ -75,57 +73,6 @@ private function getRunnerManager(ExerciseInterface $exercise, EventDispatcher $
return $runnerManager;
}

public function testIfDatabaseFolderExistsExceptionIsThrown(): void
{
$eventDispatcher = new EventDispatcher(new ResultAggregator());
mkdir($this->dbDir, 0777, true);
try {
$this->check->attach($eventDispatcher);
$this->fail('Exception was not thrown');
} catch (RuntimeException $e) {
$this->assertEquals(sprintf('Database directory: "%s" already exists', $this->dbDir), $e->getMessage());
}
}

/**
* If an exception is thrown from PDO, check that the check can be run straight away
* Previously files were not cleaned up that caused exceptions.
*/
public function testIfPDOThrowsExceptionItCleansUp(): void
{
$eventDispatcher = new EventDispatcher(new ResultAggregator());

$refProp = new ReflectionProperty(DatabaseCheck::class, 'userDsn');
$refProp->setValue($this->check, 'notvaliddsn');

try {
$this->check->attach($eventDispatcher);
$this->fail('Exception was not thrown');
} catch (\PDOException $e) {
}

//try to run the check as usual
$this->check = new DatabaseCheck();
$solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php'));
$this->exercise->setSolution($solution);
$this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3]));
$this->exercise->setVerifier(fn() => true);

$this->checkRepository->registerCheck($this->check);

$results = new ResultAggregator();
$eventDispatcher = new EventDispatcher($results);
$dispatcher = new ExerciseDispatcher(
$this->getRunnerManager($this->exercise, $eventDispatcher),
$results,
$eventDispatcher,
$this->checkRepository,
);

$dispatcher->verify($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user.php']));
$this->assertTrue($results->isSuccessful());
}

public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void
{
$solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php'));
Expand Down
Loading