diff --git a/app/config.php b/app/config.php index f61dbd34..7d5c4ddc 100644 --- a/app/config.php +++ b/app/config.php @@ -69,9 +69,9 @@ use PhpSchool\PhpWorkshop\Output\OutputInterface; use PhpSchool\PhpWorkshop\Output\StdOutput; use PhpSchool\PhpWorkshop\Patch; -use PhpSchool\PhpWorkshop\Process\DockerProcessFactory; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\DockerSandbox; +use PhpSchool\PhpWorkshop\Process\HostSandbox; +use PhpSchool\PhpWorkshop\Process\Sandbox; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure as CgiGenericFailure; use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure as CgiRequestFailure; @@ -195,28 +195,28 @@ $manager = new RunnerManager(); $manager->addFactory(new CliRunnerFactory( $c->get(EventDispatcher::class), - $c->get(ProcessFactory::class), + $c->get(Sandbox::class), $c->get(EnvironmentManager::class), )); $manager->addFactory(new CgiRunnerFactory( $c->get(EventDispatcher::class), - $c->get(ProcessFactory::class), + $c->get(Sandbox::class), $c->get(EnvironmentManager::class), )); $manager->addFactory(new CustomVerifyingRunnerFactory()); return $manager; }, - ProcessFactory::class => function (ContainerInterface $c) { - $processFactory = getenv('PROCESS_FACTORY'); + Sandbox::class => function (ContainerInterface $c) { + $sandbox = getenv('SANDBOX'); - return match ($processFactory) { - 'docker' => new DockerProcessFactory( + return match ($sandbox) { + 'docker' => new DockerSandbox( $c->get('basePath'), $c->get('appName'), $c->get('phpschoolGlobalDir') . '/composer-cache', ), - default => new HostProcessFactory(), + default => new HostSandbox(), }; }, @@ -278,7 +278,7 @@ }, PrepareSolutionListener::class => function (ContainerInterface $c) { return new PrepareSolutionListener( - $c->get(ProcessFactory::class), + $c->get(Sandbox::class), ); }, CodePatchListener::class => function (ContainerInterface $c) { diff --git a/src/Exception/CodeExecutionException.php b/src/Exception/CodeExecutionException.php index aff5c67a..fbff58be 100644 --- a/src/Exception/CodeExecutionException.php +++ b/src/Exception/CodeExecutionException.php @@ -5,7 +5,7 @@ namespace PhpSchool\PhpWorkshop\Exception; use RuntimeException; -use Symfony\Component\Process\Process; +use PhpSchool\PhpWorkshop\Process\SandboxExecution; /** * Represents the situation where some PHP code could not be executed successfully. @@ -13,17 +13,16 @@ class CodeExecutionException extends RuntimeException { /** - * Static constructor to create an instance from a failed `Symfony\Component\Process\Process` instance. + * Static constructor to create an instance from a failed sandbox execution. * - * @param Process $process The `Symfony\Component\Process\Process` instance which failed. * @return self */ - public static function fromProcess(Process $process): self + public static function fromExecution(SandboxExecution $execution): self { return new self( sprintf( 'PHP Code failed to execute. Error: "%s"', - trim($process->getErrorOutput() ?: $process->getOutput()), + trim($execution->getErrorOutput() ?: $execution->getOutput()), ), ); } diff --git a/src/ExerciseRunner/CgiRunner.php b/src/ExerciseRunner/CgiRunner.php index 8b42fd36..981a9602 100644 --- a/src/ExerciseRunner/CgiRunner.php +++ b/src/ExerciseRunner/CgiRunner.php @@ -21,8 +21,9 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; use PhpSchool\PhpWorkshop\Process\ProcessInput; +use PhpSchool\PhpWorkshop\Process\SandboxExecution; use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult; use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure; use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure; @@ -31,7 +32,6 @@ use PhpSchool\PhpWorkshop\Result\ResultInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\Process\Process; /** * The `CGI` runner. This runner executes solutions as if they were behind a web-server. They populate the `$_SERVER`, @@ -59,7 +59,7 @@ class CgiRunner implements ExerciseRunnerInterface public function __construct( private CgiExercise $exercise, private EventDispatcher $eventDispatcher, - private ProcessFactory $processFactory, + private Sandbox $sandbox, private EnvironmentManager $environmentManager, ) {} @@ -215,21 +215,21 @@ private function executePhpFile( RequestInterface $request, string $type, ): ResponseInterface { - $process = $this->getPhpProcess($workingDirectory, $fileName, $request, $scenario->getExposedPorts()); + $execution = $this->getPhpExecution($workingDirectory, $fileName, $request, $scenario->getExposedPorts()); - $process->start(); + $execution->start(); $this->eventDispatcher->dispatch( new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $context, $scenario, $request), ); - $process->wait(); + $execution->wait(); - if (!$process->isSuccessful()) { - throw CodeExecutionException::fromProcess($process); + if (!$execution->isSuccessful()) { + throw CodeExecutionException::fromExecution($execution); } //if no status line, pre-pend 200 OK - $output = $process->getOutput(); - $error = $process->getErrorOutput(); + $output = $execution->getOutput(); + $error = $execution->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; } @@ -240,7 +240,7 @@ private function executePhpFile( /** * @param list $exposedPorts */ - private function getPhpProcess(string $workingDirectory, string $fileName, RequestInterface $request, array $exposedPorts): Process + private function getPhpExecution(string $workingDirectory, string $fileName, RequestInterface $request, array $exposedPorts): SandboxExecution { $env = [ 'REQUEST_METHOD' => $request->getMethod(), @@ -272,7 +272,7 @@ private function getPhpProcess(string $workingDirectory, string $fileName, Reque $content, ); - return $this->processFactory->create($processInput); + return $this->sandbox->create($processInput); } /** @@ -308,14 +308,14 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $event = $this->eventDispatcher->dispatch( new CgiExecuteEvent('cgi.run.student-execute.pre', $context, $scenario, $request), ); - $process = $this->getPhpProcess( + $execution = $this->getPhpExecution( $context->getStudentExecutionDirectory(), basename($context->getEntryPoint()), $event->getRequest(), $scenario->getExposedPorts(), ); - $process->start(); + $execution->start(); $this->eventDispatcher->dispatch( new CgiExecuteEvent( 'cgi.run.student.executing', @@ -325,12 +325,12 @@ public function run(ExecutionContext $context, OutputInterface $output): bool ['output' => $output], ), ); - $process->wait(function ($outputType, $outputBuffer) use ($output) { + $execution->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); }); $output->emptyLine(); - if (!$process->isSuccessful()) { + if (!$execution->isSuccessful()) { $success = false; } diff --git a/src/ExerciseRunner/CliRunner.php b/src/ExerciseRunner/CliRunner.php index 3bbeadb2..0dab8b1b 100644 --- a/src/ExerciseRunner/CliRunner.php +++ b/src/ExerciseRunner/CliRunner.php @@ -20,8 +20,9 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext; use PhpSchool\PhpWorkshop\Input\Input; use PhpSchool\PhpWorkshop\Output\OutputInterface; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; use PhpSchool\PhpWorkshop\Process\ProcessInput; +use PhpSchool\PhpWorkshop\Process\SandboxExecution; use PhpSchool\PhpWorkshop\Result\Cli\RequestFailure; use PhpSchool\PhpWorkshop\Result\Cli\CliResult; use PhpSchool\PhpWorkshop\Result\Cli\GenericFailure; @@ -29,7 +30,6 @@ use PhpSchool\PhpWorkshop\Result\Cli\ResultInterface as CliResultInterface; use PhpSchool\PhpWorkshop\Result\ResultInterface; use PhpSchool\PhpWorkshop\Utils\Collection; -use Symfony\Component\Process\Process; /** * The `CLI` runner. This runner executes solutions as PHP CLI scripts, passing the arguments @@ -59,7 +59,7 @@ class CliRunner implements ExerciseRunnerInterface public function __construct( private CliExercise $exercise, private EventDispatcher $eventDispatcher, - private ProcessFactory $processFactory, + private Sandbox $sandbox, private EnvironmentManager $environmentManager, ) {} @@ -220,24 +220,24 @@ public function run(ExecutionContext $context, OutputInterface $output): bool $args = $event->getArgs(); - $process = $this->getPhpProcess( + $execution = $this->getPhpExecution( $context->getStudentExecutionDirectory(), basename($context->getEntryPoint()), $args, $scenario->getExposedPorts(), ); - $process->start(); + $execution->start(); $this->eventDispatcher->dispatch( new CliExecuteEvent('cli.run.student.executing', $context, $scenario, $args, ['output' => $output]), ); - $process->wait(function ($outputType, $outputBuffer) use ($output) { + $execution->wait(function ($outputType, $outputBuffer) use ($output) { $output->write($outputBuffer); }); $output->emptyLine(); - if (!$process->isSuccessful()) { + if (!$execution->isSuccessful()) { $success = false; } @@ -258,28 +258,28 @@ 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, $scenario->getExposedPorts()); + $execution = $this->getPhpExecution($workingDirectory, $fileName, $args, $scenario->getExposedPorts()); - $process->start(); + $execution->start(); $this->eventDispatcher->dispatch( new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $context, $scenario, $args), ); - $process->wait(); + $execution->wait(); - if (!$process->isSuccessful()) { - throw CodeExecutionException::fromProcess($process); + if (!$execution->isSuccessful()) { + throw CodeExecutionException::fromExecution($execution); } - return $process->getOutput(); + return $execution->getOutput(); } /** * @param Collection $args * @param list $exposedPorts */ - private function getPhpProcess(string $workingDirectory, string $fileName, Collection $args, array $exposedPorts): Process + private function getPhpExecution(string $workingDirectory, string $fileName, Collection $args, array $exposedPorts): SandboxExecution { - return $this->processFactory->create( + return $this->sandbox->create( new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, [], $exposedPorts), ); } diff --git a/src/ExerciseRunner/Factory/CgiRunnerFactory.php b/src/ExerciseRunner/Factory/CgiRunnerFactory.php index a53fec95..52eebeb9 100644 --- a/src/ExerciseRunner/Factory/CgiRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CgiRunnerFactory.php @@ -13,7 +13,7 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; /** * Factory class for `CgiRunner` @@ -25,7 +25,7 @@ class CgiRunnerFactory implements ExerciseRunnerFactoryInterface */ private static string $type = ExerciseType::CGI; - public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory, private EnvironmentManager $environmentManager) {} + public function __construct(private EventDispatcher $eventDispatcher, private Sandbox $sandbox, private EnvironmentManager $environmentManager) {} /** * Whether the factory supports this exercise type. @@ -56,6 +56,6 @@ public function configureInput(CommandDefinition $commandDefinition): void */ public function create(ExerciseInterface $exercise): ExerciseRunnerInterface { - return new CgiRunner($exercise, $this->eventDispatcher, $this->processFactory, $this->environmentManager); + return new CgiRunner($exercise, $this->eventDispatcher, $this->sandbox, $this->environmentManager); } } diff --git a/src/ExerciseRunner/Factory/CliRunnerFactory.php b/src/ExerciseRunner/Factory/CliRunnerFactory.php index 4ee13f18..8a584d16 100644 --- a/src/ExerciseRunner/Factory/CliRunnerFactory.php +++ b/src/ExerciseRunner/Factory/CliRunnerFactory.php @@ -13,7 +13,7 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\ExerciseRunnerInterface; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; /** * Factory class for `CliRunner` @@ -25,7 +25,7 @@ class CliRunnerFactory implements ExerciseRunnerFactoryInterface */ private static string $type = ExerciseType::CLI; - public function __construct(private EventDispatcher $eventDispatcher, private ProcessFactory $processFactory, private EnvironmentManager $environmentManager) {} + public function __construct(private EventDispatcher $eventDispatcher, private Sandbox $sandbox, private EnvironmentManager $environmentManager) {} /** * Whether the factory supports this exercise type. @@ -56,6 +56,6 @@ public function configureInput(CommandDefinition $commandDefinition): void */ public function create(ExerciseInterface $exercise): ExerciseRunnerInterface { - return new CliRunner($exercise, $this->eventDispatcher, $this->processFactory, $this->environmentManager); + return new CliRunner($exercise, $this->eventDispatcher, $this->sandbox, $this->environmentManager); } } diff --git a/src/Listener/PrepareSolutionListener.php b/src/Listener/PrepareSolutionListener.php index 620b4562..65d3a396 100644 --- a/src/Listener/PrepareSolutionListener.php +++ b/src/Listener/PrepareSolutionListener.php @@ -7,7 +7,7 @@ use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\Exercise\ProvidesSolution; use PhpSchool\PhpWorkshop\Exception\RuntimeException; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; use PhpSchool\PhpWorkshop\Process\ProcessInput; /** @@ -15,9 +15,9 @@ */ class PrepareSolutionListener { - public function __construct(private ProcessFactory $processFactory) + public function __construct(private Sandbox $sandbox) { - $this->processFactory = $processFactory; + $this->sandbox = $sandbox; } /** @@ -40,12 +40,12 @@ public function __invoke(ExerciseRunnerEvent $event): void //prepare composer deps //only install if vendor folder not available if (!file_exists(sprintf('%s/vendor', $event->getContext()->getReferenceExecutionDirectory()))) { - $process = $this->processFactory->create( + $execution = $this->sandbox->create( new ProcessInput('composer', ['install', '--no-interaction'], $event->getContext()->getReferenceExecutionDirectory(), [], []), ); try { - $process->mustRun(); + $execution->mustRun(); } catch (\Symfony\Component\Process\Exception\RuntimeException $e) { throw new RuntimeException('Composer dependencies could not be installed', 0, $e); } diff --git a/src/Process/DockerExecution.php b/src/Process/DockerExecution.php new file mode 100644 index 00000000..d7ea71a8 --- /dev/null +++ b/src/Process/DockerExecution.php @@ -0,0 +1,75 @@ + $before + * @param list $after + */ + public function __construct( + public readonly Process $process, + public readonly array $before = [], + public readonly array $after = [], + ) {} + + public function start(): void + { + foreach ($this->before as $step) { + $step->mustRun(); + } + + $this->process->start(); + } + + public function wait(?callable $callback = null): int + { + try { + return $this->process->wait($callback); + } finally { + $this->runAfter(); + } + } + + public function mustRun(): void + { + $this->start(); + $this->wait(); + + if (!$this->process->isSuccessful()) { + throw new ProcessFailedException($this->process); + } + } + + public function isSuccessful(): bool + { + return $this->process->isSuccessful(); + } + + public function getOutput(): string + { + return $this->process->getOutput(); + } + + public function getErrorOutput(): string + { + return $this->process->getErrorOutput(); + } + + private function runAfter(): void + { + foreach ($this->after as $step) { + //cleanup, so keep going if one fails + $step->run(); + } + } +} diff --git a/src/Process/DockerProcessFactory.php b/src/Process/DockerSandbox.php similarity index 94% rename from src/Process/DockerProcessFactory.php rename to src/Process/DockerSandbox.php index 26eaeeb3..ffc253f8 100644 --- a/src/Process/DockerProcessFactory.php +++ b/src/Process/DockerSandbox.php @@ -12,7 +12,7 @@ * * The workshop only supplies the image. All isolation flags are set here so a workshop cannot loosen them. */ -final class DockerProcessFactory implements ProcessFactory +final class DockerSandbox implements Sandbox { private const TIMEOUT = 30; @@ -57,7 +57,7 @@ public function createBuildProcess(): Process ); } - public function create(ProcessInput $processInput): Process + public function create(ProcessInput $processInput): DockerExecution { $isComposer = $processInput->getExecutable() === 'composer'; @@ -89,7 +89,8 @@ public function create(ProcessInput $processInput): Process $envArgs[] = $key . '=' . $value; } - return new Process( + //no steps around it yet, create/cp/rm come when files are copied in instead of mounted + return new DockerExecution(new Process( [ $this->findDocker(), 'run', @@ -131,7 +132,7 @@ public function create(ProcessInput $processInput): Process null, $processInput->getInput(), self::TIMEOUT + 5, - ); + )); } private function findDocker(): string diff --git a/src/Process/HostExecution.php b/src/Process/HostExecution.php new file mode 100644 index 00000000..9694349c --- /dev/null +++ b/src/Process/HostExecution.php @@ -0,0 +1,42 @@ +process->start(); + } + + public function wait(?callable $callback = null): int + { + return $this->process->wait($callback); + } + + public function mustRun(): void + { + $this->process->mustRun(); + } + + public function isSuccessful(): bool + { + return $this->process->isSuccessful(); + } + + public function getOutput(): string + { + return $this->process->getOutput(); + } + + public function getErrorOutput(): string + { + return $this->process->getErrorOutput(); + } +} diff --git a/src/Process/HostProcessFactory.php b/src/Process/HostSandbox.php similarity index 90% rename from src/Process/HostProcessFactory.php rename to src/Process/HostSandbox.php index af7196d8..2154ef08 100644 --- a/src/Process/HostProcessFactory.php +++ b/src/Process/HostSandbox.php @@ -7,7 +7,7 @@ use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; -final class HostProcessFactory implements ProcessFactory +final class HostSandbox implements Sandbox { /** * Inherited env vars that are kept, everything else is unset so we don't leak secrets to the process @@ -22,7 +22,7 @@ public function __construct(?ExecutableFinder $executableFinder = null) } - public function create(ProcessInput $processInput): Process + public function create(ProcessInput $processInput): HostExecution { $executablePath = $this->executableFinder->find($processInput->getExecutable()); @@ -30,13 +30,13 @@ public function create(ProcessInput $processInput): Process throw ProcessNotFoundException::fromExecutable($processInput->getExecutable()); } - return new Process( + return new HostExecution(new Process( [$executablePath, ...$processInput->getArgs()], $processInput->getWorkingDirectory(), array_merge($this->getDefaultEnv(), $processInput->getEnv()), $processInput->getInput(), 10, - ); + )); } /** diff --git a/src/Process/ProcessFactory.php b/src/Process/ProcessFactory.php deleted file mode 100644 index 8e7faffa..00000000 --- a/src/Process/ProcessFactory.php +++ /dev/null @@ -1,12 +0,0 @@ -getMockBuilder(CliRunner::class) - ->setConstructorArgs([$exercise, $eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $eventDispatcher)]) + ->setConstructorArgs([$exercise, $eventDispatcher, new HostSandbox(), new EnvironmentManager(new Filesystem(), $eventDispatcher)]) ->onlyMethods(['getRequiredChecks']) ->getMock(); diff --git a/test/Exception/CodeExecutionExceptionTest.php b/test/Exception/CodeExecutionExceptionTest.php index f774c481..63788ba2 100644 --- a/test/Exception/CodeExecutionExceptionTest.php +++ b/test/Exception/CodeExecutionExceptionTest.php @@ -4,7 +4,7 @@ use PhpSchool\PhpWorkshop\Exception\CodeExecutionException; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; +use PhpSchool\PhpWorkshop\Process\SandboxExecution; class CodeExecutionExceptionTest extends TestCase { @@ -16,20 +16,20 @@ public function testException(): void public function testFromProcessUsesErrorOutputIfNotEmpty(): void { - $process = $this->createMock(Process::class); + $process = $this->createMock(SandboxExecution::class); $process ->expects($this->once()) ->method('getErrorOutput') ->willReturn('Error Output'); - $e = CodeExecutionException::fromProcess($process); + $e = CodeExecutionException::fromExecution($process); $this->assertEquals('PHP Code failed to execute. Error: "Error Output"', $e->getMessage()); } public function testFromProcessUsesStdOutputIfErrorOutputEmpty(): void { - $process = $this->createMock(Process::class); + $process = $this->createMock(SandboxExecution::class); $process ->expects($this->once()) ->method('getErrorOutput') @@ -40,7 +40,7 @@ public function testFromProcessUsesStdOutputIfErrorOutputEmpty(): void ->method('getOutput') ->willReturn('Std Output'); - $e = CodeExecutionException::fromProcess($process); + $e = CodeExecutionException::fromExecution($process); $this->assertEquals('PHP Code failed to execute. Error: "Std Output"', $e->getMessage()); } } diff --git a/test/ExerciseRunner/CgiRunnerTest.php b/test/ExerciseRunner/CgiRunnerTest.php index d8ce0de1..ff9cb020 100644 --- a/test/ExerciseRunner/CgiRunnerTest.php +++ b/test/ExerciseRunner/CgiRunnerTest.php @@ -9,7 +9,7 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\HostSandbox; use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseImpl; use PhpSchool\Terminal\Terminal; use PhpSchool\PhpWorkshop\Check\CodeParseCheck; @@ -38,7 +38,7 @@ public function setUp(): void { $this->exercise = new CgiExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); + $this->runner = new CgiRunner($this->exercise, $this->eventDispatcher, new HostSandbox(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); $this->assertEquals('CGI Program Runner', $this->runner->getName()); } diff --git a/test/ExerciseRunner/CliRunnerTest.php b/test/ExerciseRunner/CliRunnerTest.php index 3798fe57..b2f46c27 100644 --- a/test/ExerciseRunner/CliRunnerTest.php +++ b/test/ExerciseRunner/CliRunnerTest.php @@ -8,11 +8,11 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\Listener\OutputRunInfoListener; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; -use Symfony\Component\Process\Process; +use PhpSchool\PhpWorkshop\Process\HostSandbox; +use PhpSchool\PhpWorkshop\Process\SandboxExecution; use PhpSchool\PhpWorkshop\Output\NullOutput; use PhpSchool\PhpWorkshop\Process\ProcessInput; -use PhpSchool\PhpWorkshop\Process\ProcessFactory; +use PhpSchool\PhpWorkshop\Process\Sandbox; use PhpSchool\PhpWorkshop\Utils\RequestRenderer; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PhpSchool\Terminal\Terminal; @@ -42,7 +42,7 @@ public function setUp(): void { $this->exercise = new CliExerciseImpl(); $this->eventDispatcher = new EventDispatcher(new ResultAggregator()); - $this->runner = new CliRunner($this->exercise, $this->eventDispatcher, new HostProcessFactory(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); + $this->runner = new CliRunner($this->exercise, $this->eventDispatcher, new HostSandbox(), new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); $this->assertEquals('CLI Program Runner', $this->runner->getName()); } @@ -173,25 +173,25 @@ public function testRunPassesOutputAndReturnsSuccessIfScriptIsSuccessful(): void public function testRunPassesTheEntryPointRelativeToTheWorkingDirectory(): void { //the sandbox mounts the working directory at a different path, so a host path would not exist in there - $processFactory = new class implements ProcessFactory { + $sandbox = new class implements Sandbox { /** @var array */ public array $inputs = []; - public function create(ProcessInput $processInput): Process + public function create(ProcessInput $processInput): SandboxExecution { $this->inputs[] = $processInput; - return (new HostProcessFactory())->create($processInput); + return (new HostSandbox())->create($processInput); } }; - $runner = new CliRunner($this->exercise, $this->eventDispatcher, $processFactory, new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); + $runner = new CliRunner($this->exercise, $this->eventDispatcher, $sandbox, new EnvironmentManager(new Filesystem(), $this->eventDispatcher)); $this->exercise->setScenario((new CliScenario())->withExecution([1, 2, 3])); $context = TestContext::fromExerciseAndStudentSolution($this->exercise, __DIR__ . '/../res/cli/user.php'); $runner->run($context, new NullOutput()); - $this->assertSame('user.php', $processFactory->inputs[0]->getArgs()[0]); + $this->assertSame('user.php', $sandbox->inputs[0]->getArgs()[0]); } public function testRunPassesOutputAndReturnsFailureIfScriptFails(): void diff --git a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php index 5010b80c..6bf45d40 100644 --- a/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CgiRunnerFactoryTest.php @@ -9,7 +9,7 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\CgiRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CgiRunnerFactory; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\HostSandbox; use PhpSchool\PhpWorkshopTest\Asset\CgiExerciseImpl; use PHPUnit\Framework\TestCase; @@ -21,7 +21,7 @@ class CgiRunnerFactoryTest extends TestCase public function setUp(): void { $this->eventDispatcher = $this->createMock(EventDispatcher::class); - $this->factory = new CgiRunnerFactory($this->eventDispatcher, new HostProcessFactory(), $this->createMock(EnvironmentManager::class)); + $this->factory = new CgiRunnerFactory($this->eventDispatcher, new HostSandbox(), $this->createMock(EnvironmentManager::class)); } public function testSupports(): void diff --git a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php index ce5fe6f6..c057ef8d 100644 --- a/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php +++ b/test/ExerciseRunner/Factory/CliRunnerFactoryTest.php @@ -9,7 +9,7 @@ use PhpSchool\PhpWorkshop\ExerciseRunner\CliRunner; use PhpSchool\PhpWorkshop\ExerciseRunner\EnvironmentManager; use PhpSchool\PhpWorkshop\ExerciseRunner\Factory\CliRunnerFactory; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\HostSandbox; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; use PHPUnit\Framework\TestCase; @@ -21,7 +21,7 @@ class CliRunnerFactoryTest extends TestCase public function setUp(): void { $this->eventDispatcher = $this->createMock(EventDispatcher::class); - $this->factory = new CliRunnerFactory($this->eventDispatcher, new HostProcessFactory(), $this->createMock(EnvironmentManager::class)); + $this->factory = new CliRunnerFactory($this->eventDispatcher, new HostSandbox(), $this->createMock(EnvironmentManager::class)); } public function testSupports(): void diff --git a/test/Listener/PrepareSolutionListenerTest.php b/test/Listener/PrepareSolutionListenerTest.php index 913035cd..ddd575b5 100644 --- a/test/Listener/PrepareSolutionListenerTest.php +++ b/test/Listener/PrepareSolutionListenerTest.php @@ -5,7 +5,7 @@ use PhpSchool\PhpWorkshop\Event\ExerciseRunnerEvent; use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext; use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\HostSandbox; use PhpSchool\PhpWorkshop\Process\ProcessNotFoundException; use PhpSchool\PhpWorkshop\Solution\SolutionInterface; use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl; @@ -33,7 +33,7 @@ public function testIfSolutionRequiresComposerButComposerCannotBeLocatedExceptio $this->expectException(ProcessNotFoundException::class); $this->expectExceptionMessage('Could not find executable: "composer"'); $event = new ExerciseRunnerEvent('event', $context); - (new PrepareSolutionListener(new HostProcessFactory($finder)))->__invoke($event); + (new PrepareSolutionListener(new HostSandbox($finder)))->__invoke($event); } public function testIfSolutionRequiresComposerButVendorDirExistsNothingIsDone(): void @@ -54,7 +54,7 @@ public function testIfSolutionRequiresComposerButVendorDirExistsNothingIsDone(): $exercise->setSolution($solution); $event = new ExerciseRunnerEvent('event', $context); - (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); + (new PrepareSolutionListener(new HostSandbox()))->__invoke($event); $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); //check for non existence of lock file, composer generates this when updating if it doesn't exist @@ -84,7 +84,7 @@ public function testIfSolutionRequiresComposerComposerInstallIsExecuted(): void $exercise->setSolution($solution); $event = new ExerciseRunnerEvent('event', $context); - (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); + (new PrepareSolutionListener(new HostSandbox()))->__invoke($event); $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } @@ -119,7 +119,7 @@ public function testExceptionIsThrownIfDependenciesCannotBeResolved(): void ->willReturn(true); $event = new ExerciseRunnerEvent('event', $context); - (new PrepareSolutionListener(new HostProcessFactory()))->__invoke($event); + (new PrepareSolutionListener(new HostSandbox()))->__invoke($event); $this->assertFileExists(sprintf('%s/vendor', $context->getReferenceExecutionDirectory())); } diff --git a/test/Process/DockerExecutionTest.php b/test/Process/DockerExecutionTest.php new file mode 100644 index 00000000..88444e1b --- /dev/null +++ b/test/Process/DockerExecutionTest.php @@ -0,0 +1,47 @@ +> ' . $log), + [Process::fromShellCommandline('echo before >> ' . $log)], + [Process::fromShellCommandline('echo after >> ' . $log)], + ); + + $execution->start(); + $execution->wait(); + + $this->assertSame("before\nmain\nafter\n", file_get_contents($log)); + unlink($log); + } + + public function testAfterStepsRunWhenTheMainProcessFails(): void + { + $log = tempnam(sys_get_temp_dir(), 'steps'); + + $execution = new DockerExecution( + Process::fromShellCommandline('exit 1'), + [], + [Process::fromShellCommandline('echo after >> ' . $log)], + ); + + $execution->start(); + $execution->wait(); + + $this->assertFalse($execution->isSuccessful()); + $this->assertSame("after\n", file_get_contents($log)); + unlink($log); + } +} diff --git a/test/Process/DockerProcessFactoryTest.php b/test/Process/DockerSandboxTest.php similarity index 88% rename from test/Process/DockerProcessFactoryTest.php rename to test/Process/DockerSandboxTest.php index e3e7fd5a..85e53515 100644 --- a/test/Process/DockerProcessFactoryTest.php +++ b/test/Process/DockerSandboxTest.php @@ -2,15 +2,15 @@ namespace PhpSchool\PhpWorkshopTest\Process; -use PhpSchool\PhpWorkshop\Process\DockerProcessFactory; +use PhpSchool\PhpWorkshop\Process\DockerSandbox; use PhpSchool\PhpWorkshop\Process\ProcessInput; use PhpSchool\PhpWorkshop\Process\ProcessNotFoundException; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\ExecutableFinder; -class DockerProcessFactoryTest extends TestCase +class DockerSandboxTest extends TestCase { - private function factory(string $portsNetwork = 'bridge'): DockerProcessFactory + private function factory(string $portsNetwork = 'bridge'): DockerSandbox { $finder = $this->createMock(ExecutableFinder::class); $finder->expects($this->once()) @@ -18,7 +18,7 @@ private function factory(string $portsNetwork = 'bridge'): DockerProcessFactory ->with('docker') ->willReturn('/usr/local/bin/docker'); - return new DockerProcessFactory('/docker-dir', 'PHP8Appreciate', '/composer/cache/dir', $finder, $portsNetwork); + return new DockerSandbox('/docker-dir', 'PHP8Appreciate', '/composer/cache/dir', $finder, $portsNetwork); } /** @@ -52,7 +52,7 @@ public function testCreateThrowsExceptionIfDockerNotFound(): void ->with('docker') ->willReturn(null); - $factory = new DockerProcessFactory('/docker-dir', 'php8appreciate', '/composer/cache/dir', $finder); + $factory = new DockerSandbox('/docker-dir', 'php8appreciate', '/composer/cache/dir', $finder); $input = new ProcessInput('composer', [], __DIR__, [], []); $factory->create($input); @@ -60,7 +60,7 @@ public function testCreateThrowsExceptionIfDockerNotFound(): void public function testCreateIsIsolatedWithNoNetwork(): void { - $process = $this->factory()->create(new ProcessInput('php', ['one', 'two'], __DIR__, [], [])); + $process = $this->factory()->create(new ProcessInput('php', ['one', 'two'], __DIR__, [], []))->process; $expected = $this->expectedCommand( ['--network', 'none'], @@ -74,7 +74,7 @@ public function testCreateIsIsolatedWithNoNetwork(): void public function testComposerGetsNetworkCacheAndNoPluginsOrScripts(): void { - $process = $this->factory()->create(new ProcessInput('composer', ['install'], __DIR__, [], [])); + $process = $this->factory()->create(new ProcessInput('composer', ['install'], __DIR__, [], []))->process; $expected = $this->expectedCommand( ['--network', 'bridge'], @@ -86,7 +86,7 @@ public function testComposerGetsNetworkCacheAndNoPluginsOrScripts(): void public function testExposedPortsArePublishedOnLoopbackOnly(): void { - $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, [], [8000])); + $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, [], [8000]))->process; $expected = $this->expectedCommand( ['--network', 'bridge', '-p', '127.0.0.1:8000:8000'], @@ -98,7 +98,7 @@ public function testExposedPortsArePublishedOnLoopbackOnly(): void public function testExposedPortsUseTheGivenNetwork(): void { - $process = $this->factory('php-school-sandbox')->create(new ProcessInput('php', [], __DIR__, [], [8000])); + $process = $this->factory('php-school-sandbox')->create(new ProcessInput('php', [], __DIR__, [], [8000]))->process; $expected = $this->expectedCommand( ['--network', 'php-school-sandbox', '-p', '127.0.0.1:8000:8000'], @@ -110,7 +110,7 @@ public function testExposedPortsUseTheGivenNetwork(): void public function testCreateWithEnv(): void { - $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, ['SOME_VAR' => 'value'], [])); + $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, ['SOME_VAR' => 'value'], []))->process; $expected = $this->expectedCommand( ['--network', 'none'], @@ -122,7 +122,7 @@ public function testCreateWithEnv(): void public function testWithInput(): void { - $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, [], [], 'someinput')); + $process = $this->factory()->create(new ProcessInput('php', [], __DIR__, [], [], 'someinput'))->process; static::assertSame('someinput', $process->getInput()); } diff --git a/test/Process/HostProcessFactoryTest.php b/test/Process/HostSandboxTest.php similarity index 82% rename from test/Process/HostProcessFactoryTest.php rename to test/Process/HostSandboxTest.php index 8e334f82..191ec899 100644 --- a/test/Process/HostProcessFactoryTest.php +++ b/test/Process/HostSandboxTest.php @@ -5,10 +5,10 @@ use PhpSchool\PhpWorkshop\Process\ProcessInput; use PhpSchool\PhpWorkshop\Process\ProcessNotFoundException; use Symfony\Component\Process\ExecutableFinder; -use PhpSchool\PhpWorkshop\Process\HostProcessFactory; +use PhpSchool\PhpWorkshop\Process\HostSandbox; use PHPUnit\Framework\TestCase; -class HostProcessFactoryTest extends TestCase +class HostSandboxTest extends TestCase { public function testCreateThrowsExceptionIfExecutableNotFound(): void { @@ -20,7 +20,7 @@ public function testCreateThrowsExceptionIfExecutableNotFound(): void ->with('composer') ->willReturn(null); - $factory = new HostProcessFactory($finder); + $factory = new HostSandbox($finder); $input = new ProcessInput('composer', [], __DIR__, [], []); $factory->create($input); @@ -34,10 +34,10 @@ public function testCreate(): void ->with('composer') ->willReturn('/usr/local/bin/composer'); - $factory = new HostProcessFactory($finder); + $factory = new HostSandbox($finder); $input = new ProcessInput('composer', [], __DIR__, [], []); - $process = $factory->create($input); + $process = $factory->create($input)->process; static::assertSame("'/usr/local/bin/composer'", $process->getCommandLine()); } @@ -49,10 +49,10 @@ public function testCreateWithArgs(): void ->with('composer') ->willReturn('/usr/local/bin/composer'); - $factory = new HostProcessFactory($finder); + $factory = new HostSandbox($finder); $input = new ProcessInput('composer', ['one', 'two'], __DIR__, [], []); - $process = $factory->create($input); + $process = $factory->create($input)->process; static::assertSame("'/usr/local/bin/composer' 'one' 'two'", $process->getCommandLine()); } @@ -64,10 +64,10 @@ public function testCreateWithEnv(): void ->with('composer') ->willReturn('/usr/local/bin/composer'); - $factory = new HostProcessFactory($finder); + $factory = new HostSandbox($finder); $input = new ProcessInput('composer', ['one', 'two'], __DIR__, ['SOME_VAR' => 'value'], []); - $process = $factory->create($input); + $process = $factory->create($input)->process; static::assertSame('value', $process->getEnv()['SOME_VAR']); } @@ -76,7 +76,7 @@ public function testInheritedEnvIsRemovedExceptAllowed(): void putenv('PHPSCHOOL_SECRET=secret'); try { - $process = (new HostProcessFactory())->create( + $process = (new HostSandbox())->create( new ProcessInput('php', ['-r', 'echo json_encode([getenv("PHPSCHOOL_SECRET"), getenv("PATH") !== false]);'], __DIR__, [], []), ); $process->mustRun(); @@ -95,10 +95,10 @@ public function testWithInput(): void ->with('composer') ->willReturn('/usr/local/bin/composer'); - $factory = new HostProcessFactory($finder); + $factory = new HostSandbox($finder); $input = new ProcessInput('composer', [], __DIR__, [], [], 'someinput'); - $process = $factory->create($input); + $process = $factory->create($input)->process; static::assertSame('someinput', $process->getInput()); } }