Skip to content

Commit

Permalink
Merge pull request #32 from jaredpearson/jp/state-action-arg-validation
Browse files Browse the repository at this point in the history
Add action and args state validation
  • Loading branch information
danielholmes authored Jun 1, 2021
2 parents d54e006 + ff16d73 commit ef7eaed
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 13 deletions.
51 changes: 51 additions & 0 deletions src/BGAWorkbench/Validate/StateValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
namespace BGAWorkbench\Validate;

use BGAWorkbench\Project\Project;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Symfony\Component\Config\Definition\Processor;
use Functional as F;
Expand Down Expand Up @@ -52,6 +54,7 @@ function (array $state) use ($stateIds) {
$this->validateGameSetupState($states);
$this->validateGameEndState($states);
$this->validateGameSetupToEnd($states);
$this->validateActionAndArgsMethodExist($states, $project);
}

private function validateGameSetupState(array $states)
Expand Down Expand Up @@ -193,6 +196,54 @@ private function visitState(int $stateId, array $state, array $states, array &$v
return false;
}

/**
* Verifies that every method defined in the state's "action" parameter has a corresponding action method in the
* class that extends "Table".
*
* @param array $states
* @param Project $project
*/
private function validateActionAndArgsMethodExist(array $states, Project $project): void
{
$tableObj = $project->createGameTableInstance();
$reflectionTableObj = new ReflectionClass($tableObj);

foreach ($states as $stateId => $state) {
if ($stateId === self::GAME_SETUP_ID || $stateId === self::GAME_END_ID) {
// Skip the gameSetup and gameEnd states
continue;
}

if (array_key_exists('action', $state)) {
try {
$reflectionActionMethod = $reflectionTableObj->getMethod($state['action']);
} catch (ReflectionException $exception) {
throw new RuntimeException("Action {$state['action']} defined in state {$stateId} is not defined in Table. Ensure that the method is defined in {$project->getName()}.game.php.");
}
if (!$reflectionActionMethod->isPublic()) {
throw new RuntimeException("Action {$state['action']} defined in state {$stateId} must be public. Ensure that the method is defined as 'public' in {$project->getName()}.game.php.");
}
if ($reflectionActionMethod->isAbstract()) {
throw new RuntimeException("Action {$state['action']} defined in state {$stateId} cannot be abstract. Remove the 'abstract' modifier from the method in {$project->getName()}.game.php.");
}
}

if (array_key_exists('args', $state)) {
try {
$reflectionActionMethod = $reflectionTableObj->getMethod($state['args']);
} catch (ReflectionException $exception) {
throw new RuntimeException("Arguments {$state['args']} defined in state {$stateId} is not defined in Table. Ensure that the method is defined in {$project->getName()}.game.php.");
}
if (!$reflectionActionMethod->isPublic()) {
throw new RuntimeException("Arguments {$state['args']} defined in state {$stateId} must be public. Ensure that the method is defined as 'public' in {$project->getName()}.game.php.");
}
if ($reflectionActionMethod->isAbstract()) {
throw new RuntimeException("Arguments {$state['args']} defined in state {$stateId} cannot be abstract. Remove the 'abstract' modifier from the method in {$project->getName()}.game.php.");
}
}
}
}

private function getGameSetupStateStateExample(): string
{
return <<<END
Expand Down
164 changes: 151 additions & 13 deletions tests/BGAWorkbench/Tests/Validate/StateValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ public function testValid(): void
"description" => "",
"type" => "game",
"action" => "stBar",
"transitions" => array("repeat" => 10, "continue" => 99)
"transitions" => array("repeat" => 10, "continue" => 12, "end" => 99)
),
12 => array(
"name" => "caz",
"description" => "",
"type" => "activeplayer",
"args" => "argCaz",
"transitions" => array("continue" => 99)
),
99 => array(
"name" => "gameEnd",
Expand All @@ -49,7 +56,7 @@ public function testValid(): void
);
END;

$projectDirectory = $this->createTestProject($statesIncPhpContents);
$projectDirectory = $this->createTestProject('valid', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'valid');
(new StateValidator())->validateStates($project);
}
Expand Down Expand Up @@ -79,8 +86,8 @@ public function testMissingGameSetupState(): void
)
);
END;
$projectDirectory = $this->createTestProject($statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'missing-game-setup');
$projectDirectory = $this->createTestProject('missinggamesetup', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'missinggamesetup');
(new StateValidator())->validateStates($project);
}

Expand Down Expand Up @@ -110,8 +117,8 @@ public function testMissingGameEnd(): void
);
END;

$projectDirectory = $this->createTestProject($statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'missing-game-end');
$projectDirectory = $this->createTestProject('missinggameend', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'missinggameend');
(new StateValidator())->validateStates($project);
}

Expand Down Expand Up @@ -150,8 +157,8 @@ public function testInvalidStateInTransition(): void
);
END;

$projectDirectory = $this->createTestProject($statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'looping');
$projectDirectory = $this->createTestProject('invalidstate', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'invalidstate');
(new StateValidator())->validateStates($project);
}

Expand Down Expand Up @@ -197,8 +204,8 @@ public function testGameSetupDoesNotReachGameEnd(): void
);
END;

$projectDirectory = $this->createTestProject($statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'looping');
$projectDirectory = $this->createTestProject('doesnotend', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'doesnotend');
(new StateValidator())->validateStates($project);
}

Expand Down Expand Up @@ -244,18 +251,149 @@ public function testStateLoop(): void
);
END;

$projectDirectory = $this->createTestProject($statesIncPhpContents);
$projectDirectory = $this->createTestProject('looping', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'looping');
(new StateValidator())->validateStates($project);
}

private function createTestProject(string $statesIncPhpContents): WorkingDirectory
/**
* Verifies that an exception is thrown when state defines an unknown function for "action" property.
*/
public function testUnknownMethodInAction(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Action stUnknown defined in state 11 is not defined in Table.');

$statesIncPhpContents = <<<END
<?php
\$machinestates = array(
1 => array(
"name" => "gameSetup",
"description" => "",
"type" => "manager",
"action" => "stGameSetup",
"transitions" => array("" => 10)
),
10 => array(
"name" => "foo",
"description" => "",
"type" => "game",
"action" => "stFoo",
"transitions" => array("continue" => 11)
),
11 => array(
"name" => "unknown",
"description" => "",
"type" => "game",
"action" => "stUnknown", // intentionally set to an unknown method
"transitions" => array("continue" => 99)
),
99 => array(
"name" => "gameEnd",
"description" => "End of game",
"type" => "manager",
"action" => "stGameEnd",
"args" => "argGameEnd"
)
);
END;

$projectDirectory = $this->createTestProject('unknownaction', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'unknownaction');
(new StateValidator())->validateStates($project);
}

/**
* Verifies that an exception is thrown when state defines an unknown function for "args" property.
*/
public function testUnknownMethodInArgs(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Arguments argFoo defined in state 10 is not defined in Table.');

$statesIncPhpContents = <<<END
<?php
\$machinestates = array(
1 => array(
"name" => "gameSetup",
"description" => "",
"type" => "manager",
"action" => "stGameSetup",
"transitions" => array("" => 10)
),
10 => array(
"name" => "foo",
"description" => "",
"type" => "activeplayer",
"args" => "argFoo",
"transitions" => array("continue" => 99)
),
99 => array(
"name" => "gameEnd",
"description" => "End of game",
"type" => "manager",
"action" => "stGameEnd",
"args" => "argGameEnd"
)
);
END;

$projectDirectory = $this->createTestProject('unknownargs', $statesIncPhpContents);
$project = new Project($projectDirectory->getFileInfo(), 'unknownargs');
(new StateValidator())->validateStates($project);
}

private function createTestProject(string $projectName, string $statesIncPhpContents, ?string $gamePhpContents = null): WorkingDirectory
{
$workingDir = WorkingDirectory::createTemp();
if (!file_put_contents(join(DIRECTORY_SEPARATOR, [$workingDir->getPathname(), 'states.inc.php']), $statesIncPhpContents)) {
throw new \RuntimeException("Failed to write states.inc.php");
throw new RuntimeException("Failed to write states.inc.php");
}

if (is_null($gamePhpContents)) {
$gamePhpContents = $this->createValidGamePhpContents($projectName);
}
if (!file_put_contents(join(DIRECTORY_SEPARATOR, [$workingDir->getPathname(), $projectName . '.game.php']), $gamePhpContents)) {
throw new RuntimeException("Failed to write {$projectName}.game.php");
}

return $workingDir;
}

private function createValidGamePhpContents(string $projectName): string
{
return <<<END
<?php
require_once(APP_GAMEMODULE_PATH . 'module/table/table.game.php');
class {$projectName}Example extends Table
{
protected function getGameName()
{
return $projectName;
}
protected function setupNewGame(\$players, \$options = [])
{
}
public function upgradeTableDb(\$from_version)
{
}
public function stStuff()
{
}
public function stFoo()
{
}
public function stBar()
{
}
public function argCaz()
{
return array();
}
}
END;

}
}

0 comments on commit ef7eaed

Please sign in to comment.