repository = $repository; $this->manifestPath = $manifestPath; } /** * @param list|string $selection * @return array{report: array, 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> $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 $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 $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 = ['', '']; foreach ($report['results'] as $module) { foreach ($module['cases'] as $case) { $xml[] = sprintf( ' ', self::xml((string) $report['repository']), self::xml((string) $module['suite']), self::xml((string) $case['name']), $case['duration_ms'] / 1000 ); if ($case['status'] === 'failed') { $xml[] = ' '; } $xml[] = ' '; } foreach ($module['cleanup_errors'] as $index => $error) { $xml[] = sprintf( ' ', self::xml((string) $report['repository']), self::xml((string) $module['suite']), $index + 1 ); $xml[] = ' '; $xml[] = ' '; } } $xml[] = ''; return implode("\n", $xml); } private static function xml(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8'); } }