230 lines
8.8 KiB
PHP
230 lines
8.8 KiB
PHP
<?php
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
/**
|
|
* The update agent's version arithmetic, run as the shell actually runs it.
|
|
*
|
|
* These exist because a PHP suite cannot reach a bash pipeline, and the bug
|
|
* that made them necessary was invisible to every other kind of test: the tag
|
|
* count was written as `release_version_gt … && echo`, so the LAST iteration —
|
|
* almost always a tag that is not newer — left the loop with a non-zero status.
|
|
* `pipefail` carried it out of the pipeline, the command substitution inherited
|
|
* it, and `set -e` ended the agent before it could write a status file. On the
|
|
* server the console showed a check that never finished and a last-checked time
|
|
* frozen at the moment of the deployment.
|
|
*
|
|
* Every case therefore runs under the agent's own `set -Eeuo pipefail`, and
|
|
* asserts the EXIT CODE as well as the answer. An answer nobody lives to read
|
|
* is not an answer.
|
|
*/
|
|
function runRelease(string $snippet, array $tags = []): Process
|
|
{
|
|
$lib = base_path('deploy/lib/release.sh');
|
|
|
|
// A throwaway repository, because release_tags_ahead reads real tags. A
|
|
// fixed identity and no signing, so this cannot pick up whatever the
|
|
// machine running the suite happens to have configured.
|
|
$repo = sys_get_temp_dir().'/clupilot-release-test-'.bin2hex(random_bytes(6));
|
|
$tagCommands = '';
|
|
|
|
foreach ($tags as $tag) {
|
|
$tagCommands .= "git tag {$tag}\n";
|
|
}
|
|
|
|
$script = <<<BASH
|
|
set -Eeuo pipefail
|
|
mkdir -p {$repo} && cd {$repo}
|
|
git init -q .
|
|
git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m init
|
|
{$tagCommands}
|
|
. {$lib}
|
|
{$snippet}
|
|
BASH;
|
|
|
|
$process = Process::fromShellCommandline('bash -c '.escapeshellarg($script));
|
|
$process->run();
|
|
|
|
(new Process(['rm', '-rf', $repo]))->run();
|
|
|
|
return $process;
|
|
}
|
|
|
|
it('counts the releases ahead without taking the agent down with it', function () {
|
|
// The exact shape that failed: the newest tag is ahead, and the tags after
|
|
// it in the list are not — so the loop's final iteration is a false one.
|
|
$process = runRelease(
|
|
'release_tags_ahead 1.2.0',
|
|
['v1.1.0', 'v1.1.1', 'v1.2.0', 'v1.2.1'],
|
|
);
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('1');
|
|
});
|
|
|
|
it('survives a run where no tag at all is ahead', function () {
|
|
// Every iteration false, including the last. This is the ordinary state of
|
|
// an up-to-date server, which is to say: most ticks, on most machines.
|
|
$process = runRelease(
|
|
'release_tags_ahead 1.2.1',
|
|
['v1.1.0', 'v1.2.0', 'v1.2.1'],
|
|
);
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('0');
|
|
});
|
|
|
|
it('survives a repository with no tags whatsoever', function () {
|
|
$process = runRelease('release_tags_ahead 1.0.0');
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('0');
|
|
});
|
|
|
|
it('counts several releases ahead', function () {
|
|
$process = runRelease(
|
|
'release_tags_ahead 1.1.0',
|
|
['v1.1.0', 'v1.1.1', 'v1.2.0', 'v1.2.1'],
|
|
);
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('3');
|
|
});
|
|
|
|
it('picks the newest tag out of a long list without dying on a broken pipe', function () {
|
|
// The second instance of the same trap. `git tag … | head -1` makes head
|
|
// exit after one line, git takes SIGPIPE, and pipefail ends the caller.
|
|
// Few tags hide it — git finishes writing before head leaves — so it needs
|
|
// enough of them to actually fill a pipe buffer.
|
|
$tags = [];
|
|
|
|
for ($i = 1; $i <= 400; $i++) {
|
|
$tags[] = 'v1.'.$i.'.0';
|
|
}
|
|
|
|
$process = runRelease('release_newest_tag', $tags);
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('v1.400.0');
|
|
});
|
|
|
|
it('answers with nothing, and cleanly, when there are no releases yet', function () {
|
|
$process = runRelease('release_newest_tag; echo "[$?]"');
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('[0]');
|
|
});
|
|
|
|
it('survives a missing manifest instead of taking the agent down with it', function () {
|
|
// Kein storage/app/deployment.json in diesem Wegwerf-Repo — der Zustand
|
|
// eines frischen Checkouts vor der ersten Installation. `sed` auf einer
|
|
// fehlenden Datei liefert Status 2; die Zeile, die diese Funktion in
|
|
// update-agent.sh tatsaechlich verbraucht — eine reine Zuweisung,
|
|
// `DEPLOYED_VERSION="$(release_manifest_version)"` — reicht diesen
|
|
// Status unter `pipefail` an `set -e` weiter. Ohne Absicherung stirbt der
|
|
// Agent bei JEDEM Tick, bevor er je eine Statusdatei schreibt — dieselbe
|
|
// Fehlerklasse wie bei sync_vpn_certificate und einem fehlenden `.env`.
|
|
$process = runRelease('DEPLOYED_VERSION="$(release_manifest_version)"; echo "[$?][$DEPLOYED_VERSION]"');
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('[0][]');
|
|
});
|
|
|
|
it('orders by version, not alphabetically', function () {
|
|
// v1.10.0 is newer than v1.9.0 and sorts earlier as a string. Getting this
|
|
// wrong means a server sitting on 1.9.0 is told it is current.
|
|
$process = runRelease(
|
|
'release_tags_ahead 1.9.0',
|
|
['v1.9.0', 'v1.10.0'],
|
|
);
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe('1');
|
|
});
|
|
|
|
it('answers the pairwise comparison the same way, and exits cleanly either way', function () {
|
|
$cases = [
|
|
['1.2.1', '1.2.0', 'ja'],
|
|
['1.2.0', '1.2.1', 'nein'],
|
|
['1.2.0', '1.2.0', 'nein'],
|
|
['1.10.0', '1.9.0', 'ja'],
|
|
['2.0.0', '1.99.99', 'ja'],
|
|
];
|
|
|
|
foreach ($cases as [$a, $b, $expected]) {
|
|
$process = runRelease("if release_version_gt {$a} {$b}; then echo ja; else echo nein; fi");
|
|
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(trim($process->getOutput()))->toBe($expected, "{$a} > {$b}");
|
|
}
|
|
});
|
|
|
|
it('stops at the ceiling instead of taking the newest tag', function () {
|
|
// Der Kern des Festnagelns: v1.8.1 existiert, darf aber nicht genommen
|
|
// werden, weil die Decke auf v1.8.0 steht.
|
|
$p = runRelease('release_newest_tag v1.8.0', ['v1.7.3', 'v1.8.0', 'v1.8.1']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe('v1.8.0');
|
|
});
|
|
|
|
it('takes the newest tag when no ceiling is given', function () {
|
|
// Das bisherige Verhalten muss unveraendert bleiben — ohne Decke aendert
|
|
// sich nichts an einem Server, der nie festgenagelt wurde.
|
|
$p = runRelease('release_newest_tag', ['v1.7.3', 'v1.8.0', 'v1.8.1']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe('v1.8.1');
|
|
});
|
|
|
|
it('answers nothing when every tag is above the ceiling', function () {
|
|
$p = runRelease('release_newest_tag v1.0.0', ['v1.7.3', 'v1.8.1']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe('');
|
|
});
|
|
|
|
it('counts only the tags up to the ceiling as ahead', function () {
|
|
// Ausgeliefert 1.7.3, Decke v1.8.0, vorhanden bis v1.8.2: genau EINE
|
|
// Aktualisierung ist verfuegbar, nicht drei.
|
|
$p = runRelease('release_tags_ahead 1.7.3 v1.8.0', ['v1.7.3', 'v1.8.0', 'v1.8.1', 'v1.8.2']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe('1');
|
|
});
|
|
|
|
it('counts nothing ahead when the ceiling equals what is deployed', function () {
|
|
// Der haeufigste Fall: "hier einfrieren". Muss sich wie "aktuell"
|
|
// rechnen, sonst bietet die Konsole etwas an, das nie installiert wird.
|
|
$p = runRelease('release_tags_ahead 1.8.0 v1.8.0', ['v1.8.0', 'v1.8.1', 'v1.8.2']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe('0');
|
|
});
|
|
|
|
it('knows whether a tag really exists', function () {
|
|
// Form und Existenz sind zwei Fragen. Der Release-Prozess loescht einen
|
|
// falschen Tag und ueberspringt die Nummer — eine Decke kann also ohne
|
|
// Zutun ihres Setzers ins Leere zeigen.
|
|
$p = runRelease('release_tag_exists v1.8.0 && echo ja; release_tag_exists v9.9.9 || echo nein', ['v1.8.0']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe("ja\nnein");
|
|
});
|
|
|
|
it('offers the deployed version itself as a ceiling, newest first', function () {
|
|
// Die ausgelieferte Version steht MIT in der Liste: "einfrieren, nichts
|
|
// Neues nehmen" ist das, was neun von zehn Servern brauchen.
|
|
$p = runRelease('release_tags_from 1.8.0', ['v1.7.3', 'v1.8.0', 'v1.8.1']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe("v1.8.1\nv1.8.0");
|
|
});
|
|
|
|
it('caps the offered list', function () {
|
|
$p = runRelease('release_tags_from 1.0.0 2', ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v1.3.0']);
|
|
|
|
expect($p->getExitCode())->toBe(0)
|
|
->and(trim($p->getOutput()))->toBe("v1.3.0\nv1.2.0");
|
|
});
|