Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c0a9650

Browse files
committedMay 5, 2024
Context
1 parent fb82157 commit c0a9650

13 files changed

+497
-0
lines changed
 

‎src/Exercise/MockExercise.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Exercise;
4+
5+
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
6+
7+
class MockExercise extends AbstractExercise implements ExerciseInterface
8+
{
9+
public function getName(): string
10+
{
11+
return 'Mock Exercise';
12+
}
13+
14+
public function getDescription(): string
15+
{
16+
return 'Mock Exercise';
17+
}
18+
19+
public function getType(): ExerciseType
20+
{
21+
return ExerciseType::CUSTOM();
22+
}
23+
24+
public function getProblem(): string
25+
{
26+
return 'problem-file.md';
27+
}
28+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
6+
use PhpSchool\PhpWorkshop\Input\Input;
7+
use PhpSchool\PhpWorkshop\Utils\Path;
8+
use PhpSchool\PhpWorkshop\Utils\System;
9+
10+
class ExecutionContext
11+
{
12+
public function __construct(
13+
private string $studentExecutionDirectory,
14+
private string $referenceExecutionDirectory,
15+
private ExerciseInterface $exercise,
16+
private Input $input,
17+
) {
18+
}
19+
20+
public function getExercise(): ExerciseInterface
21+
{
22+
return $this->exercise;
23+
}
24+
25+
public function getInput(): Input
26+
{
27+
return $this->input;
28+
}
29+
30+
public function hasStudentSolution(): bool
31+
{
32+
return $this->input->hasArgument('program');
33+
}
34+
35+
public function getEntryPoint(): string
36+
{
37+
if (!$this->hasStudentSolution()) {
38+
throw new NoEntryPoint();
39+
}
40+
41+
return Path::join(
42+
$this->studentExecutionDirectory,
43+
basename($this->input->getRequiredArgument('program'))
44+
);
45+
}
46+
47+
public function getStudentExecutionDirectory(): string
48+
{
49+
return $this->studentExecutionDirectory;
50+
}
51+
52+
public function getReferenceExecutionDirectory(): string
53+
{
54+
return $this->referenceExecutionDirectory;
55+
}
56+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
6+
use PhpSchool\PhpWorkshop\Input\Input;
7+
use PhpSchool\PhpWorkshop\Utils\System;
8+
9+
class ExecutionContextFactory
10+
{
11+
public function fromInputAndExercise(Input $input, ExerciseInterface $exercise): ExecutionContext
12+
{
13+
return new ExecutionContext(
14+
dirname($input->getRequiredArgument('program')),
15+
System::randomTempDir(),
16+
$exercise,
17+
$input
18+
);
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exception\RuntimeException;
6+
7+
class NoEntryPoint extends RuntimeException
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct('No entry point provided');
12+
}
13+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
6+
use PhpSchool\PhpWorkshop\Input\Input;
7+
use PhpSchool\PhpWorkshop\Utils\System;
8+
use Symfony\Component\Filesystem\Filesystem;
9+
10+
class StaticExecutionContextFactory extends ExecutionContextFactory
11+
{
12+
public function __construct(private TestContext $context)
13+
{
14+
}
15+
16+
public function fromInputAndExercise(Input $input, ExerciseInterface $exercise): ExecutionContext
17+
{
18+
return $this->context;
19+
}
20+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exception\RuntimeException;
6+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
7+
use PhpSchool\PhpWorkshop\Exercise\MockExercise;
8+
use PhpSchool\PhpWorkshop\Input\Input;
9+
use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
10+
use PhpSchool\PhpWorkshop\Utils\System;
11+
use Symfony\Component\Filesystem\Filesystem;
12+
use PhpSchool\PhpWorkshop\Utils\Path;
13+
14+
class TestContext extends ExecutionContext
15+
{
16+
public Filesystem $filesystem;
17+
public ExerciseInterface $exercise;
18+
19+
private function __construct(
20+
ExerciseInterface $exercise = null,
21+
Input $input = null
22+
) {
23+
$this->exercise = $exercise ?? new MockExercise();
24+
25+
$this->filesystem = new Filesystem();
26+
27+
parent::__construct(
28+
System::randomTempDir(),
29+
System::randomTempDir(),
30+
$this->exercise,
31+
$input ? $input : new Input('test', ['program' => 'solution.php']),
32+
);
33+
}
34+
35+
public function importStudentSolution(string $file): void
36+
{
37+
if (!$this->filesystem->exists($this->getStudentExecutionDirectory())) {
38+
throw new RuntimeException(
39+
sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class)
40+
);
41+
}
42+
43+
copy($file, Path::join($this->getStudentExecutionDirectory(), 'solution.php'));
44+
}
45+
46+
public function importStudentSolutionFolder(string $folder): void
47+
{
48+
if (!$this->filesystem->exists($this->getStudentExecutionDirectory())) {
49+
throw new RuntimeException(
50+
sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class)
51+
);
52+
}
53+
54+
$this->filesystem->mirror($folder, $this->getStudentExecutionDirectory());
55+
}
56+
57+
public function importReferenceSolution(SolutionInterface $solution): void
58+
{
59+
if (!$this->filesystem->exists($this->getReferenceExecutionDirectory())) {
60+
throw new RuntimeException(
61+
sprintf('Execution directories not created. Use %s::withDirectories() method instead.', self::class)
62+
);
63+
}
64+
65+
foreach ($solution->getFiles() as $file) {
66+
$this->filesystem->copy(
67+
$file->getAbsolutePath(),
68+
Path::join($this->getReferenceExecutionDirectory(), $file->getRelativePath())
69+
);
70+
}
71+
}
72+
73+
public static function withDirectories(Input $input = null, ExerciseInterface $exercise = null): self
74+
{
75+
$self = new self($exercise, $input);
76+
77+
$self->filesystem->mkdir($self->getStudentExecutionDirectory());
78+
$self->filesystem->mkdir($self->getReferenceExecutionDirectory());
79+
80+
return $self;
81+
}
82+
83+
public static function withoutDirectories(Input $input = null, ExerciseInterface $exercise = null): self
84+
{
85+
return new self($exercise, $input);
86+
}
87+
88+
public function __destruct()
89+
{
90+
$this->filesystem->remove($this->getStudentExecutionDirectory());
91+
$this->filesystem->remove($this->getReferenceExecutionDirectory());
92+
}
93+
}

‎src/Utils/System.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ public static function tempDir(string $path = ''): string
2323
{
2424
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', $path);
2525
}
26+
27+
public static function randomTempDir(): string
28+
{
29+
return Path::join(self::realpath(sys_get_temp_dir()), 'php-school', bin2hex(random_bytes(4)));
30+
}
2631
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\MockExercise;
6+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContextFactory;
7+
use PhpSchool\PhpWorkshop\Input\Input;
8+
use PhpSchool\PhpWorkshop\Utils\System;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ExecutionContextFactoryTest extends TestCase
12+
{
13+
public function testFactory(): void
14+
{
15+
$factory = new ExecutionContextFactory();
16+
17+
$temporaryDirectory = System::randomTempDir();
18+
19+
$input = new Input('test', ['program' => $temporaryDirectory . '/solution.php']);
20+
$exercise = new MockExercise();
21+
22+
$context = $factory->fromInputAndExercise($input, $exercise);
23+
24+
//check that student execution directory uses the parent directory of the program from the input
25+
static::assertSame($temporaryDirectory, $context->getStudentExecutionDirectory());
26+
static::assertSame($temporaryDirectory . '/solution.php', $context->getEntryPoint());
27+
28+
//check that reference execution directory is a random temporary directory
29+
static::assertTrue(str_starts_with($context->getReferenceExecutionDirectory(), System::tempDir()));
30+
}
31+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\MockExercise;
6+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\ExecutionContext;
7+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\NoEntryPoint;
8+
use PhpSchool\PhpWorkshop\Input\Input;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ExecutionContextTest extends TestCase
12+
{
13+
public function testGetters(): void
14+
{
15+
$exercise = new MockExercise();
16+
$input = new Input('test', ['program' => 'solution.php']);
17+
$context = new ExecutionContext(
18+
'/student-dir',
19+
'/reference-dir',
20+
$exercise,
21+
$input
22+
);
23+
24+
static::assertSame($exercise, $context->getExercise());
25+
static::assertSame($input, $context->getInput());
26+
static::assertSame('/student-dir', $context->getStudentExecutionDirectory());
27+
static::assertSame('/reference-dir', $context->getReferenceExecutionDirectory());
28+
}
29+
30+
public function testHasStudentSolution(): void
31+
{
32+
$exercise = new MockExercise();
33+
$input = new Input('test', ['program' => 'solution.php']);
34+
$context = new ExecutionContext(
35+
'/student-dir',
36+
'/reference-dir',
37+
$exercise,
38+
$input
39+
);
40+
41+
static::assertTrue($context->hasStudentSolution());
42+
43+
$exercise = new MockExercise();
44+
$input = new Input('test');
45+
$context = new ExecutionContext(
46+
'/student-dir',
47+
'/reference-dir',
48+
$exercise,
49+
$input
50+
);
51+
52+
static::assertFalse($context->hasStudentSolution());
53+
}
54+
55+
public function testGetEntryPoint(): void
56+
{
57+
$exercise = new MockExercise();
58+
$input = new Input('test', ['program' => 'solution.php']);
59+
$context = new ExecutionContext(
60+
'/student-dir',
61+
'/reference-dir',
62+
$exercise,
63+
$input
64+
);
65+
66+
static::assertSame('/student-dir/solution.php', $context->getEntryPoint());
67+
}
68+
69+
public function testGetEntryPointThrowsExceptionWhenNoStudentSolution(): void
70+
{
71+
static::expectException(NoEntryPoint::class);
72+
73+
$exercise = new MockExercise();
74+
$input = new Input('test');
75+
$context = new ExecutionContext(
76+
'/student-dir',
77+
'/reference-dir',
78+
$exercise,
79+
$input
80+
);
81+
82+
$context->getEntryPoint();
83+
}
84+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\NoEntryPoint;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class NoEntryPointTest extends TestCase
9+
{
10+
public function testException(): void
11+
{
12+
$e = new NoEntryPoint();
13+
static::assertSame('No entry point provided', $e->getMessage());
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\MockExercise;
6+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\StaticExecutionContextFactory;
7+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext;
8+
use PhpSchool\PhpWorkshop\Input\Input;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class StaticExecutionContextFactoryTest extends TestCase
12+
{
13+
public function testFactoryReturnsGivenTestContext(): void
14+
{
15+
$context = TestContext::withoutDirectories(
16+
new Input('test', ['program' => 'solution.php']),
17+
new MockExercise()
18+
);
19+
20+
$factory = new StaticExecutionContextFactory($context);
21+
22+
static::assertSame($context, $factory->fromInputAndExercise(new Input('test', []), new MockExercise()));
23+
}
24+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\ExerciseRunner\Context;
4+
5+
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
6+
use PhpSchool\PhpWorkshop\ExerciseRunner\Context\TestContext;
7+
use PhpSchool\PhpWorkshop\Solution\DirectorySolution;
8+
use PhpSchool\PhpWorkshop\Utils\System;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class TestContextTest extends TestCase
12+
{
13+
public function testThatWithoutDirectoriesDoesNotCreateExecutionDirectories(): void
14+
{
15+
$context = TestContext::withoutDirectories();
16+
17+
static::assertFileNotExists($context->getStudentExecutionDirectory());
18+
static::assertFileNotExists($context->getReferenceExecutionDirectory());
19+
}
20+
21+
public function testWithDirectoriesCreatesExecutionDirectories(): void
22+
{
23+
$context = TestContext::withDirectories();
24+
25+
static::assertFileExists($context->getStudentExecutionDirectory());
26+
static::assertFileExists($context->getReferenceExecutionDirectory());
27+
}
28+
29+
public function testImportStudentSolutionThrowsExceptionIfExecutionDirectoryDoesNotExist(): void
30+
{
31+
$this->expectException(\RuntimeException::class);
32+
33+
$context = TestContext::withoutDirectories();
34+
$context->importStudentSolution('path/to/solution.php');
35+
}
36+
37+
public function testImportStudentSolutionCopiesSolutionToExecutionDirectory(): void
38+
{
39+
$context = TestContext::withDirectories();
40+
41+
$context->importStudentSolution(__FILE__);
42+
43+
static::assertFileExists($context->getStudentExecutionDirectory() . '/solution.php');
44+
static::assertFileEquals(__FILE__, $context->getStudentExecutionDirectory() . '/solution.php');
45+
}
46+
47+
public function testImportStudentSolutionFolderCopiesSolutionToExecutionDirectory(): void
48+
{
49+
$context = TestContext::withDirectories();
50+
51+
$context->importStudentSolutionFolder(__DIR__);
52+
53+
static::assertFileExists($context->getStudentExecutionDirectory());
54+
static::assertCount($this->getFileCountInThisDirectory(), scandir($context->getStudentExecutionDirectory()));
55+
}
56+
57+
public function testImportStudentSolutionFolderThrowsExceptionIfExecutionDirectoryDoesNotExist(): void
58+
{
59+
$this->expectException(\RuntimeException::class);
60+
61+
$context = TestContext::withoutDirectories();
62+
$context->importStudentSolutionFolder('path/to/solution');
63+
}
64+
65+
public function testImportReferenceSolutionFolderThrowsExceptionIfExecutionDirectoryDoesNotExist(): void
66+
{
67+
$this->expectException(\RuntimeException::class);
68+
69+
$context = TestContext::withoutDirectories();
70+
$context->importReferenceSolution(DirectorySolution::fromDirectory('path/to/solution'));
71+
}
72+
73+
public function testImportReferenceSolutionFolderCopiesSolutionToExecutionDirectory(): void
74+
{
75+
$context = TestContext::withDirectories();
76+
77+
$context->importReferenceSolution(DirectorySolution::fromDirectory(__DIR__, [], basename(__FILE__)));
78+
79+
static::assertFileExists($context->getStudentExecutionDirectory());
80+
static::assertCount($this->getFileCountInThisDirectory(), scandir($context->getReferenceExecutionDirectory()));
81+
}
82+
83+
public function testDestructCleansUpExecutionDirectories(): void
84+
{
85+
$context = TestContext::withDirectories();
86+
87+
$studentExecutionDirectory = $context->getStudentExecutionDirectory();
88+
$referenceExecutionDirectory = $context->getReferenceExecutionDirectory();
89+
90+
static::assertFileExists($studentExecutionDirectory);
91+
static::assertFileExists($referenceExecutionDirectory);
92+
93+
unset($context);
94+
95+
static::assertFileNotExists($studentExecutionDirectory);
96+
static::assertFileNotExists($referenceExecutionDirectory);
97+
}
98+
99+
private function getFileCountInThisDirectory(): int
100+
{
101+
return count(scandir(__DIR__));
102+
}
103+
}

‎test/Utils/SystemTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ public function testTempDirWithPath(): void
3333
$expect = sprintf('%s/php-school/%s', realpath(sys_get_temp_dir()), 'test');
3434
self::assertSame($expect, System::tempDir('test'));
3535
}
36+
37+
public function testRandomTempDir(): void
38+
{
39+
self::assertTrue(str_starts_with(System::randomTempDir(), realpath(sys_get_temp_dir()) . '/php-school'));
40+
}
3641
}

0 commit comments

Comments
 (0)
Please sign in to comment.