Files
coolify/tests/Unit/Support/RuntimeLogTargetsTest.php
T
Andras Bacsai 3bfeb14b7d style(logs): show deployment status dots like the service menu
Deployment options get the same status dot and Stopped caption as
services instead of colored badges, so the listbox no longer needs
labelClass.
2026-10-23 12:25:57 +02:00

251 lines
10 KiB
PHP

<?php
use App\Support\RuntimeLogTargets;
function runtimeLogServer(string $uuid, string $name, array $containers): array
{
return [
'uuid' => $uuid,
'name' => $name,
'containers' => $containers,
];
}
function runtimeLogContainer(string $name, string $labels, string $state = 'running', string $id = ''): array
{
return [
'ID' => $id !== '' ? $id : $name,
'Names' => $name,
'State' => $state,
'Labels' => $labels,
];
}
function runtimeLogTargets(array $servers, string $deployment = '0', string $service = '', bool $groupDeployments = true): array
{
return RuntimeLogTargets::select($servers, $deployment, $service, $groupDeployments);
}
it('orders production ahead of newer previews and uses the top bar pills', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app-pr-7', 'coolify.pullRequestId=7,com.docker.compose.service=web', 'exited'),
runtimeLogContainer('app', 'coolify.pullRequestId=0,com.docker.compose.service=web'),
runtimeLogContainer('app-pr-42', 'coolify.pullRequestId=42,com.docker.compose.service=web'),
]),
]);
expect($targets['deployments'])->toBe([
['value' => '0', 'label' => 'Production', 'dot' => 'success'],
['value' => '42', 'label' => 'PR #42', 'dot' => 'success'],
['value' => '7', 'label' => 'PR #7', 'dot' => 'error', 'state' => 'Stopped'],
])
->and($targets['deployment'])->toBe('0')
->and($targets['service'])->toBe('')
->and($targets['sources'][0]['container'])->toBe('app')
->and($targets)->not->toHaveKey('servers');
});
it('treats a missing pull request label as production', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app', 'com.docker.compose.service=web'),
runtimeLogContainer('app-pr-3', 'coolify.pullRequestId=3,com.docker.compose.service=web'),
]),
]);
expect($targets['deployments'][0]['label'])->toBe('Production')
->and($targets['sources'][0]['container'])->toBe('app');
});
it('uses the preview from the query when that deployment exists', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app', 'coolify.pullRequestId=0,com.docker.compose.service=web'),
runtimeLogContainer('app-pr-12', 'coolify.pullRequestId=12,com.docker.compose.service=web'),
]),
], '12');
expect($targets['deployment'])->toBe('12')
->and($targets['sources'][0]['container'])->toBe('app-pr-12');
});
it('falls back to production when the requested preview is gone', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app', 'coolify.pullRequestId=0,com.docker.compose.service=web'),
runtimeLogContainer('app-pr-7', 'coolify.pullRequestId=7,com.docker.compose.service=web'),
]),
], '99');
expect($targets['deployment'])->toBe('0')
->and($targets['sources'][0]['container'])->toBe('app');
});
it('falls back to the newest preview when production has no container', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app-pr-3', 'coolify.pullRequestId=3,com.docker.compose.service=web'),
runtimeLogContainer('app-pr-12', 'coolify.pullRequestId=12,com.docker.compose.service=web'),
]),
]);
expect($targets['deployment'])->toBe('12')
->and($targets['sources'][0]['container'])->toBe('app-pr-12');
});
it('names services from the compose label, then coolify.name, then the container headline', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('compose-name', 'com.docker.compose.service=api,coolify.name=ignored'),
runtimeLogContainer('labeled-name', 'coolify.name=worker'),
runtimeLogContainer('plain-container-1', ''),
]),
], '0', '', false);
expect(array_column($targets['services'], 'label'))->toBe(['api', 'Plain Container', 'worker'])
->and($targets['service'])->toBe('api')
->and($targets['sources'][0]['container'])->toBe('compose-name');
});
it('gives each service one option and colors it from the container states', function (array $states, string $dot, ?string $description, ?string $stateLabel) {
$containers = [];
foreach ($states as $index => $state) {
$containers[] = runtimeLogContainer('web-'.$index, 'com.docker.compose.service=web', $state);
}
$option = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', $containers),
])['services'][0];
expect($option['label'])->toBe('web')
->and($option['value'])->toBe('web')
->and($option['dot'])->toBe($dot)
->and($option['description'] ?? null)->toBe($description)
->and($option['state'] ?? null)->toBe($stateLabel);
})->with([
'running' => [['running'], 'success', null, null],
'starting' => [['starting'], 'warning', null, 'Starting'],
'restarting' => [['running', 'restarting'], 'warning', '2 containers', 'Restarting'],
'mixed' => [['running', 'exited'], 'warning', '2 containers', 'Degraded'],
'stopped' => [['exited', 'dead'], 'error', '2 containers', 'Stopped'],
]);
it('defaults to the first running service and keeps the requested service key', function () {
$servers = [
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('api', 'com.docker.compose.service=api', 'exited'),
runtimeLogContainer('web-old', 'com.docker.compose.service=web', 'exited'),
runtimeLogContainer('web-new', 'com.docker.compose.service=web', 'running'),
runtimeLogContainer('worker', 'com.docker.compose.service=worker', 'running'),
]),
];
$targets = runtimeLogTargets($servers);
expect(array_column($targets['services'], 'label'))->toBe(['api', 'web', 'worker'])
->and($targets['services'][0]['dot'])->toBe('error')
->and($targets['services'][0]['state'])->toBe('Stopped')
->and($targets['services'][1]['dot'])->toBe('warning')
->and($targets['services'][1]['state'])->toBe('Degraded')
->and($targets['services'][1]['description'])->toBe('2 containers')
->and($targets['services'][2]['state'] ?? null)->toBeNull()
->and($targets['service'])->toBe('web')
->and(array_column($targets['sources'], 'container'))->toBe(['web-new', 'web-old']);
$requested = runtimeLogTargets($servers, '0', 'worker');
expect($requested['service'])->toBe('worker')
->and($requested['sources'][0]['container'])->toBe('worker');
});
it('merges a service across servers and a rollout duplicate', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Frankfurt', [
runtimeLogContainer('web-new', 'com.docker.compose.service=web', 'running'),
runtimeLogContainer('web-old', 'com.docker.compose.service=web', 'exited'),
runtimeLogContainer('api', 'com.docker.compose.service=api', 'running'),
]),
runtimeLogServer('edge', 'Helsinki', [
runtimeLogContainer('web-edge', 'com.docker.compose.service=web', 'running'),
]),
], '0', 'web');
expect($targets)->not->toHaveKey('servers')
->and($targets['sources'])->toBe([
['server' => 'primary', 'container' => 'web-new', 'label' => 'Frankfurt web-new'],
['server' => 'primary', 'container' => 'web-old', 'label' => 'Frankfurt web-old'],
['server' => 'edge', 'container' => 'web-edge', 'label' => 'Helsinki'],
])
->and($targets['streamKey'])->toBe('primary:web-new|primary:web-old|edge:web-edge');
});
it('opens a preview that exists on only one server', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('primary-app', 'coolify.pullRequestId=0'),
]),
runtimeLogServer('edge', 'Edge', [
runtimeLogContainer('edge-pr-8', 'coolify.pullRequestId=8'),
]),
], '8');
expect($targets['deployment'])->toBe('8')
->and($targets['sources'])->toBe([
['server' => 'edge', 'container' => 'edge-pr-8', 'label' => 'edge-pr-8'],
]);
});
it('does not split one-click services into preview deployments', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('app-service', 'coolify.pullRequestId=4,com.docker.compose.service=app'),
runtimeLogContainer('db-service', 'coolify.pullRequestId=4,com.docker.compose.service=db'),
]),
], '4', '', false);
expect($targets['deployments'])->toBe([])
->and($targets['deployment'])->toBe('0')
->and(array_column($targets['services'], 'label'))->toBe(['app', 'db']);
});
it('reads labels given as an array', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
[
'ID' => 'app-id',
'Names' => 'app',
'State' => 'running',
'Labels' => [
'coolify.pullRequestId' => '0',
'com.docker.compose.service' => 'web',
],
],
]),
]);
expect($targets['sources'][0]['container'])->toBe('app')
->and($targets['services'][0]['label'])->toBe('web');
});
it('drops containers whose names cannot be passed to docker logs', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', [
runtimeLogContainer('valid-name', 'com.docker.compose.service=web'),
runtimeLogContainer('bad name', 'com.docker.compose.service=api'),
]),
]);
expect(array_column($targets['services'], 'value'))->toBe(['web']);
});
it('returns an empty selection when nothing is running', function () {
$targets = runtimeLogTargets([
runtimeLogServer('primary', 'Primary', []),
], '12', 'web');
expect($targets['sources'])->toBe([])
->and($targets['services'])->toBe([])
->and($targets['streamKey'])->toBe('');
});