@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixUtils\SymconTest;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class TestRunner
|
||||
{
|
||||
private string $repository;
|
||||
private string $manifestPath;
|
||||
|
||||
public function __construct(string $repository, string $manifestPath)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->manifestPath = $manifestPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string>|string $selection
|
||||
* @return array{report: array<string, mixed>, console: string, junit: string}
|
||||
*/
|
||||
public function run(string $mode = 'all', $selection = []): array
|
||||
{
|
||||
$definitions = require $this->manifestPath;
|
||||
if (!is_array($definitions) || $definitions === []) {
|
||||
throw new RuntimeException('Das Symcon-Testmanifest ist leer oder ungültig.');
|
||||
}
|
||||
|
||||
$selected = $this->selectDefinitions($definitions, $mode, $selection);
|
||||
$started = microtime(true);
|
||||
$moduleReports = [];
|
||||
|
||||
foreach ($selected as $name => $definition) {
|
||||
$context = new TestContext($this->repository, (string) $name);
|
||||
try {
|
||||
$context->runCase('IP-Symcon 8.x ist aktiv', function (TestContext $test): void {
|
||||
$version = IPS_GetKernelVersion();
|
||||
$test->assertTrue(
|
||||
version_compare($version, '8.0', '>=') && version_compare($version, '9.0', '<'),
|
||||
'Erwartet wird IP-Symcon 8.x, aktiv ist ' . $version . '.'
|
||||
);
|
||||
});
|
||||
$context->runCase('Modultest ausführen', function (TestContext $test) use ($definition): void {
|
||||
$file = $definition['file'] ?? null;
|
||||
if (!is_string($file) || !is_file($file)) {
|
||||
$test->fail('Testskript wurde nicht gefunden: ' . (string) $file);
|
||||
}
|
||||
$moduleTest = require $file;
|
||||
if (!is_callable($moduleTest)) {
|
||||
$test->fail('Testskript muss eine aufrufbare Funktion zurückgeben.');
|
||||
}
|
||||
$moduleTest($test);
|
||||
});
|
||||
} finally {
|
||||
$context->cleanup();
|
||||
}
|
||||
$moduleReports[] = $context->report();
|
||||
}
|
||||
|
||||
$tests = array_sum(array_column($moduleReports, 'tests'));
|
||||
$failures = array_sum(array_column($moduleReports, 'failures'));
|
||||
$report = [
|
||||
'schema_version' => 1,
|
||||
'repository' => $this->repository,
|
||||
'mode' => $mode,
|
||||
'kernel_version' => IPS_GetKernelVersion(),
|
||||
'started_at' => date(DATE_ATOM, (int) $started),
|
||||
'duration_ms' => (int) round((microtime(true) - $started) * 1000),
|
||||
'passed' => $failures === 0,
|
||||
'modules' => count($moduleReports),
|
||||
'tests' => $tests,
|
||||
'failures' => $failures,
|
||||
'results' => $moduleReports,
|
||||
];
|
||||
|
||||
$console = self::toConsole($report);
|
||||
IPS_LogMessage($this->repository . ' Tests', $console);
|
||||
|
||||
return [
|
||||
'report' => $report,
|
||||
'console' => $console,
|
||||
'junit' => self::toJunit($report),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, array<string, mixed>> $definitions */
|
||||
private function selectDefinitions(array $definitions, string $mode, $selection): array
|
||||
{
|
||||
if (!in_array($mode, ['all', 'single', 'affected'], true)) {
|
||||
throw new InvalidArgumentException('Modus muss all, single oder affected sein.');
|
||||
}
|
||||
if ($mode === 'all') {
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
$names = is_string($selection) ? [$selection] : $selection;
|
||||
if (!is_array($names) || $names === []) {
|
||||
throw new InvalidArgumentException("Für {$mode} muss mindestens ein Modul angegeben werden.");
|
||||
}
|
||||
|
||||
$selected = [];
|
||||
foreach (array_values(array_unique($names)) as $name) {
|
||||
if (!is_string($name) || !isset($definitions[$name])) {
|
||||
throw new InvalidArgumentException('Unbekanntes Testmodul: ' . (string) $name);
|
||||
}
|
||||
$selected[$name] = $definitions[$name];
|
||||
}
|
||||
|
||||
return $selected;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $report */
|
||||
public static function toConsole(array $report): string
|
||||
{
|
||||
$state = $report['passed'] ? 'BESTANDEN' : 'FEHLGESCHLAGEN';
|
||||
$lines = [
|
||||
sprintf(
|
||||
'%s %s: %s (%d Module, %d Tests, %d Fehler, %d ms)',
|
||||
$report['repository'],
|
||||
strtoupper((string) $report['mode']),
|
||||
$state,
|
||||
$report['modules'],
|
||||
$report['tests'],
|
||||
$report['failures'],
|
||||
$report['duration_ms']
|
||||
),
|
||||
];
|
||||
|
||||
foreach ($report['results'] as $module) {
|
||||
$lines[] = sprintf(
|
||||
'%s %s (%d Tests, %d Fehler)',
|
||||
$module['passed'] ? '[OK]' : '[FEHLER]',
|
||||
$module['suite'],
|
||||
$module['tests'],
|
||||
$module['failures']
|
||||
);
|
||||
foreach ($module['cases'] as $case) {
|
||||
if ($case['status'] === 'failed') {
|
||||
$lines[] = ' - ' . $case['name'] . ': ' . $case['message'];
|
||||
}
|
||||
}
|
||||
foreach ($module['cleanup_errors'] as $error) {
|
||||
$lines[] = ' - Cleanup: ' . $error;
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $report */
|
||||
public static function toJunit(array $report): string
|
||||
{
|
||||
$attributes = sprintf(
|
||||
'name="%s" tests="%d" failures="%d" time="%.3f"',
|
||||
self::xml((string) $report['repository']),
|
||||
$report['tests'],
|
||||
$report['failures'],
|
||||
$report['duration_ms'] / 1000
|
||||
);
|
||||
$xml = ['<?xml version="1.0" encoding="UTF-8"?>', '<testsuite ' . $attributes . '>'];
|
||||
|
||||
foreach ($report['results'] as $module) {
|
||||
foreach ($module['cases'] as $case) {
|
||||
$xml[] = sprintf(
|
||||
' <testcase classname="%s.%s" name="%s" time="%.3f">',
|
||||
self::xml((string) $report['repository']),
|
||||
self::xml((string) $module['suite']),
|
||||
self::xml((string) $case['name']),
|
||||
$case['duration_ms'] / 1000
|
||||
);
|
||||
if ($case['status'] === 'failed') {
|
||||
$xml[] = ' <failure message="' . self::xml((string) $case['message']) . '"/>';
|
||||
}
|
||||
$xml[] = ' </testcase>';
|
||||
}
|
||||
foreach ($module['cleanup_errors'] as $index => $error) {
|
||||
$xml[] = sprintf(
|
||||
' <testcase classname="%s.%s" name="Cleanup %d">',
|
||||
self::xml((string) $report['repository']),
|
||||
self::xml((string) $module['suite']),
|
||||
$index + 1
|
||||
);
|
||||
$xml[] = ' <failure message="' . self::xml((string) $error) . '"/>';
|
||||
$xml[] = ' </testcase>';
|
||||
}
|
||||
}
|
||||
$xml[] = '</testsuite>';
|
||||
|
||||
return implode("\n", $xml);
|
||||
}
|
||||
|
||||
private static function xml(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user