Skip to content

Allow exercises to provide initial code #183

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

Merged
merged 2 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 13 additions & 4 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Colors\Color;
use PhpSchool\PhpWorkshop\Listener\InitialCodeListener;
use function DI\create;
use function DI\factory;
use Kadet\Highlighter\KeyLighter;
Expand Down Expand Up @@ -201,17 +202,20 @@
},

//Listeners
PrepareSolutionListener::class => create(),
CodePatchListener::class => function (ContainerInterface $c) {
InitialCodeListener::class => function (ContainerInterface $c) {
return new InitialCodeListener(getcwd());
},
PrepareSolutionListener::class => create(),
CodePatchListener::class => function (ContainerInterface $c) {
return new CodePatchListener($c->get(CodePatcher::class));
},
SelfCheckListener::class => function (ContainerInterface $c) {
SelfCheckListener::class => function (ContainerInterface $c) {
return new SelfCheckListener($c->get(ResultAggregator::class));
},
CheckExerciseAssignedListener::class => function (ContainerInterface $c) {
return new CheckExerciseAssignedListener($c->get(UserState::class));
},
ConfigureCommandListener::class => function (ContainerInterface $c) {
ConfigureCommandListener::class => function (ContainerInterface $c) {
return new ConfigureCommandListener(
$c->get(UserState::class),
$c->get(ExerciseRepository::class),
Expand Down Expand Up @@ -393,5 +397,10 @@ function (CgiResult $result) use ($c) {
containerListener(SelfCheckListener::class)
],
],
'create-initial-code' => [
'exercise.selected' => [
containerListener(InitialCodeListener::class)
]
]
],
];
21 changes: 21 additions & 0 deletions src/Exercise/ProvidesInitialCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshop\Exercise;

use PhpSchool\PhpWorkshop\Solution\SolutionInterface;

/**
* Exercises can implement this method if they want to provide some
* code for the user to start with, eg a failing solution.
*/
interface ProvidesInitialCode
{
/**
* Get the exercise solution.
*
* @return SolutionInterface
*/
public function getInitialCode(): SolutionInterface;
}
8 changes: 2 additions & 6 deletions src/Factory/MenuFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,9 @@ private function isExerciseDisabled(ExerciseInterface $exercise, UserState $user
*/
private function dispatchExerciseSelectedEvent(EventDispatcher $eventDispatcher, ExerciseInterface $exercise): void
{
$eventDispatcher->dispatch(new Event('exercise.selected', ['exercise' => $exercise]));
$eventDispatcher->dispatch(
new Event(
sprintf(
'exercise.selected.%s',
AbstractExercise::normaliseName($exercise->getName())
)
)
new Event(sprintf('exercise.selected.%s', AbstractExercise::normaliseName($exercise->getName())))
);
}
}
46 changes: 46 additions & 0 deletions src/Listener/InitialCodeListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshop\Listener;

use PhpSchool\PhpWorkshop\Event\Event;
use PhpSchool\PhpWorkshop\Exercise\ProvidesInitialCode;
use PhpSchool\PhpWorkshop\Solution\SolutionFile;

/**
* Copy over any initial files for this exercise when
* it is selected in the menu - only if they do not exist already
*
* We might want to ask the user to force this if the files exist, we could also check
* the contents match what we expect.
*/
class InitialCodeListener
{
/**
* @var string
*/
private $workingDirectory;

public function __construct(string $workingDirectory)
{
$this->workingDirectory = $workingDirectory;
}

/**
* @param Event $event
*/
public function __invoke(Event $event): void
{
$exercise = $event->getParameter('exercise');

if ($exercise instanceof ProvidesInitialCode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return early instead of nesting ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure - i'll refactor

foreach ($exercise->getInitialCode()->getFiles() as $file) {
/** @var SolutionFile $file */
if (!file_exists($this->workingDirectory . '/' . $file->getRelativePath())) {
copy($file->getAbsolutePath(), $this->workingDirectory . '/' . $file->getRelativePath());
}
}
}
}
}
4 changes: 4 additions & 0 deletions src/Solution/SingleFileSolution.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class SingleFileSolution implements SolutionInterface
*/
public function __construct(string $file)
{
if (!file_exists($file)) {
throw new InvalidArgumentException(sprintf('File: "%s" does not exist', $file));
}

$this->file = SolutionFile::fromFile((string) realpath($file));
}

Expand Down
2 changes: 1 addition & 1 deletion src/Solution/SolutionFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static function fromFile(string $file): self
*
* @return string
*/
private function getAbsolutePath(): string
public function getAbsolutePath(): string
{
return sprintf('%s/%s', $this->baseDirectory, $this->relativePath);
}
Expand Down
53 changes: 53 additions & 0 deletions test/Asset/ExerciseWithInitialCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace PhpSchool\PhpWorkshopTest\Asset;

use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
use PhpSchool\PhpWorkshop\Exercise\ProvidesInitialCode;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
use PhpSchool\PhpWorkshop\Solution\SingleFileSolution;
use PhpSchool\PhpWorkshop\Solution\SolutionInterface;

class ExerciseWithInitialCode implements ExerciseInterface, ProvidesInitialCode
{
public function getName(): string
{
// TODO: Implement getName() method.
}

public function getDescription(): string
{
// TODO: Implement getDescription() method.
}

public function getSolution(): string
{
// TODO: Implement getSolution() method.
}

public function getProblem(): string
{
// TODO: Implement getProblem() method.
}

public function tearDown(): void
{
// TODO: Implement tearDown() method.
}

public function getType(): ExerciseType
{
// TODO: Implement getType() method.
}

public function configure(ExerciseDispatcher $dispatcher): void
{
// TODO: Implement configure() method.
}

public function getInitialCode(): SolutionInterface
{
return SingleFileSolution::fromFile(__DIR__ . '/initial-code/init-solution.php');
}
}
5 changes: 5 additions & 0 deletions test/Asset/initial-code/init-solution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

declare(strict_types=1);

echo "This is an initial solution";
66 changes: 66 additions & 0 deletions test/Listener/InitialCodeListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshopTest\Listener;

use PhpSchool\PhpWorkshop\Event\Event;
use PhpSchool\PhpWorkshop\Listener\InitialCodeListener;
use PhpSchool\PhpWorkshopTest\Asset\CliExerciseImpl;
use PhpSchool\PhpWorkshopTest\Asset\ExerciseWithInitialCode;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Filesystem\Filesystem;

class InitialCodeListenerTest extends TestCase
{
/**
* @var Filesystem
*/
private $filesystem;

/**
* @var string
*/
private $cwd;

public function setUp(): void
{
$this->filesystem = new Filesystem();

$this->cwd = sprintf('%s/%s', str_replace('\\', '/', sys_get_temp_dir()), $this->getName());
mkdir($this->cwd, 0775, true);
}

public function testExerciseCodeIsCopiedIfExerciseProvidesInitialCode(): void
{
$exercise = new ExerciseWithInitialCode();

$event = new Event('exercise.selected', ['exercise' => $exercise]);

$listener = new InitialCodeListener($this->cwd);
$listener->__invoke($event);

$this->assertFileExists($this->cwd . '/init-solution.php');
$this->assertFileEquals(
$exercise->getInitialCode()->getFiles()[0]->getAbsolutePath(),
$this->cwd . '/init-solution.php'
);
}

public function testExerciseCodeIsNotCopiedIfExerciseDoesNotProvideInitialCode(): void
{
$exercise = new CliExerciseImpl();

$event = new Event('exercise.selected', ['exercise' => $exercise]);

$listener = new InitialCodeListener($this->cwd);
$listener->__invoke($event);

$this->assertEmpty(array_diff(scandir($this->cwd), ['.', '..']));
}

public function tearDown(): void
{
$this->filesystem->remove($this->cwd);
}
}