@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixUtils\SymconTest;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class TestContext
|
||||
{
|
||||
private string $repository;
|
||||
private string $suite;
|
||||
private string $runId;
|
||||
private int $rootId;
|
||||
/** @var list<array{name: string, status: string, duration_ms: int, message: string}> */
|
||||
private array $cases = [];
|
||||
/** @var list<string> */
|
||||
private array $cleanupErrors = [];
|
||||
/** @var list<string> */
|
||||
private array $profilesToDelete = [];
|
||||
|
||||
public function __construct(string $repository, string $suite)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->suite = $suite;
|
||||
$this->runId = date('Ymd-His') . '-' . strtoupper(bin2hex(random_bytes(4)));
|
||||
$this->rootId = IPS_CreateCategory();
|
||||
IPS_SetParent($this->rootId, 0);
|
||||
IPS_SetIdent($this->rootId, 'ENELIX_TEST_' . str_replace('-', '_', $this->runId));
|
||||
IPS_SetName($this->rootId, 'Enelix Test ' . $suite . ' ' . $this->runId);
|
||||
IPS_SetInfo($this->rootId, 'Temporär; wird durch den Enelix-Test-Runner entfernt.');
|
||||
}
|
||||
|
||||
public function rootId(): int
|
||||
{
|
||||
return $this->rootId;
|
||||
}
|
||||
|
||||
public function runId(): string
|
||||
{
|
||||
return $this->runId;
|
||||
}
|
||||
|
||||
public function runCase(string $name, callable $test): void
|
||||
{
|
||||
$started = microtime(true);
|
||||
try {
|
||||
$test($this);
|
||||
$this->cases[] = $this->caseResult($name, 'passed', $started, '');
|
||||
} catch (Throwable $error) {
|
||||
$this->cases[] = $this->caseResult($name, 'failed', $started, $error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function fail(string $message): void
|
||||
{
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
public function assertTrue(bool $condition, string $message = 'Bedingung ist nicht erfüllt.'): void
|
||||
{
|
||||
if (!$condition) {
|
||||
$this->fail($message);
|
||||
}
|
||||
}
|
||||
|
||||
public function assertSame($expected, $actual, string $message = ''): void
|
||||
{
|
||||
if ($expected === $actual) {
|
||||
return;
|
||||
}
|
||||
$prefix = $message === '' ? '' : $message . ' ';
|
||||
$this->fail(
|
||||
$prefix . 'Erwartet: ' . $this->export($expected) . '; erhalten: ' . $this->export($actual)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertEquals(float $expected, float $actual, float $delta, string $message = ''): void
|
||||
{
|
||||
if (abs($expected - $actual) <= $delta) {
|
||||
return;
|
||||
}
|
||||
$prefix = $message === '' ? '' : $message . ' ';
|
||||
$this->fail($prefix . "Erwartet: {$expected} ± {$delta}; erhalten: {$actual}");
|
||||
}
|
||||
|
||||
public function assertInstanceStatus(int $instanceId, int $expected = 102): void
|
||||
{
|
||||
$this->assertTrue(IPS_InstanceExists($instanceId), 'Instanz existiert nicht.');
|
||||
$status = (int) IPS_GetInstance($instanceId)['InstanceStatus'];
|
||||
$this->assertSame($expected, $status, 'Unerwarteter Instanzstatus.');
|
||||
}
|
||||
|
||||
public function objectByIdent(string $ident, int $parentId): int
|
||||
{
|
||||
$objectId = @IPS_GetObjectIDByIdent($ident, $parentId);
|
||||
if ($objectId === false) {
|
||||
$this->fail("Objekt {$ident} unter {$parentId} wurde nicht angelegt.");
|
||||
}
|
||||
|
||||
return (int) $objectId;
|
||||
}
|
||||
|
||||
public function createCategory(string $name, ?int $parentId = null): int
|
||||
{
|
||||
$id = IPS_CreateCategory();
|
||||
IPS_SetParent($id, $parentId ?? $this->rootId);
|
||||
IPS_SetName($id, $name);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function createVariable(
|
||||
int $type,
|
||||
string $name,
|
||||
$value,
|
||||
?int $parentId = null,
|
||||
string $profile = ''
|
||||
): int {
|
||||
$id = IPS_CreateVariable($type);
|
||||
IPS_SetParent($id, $parentId ?? $this->rootId);
|
||||
IPS_SetName($id, $name);
|
||||
if ($profile !== '' && IPS_VariableProfileExists($profile)) {
|
||||
IPS_SetVariableCustomProfile($id, $profile);
|
||||
}
|
||||
SetValue($id, $value);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function createActionVariable(string $name, bool $value = false): int
|
||||
{
|
||||
$variableId = $this->createVariable(0, $name, $value);
|
||||
$scriptId = IPS_CreateScript(0);
|
||||
IPS_SetParent($scriptId, $this->rootId);
|
||||
IPS_SetName($scriptId, $name . ' Aktion');
|
||||
IPS_SetHidden($scriptId, true);
|
||||
IPS_SetScriptContent($scriptId, <<<'PHP'
|
||||
<?php
|
||||
|
||||
SetValue((int) $_IPS['VARIABLE'], (bool) $_IPS['VALUE']);
|
||||
PHP
|
||||
);
|
||||
IPS_SetVariableCustomAction($variableId, $scriptId);
|
||||
|
||||
return $variableId;
|
||||
}
|
||||
|
||||
/** @param array<string, bool|int|float|string> $properties */
|
||||
public function createInstance(string $moduleId, string $name, array $properties = []): int
|
||||
{
|
||||
if (!in_array($moduleId, IPS_GetModuleList(), true)) {
|
||||
$this->fail("Modul {$moduleId} ist in IP-Symcon nicht verfügbar.");
|
||||
}
|
||||
|
||||
$id = IPS_CreateInstance($moduleId);
|
||||
IPS_SetParent($id, $this->rootId);
|
||||
IPS_SetName($id, $name);
|
||||
foreach ($properties as $property => $value) {
|
||||
IPS_SetProperty($id, $property, $value);
|
||||
}
|
||||
IPS_ApplyChanges($id);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @param array<string, bool|int|float|string> $properties */
|
||||
public function configureInstance(int $instanceId, array $properties): void
|
||||
{
|
||||
foreach ($properties as $property => $value) {
|
||||
IPS_SetProperty($instanceId, $property, $value);
|
||||
}
|
||||
IPS_ApplyChanges($instanceId);
|
||||
}
|
||||
|
||||
public function trackVariableProfile(string $name, bool $existedBefore): void
|
||||
{
|
||||
if (!$existedBefore && IPS_VariableProfileExists($name)) {
|
||||
$this->profilesToDelete[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
public function waitUntil(callable $condition, int $timeoutMs = 2000, int $intervalMs = 50): void
|
||||
{
|
||||
$deadline = microtime(true) + ($timeoutMs / 1000);
|
||||
do {
|
||||
if ($condition()) {
|
||||
return;
|
||||
}
|
||||
IPS_Sleep($intervalMs);
|
||||
} while (microtime(true) < $deadline);
|
||||
|
||||
$this->fail("Bedingung wurde nach {$timeoutMs} ms nicht erfüllt.");
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
if (IPS_ObjectExists($this->rootId)) {
|
||||
try {
|
||||
$this->disconnectInstances($this->rootId);
|
||||
$this->deleteObjectTree($this->rootId);
|
||||
} catch (Throwable $error) {
|
||||
$this->cleanupErrors[] = $error->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_unique($this->profilesToDelete) as $profile) {
|
||||
try {
|
||||
if (IPS_VariableProfileExists($profile)) {
|
||||
IPS_DeleteVariableProfile($profile);
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
$this->cleanupErrors[] = "Profil {$profile}: " . $error->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (IPS_ObjectExists($this->rootId)) {
|
||||
$this->cleanupErrors[] = 'Temporäre Testkategorie konnte nicht vollständig entfernt werden.';
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function report(): array
|
||||
{
|
||||
$failed = count(array_filter(
|
||||
$this->cases,
|
||||
static fn (array $case): bool => $case['status'] === 'failed'
|
||||
));
|
||||
$failed += count($this->cleanupErrors);
|
||||
|
||||
return [
|
||||
'repository' => $this->repository,
|
||||
'suite' => $this->suite,
|
||||
'run_id' => $this->runId,
|
||||
'passed' => $failed === 0,
|
||||
'tests' => count($this->cases) + count($this->cleanupErrors),
|
||||
'failures' => $failed,
|
||||
'cases' => $this->cases,
|
||||
'cleanup_errors' => $this->cleanupErrors,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{name: string, status: string, duration_ms: int, message: string} */
|
||||
private function caseResult(string $name, string $status, float $started, string $message): array
|
||||
{
|
||||
return [
|
||||
'name' => $name,
|
||||
'status' => $status,
|
||||
'duration_ms' => (int) round((microtime(true) - $started) * 1000),
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
private function export($value): string
|
||||
{
|
||||
return str_replace(["\r", "\n"], ['\\r', '\\n'], var_export($value, true));
|
||||
}
|
||||
|
||||
private function disconnectInstances(int $objectId): void
|
||||
{
|
||||
if (!IPS_ObjectExists($objectId)) {
|
||||
return;
|
||||
}
|
||||
foreach (IPS_GetChildrenIDs($objectId) as $childId) {
|
||||
$this->disconnectInstances((int) $childId);
|
||||
}
|
||||
if ((int) IPS_GetObject($objectId)['ObjectType'] !== 1) {
|
||||
return;
|
||||
}
|
||||
$connectionId = (int) IPS_GetInstance($objectId)['ConnectionID'];
|
||||
if ($connectionId > 0) {
|
||||
IPS_DisconnectInstance($objectId);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteObjectTree(int $objectId): void
|
||||
{
|
||||
if (!IPS_ObjectExists($objectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_reverse(IPS_GetChildrenIDs($objectId)) as $childId) {
|
||||
$this->deleteObjectTree((int) $childId);
|
||||
}
|
||||
|
||||
$type = (int) IPS_GetObject($objectId)['ObjectType'];
|
||||
switch ($type) {
|
||||
case 0:
|
||||
IPS_DeleteCategory($objectId);
|
||||
return;
|
||||
case 1:
|
||||
IPS_DeleteInstance($objectId);
|
||||
return;
|
||||
case 2:
|
||||
IPS_DeleteVariable($objectId);
|
||||
return;
|
||||
case 3:
|
||||
IPS_DeleteScript($objectId, true);
|
||||
return;
|
||||
case 4:
|
||||
IPS_DeleteEvent($objectId);
|
||||
return;
|
||||
case 5:
|
||||
IPS_DeleteMedia($objectId, true);
|
||||
return;
|
||||
case 6:
|
||||
IPS_DeleteLink($objectId);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unbekannter Objekttyp {$type} bei Objekt {$objectId}.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user