Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Various fixes #300

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
},

ProcessFactory::class => function (ContainerInterface $c) {
$processFactory = $_ENV['process_factory'] ?? null;
$processFactory = $_ENV['PROCESS_FACTORY'] ?? null;

return match ($processFactory) {
'docker' => new DockerProcessFactory(
Expand Down
691 changes: 347 additions & 344 deletions composer.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Check/CodeExistsCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getName(): string
*/
public function check(ExecutionContext $context): ResultInterface
{
$noopHandler = new class () implements ErrorHandler {
$noopHandler = new class implements ErrorHandler {
public function handleError(Error $error): void {}
};

Expand Down
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 ? $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'],
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;
}
}
20 changes: 20 additions & 0 deletions src/Exercise/Scenario/ExerciseScenario.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ abstract class ExerciseScenario
*/
private array $files = [];

/**
* @var list<int>
*/
private array $exposedPorts = [];

public function withFile(string $relativeFileName, string $content): static
{
$this->files[$relativeFileName] = $content;
Expand All @@ -23,4 +28,19 @@ public function getFiles(): array
{
return $this->files;
}

public function exposePort(int $port): static
{
$this->exposedPorts = [$port];

return $this;
}

/**
* @return list<int>
*/
public function getExposedPorts(): array
{
return $this->exposedPorts;
}
}
1 change: 1 addition & 0 deletions src/ExerciseDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega
$this->results->add($check->check($context));

if (!$this->results->isSuccessful()) {
$exercise->tearDown();
return $this->results;
}
}
Expand Down
15 changes: 9 additions & 6 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 @@ -157,7 +159,7 @@ private function doVerify(ExecutionContext $context, CgiScenario $scenario, Requ
$context,
$scenario,
$context->getStudentExecutionDirectory(),
$context->getEntryPoint(),
basename($context->getEntryPoint()),
$event->getRequest(),
'student',
);
Expand Down Expand Up @@ -213,7 +215,7 @@ private function executePhpFile(
RequestInterface $request,
string $type,
): ResponseInterface {
$process = $this->getPhpProcess($workingDirectory, $fileName, $request);
$process = $this->getPhpProcess($workingDirectory, $fileName, $request, $scenario->getExposedPorts());

$process->start();
$this->eventDispatcher->dispatch(
Expand All @@ -227,6 +229,7 @@ private function executePhpFile(

//if no status line, pre-pend 200 OK
$output = $process->getOutput();
$error = $process->getErrorOutput();
if (!preg_match('/^HTTP\/([1-9]\d*\.\d) ([1-5]\d{2})(\s+(.+))?\\r\\n/', $output)) {
$output = "HTTP/1.0 200 OK\r\n" . $output;
}
Expand All @@ -235,11 +238,9 @@ private function executePhpFile(
}

/**
* @param string $fileName
* @param RequestInterface $request
* @return Process
* @param list<int> $exposedPorts
*/
private function getPhpProcess(string $workingDirectory, string $fileName, RequestInterface $request): Process
private function getPhpProcess(string $workingDirectory, string $fileName, RequestInterface $request, array $exposedPorts): Process
{
$env = [
'REQUEST_METHOD' => $request->getMethod(),
Expand Down Expand Up @@ -267,6 +268,7 @@ private function getPhpProcess(string $workingDirectory, string $fileName, Reque
],
$workingDirectory,
$env,
$exposedPorts,
$content,
);

Expand Down Expand Up @@ -308,6 +310,7 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
$context->getStudentExecutionDirectory(),
$context->getEntryPoint(),
$event->getRequest(),
$scenario->getExposedPorts(),
);

$process->start();
Expand Down
13 changes: 9 additions & 4 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 @@ -160,7 +162,7 @@ private function doVerify(ExecutionContext $context, CliScenario $scenario, Coll
$context,
$scenario,
$context->getStudentExecutionDirectory(),
$context->getEntryPoint(),
basename($context->getEntryPoint()),
$event->getArgs(),
'student',
);
Expand Down Expand Up @@ -220,6 +222,7 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
$context->getStudentExecutionDirectory(),
$context->getEntryPoint(),
$args,
$scenario->getExposedPorts(),
);

$process->start();
Expand All @@ -229,6 +232,7 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
$process->wait(function ($outputType, $outputBuffer) use ($output) {
$output->write($outputBuffer);
});

$output->emptyLine();

if (!$process->isSuccessful()) {
Expand All @@ -252,7 +256,7 @@ public function run(ExecutionContext $context, OutputInterface $output): bool
*/
private function executePhpFile(ExecutionContext $context, CliScenario $scenario, string $workingDirectory, string $fileName, Collection $args, string $type): string
{
$process = $this->getPhpProcess($workingDirectory, $fileName, $args);
$process = $this->getPhpProcess($workingDirectory, $fileName, $args, $scenario->getExposedPorts());

$process->start();
$this->eventDispatcher->dispatch(
Expand All @@ -269,11 +273,12 @@ private function executePhpFile(ExecutionContext $context, CliScenario $scenario

/**
* @param Collection<int, string> $args
* @param list<int> $exposedPorts
*/
private function getPhpProcess(string $workingDirectory, string $fileName, Collection $args): Process
private function getPhpProcess(string $workingDirectory, string $fileName, Collection $args, array $exposedPorts): Process
{
return $this->processFactory->create(
new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, []),
new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, [], $exposedPorts),
);
}
}
2 changes: 1 addition & 1 deletion src/ExerciseRunner/Context/TestContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function importReferenceSolution(): void
);
}

$scenario = new class () extends ExerciseScenario {};
$scenario = new class extends ExerciseScenario {};
if ($this->exercise instanceof CliExercise || $this->exercise instanceof CgiExercise) {
$scenario = $this->exercise->defineTestScenario();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Listener/PrepareSolutionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function __invoke(ExerciseRunnerEvent $event): void
//only install if vendor folder not available
if (!file_exists(sprintf('%s/vendor', $event->getContext()->getReferenceExecutionDirectory()))) {
$process = $this->processFactory->create(
new ProcessInput('composer', ['install', '--no-interaction'], $event->getContext()->getReferenceExecutionDirectory(), []),
new ProcessInput('composer', ['install', '--no-interaction'], $event->getContext()->getReferenceExecutionDirectory(), [], []),
);

try {
Expand Down
Loading
Loading