Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The following commands are exposed:

+ `gt add` - add a page, API endpoint or cron script from a template
+ `gt create` - create a new WebEngine application
+ `gt test` - run configured PHP and JavaScript test suites
+ `gt serve` - run the inbuilt development server
+ `gt build` - compile client-side assets
+ `gt cron` - invoke scripts or static functions at regular intervals
Expand Down
2 changes: 2 additions & 0 deletions bin/gt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use GT\GtCommand\Command\MigrateCommand;
use GT\GtCommand\Command\ServeCommand;
use GT\GtCommand\Command\CronCommand;
use GT\GtCommand\Command\RunCommand;
use GT\GtCommand\Command\TestCommand;
use GT\GtCommand\Command\DeployCommand;

foreach([ __DIR__ . "/../../..", __DIR__ . "/../vendor" ] as $vendor) {
Expand All @@ -24,6 +25,7 @@ $app = new Application(
new ArgumentList(...$argv),
new AddCommand(),
new CreateCommand(),
new TestCommand(),
new RunCommand(),
new DeployCommand(),
new MigrateCommand(),
Expand Down
10 changes: 9 additions & 1 deletion src/Command/BuildCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ public function __construct() {

public function run(?ArgumentValueList $arguments = null):int {
if(!$arguments->contains("default")) {
$arguments->set("default", "vendor/phpgt/webengine/build.default.json");
$defaultPathPrefix = "vendor/phpgt/webengine/build.default";
$defaultPathExtensionPriority = ["ini", "json"];
foreach($defaultPathExtensionPriority as $ext) {
$defaultPath = "$defaultPathPrefix.$ext";
if(file_exists($defaultPath)) {
$arguments->set("default", $defaultPath);
break;
}
}
}

return parent::run($arguments);
Expand Down
67 changes: 43 additions & 24 deletions src/Command/CreateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,9 @@ class CreateCommand extends Command {
*/
public function run(?ArgumentValueList $arguments = null):int {
$name = $this->readValidName($arguments->get("projectName", ""));
$namespace = $this->readValidNamespace($arguments->get("namespace", ""));

$blueprintCollection = new BlueprintCollection();
$blueprintInput = "0";
if($arguments->contains("blueprint")) {
$blueprintInput = $arguments->get("blueprint")->get();
}
elseif($arguments->contains("empty")) {
$blueprintInput = "empty";
}
else {
$this->writeLine("What blueprint would you like to start with? (type the number)");

foreach($blueprintCollection as $i => $blueprint) {
$title = $blueprint->getTitle();
$description = $blueprint->getDescription();
$this->writeLine( " $i: $title - $description");
}
$blueprintInput = $this->readLine($blueprintInput);

if($blueprintInput < 0 || $blueprintInput >= count($blueprintCollection)) {
$this->writeLine("Cancelling due to invalid blueprint.");
exit; // phpcs:ignore
}
}
$blueprintInput = $this->readBlueprintInput($arguments, $blueprintCollection);
$namespace = $this->readNamespaceForArguments($arguments);

if(is_numeric($blueprintInput)) {
$selectedBlueprint = $blueprintCollection->getByIndex((int)$blueprintInput);
Expand Down Expand Up @@ -223,5 +201,46 @@ private function readValidNamespace(ArgumentValue $namespace):string {
return $namespace;
}

private function readBlueprintInput(
ArgumentValueList $arguments,
BlueprintCollection $blueprintCollection
):string {
if($arguments->contains("blueprint")) {
return $arguments->get("blueprint")->get();
}

if($arguments->contains("empty")) {
return "empty";
}

$this->writeLine("What blueprint would you like to start with? (type the number)");

foreach($blueprintCollection as $i => $blueprint) {
$title = $blueprint->getTitle();
$description = $blueprint->getDescription();
$this->writeLine( " $i: $title - $description");
}

$blueprintInput = $this->readLine("0");
if($blueprintInput < 0 || $blueprintInput >= count($blueprintCollection)) {
$this->writeLine("Cancelling due to invalid blueprint.");
exit; // phpcs:ignore
}

return $blueprintInput;
}

private function readNamespaceForArguments(ArgumentValueList $arguments):string {
if($arguments->contains("empty")) {
$namespace = "App";
$this->writeLine();
$this->writeLine("Using namespace '$namespace'.");
$this->writeLine();
return $namespace;
}

return $this->readValidNamespace($arguments->get("namespace", ""));
}


}
140 changes: 140 additions & 0 deletions src/Command/TestCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
namespace GT\GtCommand\Command;

use Gt\Cli\Argument\ArgumentValueList;
use Gt\Cli\Command\Command;
use Gt\Cli\Stream;
use Gt\Daemon\Process;
use JsonException;

class TestCommand extends Command {
/** @SuppressWarnings(PHPMD.UnusedFormalParameter) */
public function run(?ArgumentValueList $arguments = null):int {
unset($arguments);

$testSuiteList = $this->getTestSuiteList();
if(!$testSuiteList) {
$this->writeLine("No test suites were found.", Stream::ERROR);
$this->writeLine("Checked composer.json and package.json for a test script.", Stream::ERROR);
return 1;
}

$passedCount = 0;
$failedCount = 0;
foreach($testSuiteList as $testSuite) {
if($this->runTestSuite($testSuite)) {
$passedCount++;
}
else {
$failedCount++;
}
}

$this->writeLine();
if($failedCount > 0) {
$this->writeLine("Test result: $passedCount passed, $failedCount failed.", Stream::ERROR);
return 1;
}

$this->writeLine("Test result: $passedCount passed.");
return 0;
}

public function getName():string {
return "test";
}

public function getDescription():string {
return "Run configured PHP and JavaScript test suites";
}

public function getRequiredNamedParameterList():array {
return [];
}

public function getOptionalNamedParameterList():array {
return [];
}

public function getRequiredParameterList():array {
return [];
}

public function getOptionalParameterList():array {
return [];
}

/** @return array<int, array{name: string, source: string, command: array<int, string>}> */
private function getTestSuiteList():array {
$testSuiteList = [];

if($this->hasScript("composer.json", "test")) {
$testSuiteList[] = [
"name" => "PHP",
"source" => "composer.json",
"command" => ["composer", "test"],
];
}

if($this->hasScript("package.json", "test")) {
$testSuiteList[] = [
"name" => "JavaScript",
"source" => "package.json",
"command" => ["npm", "run", "test"],
];
}

return $testSuiteList;
}

private function hasScript(string $fileName, string $scriptName):bool {
if(!is_file($fileName)) {
return false;
}

$contents = file_get_contents($fileName);
if($contents === false) {
$this->writeLine("Unable to read $fileName.", Stream::ERROR);
return false;
}

try {
$json = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
}
catch(JsonException $exception) {
$this->writeLine("Unable to parse $fileName: " . $exception->getMessage(), Stream::ERROR);
return false;
}

if(!isset($json["scripts"]) || !is_array($json["scripts"])) {
return false;
}

return isset($json["scripts"][$scriptName]);
}

/** @param array{name: string, source: string, command: array<int, string>} $testSuite */
private function runTestSuite(array $testSuite):bool {
$this->writeLine();
$this->writeLine("Running {$testSuite["name"]} tests from {$testSuite["source"]}...");

$process = new Process(...$testSuite["command"]);
$process->exec();

do {
$this->write($process->getOutput());
$this->write($process->getErrorOutput(), Stream::ERROR);
usleep(100_000);
}
while($process->isRunning());

$exitCode = $process->getExitCode();
if($exitCode === 0) {
$this->writeLine("{$testSuite["name"]} tests passed.");
return true;
}

$this->writeLine("{$testSuite["name"]} tests failed with exit code $exitCode.", Stream::ERROR);
return false;
}
}
Loading