107 lines
4.8 KiB
JavaScript
107 lines
4.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { accessSync, constants, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
// Pins the diagnostics wiring of the Docker leg of the release smoke.
|
|
//
|
|
// The smoke itself only runs post-merge, against a published artifact, so its
|
|
// own failures are the only signal it ever sends — and for a long time that
|
|
// signal arrived with no container logs attached: the workflow learned the
|
|
// container's name from the harness's output, which a failing harness never
|
|
// produced, and the harness ran the container with `--rm` so stopping it
|
|
// deleted the logs anyway. These assertions keep both halves fixed.
|
|
|
|
const repoRoot = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
|
const scriptPath = join(repoRoot, "scripts", "docker-onboard-smoke.sh");
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const workflow = readFileSync(
|
|
join(repoRoot, ".github", "workflows", "release-smoke.yml"),
|
|
"utf8",
|
|
);
|
|
const dockerJob = workflow.split(/^ smoke:$/m)[1] ?? "";
|
|
|
|
test("smoke script is executable and parses", () => {
|
|
accessSync(scriptPath, constants.X_OK);
|
|
execFileSync("bash", ["-n", scriptPath]);
|
|
});
|
|
|
|
test("container name can be fixed by the caller", () => {
|
|
// A name the caller chose is a name it still has when this script fails.
|
|
assert.match(script, /CONTAINER_NAME="\$\{SMOKE_CONTAINER_NAME:-\$IMAGE_NAME\}"/);
|
|
// And it is still sanitized into something Docker will accept.
|
|
assert.match(script, /CONTAINER_NAME="\$\{CONTAINER_NAME\/\/\[\^a-zA-Z0-9_\.-\]\/-\}"/);
|
|
});
|
|
|
|
test("the container does not remove itself", () => {
|
|
// `--rm` deletes the container the instant its process exits, so a crash
|
|
// takes the logs with it before any cleanup can read them.
|
|
assert.match(script, /^docker run -d \\$/m);
|
|
assert.doesNotMatch(script, /docker run [^\n]*--rm/);
|
|
});
|
|
|
|
test("cleanup dumps the container logs before it tears the container down", () => {
|
|
const cleanup = script.match(/^cleanup\(\) \{$[\s\S]*?^\}$/m)?.[0];
|
|
assert.ok(cleanup, "cleanup() must exist");
|
|
const dumpAt = cleanup.indexOf("dump_container_logs");
|
|
const stopAt = cleanup.indexOf("docker stop");
|
|
const removeAt = cleanup.indexOf("docker rm");
|
|
assert.ok(dumpAt !== -1, "cleanup() must dump the container logs");
|
|
assert.ok(stopAt !== -1, "cleanup() must still stop the container");
|
|
assert.ok(
|
|
removeAt !== -1,
|
|
"cleanup() must remove the container now that it no longer removes itself",
|
|
);
|
|
assert.ok(
|
|
dumpAt < stopAt && dumpAt < removeAt,
|
|
"cleanup() must dump the logs before the teardown, or the teardown deletes them first",
|
|
);
|
|
});
|
|
|
|
test("the log dump has a destination, and callers are told where it is", () => {
|
|
assert.match(script, /SMOKE_LOG_FILE="\$\{SMOKE_LOG_FILE:-/);
|
|
assert.match(script, /docker logs "\$CONTAINER_NAME" >"\$SMOKE_LOG_FILE"/);
|
|
assert.match(script, /printf 'SMOKE_LOG_FILE=%q\\n' "\$SMOKE_LOG_FILE"/);
|
|
});
|
|
|
|
test("the log dump starts empty on every run", () => {
|
|
// A caller that reuses one path — the default does, for a fixed container
|
|
// name — must not be handed the previous run's logs as this run's evidence
|
|
// when this run fails before a container exists.
|
|
const truncateAt = script.search(/^\s*: >"\$SMOKE_LOG_FILE"/m);
|
|
const dumpAt = script.indexOf("dump_container_logs() {");
|
|
assert.ok(truncateAt !== -1, "the script must truncate SMOKE_LOG_FILE at startup");
|
|
assert.ok(
|
|
truncateAt < dumpAt,
|
|
"the truncation must happen before anything can write the dump",
|
|
);
|
|
});
|
|
|
|
test("workflow fixes the container name before the harness runs", () => {
|
|
assert.match(dockerJob, /^ env:$/m);
|
|
assert.match(dockerJob, /SMOKE_CONTAINER_NAME: release-smoke-onboard/);
|
|
// Reading the name back out of the harness is the defect: a step that only
|
|
// learns it on success cannot use it on failure.
|
|
assert.doesNotMatch(dockerJob, /echo "SMOKE_CONTAINER_NAME=/);
|
|
assert.match(dockerJob, /SMOKE_LOG_FILE="\$\{\{ runner\.temp \}\}\/docker-onboard-smoke\.log"/);
|
|
});
|
|
|
|
test("workflow captures and uploads the logs unconditionally", () => {
|
|
const capture = dockerJob.split("- name: Capture Docker logs")[1] ?? "";
|
|
assert.ok(capture, "the Capture Docker logs step must exist");
|
|
assert.match(capture.split("- name:")[0], /if: always\(\)/);
|
|
// No guard that a failing launch would leave false.
|
|
assert.doesNotMatch(
|
|
capture.split("- name:")[0],
|
|
/\[\[ -n "\$\{SMOKE_CONTAINER_NAME:-\}" \]\]/,
|
|
);
|
|
|
|
const upload = dockerJob.split("- name: Upload diagnostics")[1] ?? "";
|
|
assert.ok(upload, "the Upload diagnostics step must exist");
|
|
assert.match(upload, /docker-onboard-smoke\.log/);
|
|
assert.match(upload, /if-no-files-found: error/);
|
|
// The metadata path is a literal, not a variable a failed launch never set.
|
|
assert.doesNotMatch(upload, /\$\{\{ env\.SMOKE_METADATA_FILE \}\}/);
|
|
});
|