985 lines
19 KiB
PHP
985 lines
19 KiB
PHP
<?php
|
|
|
|
namespace alveolata\test;
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _fatal_error(
|
|
string $message
|
|
) : void
|
|
{
|
|
// throw (new \Exception($message));
|
|
error_log(sprintf('FATAL ERROR: %s', $message));
|
|
exit(1);
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _string_coin(
|
|
string $template,
|
|
array $arguments = []
|
|
) : string
|
|
{
|
|
$result = $template;
|
|
foreach ($arguments as $key => $value) {
|
|
$pattern = sprintf('{{%s}}', $key);
|
|
$replacement = strval($value);
|
|
$result = str_replace($pattern, $replacement, $result);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _list_filter(
|
|
array $list,
|
|
\Closure $predicate
|
|
) : array
|
|
{
|
|
return array_values(array_filter($list, $predicate));
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _colorize(
|
|
string $string,
|
|
string $color,
|
|
string $mode = 'foreground'
|
|
) : string
|
|
{
|
|
$map = [
|
|
'foreground' => [
|
|
'blue' => '34',
|
|
'green' => '32',
|
|
'purple' => '35',
|
|
'red' => '31',
|
|
'white' => '37',
|
|
'yellow' => '33',
|
|
],
|
|
'background' => [
|
|
'blue' => '44',
|
|
'green' => '42',
|
|
'purple' => '45',
|
|
'red' => '41',
|
|
'white' => '47',
|
|
'yellow' => '43',
|
|
],
|
|
];
|
|
return _string_coin(
|
|
"\033[{{colorcode}}m{{string}}\033[0m",
|
|
[
|
|
'colorcode' => $map[$mode][$color],
|
|
'string' => $string,
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _log(
|
|
int $level,
|
|
string $message
|
|
) : void
|
|
{
|
|
error_log(
|
|
_string_coin(
|
|
'{{indentation}}{{message}}',
|
|
[
|
|
'indentation' => str_repeat(" ", $level-1),
|
|
'message' => $message,
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
class struct_case
|
|
{
|
|
public $name;
|
|
public $active;
|
|
public $procedure;
|
|
public function __construct(
|
|
string $name,
|
|
bool $active,
|
|
\Closure $procedure
|
|
)
|
|
{
|
|
$this->name = $name;
|
|
$this->active = $active;
|
|
$this->procedure = $procedure;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
class struct_section
|
|
{
|
|
public $name;
|
|
public $active;
|
|
public $setup;
|
|
public $cleanup;
|
|
public $subsections;
|
|
public $cases;
|
|
public function __construct(
|
|
string $name,
|
|
bool $active,
|
|
\Closure $setup,
|
|
\Closure $cleanup,
|
|
array $subsections,
|
|
array $cases
|
|
)
|
|
{
|
|
$this->name = $name;
|
|
$this->active = $active;
|
|
$this->setup = $setup;
|
|
$this->cleanup = $cleanup;
|
|
$this->subsections = $subsections;
|
|
$this->cases = $cases;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
class struct_state
|
|
{
|
|
public $name;
|
|
public $sign;
|
|
public $color;
|
|
public function __construct(
|
|
$name,
|
|
$sign,
|
|
$color
|
|
)
|
|
{
|
|
$this->name = $name;
|
|
$this->sign = $sign;
|
|
$this->color = $color;
|
|
}
|
|
public static $skipped = null;
|
|
public static $aborted = null;
|
|
public static $failed = null;
|
|
public static $passed = null;
|
|
public static function init(
|
|
) : void
|
|
{
|
|
self::$skipped = new static('skipped', 'o', 'yellow');
|
|
self::$aborted = new static('aborted', '!', 'purple');
|
|
self::$failed = new static('failed', 'x', 'red');
|
|
self::$passed = new static('passed', '+', 'green');
|
|
}
|
|
}
|
|
struct_state::init();
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
class struct_report
|
|
{
|
|
public /*array */$path;
|
|
public /*struct_state */$state;
|
|
public /*string */$messages;
|
|
public function __construct(
|
|
array $path,
|
|
struct_state $state,
|
|
array $messages
|
|
)
|
|
{
|
|
$this->path = $path;
|
|
$this->state = $state;
|
|
$this->messages = $messages;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
class class_assertion
|
|
{
|
|
|
|
/**
|
|
* @var function<~string,void>
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
private $finalize;
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function __construct(
|
|
\Closure $finalize
|
|
)
|
|
{
|
|
$this->finalize = $finalize;
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function not(
|
|
) : class_assertion
|
|
{
|
|
return (
|
|
new class_assertion(
|
|
function (/*?string */$message) : void {
|
|
if (is_null($message)) {
|
|
($this->finalize)('should NOT be');
|
|
}
|
|
else {
|
|
($this->finalize)(null);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* unspecific explicit fail
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function fail(
|
|
$reason = null
|
|
)
|
|
{
|
|
$message = ($reason ?: '(unexplained fail)');
|
|
($this->finalize)($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* unspecific generic test
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function is(
|
|
bool $value
|
|
)
|
|
{
|
|
$message = ($value ? null : 'condition not met');
|
|
($this->finalize)($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* tests whether the piece of code runs without throwing an error
|
|
*
|
|
* @param \Closure $procedure
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function runs(
|
|
\Closure $procedure
|
|
)
|
|
{
|
|
try {
|
|
$procedure();
|
|
$message = null;
|
|
}
|
|
catch (\Throwable $throwable) {
|
|
$message = _string_coin(
|
|
'does not run; reason: {{reason}}',
|
|
[
|
|
'reason' => $throwable->getMessage(),
|
|
]
|
|
);
|
|
}
|
|
($this->finalize)($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* tests whether the piece of code runs with throwing an error
|
|
*
|
|
* @param \Closure $procedure
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function crashes(
|
|
\Closure $procedure,
|
|
?string $throwable_class = null
|
|
)
|
|
{
|
|
try {
|
|
$procedure();
|
|
$message = _string_coin(
|
|
'does run though it is not supposed to do',
|
|
[
|
|
]
|
|
);
|
|
}
|
|
catch (\Throwable $throwable) {
|
|
if (
|
|
(is_null($throwable_class))
|
|
||
|
|
(get_class($throwable) === $throwable_class)
|
|
) {
|
|
$message = null;
|
|
}
|
|
else {
|
|
$message = _string_coin(
|
|
'wrong throwable class: [actual] {{actual}}; [expected] {{expected}}',
|
|
[
|
|
'actual' => get_class($throwable),
|
|
'expected' => $throwable_class,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
($this->finalize)($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* tests whether two values are equal
|
|
*
|
|
* @param any $value_actual
|
|
* @param any $value_expected
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
public function equal(
|
|
$value_actual,
|
|
$value_expected
|
|
)
|
|
{
|
|
if ($value_actual === $value_expected) {
|
|
$message = null;
|
|
}
|
|
else {
|
|
$message = _string_coin(
|
|
'[actual] {{actual}}; [expected] {{expected}}',
|
|
[
|
|
'actual' => json_encode($value_actual),
|
|
'expected' => json_encode($value_expected),
|
|
]
|
|
);
|
|
}
|
|
($this->finalize)($message);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @var struct_section
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
$_root = null;
|
|
|
|
|
|
|
|
/**
|
|
* merges two sections
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _merge(
|
|
struct_section $section1,
|
|
struct_section $section2
|
|
) : struct_section
|
|
{
|
|
if (! ($section1->name === $section2->name)) {
|
|
throw (new \Exception('sections with different names may not be merged'));
|
|
}
|
|
else {
|
|
if (! ($section1->active === $section2->active)) {
|
|
throw (new \Exception('sections with different active states may not be merged'));
|
|
}
|
|
else {
|
|
$subsections = [];
|
|
foreach ([$section1, $section2] as $section) {
|
|
foreach ($section->subsections as $subsection_name => $subsection) {
|
|
if (! array_key_exists($subsection_name, $subsections)) {
|
|
$subsections[$subsection_name] = $subsection;
|
|
}
|
|
else {
|
|
$subsections[$subsection_name] = _merge(
|
|
$subsections[$subsection_name],
|
|
$subsection
|
|
);
|
|
}
|
|
}
|
|
}
|
|
$section_merged = (
|
|
new struct_section(
|
|
$section1->name,
|
|
$section1->active,
|
|
// TODO: subenvironments?
|
|
function (&$environment) use ($section1, $section2) : void {
|
|
($section1->setup)($environment);
|
|
($section2->setup)($environment);
|
|
},
|
|
function (&$environment) use ($section1, $section2) : void {
|
|
($section1->cleanup)($environment);
|
|
($section2->cleanup)($environment);
|
|
},
|
|
$subsections,
|
|
array_merge(
|
|
$section1->cases,
|
|
$section2->cases
|
|
)
|
|
)
|
|
);
|
|
return $section_merged;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* adds a regular test definition
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _add(
|
|
struct_section &$section_parent,
|
|
array $section_raw_given
|
|
)
|
|
{
|
|
$section_raw_default = [
|
|
'name' => '?',
|
|
'active' => true,
|
|
'setup' => (function (&$environment) {}),
|
|
'cleanup' => (function (&$environment) {}),
|
|
'sections' => [],
|
|
'cases' => [],
|
|
];
|
|
$section_raw = array_merge(
|
|
$section_raw_default,
|
|
$section_raw_given
|
|
);
|
|
$section = new struct_section(
|
|
$section_raw['name'],
|
|
$section_raw['active'],
|
|
$section_raw['setup'],
|
|
$section_raw['cleanup'],
|
|
[],
|
|
[]
|
|
);
|
|
// sections
|
|
{
|
|
foreach ($section_raw['sections'] as $section_sub_raw) {
|
|
_add($section, $section_sub_raw);
|
|
}
|
|
}
|
|
// cases
|
|
{
|
|
foreach ($section_raw['cases'] as $case_raw_given) {
|
|
$case_raw_default = [
|
|
'name' => '?',
|
|
'active' => true,
|
|
'procedure' => function ($assert, &$environment) {
|
|
$assert->fail('not implemented');
|
|
},
|
|
];
|
|
$case_raw = array_merge($case_raw_default, $case_raw_given);
|
|
$case = new struct_case(
|
|
$case_raw['name'],
|
|
$case_raw['active'],
|
|
$case_raw['procedure']
|
|
);
|
|
array_push($section->cases, $case);
|
|
}
|
|
}
|
|
// array_push($section_parent->subsections, $section);
|
|
{
|
|
if (array_key_exists($section->name, $section_parent->subsections)) {
|
|
$section_parent->subsections[$section->name] = _merge(
|
|
$section_parent->subsections[$section->name],
|
|
$section
|
|
);
|
|
}
|
|
else {
|
|
$section_parent->subsections[$section->name] = $section;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* adds a json based test definition
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _adapt(
|
|
string $spec_path
|
|
)
|
|
{
|
|
global $_root;
|
|
$steps = explode('/', $spec_path);
|
|
$paths_to_try = [
|
|
sprintf('%s/%s.php', implode('/', array_slice($steps, 0, count($steps)-1)), $steps[count($steps)-1]),
|
|
sprintf('%s/functions.php', implode('/', array_slice($steps, 0, count($steps)-1))),
|
|
];
|
|
foreach ($paths_to_try as $path) {
|
|
if (file_exists($path)) {
|
|
require_once($path);
|
|
break;
|
|
}
|
|
}
|
|
$spec = json_decode(file_get_contents(sprintf('%s.spec.json', $spec_path)), true);
|
|
if (empty($spec)) {
|
|
$message = _string_coin(
|
|
'json based test definition "{{path}}" malformed',
|
|
[
|
|
'path' => $spec_path,
|
|
]
|
|
);
|
|
throw (new \Exception($message));
|
|
}
|
|
_add(
|
|
$_root,
|
|
[
|
|
'name' => $spec_path,
|
|
'active' => $spec['active'],
|
|
'sections' => array_map(
|
|
function ($section_raw) {
|
|
return [
|
|
'name' => $section_raw['name'],
|
|
'active' => $section_raw['active'],
|
|
'cases' => array_map(
|
|
function ($case_raw) use ($section_raw) {
|
|
return [
|
|
'name' => $case_raw['name'],
|
|
'active' => $case_raw['active'],
|
|
'procedure' => function ($assert, $environment) use ($section_raw, $case_raw) {
|
|
$expression = _string_coin(
|
|
$section_raw['execution']['call'],
|
|
array_map(
|
|
function ($value) {
|
|
return var_export($value, true);
|
|
},
|
|
$case_raw['input']
|
|
)
|
|
);
|
|
switch ($case_raw['output']['kind']) {
|
|
case 'runs': {
|
|
$assert->runs(
|
|
function () use (&$expression) {
|
|
eval(sprintf('return %s;', $expression));
|
|
}
|
|
);
|
|
break;
|
|
}
|
|
case 'crashes': {
|
|
$assert->crashes(
|
|
function () use (&$expression) {
|
|
eval(sprintf('return %s;', $expression));
|
|
}
|
|
);
|
|
break;
|
|
}
|
|
case 'regular': {
|
|
$result_actual = eval(sprintf('return %s;', $expression));
|
|
$result_expected = $case_raw['output']['value'];
|
|
$assert->equal($result_actual, $result_expected);
|
|
break;
|
|
}
|
|
default: {
|
|
$message = _string_coin(
|
|
'unhadled output kind "{{kind}}"',
|
|
[
|
|
'kind' => $case['output']['kind']
|
|
]
|
|
);
|
|
throw (new \Exception($message));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
];
|
|
},
|
|
$section_raw['cases']
|
|
)
|
|
];
|
|
},
|
|
$spec['sections']
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param array $path
|
|
* @return struct_section section
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _get(
|
|
array $path,
|
|
struct_section $section = null,
|
|
array $path_complete = null
|
|
) : ?struct_section
|
|
{
|
|
global $_root;
|
|
if ($section === null) {
|
|
$section = $_root;
|
|
}
|
|
if ($path_complete === null) {
|
|
$path_complete = $path;
|
|
}
|
|
if (count($path) === 0) {
|
|
return $section;
|
|
}
|
|
else {
|
|
$name = $path[0];
|
|
foreach ($section->subsections as $section_sub_name => $section_sub) {
|
|
if ($section_sub->name === $name) {
|
|
return _get(
|
|
array_slice($path, 1),
|
|
$section_sub,
|
|
$path_complete
|
|
);
|
|
}
|
|
}
|
|
$message = _string_coin(
|
|
'no test section "{{path}}"',
|
|
[
|
|
'path' => implode('.', $path_complete),
|
|
]
|
|
);
|
|
throw (new \Exception($message));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param struct_section $section
|
|
* @param bool $log
|
|
* @return list<struct_report>
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _execute(
|
|
struct_section $section = null,
|
|
bool $log = true,
|
|
array $path = [],
|
|
&$environment = [],
|
|
$active = true
|
|
) : array
|
|
{
|
|
global $_root;
|
|
if ($section === null) {
|
|
$section = $_root;
|
|
}
|
|
if (! $active) {
|
|
return [];
|
|
}
|
|
else {
|
|
$level = count($path);
|
|
$reports = [];
|
|
if (count($path) > 0) {
|
|
if ($log) {
|
|
_log(
|
|
$level,
|
|
_string_coin(
|
|
'<{{name}}>',
|
|
[
|
|
'name' => $section->name,
|
|
]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
($section->setup)($environment);
|
|
foreach ($section->subsections as $section_sub_name => &$section_sub) {
|
|
$reports_sub = _execute(
|
|
$section_sub,
|
|
$log,
|
|
array_merge($path, [$section_sub->name]),
|
|
$environment,
|
|
$active && $section_sub->active
|
|
);
|
|
$reports = array_merge($reports, $reports_sub);
|
|
}
|
|
unset($section_sub);
|
|
foreach ($section->cases as &$case) {
|
|
$messages = [];
|
|
if (!$active || !$case->active) {
|
|
$state = struct_state::$skipped;
|
|
$messages = [];
|
|
}
|
|
else {
|
|
$state = struct_state::$passed;
|
|
$assert = new class_assertion(
|
|
function ($message) use (&$state, &$messages) {
|
|
if ($message === null) {
|
|
// do nothing
|
|
}
|
|
else {
|
|
array_push($messages, $message);
|
|
$state = struct_state::$failed;
|
|
}
|
|
}
|
|
);
|
|
try {
|
|
($case->procedure)($assert, $environment);
|
|
}
|
|
catch (\Throwable $throwable) {
|
|
$state = struct_state::$aborted;
|
|
$message = _string_coin(
|
|
"{{description}} in {{file}}:{{line}}",
|
|
[
|
|
'description' => $throwable->getMessage(),
|
|
'file' => $throwable->getFile(),
|
|
'line' => $throwable->getLine(),
|
|
'stacktrace' => $throwable->getTraceAsString(),
|
|
]
|
|
);
|
|
array_push($messages, $message);
|
|
}
|
|
}
|
|
$report = new struct_report(
|
|
array_merge($path, [$case->name]),
|
|
$state,
|
|
$messages
|
|
);
|
|
if ($log) {
|
|
_log(
|
|
$level+1,
|
|
_colorize(
|
|
_string_coin(
|
|
'[{{indicator}}] {{name}}',
|
|
[
|
|
'indicator' => $state->sign,
|
|
// 'indicator' => _colorize($state->sign, $state->color),
|
|
// 'indicator' => _colorize('x', $state->color, 'background'),
|
|
// 'indicator' => _colorize('x', $state->color, 'foreground'),
|
|
'name' => $case->name,
|
|
]
|
|
),
|
|
$state->color
|
|
)
|
|
);
|
|
}
|
|
array_push($reports, $report);
|
|
}
|
|
unset($case);
|
|
($section->cleanup)($environment);
|
|
return $reports;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* searching and adding test definitions
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _gather(
|
|
) : void
|
|
{
|
|
global $_root;
|
|
// .spec.php
|
|
{
|
|
exec(
|
|
'find . -name "*.spec.php" | cut -d "/" -f "2-"',
|
|
$paths
|
|
);
|
|
foreach ($paths as $path) {
|
|
include($path);
|
|
}
|
|
}
|
|
// .spec.json
|
|
{
|
|
exec(
|
|
'find . -name "*.spec.json" | sed -e "s:.spec.json::g" | cut -d "/" -f "2-"',
|
|
$spec_paths
|
|
);
|
|
foreach ($spec_paths as $spec_path) {
|
|
_adapt($spec_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param list<struct_report>
|
|
* @return record<apt:bool,script:string>
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function _evaluation(
|
|
array $reports
|
|
) : array
|
|
{
|
|
$counts = [
|
|
struct_state::$skipped->name => 0,
|
|
struct_state::$aborted->name => 0,
|
|
struct_state::$failed->name => 0,
|
|
struct_state::$passed->name => 0,
|
|
];
|
|
foreach ($reports as $report) {
|
|
$counts[$report->state->name] += 1;
|
|
}
|
|
$count_total = count($reports);
|
|
$apt = ($counts[struct_state::$skipped->name] + $counts[struct_state::$passed->name] === $count_total);
|
|
$script = '';
|
|
$script .= ('--------' . "\n");
|
|
{
|
|
$script .= ('# summary' . "\n");
|
|
$script .= ('' . "\n");
|
|
if ($count_total === 0) {
|
|
$script .= ('(no tests found)' . "\n");
|
|
}
|
|
else {
|
|
$name = (struct_state::$passed)->name;
|
|
$share = (1.0 * $counts[$name] / $count_total);
|
|
$script .= (
|
|
_string_coin(
|
|
'{{count_passed}}/{{count_total}} passed ({{percentage}}%)' . "\n",
|
|
[
|
|
'count_passed' => sprintf('%d', $counts[struct_state::$passed->name]),
|
|
'count_total' => sprintf('%d', $count_total),
|
|
'percentage' => sprintf('%.2f', $share*100),
|
|
]
|
|
)
|
|
);
|
|
}
|
|
$script .= ('' . "\n");
|
|
$script .= ('' . "\n");
|
|
}
|
|
{
|
|
$states = [
|
|
struct_state::$skipped,
|
|
struct_state::$failed,
|
|
struct_state::$aborted,
|
|
];
|
|
foreach ($states as $state) {
|
|
$reports_filtered = _list_filter(
|
|
$reports,
|
|
function ($report) use (&$state) {
|
|
return ($report->state === $state);
|
|
}
|
|
);
|
|
if (count($reports_filtered) > 0) {
|
|
$script .= (
|
|
_string_coin(
|
|
'# {{name}}' . "\n",
|
|
[
|
|
'name' => $state->name
|
|
]
|
|
)
|
|
);
|
|
$script .= ('' . "\n");
|
|
foreach ($reports_filtered as $report) {
|
|
$script .= (
|
|
_string_coin(
|
|
'- `{{path}}`' . "\n",
|
|
[
|
|
'path' => implode('.', $report->path),
|
|
]
|
|
)
|
|
);
|
|
// $script .= ('' . "\n");
|
|
foreach ($report->messages as $message) {
|
|
$script .= (
|
|
_string_coin(
|
|
"\t" . '- {{message}}' . "\n",
|
|
[
|
|
'message' => $message,
|
|
]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
$script .= ('' . "\n");
|
|
$script .= ('' . "\n");
|
|
}
|
|
}
|
|
}
|
|
return [
|
|
'apt' => $apt,
|
|
'script' => $script,
|
|
];
|
|
}
|
|
|
|
|
|
/**
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function get_data(
|
|
string $path = null
|
|
) : array
|
|
{
|
|
if (is_null($path)) {
|
|
$directory = dirname(debug_backtrace()[0]['file']);
|
|
$path = ($directory . '/testdata.json');
|
|
}
|
|
$content = file_get_contents($path);
|
|
if ($content === false) {
|
|
$message = sprintf('testdata file not readable: %s', $path);
|
|
_fatal_error($message);
|
|
}
|
|
else {
|
|
$data = json_decode($content, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$message = sprintf('testdata json not decodable: %s', $path);
|
|
_fatal_error($message);
|
|
}
|
|
else {
|
|
return $data;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* adds a regular test definition
|
|
*
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function add(
|
|
array $section_raw
|
|
) : void
|
|
{
|
|
global $_root;
|
|
_add($_root, $section_raw);
|
|
}
|
|
|
|
|
|
/**
|
|
* searches for test definitions, executes all the tests the selected branch and writes the summary to stderr
|
|
*
|
|
* @return true if all non skipped tests have passed; false if there are aborted or failing tests
|
|
* @author Christian Fraß <frass@greenscale.de>
|
|
*/
|
|
function run(
|
|
array $path = [],
|
|
bool $log = true
|
|
) : bool
|
|
{
|
|
global $_root;
|
|
$_root = new struct_section(
|
|
'root',
|
|
true,
|
|
function () {},
|
|
function () {},
|
|
[],
|
|
[]
|
|
);
|
|
_gather();
|
|
$section = _get($path);
|
|
$reports = _execute($section, $log);
|
|
$evaluation = _evaluation($reports);
|
|
error_log($evaluation['script']);
|
|
return $evaluation['apt'];
|
|
}
|
|
|