Skip to content

Commit 6f461ac

Browse files
committed
Rewrite DatabaseCheck
1 parent 353e5e3 commit 6f461ac

File tree

6 files changed

+68
-132
lines changed

6 files changed

+68
-132
lines changed

src/Check/DatabaseCheck.php

+59-77
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
namespace PhpSchool\PhpWorkshop\Check;
66

77
use PDO;
8-
use PDOException;
8+
use PhpSchool\PhpWorkshop\Event\CgiExerciseRunnerEvent;
99
use PhpSchool\PhpWorkshop\Event\CliExecuteEvent;
10+
use PhpSchool\PhpWorkshop\Event\CliExerciseRunnerEvent;
1011
use PhpSchool\PhpWorkshop\Event\Event;
1112
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
12-
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
13+
use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent;
1314
use PhpSchool\PhpWorkshop\ExerciseCheck\DatabaseExerciseCheck;
1415
use PhpSchool\PhpWorkshop\Result\Failure;
1516
use PhpSchool\PhpWorkshop\Result\Success;
16-
use RuntimeException;
17+
use PhpSchool\PhpWorkshop\Utils\Path;
18+
use PhpSchool\PhpWorkshop\Utils\System;
19+
use Symfony\Component\Filesystem\Filesystem;
1720

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

28-
private string $databaseDirectory;
29-
private string $userDatabasePath;
30-
private string $solutionDatabasePath;
31-
private string $userDsn;
32-
private string $solutionDsn;
33-
34-
/**
35-
* Setup paths and DSN's.
36-
*/
37-
public function __construct()
32+
public function __construct(Filesystem $filesystem = null)
3833
{
39-
$this->databaseDirectory = $this->getTemporaryPath();
40-
$this->userDatabasePath = sprintf('%s/user-db.sqlite', $this->databaseDirectory);
41-
$this->solutionDatabasePath = sprintf('%s/solution-db.sqlite', $this->databaseDirectory);
42-
$this->solutionDsn = sprintf('sqlite:%s', $this->solutionDatabasePath);
43-
$this->userDsn = sprintf('sqlite:%s', $this->userDatabasePath);
34+
$this->filesystem = $filesystem ? $filesystem : new Filesystem();
4435
}
4536

4637
/**
@@ -64,78 +55,69 @@ public function getExerciseInterface(): string
6455
*/
6556
public function attach(EventDispatcher $eventDispatcher): void
6657
{
67-
if (file_exists($this->databaseDirectory)) {
68-
throw new RuntimeException(
69-
sprintf('Database directory: "%s" already exists', $this->databaseDirectory),
70-
);
71-
}
72-
73-
mkdir($this->databaseDirectory, 0777, true);
74-
75-
try {
76-
$db = new PDO($this->userDsn);
77-
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
78-
} catch (PDOException $e) {
79-
rmdir($this->databaseDirectory);
80-
throw $e;
81-
}
82-
83-
$eventDispatcher->listen('verify.start', function (Event $e) use ($db) {
84-
/** @var DatabaseExerciseCheck $exercise */
85-
$exercise = $e->getParameter('exercise');
86-
$exercise->seed($db);
87-
//make a copy - so solution can modify without effecting database user has access to
88-
copy($this->userDatabasePath, $this->solutionDatabasePath);
89-
});
58+
$eventDispatcher->listen(['verify.start', 'run.start'], function (Event $e) {
59+
$path = System::randomTempPath('sqlite');
9060

91-
$eventDispatcher->listen('run.start', function (Event $e) use ($db) {
92-
/** @var DatabaseExerciseCheck $exercise */
93-
$exercise = $e->getParameter('exercise');
94-
$exercise->seed($db);
95-
});
61+
$this->filesystem->touch($path);
62+
63+
try {
64+
$db = $this->getPDO($path);
65+
66+
/** @var DatabaseExerciseCheck $exercise */
67+
$exercise = $e->getParameter('exercise');
68+
$exercise->seed($db);
9669

97-
$eventDispatcher->listen('cli.verify.reference-execute.pre', function (CliExecuteEvent $e) {
98-
$e->prependArg($this->solutionDsn);
70+
$this->dbContent = (string) file_get_contents($path);
71+
} finally {
72+
unset($db);
73+
74+
$this->filesystem->remove($path);
75+
}
9976
});
10077

10178
$eventDispatcher->listen(
102-
['cli.verify.student-execute.pre', 'cli.run.student-execute.pre'],
103-
function (CliExecuteEvent $e) {
104-
$e->prependArg($this->userDsn);
79+
['cli.verify.prepare', 'cgi.verify.prepare'],
80+
function (CliExerciseRunnerEvent|CgiExerciseRunnerEvent $e) {
81+
$e->getScenario()->withFile('db.sqlite', (string) $this->dbContent);
82+
83+
$this->dbContent = null;
10584
},
10685
);
10786

108-
$eventDispatcher->insertVerifier('verify.finish', function (Event $e) use ($db) {
109-
/** @var DatabaseExerciseCheck $exercise */
110-
$exercise = $e->getParameter('exercise');
111-
$verifyResult = $exercise->verify($db);
87+
$eventDispatcher->listen(
88+
'cli.verify.reference-execute.pre',
89+
fn(CliExecuteEvent $e) => $e->prependArg('sqlite:db.sqlite'),
90+
);
11291

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

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

120-
$eventDispatcher->listen(
121-
[
122-
'cli.verify.reference-execute.fail',
123-
'verify.finish',
124-
'run.finish',
125-
],
126-
function () use ($db) {
100+
try {
101+
/** @var DatabaseExerciseCheck $exercise */
102+
$exercise = $e->getParameter('exercise');
103+
$verifyResult = $exercise->verify($db);
104+
105+
if (false === $verifyResult) {
106+
return Failure::fromNameAndReason($this->getName(), 'Database verification failed');
107+
}
108+
109+
return new Success('Database Verification Check');
110+
} finally {
127111
unset($db);
128-
$this->unlink($this->userDatabasePath);
129-
$this->unlink($this->solutionDatabasePath);
130-
rmdir($this->databaseDirectory);
131-
},
132-
);
112+
}
113+
});
133114
}
134115

135-
private function unlink(string $file): void
116+
private function getPDO(string $path): PDO
136117
{
137-
if (file_exists($file)) {
138-
unlink($file);
139-
}
118+
$db = new PDO('sqlite:' . $path);
119+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
120+
121+
return $db;
140122
}
141123
}

src/ExerciseDispatcher.php

-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega
126126

127127
if (!$this->results->isSuccessful()) {
128128
$exercise->tearDown();
129-
$this->eventDispatcher->dispatch(new ExerciseRunnerEvent('verify.finish', $context));
130129
return $this->results;
131130
}
132131
}

src/ExerciseRunner/CgiRunner.php

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public function verify(ExecutionContext $context): ResultInterface
102102
{
103103
$scenario = $this->exercise->defineTestScenario();
104104

105+
$this->eventDispatcher->dispatch(new CgiExerciseRunnerEvent('cgi.verify.prepare', $context, $scenario));
106+
105107
$this->environmentManager->prepareStudent($context, $scenario);
106108
$this->environmentManager->prepareReference($context, $scenario);
107109

src/ExerciseRunner/CliRunner.php

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public function verify(ExecutionContext $context): ResultInterface
101101
{
102102
$scenario = $this->exercise->defineTestScenario();
103103

104+
$this->eventDispatcher->dispatch(new CliExerciseRunnerEvent('cli.verify.prepare', $context, $scenario));
105+
104106
$this->environmentManager->prepareStudent($context, $scenario);
105107
$this->environmentManager->prepareReference($context, $scenario);
106108

src/Utils/System.php

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public static function realpath(string $path): string
1919
return $realpath;
2020
}
2121

22+
public static function randomTempPath(string $ext): string
23+
{
24+
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', bin2hex(random_bytes(4)) . '.' . $ext);
25+
}
26+
2227
public static function tempDir(string $path = ''): string
2328
{
2429
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', $path);

test/Check/DatabaseCheckTest.php

-54
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
use PhpSchool\PhpWorkshopTest\Asset\DatabaseExercise;
2323
use PHPUnit\Framework\MockObject\MockObject;
2424
use PHPUnit\Framework\TestCase;
25-
use ReflectionProperty;
26-
use RuntimeException;
2725
use Symfony\Component\Filesystem\Filesystem;
2826

2927
class DatabaseCheckTest extends TestCase
@@ -75,58 +73,6 @@ private function getRunnerManager(ExerciseInterface $exercise, EventDispatcher $
7573
return $runnerManager;
7674
}
7775

78-
public function testIfDatabaseFolderExistsExceptionIsThrown(): void
79-
{
80-
$eventDispatcher = new EventDispatcher(new ResultAggregator());
81-
mkdir($this->dbDir, 0777, true);
82-
try {
83-
$this->check->attach($eventDispatcher);
84-
$this->fail('Exception was not thrown');
85-
} catch (RuntimeException $e) {
86-
$this->assertEquals(sprintf('Database directory: "%s" already exists', $this->dbDir), $e->getMessage());
87-
}
88-
}
89-
90-
/**
91-
* If an exception is thrown from PDO, check that the check can be run straight away
92-
* Previously files were not cleaned up that caused exceptions.
93-
*/
94-
public function testIfPDOThrowsExceptionItCleansUp(): void
95-
{
96-
$eventDispatcher = new EventDispatcher(new ResultAggregator());
97-
98-
$refProp = new ReflectionProperty(DatabaseCheck::class, 'userDsn');
99-
$refProp->setAccessible(true);
100-
$refProp->setValue($this->check, 'notvaliddsn');
101-
102-
try {
103-
$this->check->attach($eventDispatcher);
104-
$this->fail('Exception was not thrown');
105-
} catch (\PDOException $e) {
106-
}
107-
108-
//try to run the check as usual
109-
$this->check = new DatabaseCheck();
110-
$solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php'));
111-
$this->exercise->setSolution($solution);
112-
$this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3]));
113-
$this->exercise->setVerifier(fn() => true);
114-
115-
$this->checkRepository->registerCheck($this->check);
116-
117-
$results = new ResultAggregator();
118-
$eventDispatcher = new EventDispatcher($results);
119-
$dispatcher = new ExerciseDispatcher(
120-
$this->getRunnerManager($this->exercise, $eventDispatcher),
121-
$results,
122-
$eventDispatcher,
123-
$this->checkRepository,
124-
);
125-
126-
$dispatcher->verify($this->exercise, new Input('app', ['program' => __DIR__ . '/../res/database/user.php']));
127-
$this->assertTrue($results->isSuccessful());
128-
}
129-
13076
public function testSuccessIsReturnedIfDatabaseVerificationPassed(): void
13177
{
13278
$solution = SingleFileSolution::fromFile(realpath(__DIR__ . '/../res/database/solution.php'));

0 commit comments

Comments
 (0)