Files
cozystack/hack/runner-identity.bats
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

1453 lines
59 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/e2e-runner-identity.sh -- the record of which machine an
# e2e run landed on.
#
# What this suite is guarding, and why it is not a numbers test: the capture
# cannot be wrong about the hardware, because it copies what the machine says.
# What it can be is absent, or present and empty, or present and describing the
# wrong layer -- and each of those reads, in the artifact, exactly like a run on
# an unremarkable runner. So the assertions below are about the arms that keep
# those apart: an `imds unreachable` line rather than a missing block, a named
# absent field rather than a silent one, and a legend saying the numbers belong
# to the runner VM and not to anything inside it.
#
# Two shapes of test, like hack/capture-dataplane.bats.
#
# The first sources the script with E2E_RUNNER_IDENTITY_LIB set, which its guard
# honours by defining the helpers and returning before it reads or writes
# anything. Those tests drive the pure helpers directly: the number reader over the
# shape sizing object, the scalar guard that decides whether a per-key answer is
# one value, and the ceiling knob.
#
# The second runs the script as a subprocess against stubs on PATH. That is the
# only way to reach the body: `curl` is invoked in a child process, so a shell
# function here would be invisible exactly where the assertion needs it, and the
# arms that matter -- what reaches the file, what reaches stdout, what the exit
# status is -- are properties of the running script.
#
# The root-document fixture uses an obviously synthetic OCID and display name, and
# it is there to be asserted ABSENT twice over: this capture never requests that
# document, and the arm that refuses a value which is not one scalar is what holds
# that when a service answers it anyway. A real instance's root document carries
# the instance and compartment OCIDs, the image OCID, the display name, the
# hostname and the user metadata bag, and this record is published as a CI
# artifact.
#
# Title syntax constraints (inherited from cozytest.sh's awk parser):
# - Titles delimited by ASCII double quotes; embedded quotes truncate.
# - Only [A-Za-z0-9] from the title survives into the function name, so keep
# titles distinctive in their alphanumeric run.
# - An @test block ends at the first closing brace in column zero, so helpers
# and stub heredocs stay at top level.
#
# Run from the repo root with `make bats-unit-tests`, or directly with
# `bats hack/runner-identity.bats`. The legacy `hack/cozytest.sh` runner remains
# a supported compatibility path.
# -----------------------------------------------------------------------------
load test_helper
HACK_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")" && pwd)"
SCRIPT="$HACK_DIR/e2e-runner-identity.sh"
REPO_ROOT="$(cd "$HACK_DIR/.." && pwd)"
# Loads the pure helpers. The guard returns before anything is read or written,
# so this needs no runner and no network. It also brings in the two defaults, so
# the assertions below read them from the script instead of restating them --
# a restated default is one that goes stale in the direction that stays green.
E2E_RUNNER_IDENTITY_LIB=1
# shellcheck source=/dev/null
. "$SCRIPT"
# And the report side, whose fold-in is one function for the same reason: the
# module around it needs a cluster, while the arm that decides between a record
# and an explanation is decided entirely by a local file.
COZYREPORT_LIB=1
# shellcheck source=/dev/null
. "$REPO_ROOT/hack/cozyreport.sh"
assert_file_contains() {
local needle="$1"
local file="$2"
if [ ! -f "${file}" ]; then
printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${needle}" >&2
return 1
fi
case "$(cat "${file}")" in
*"${needle}"*) return 0 ;;
esac
printf 'expected %s to contain: %s\n' "${file}" "${needle}" >&2
cat "${file}" >&2
return 1
}
assert_file_lacks_pattern() {
local pattern="$1"
local file="$2"
# A missing file must fail rather than vacuously pass: a bare `! grep -q`
# succeeds on an unreadable path, which is indistinguishable from "the file
# exists and does not carry the pattern".
if [ ! -f "${file}" ]; then
printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${pattern}" >&2
return 1
fi
# Branched on awk's exact status. awk exits 1 for "no line matched" and 2 for
# "I could not evaluate this"; folded together, a negative assertion is
# satisfied by its own matcher giving up, which is the direction that goes
# green and stays green.
local _rc=0
awk -v pattern="${pattern}" '$0 ~ pattern { found = 1 } END { exit found ? 0 : 1 }' "${file}" || _rc=$?
case "${_rc}" in
0)
printf 'expected %s not to match: %s\n' "${file}" "${pattern}" >&2
cat "${file}" >&2
return 1
;;
1) return 0 ;;
*)
printf 'awk could not evaluate pattern %s against %s\n' "${pattern}" "${file}" >&2
return 1
;;
esac
}
# The shape sizing object, as the `shapeConfig` subpath serves it: a flat object of
# sizing numbers with one string in it, pretty-printed because the service is free
# to answer either way and the reader buffers its whole input for that reason.
shape_config() {
cat <<'JSON'
{
"ocpus" : 16.0,
"memoryInGBs" : 128.0,
"networkingBandwidthInGbps" : 16.0,
"maxVnicAttachments" : 16,
"processorDescription" : "AMD EPYC 9J14 96-Core Processor"
}
JSON
}
# The ROOT instance document, which this capture deliberately never requests.
#
# It is here to be asserted absent, and to be fed to the scalar guard as the thing
# a per-key request must never be allowed to render: on a real instance it carries
# the instance and compartment OCIDs, the image OCID, the display name, the
# hostname and the user-supplied metadata bag. The values below are obviously
# synthetic.
imds_root_document() {
cat <<'JSON'
{
"availabilityDomain" : "xYzA:US-CHICAGO-1-AD-1",
"faultDomain" : "FAULT-DOMAIN-2",
"timeCreated" : 1600381928581,
"compartmentId" : "ocid1.compartment.oc1..aaaaexample",
"displayName" : "runner-pool-node-7",
"hostname" : "runner-pool-node-7",
"id" : "ocid1.instance.oc1.us-chicago-1.anexampleocid",
"image" : "ocid1.image.oc1.us-chicago-1.anexampleocid",
"metadata" : {
"ssh_authorized_keys" : "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5example"
},
"region" : "us-chicago-1",
"shape" : "VM.Standard.E5.Flex"
}
JSON
}
# Stubs for the tools the body runs, staged as executables rather than mocked as
# functions: every one of them is run in a child process, where a function defined
# in this shell does not exist.
#
# Each stub reads what it should do from a FILE beside it rather than from a value
# baked in when it was written. A stub carrying its behaviour in its own text has
# to be regenerated after every change, and a test that changes the behaviour and
# forgets to regenerate passes against the previous fixture -- which is a green
# test asserting nothing, in a suite whose whole subject is the difference between
# a capture that ran and one that did not.
#
# The curl stub answers PER KEY, out of `imds/<key>`, because the capture reads one
# key at a time and a stub answering the same bytes to every URL could not tell a
# test about one key from a test about another. It records its whole argv, since the
# ceiling, the header and the path are the three things about these reads that a
# test can check and a reader of the record cannot.
stage_stubs() {
local dir="$1"
mkdir -p "${dir}/bin" "${dir}/imds"
cat >"${dir}/bin/curl" <<STUB
#!/bin/sh
printf '%s\n' "\$*" >>"${dir}/curl.args"
for a in "\$@"; do url=\$a; done
key=\${url##*/instance/}
if [ -f "${dir}/imds/\$key.rc" ]; then
rc=\$(cat "${dir}/imds/\$key.rc")
else
rc=\$(cat "${dir}/curl.rc")
fi
if [ -s "${dir}/imds/\$key.err" ]; then
cat "${dir}/imds/\$key.err" >&2
elif [ -s "${dir}/curl.stderr" ]; then
cat "${dir}/curl.stderr" >&2
fi
[ ! -f "${dir}/imds/\$key" ] || cat "${dir}/imds/\$key"
exit "\$rc"
STUB
cat >"${dir}/bin/lscpu" <<STUB
#!/bin/sh
cat "${dir}/lscpu.out"
STUB
cat >"${dir}/bin/nproc" <<STUB
#!/bin/sh
cat "${dir}/nproc.out"
STUB
cat >"${dir}/bin/uname" <<STUB
#!/bin/sh
cat "${dir}/uname.out"
STUB
chmod +x "${dir}/bin/curl" "${dir}/bin/lscpu" "${dir}/bin/nproc" "${dir}/bin/uname"
curl_args="${dir}/curl.args"
stub_bin="${dir}/bin"
}
curl_args=/dev/null
stub_bin=
# Runs the script with the staged stubs ahead of the real PATH and captures its
# stdout. Wrapped in `set +x` because cozytest.sh runs test bodies under the
# tracer, and every line of the trace would otherwise land in the capture this
# suite reads.
#
# The status is returned rather than asserted here: several tests are about the
# script exiting zero on a path that failed, so the status is the subject.
run_identity() {
local _rc=0
( set +x
PATH="${stub_bin}:${PATH}" \
COZY_RUNNER_IDENTITY_FILE="${identity_file}" \
sh "$SCRIPT" >"${stdout_file}" 2>"${stderr_file}"
) || _rc=$?
return "${_rc}"
}
# The healthy fixture: an OCI instance answering every key this capture asks for, a
# processor table with a model line, a CPU count and a kernel release. Every test
# starts here and overwrites the one file it is about, so what a test changes is
# visible in the test rather than in a stub regenerated behind it.
#
# `region` is staged short and `canonicalRegionName` long, which is what the
# service does for some regions and the reason the record keeps both.
stage() {
stage_stubs "$1"
identity_file="$1/out/runner-identity.txt"
stdout_file="$1/stdout"
stderr_file="$1/stderr"
printf '0\n' >"$1/curl.rc"
: >"$1/curl.stderr"
printf 'VM.Standard.E5.Flex\n' >"$1/imds/shape"
printf 'chi\n' >"$1/imds/region"
printf 'us-chicago-1\n' >"$1/imds/canonicalRegionName"
printf 'xYzA:US-CHICAGO-1-AD-1\n' >"$1/imds/availabilityDomain"
printf 'FAULT-DOMAIN-2\n' >"$1/imds/faultDomain"
shape_config >"$1/imds/shapeConfig"
{
printf 'Architecture: x86_64\n'
printf 'Model name: AMD EPYC 9J14 96-Core Processor\n'
printf 'Flags: fpu vme de pse tsc msr svm npt nrip_save vgif umip pku\n'
} >"$1/lscpu.out"
printf '32\n' >"$1/nproc.out"
printf '6.8.0-1021-oracle\n' >"$1/uname.out"
}
# ---------------------------------------------------------------------------
# The SVM sub-feature reader, driven directly.
#
# These exist because the flag this reader was added for, vgif, is the one field
# in the record that decides whether a nested guest runs at hardware speed or
# traps to the layer below on every world switch, and it is read out of a flag
# list where a whole-word match is the only thing standing between `svm` and
# `svm_lock`.
# ---------------------------------------------------------------------------
@test "the svm reader keeps the sub-features an AMD guest was given" {
tmp=$(mktemp -d)
printf 'Model name: AMD EPYC 7J13 64-Core Processor\n' >"$tmp/lscpu"
printf 'Flags: fpu de tsc svm cr8_legacy npt nrip_save vgif umip pku\n' >>"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
# vgif is the whole reason for this line: present here, and its absence on an
# otherwise identical host is the only difference measured between a runner
# whose nested guests boot and one whose do not.
[ "$out" = "svm npt nrip_save vgif" ] || {
echo "expected 'svm npt nrip_save vgif', got '$out'" >&2
return 1
}
}
@test "the svm reader reports the same host without vgif as lacking it" {
tmp=$(mktemp -d)
printf 'Flags: fpu de tsc svm cr8_legacy npt nrip_save umip pku\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ "$out" = "svm npt nrip_save" ] || {
echo "expected 'svm npt nrip_save', got '$out'" >&2
return 1
}
}
@test "the svm reader says nothing about an Intel host, whose vnmi is not SVM" {
tmp=$(mktemp -d)
# vnmi is a flag name under VMX as well as SVM. Ungated, this reader would
# print one SVM sub-feature for a machine that has no SVM, which is not a
# shorter answer but a wrong one.
printf 'Flags: fpu vme de pse tsc msr vmx ept vpid vnmi\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ -z "$out" ] || {
echo "expected no output for an Intel flag list, got '$out'" >&2
return 1
}
}
@test "the svm reader does not take svm from svm_lock alone" {
tmp=$(mktemp -d)
# A whole-word match is what keeps this honest: svm_lock is a sub-feature name
# in its own right and its presence does not mean SVM is exposed.
printf 'Flags: fpu de tsc svm_lock npt umip\n' >"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ -z "$out" ] || {
echo "expected no output when only svm_lock is present, got '$out'" >&2
return 1
}
}
@test "the svm reader reads the first Flags line only" {
tmp=$(mktemp -d)
# lscpu prints one Flags line, but a caller handing this a concatenated table
# would otherwise get two machines' features merged into one answer.
printf 'Flags: fpu svm npt vgif\n' >"$tmp/lscpu"
printf 'Flags: fpu svm npt avic\n' >>"$tmp/lscpu"
out=$(runner_svm_features <"$tmp/lscpu")
[ "$out" = "svm npt vgif" ] || {
echo "expected only the first Flags line, got '$out'" >&2
return 1
}
}
# ---------------------------------------------------------------------------
# The number reader and the scalar guard, driven directly.
# ---------------------------------------------------------------------------
@test "the number reader keeps every number the sizing object carries" {
tmp=$(mktemp -d)
shape_config >"$tmp/obj"
runner_imds_numbers shapeConfig <"$tmp/obj" >"$tmp/out"
# ocpus against the vCPU count the lane advertises is the one arithmetic nothing
# else in the report can do: a 32-vCPU lane served 16 physical cores is a
# different machine from one served 32, and only these two numbers together say
# which it was.
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains 'shapeConfig.memoryInGBs: 128.0' "$tmp/out"
assert_file_contains 'shapeConfig.networkingBandwidthInGbps: 16.0' "$tmp/out"
assert_file_contains 'shapeConfig.maxVnicAttachments: 16' "$tmp/out"
rm -rf "$tmp"
}
@test "the number reader reads by key rather than by a list of key names" {
tmp=$(mktemp -d)
# A key nobody here has heard of. The sizing keys have been renamed upstream
# before, and a reader built on a hardcoded list reports the size of the machine
# as unknown on every run, quietly, from the day the rename lands.
printf '%s' '{"ocpusRenamedUpstream":48.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpusRenamedUpstream: 48.0' "$tmp/out"
rm -rf "$tmp"
}
@test "the number reader drops what is not a number and says how much it dropped" {
tmp=$(mktemp -d)
# Numbers only, and that is what makes the privacy rule structural rather than
# an argument about whatever Oracle may add to this object later: a size is a
# number and an identifier is a string.
printf '%s' '{"ocpus":16.0,"processorDescription":"AMD EPYC 9J14","gpus":null,"burstable":true}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'processorDescription' "$tmp/out"
assert_file_lacks_pattern 'AMD EPYC' "$tmp/out"
# `null` and `true` are dropped rather than printed: neither is a measurement,
# and the word in a record reads as a value the service published.
assert_file_lacks_pattern 'null' "$tmp/out"
assert_file_lacks_pattern 'true' "$tmp/out"
# Counted, not listed. Naming the keys would print the very strings the rule
# exists to keep out, and a count is enough to say the object held more.
assert_file_contains 'shapeConfig: 3 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "an object answered on one line reads the same as a pretty printed one" {
tmp=$(mktemp -d)
# The service is free to answer either way, and a reader that scanned line by
# line would find a key whose value sits on the next line and call it absent.
printf '{\n "ocpus" : 12.0,\n "memoryInGBs" : 96.0\n}\n' |
runner_imds_numbers shapeConfig >"$tmp/pretty"
printf '{"ocpus":12.0,"memoryInGBs":96.0}' | runner_imds_numbers shapeConfig >"$tmp/flat"
assert_file_contains 'shapeConfig.ocpus: 12.0' "$tmp/pretty"
assert_file_contains 'shapeConfig.memoryInGBs: 96.0' "$tmp/pretty"
diff "$tmp/pretty" "$tmp/flat"
rm -rf "$tmp"
}
@test "an object holding no number at all is not rendered as a size" {
tmp=$(mktemp -d)
printf '%s' '{"gpuDescription":"none"}' | runner_imds_numbers shapeConfig >"$tmp/out"
# The object was fetched and had nothing this reader keeps. Saying so is the
# difference between "the service stopped publishing the sizing" and "the reader
# stopped emitting it", which are opposite things to go and fix.
assert_file_contains 'shapeConfig: unavailable' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.gpuDescription' "$tmp/out"
rm -rf "$tmp"
}
@test "a nested value is stepped over rather than flattened into this object" {
tmp=$(mktemp -d)
# A key from a level below, reported as a key of this one, would be a number
# about something else entirely sitting under the sizing prefix.
printf '%s' '{"gpuInfo":{"count":0},"ocpus":16.0,"tags":[1,2,3]}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'count' "$tmp/out"
assert_file_contains 'shapeConfig: 2 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a string value carrying an escaped quote does not swallow the key after it" {
tmp=$(mktemp -d)
# The one place an escape matters here: a `\"` read as the end of the string
# leaves the walk inside a value it thinks is a key, and the number after it is
# lost. Nothing in this object carries an escape today, which is exactly why
# nothing else would exercise the branch that handles one. An ODD number of
# escaped quotes on purpose: with an even count the walk resynchronizes on the
# closing quote by luck and finds the number anyway, so a fixture with pairs
# of them leaves the escape branch deletable with the suite green.
printf '%s' '{"gpuDescription":"one \" quote","ocpus":16.0}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_lacks_pattern 'quote' "$tmp/out"
rm -rf "$tmp"
}
@test "an object that is not JSON at all is reported as holding no number" {
tmp=$(mktemp -d)
# What an error page, or another cloud service, answers with. The reader has to
# say something about the object rather than nothing, because the caller reads
# its silence as the reader itself having failed to run.
printf '%s\n' 'Not Found' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'shapeConfig: unavailable' "$tmp/out"
rm -rf "$tmp"
}
@test "an object cut off part way is refused rather than read in part" {
tmp=$(mktemp -d)
# Read in part, a truncated answer is indistinguishable in the record from a
# machine whose sizing simply has fewer fields, and nothing above it would say
# the rest was never read. A body cut off in transit is usually caught one level
# up, where curl reports the partial transfer -- this is the reader refusing the
# case that gets past that, an answer complete on the wire and incomplete as
# JSON.
printf '%s' '{"ocpus":16.0,"memoryInGBs"' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
rm -rf "$tmp"
}
@test "a top level array is refused rather than published as this object" {
tmp=$(mktemp -d)
# An array of objects walked as one level would print each nested key under the
# sizing prefix, so the record would carry numbers about something else entirely
# as though this object had published them.
printf '%s' '[{"ocpus":1},{"ocpus":2}]' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
rm -rf "$tmp"
}
@test "a key too long to be a field name is dropped rather than printed" {
tmp=$(mktemp -d)
# In the alphabet a field name is drawn from, so only the length bound can
# refuse it. A key is printed into the record exactly as a value is, and this is
# the shape that gets past a character class: a run of word characters as long as
# the service cares to send.
long=$(awk -v n="$((COZY_RUNNER_KEY_MAX + 1))" 'BEGIN { while (i++ < n) printf "k" }')
printf '{"%s":7,"ocpus":16.0}' "$long" | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'kkkk' "$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a key that forges a line of its own is dropped rather than printed" {
tmp=$(mktemp -d)
# A raw newline inside a key would otherwise print a line of the reader's own
# shape into the record, under a name the record uses for something else. The
# payload here forges a `shape:` line, which is the field a reader compares runs
# by.
printf '{"ocpus\nshape: TOTALLY-FAKE-SHAPE\nx":16.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'TOTALLY-FAKE-SHAPE' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "two objects glued together are refused whole, not read as one" {
tmp=$(mktemp -d)
# Starts with `{` and ends with `}`, so a check of the endpoints alone lets it
# through -- and the walk is flat, so every key of the second object would
# publish under the sizing prefix as though this object had held it.
printf '%s' '{"ocpus":16.0}{"memoryInGBs":128.0,"stowaway":4200}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
assert_file_lacks_pattern 'stowaway' "$tmp/out"
rm -rf "$tmp"
}
@test "an object that never closes its last brace is refused whole" {
tmp=$(mktemp -d)
# The endpoint check cannot see this one either: `{` first and `}` last both
# hold, while one brace inside never closes. The balance is what refuses it.
printf '%s' '{"a":{"ocpus":16.0}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_contains 'not one complete JSON object' "$tmp/out"
assert_file_lacks_pattern 'ocpus: 16.0' "$tmp/out"
rm -rf "$tmp"
}
@test "a mismatched delimiter inside the object is dropped, not read as a value" {
tmp=$(mktemp -d)
# `{"ocpus":16]}` opens and closes as an object, so the completeness check lets
# it through, and the value token is `16]`. Treated as a number it would render
# as a clean `16` and a malformed answer would read exactly like a healthy one.
printf '%s' '{"ocpus":16]}' | runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "a key that is not a field name is dropped rather than printed" {
tmp=$(mktemp -d)
# A key is printed into the record exactly as a value is, so it is held to the
# same standard as one. Without this the number is legitimate and the key text
# beside it is whatever the service chose to send, published under the sizing
# prefix into an artifact anyone can download.
printf '%s' '{"ocid1.instance.oc1..anexampleocid":7,"ocpus":16.0}' |
runner_imds_numbers shapeConfig >"$tmp/out"
assert_file_lacks_pattern 'ocid1' "$tmp/out"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$tmp/out"
assert_file_contains '1 further field(s) were not kept' "$tmp/out"
rm -rf "$tmp"
}
@test "the scalar guard passes the shapes the metadata service returns" {
# The four values this record keeps, in the spellings Oracle publishes: a shape
# name, a canonical region, a tenancy-prefixed availability domain and a fault
# domain.
[ "$(runner_imds_scalar 'VM.Standard.E5.Flex')" = 'VM.Standard.E5.Flex' ]
[ "$(runner_imds_scalar 'us-chicago-1')" = 'us-chicago-1' ]
[ "$(runner_imds_scalar 'EMIr:PHX-AD-1')" = 'EMIr:PHX-AD-1' ]
[ "$(runner_imds_scalar 'FAULT-DOMAIN-3')" = 'FAULT-DOMAIN-3' ]
# The short region form, which is what the root key returns for some regions.
[ "$(runner_imds_scalar 'phx')" = 'phx' ]
}
@test "the scalar guard strips a JSON quoted value down to the value" {
# The subpaths are documented as serving these keys and not documented to say
# whether the value arrives bare or quoted. Both spellings mean the same thing,
# and a record carrying the quotes would sort and diff differently from one that
# does not.
[ "$(runner_imds_scalar '"VM.Standard.E5.Flex"')" = 'VM.Standard.E5.Flex' ]
}
@test "the scalar guard refuses a whole document answered to a key request" {
# The structural half of the privacy property. Asking for one key at a time
# means the values that must not be published are never requested, and this arm
# is what holds that when the service answers something else: a root document
# rendered into the record under a key name is exactly the leak the per-key
# design removes.
rc=0
out=$(runner_imds_scalar "$(imds_root_document)") || rc=$?
[ "$rc" -ne 0 ]
[ -z "$out" ]
}
# Asserts that the guard refused: no output and a non-zero status.
#
# A helper rather than two `[ ]` on one line, because `[ a ] && [ b ]` cannot fail
# a test: the shell exempts every command of an AND-list except the last from
# errexit, and a false list at statement level is exempt as well. Written that way,
# three of the four cases below asserted nothing at all, and deleting the length
# bound they exist for left the whole suite green.
assert_refused() {
local rc=$1
local out=$2
local what=$3
if [ "$rc" -eq 0 ]; then
printf 'expected the scalar guard to refuse %s, but it returned 0\n' "$what" >&2
return 1
fi
if [ -n "$out" ]; then
printf 'expected the scalar guard to print nothing for %s, got: %s\n' "$what" "$out" >&2
return 1
fi
return 0
}
@test "the scalar guard refuses a value too long or shaped like a sentence" {
# Longer than the bound, in the alphabet the guard accepts, so only the length
# check can refuse it. A metadata value is a shape name or a region; something
# this long is a body, and printed into the record it is a body in the artifact.
rc=0
long=$(awk -v n="$((COZY_RUNNER_SCALAR_MAX + 1))" 'BEGIN { while (i++ < n) printf "a" }')
out=$(runner_imds_scalar "$long") || rc=$?
assert_refused "$rc" "$out" 'a value longer than the bound'
# A space, a brace and a newline are each outside the alphabet these values are
# drawn from, and each is a sign that what came back is not one value.
rc=0
out=$(runner_imds_scalar 'VM.Standard.E5.Flex and then some prose') || rc=$?
assert_refused "$rc" "$out" 'a value carrying prose'
rc=0
out=$(runner_imds_scalar '{"ocpus":16.0}') || rc=$?
assert_refused "$rc" "$out" 'a JSON object'
rc=0
out=$(runner_imds_scalar 'phx
iad') || rc=$?
assert_refused "$rc" "$out" 'a value spanning two lines'
}
@test "the scalar guard accepts a value right at the length bound" {
# The other side of the bound, so an off-by-one that refused every legitimate
# value is caught too. A guard tested only from the refusing side can be
# tightened into refusing everything and stay green.
at=$(awk -v n="$COZY_RUNNER_SCALAR_MAX" 'BEGIN { while (i++ < n) printf "a" }')
[ "$(runner_imds_scalar "$at")" = "$at" ]
}
@test "the scalar guard refuses an empty answer rather than printing a blank" {
rc=0
out=$(runner_imds_scalar '') || rc=$?
assert_refused "$rc" "$out" 'an empty answer'
}
# ---------------------------------------------------------------------------
# The ceiling knob, driven directly.
# ---------------------------------------------------------------------------
@test "the ceiling knob is taken when it is a plain positive number" {
COZY_RUNNER_IMDS_TIMEOUT=3
[ "$(runner_imds_timeout)" = 3 ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a zero ceiling is corrected rather than removing the bound entirely" {
# `--max-time 0` does not mean "do not wait", it means "no ceiling" -- the same
# trap `timeout -k 5 0` has elsewhere in this tree. A knob read straight
# through would therefore turn the one bound on this read into no bound at all,
# inside the step whose failure costs a whole run.
COZY_RUNNER_IMDS_TIMEOUT=0
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a padded zero and a leading zero are refused the same as a plain zero" {
# Refused rather than read: a leading zero is octal to the shell's arithmetic
# and decimal to curl, and there is no reading of `08` worth carrying that
# disagreement for.
COZY_RUNNER_IMDS_TIMEOUT=00
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=08
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "a ceiling that is not a number and one too long to be meant fall back" {
COZY_RUNNER_IMDS_TIMEOUT=5s
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
# curl parses the number itself and refuses what it cannot hold, and its
# refusal arrives as an `imds unreachable` line blaming the runner for a value
# this script chose to forward.
COZY_RUNNER_IMDS_TIMEOUT=99999
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
COZY_RUNNER_IMDS_TIMEOUT=
}
@test "an unset ceiling resolves to the documented default" {
COZY_RUNNER_IMDS_TIMEOUT=
[ "$(runner_imds_timeout)" = "$COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" ]
}
# ---------------------------------------------------------------------------
# The running script.
# ---------------------------------------------------------------------------
@test "the record reaches the file and the job log with the same content" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# Both, and neither conditional on the other. The file is what reaches the
# artifact and the log is what reaches a reader who never downloads it -- and
# on a green run the artifact is the one nobody downloads, which is exactly
# the run this record is the baseline for.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'kernel release: 6.8.0-1021-oracle' "$identity_file"
assert_file_contains 'nproc: 32' "$identity_file"
assert_file_contains 'cpu model: AMD EPYC 9J14 96-Core Processor' "$identity_file"
rm -rf "$tmp"
}
@test "the record says which layer its numbers belong to" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The one reading that would invert every conclusion drawn from this file. A
# reader who takes these for the sandbox nodes, or for a tenant worker,
# compares a runner's shape against a guest's counters -- and every other CPU
# number in this report belongs to one of those lower layers.
assert_file_contains 'RUNNER VM' "$identity_file"
assert_file_contains 'before the container existed' "$identity_file"
rm -rf "$tmp"
}
@test "the record carries the absolute clock in both forms with the units named" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# An epoch integer and an RFC 3339 string subtract to nonsense if a reader
# takes them as printed, and the instant matters here for a reason the file
# cannot state for itself: the machine is identified at the start of the run,
# so this clock is not the clock of any failure later in it.
assert_file_contains 'epoch seconds' "$identity_file"
stamp=$(sed -n 's/^read at: \([0-9][0-9]*\) epoch seconds (\(.*\) UTC)$/\1 \2/p' "$identity_file")
if [ -z "$stamp" ]; then
echo "expected an epoch stamp beside an RFC 3339 string in $identity_file" >&2
cat "$identity_file" >&2
return 1
fi
rm -rf "$tmp"
}
@test "the metadata read is bounded and asks the version two endpoint" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The ceiling is the whole reason this read is allowed to sit in a step that
# gates the run. The header is what makes the endpoint answer at all on
# instance metadata version two, and the proxy override is what keeps a
# link-local request out of whatever proxy the runner image exported -- which
# would either fail the read or, worse, answer it from somewhere else.
assert_file_contains "--max-time $COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" "$curl_args"
assert_file_contains 'Authorization: Bearer Oracle' "$curl_args"
assert_file_contains '/opc/v2/instance/' "$curl_args"
# By value, not by name. `--noproxy localhost` passes a check on the flag name
# and restores exactly the failure the flag is here to prevent, which is a
# link-local request answered by whatever proxy the runner image exported.
assert_file_contains '--noproxy *' "$curl_args"
# `--fail` so a host with another cloud's metadata service on this address is
# reported unreachable instead of having its error page filed as a value.
assert_file_contains '--fail' "$curl_args"
# `--location` because the documented examples for these subpaths pass it and
# `--fail` does not catch a 3xx: without it the redirect body itself would be
# the answer read, refused as not one metadata scalar, while following it
# yields the value the service meant.
assert_file_contains '--location' "$curl_args"
rm -rf "$tmp"
}
@test "a lowered ceiling reaches curl and a zero one does not" {
tmp=$(mktemp -d)
stage "$tmp"
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT=3 \
COZY_RUNNER_IDENTITY_FILE="$identity_file" sh "$SCRIPT" >"$stdout_file" 2>&1
)
assert_file_contains '--max-time 3' "$curl_args"
rm -f "$curl_args"
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT=0 \
COZY_RUNNER_IDENTITY_FILE="$identity_file" sh "$SCRIPT" >"$stdout_file" 2>&1
)
assert_file_contains "--max-time $COZY_RUNNER_IMDS_TIMEOUT_DEFAULT" "$curl_args"
assert_file_lacks_pattern '--max-time 0' "$curl_args"
# And the substitution is written down. A ceiling silently replaced is a knob
# an operator believes they set.
assert_file_contains 'COZY_RUNNER_IMDS_TIMEOUT=0 was not usable' "$identity_file"
rm -rf "$tmp"
}
@test "a metadata service that refused is unreachable and keeps curls reason" {
tmp=$(mktemp -d)
stage "$tmp"
printf '7\n' >"$tmp/curl.rc"
: >"$tmp/curl.stdout"
printf 'curl: (7) Failed to connect to 169.254.169.254 port 80 after 1 ms: Connection refused\n' \
>"$tmp/curl.stderr"
rc=0
run_identity || rc=$?
# Zero, on the path that failed. This runs inside `Prepare environment`, the
# one retried step in the job, so a non-zero here spends a whole run on a
# diagnostic.
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
# curl's own words, not just its number. "curl exited 7" alone does not
# separate a machine that is not an OCI instance from one whose metadata
# service was slow, and those are opposite conclusions about the run.
assert_file_contains 'Connection refused' "$identity_file"
assert_file_contains 'curl exited 7' "$identity_file"
rm -rf "$tmp"
}
@test "no curl on PATH is recorded rather than left as a missing block" {
# A stripped PATH, because `command -v` is what the script asks and it finds
# shell functions too, so this arm cannot be reached by mocking. Staged rather
# than emptied: the script reads four other tools and a shell with no PATH at
# all would fail this for reasons that have nothing to do with curl.
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/out"
# `sh` is in the list because the assignment below sets the PATH the shell then
# resolves `sh` against: with it absent the test fails on the interpreter
# rather than on the arm it is about, which reads as this arm being broken.
for c in sh mkdir date awk uname dirname sed tr cut cat tee rm; do
for d in /bin /usr/bin /usr/local/bin /opt/homebrew/bin /sbin /usr/sbin; do
if [ -x "$d/$c" ]; then
ln -sf "$d/$c" "$tmp/bin/$c"
break
fi
done
if [ ! -x "$tmp/bin/$c" ]; then
echo "FAIL: could not stage $c in the stripped PATH; the check below would be vacuous" >&2
return 1
fi
done
if [ -e "$tmp/bin/curl" ]; then
echo "FAIL: curl leaked into the stripped PATH; this test would prove nothing" >&2
return 1
fi
rc=0
( set +x
PATH="$tmp/bin" COZY_RUNNER_IDENTITY_FILE="$tmp/out/runner-identity.txt" \
sh "$SCRIPT" >"$tmp/stdout" 2>"$tmp/stderr"
) || rc=$?
# A runner with no curl says so and goes on. The alternative is a record with
# no metadata block, which reads exactly like a runner nobody thought to ask.
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$tmp/out/runner-identity.txt"
assert_file_contains 'curl is not on PATH' "$tmp/out/runner-identity.txt"
rm -rf "$tmp"
}
@test "each key is asked for by name and the root document never is" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The structural half of the privacy property: the values that must not be
# published are not filtered out of this record, they are never requested. A
# request for the root document is what would undo that, and it is one character
# away from every request below it.
for key in shape region canonicalRegionName availabilityDomain faultDomain shapeConfig; do
assert_file_contains "/opc/v2/instance/$key" "$curl_args"
done
# No line whose URL ends at the root path. Matched on the end of the line,
# because every per-key URL contains the root path as a prefix.
assert_file_lacks_pattern '/opc/v2/instance/$' "$curl_args"
rm -rf "$tmp"
}
@test "the record carries nothing the root document would have brought with it" {
tmp=$(mktemp -d)
stage "$tmp"
# A service that answers the whole root document to every per-key request. Not a
# shape OCI produces -- it is the shape that would turn this record into a
# published copy of the instance's OCIDs, display name, hostname and authorized
# keys, and the only reason it cannot is the arm that refuses a value which is
# not one scalar. Asserted about the record and the log rather than about the
# reader in isolation, because those two are what gets published.
#
# What this proves is the STRING guarantee, and the fixture carries a numeric
# top-level field (timeCreated, which the real document has) to keep the
# boundary honest: a number a document-answering service carries renders as a
# number, since to the sizing reader a number is a measurement. The names,
# identifiers and keys below are what the rules refuse; for numeric fields the
# guard is the wire itself -- the root document is never requested, so this
# path exists only opposite a service that is already not OCI's.
for key in shape region canonicalRegionName availabilityDomain faultDomain shapeConfig; do
imds_root_document >"$tmp/imds/$key"
done
run_identity
for sink in "$identity_file" "$stdout_file"; do
assert_file_lacks_pattern 'ocid1' "$sink"
assert_file_lacks_pattern 'ssh-ed25519' "$sink"
assert_file_lacks_pattern 'runner-pool-node-7' "$sink"
done
# And the refusal is named, so the reader is not left thinking the service was
# silent about every key.
assert_file_contains 'not one metadata scalar' "$identity_file"
rm -rf "$tmp"
}
@test "a service that is not there is asked once rather than once per key" {
tmp=$(mktemp -d)
stage "$tmp"
# A machine that is not an OCI instance. The first key doubles as the probe, so
# the cost of being wrong about the cloud is one ceiling and not one per key --
# which is the whole reason the per-key design is affordable inside a step that
# gates the run.
printf '7\n' >"$tmp/curl.rc"
printf 'curl: (7) Failed to connect to 169.254.169.254 port 80 after 1 ms: Connection refused\n' \
>"$tmp/curl.stderr"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
attempts=$(awk 'END { print NR }' "$curl_args")
[ "$attempts" -eq 1 ] || {
echo "FAIL: the capture made $attempts metadata requests against a service that is not there"
cat "$curl_args"
false
}
assert_file_contains 'imds unreachable' "$identity_file"
assert_file_contains 'no further key was asked for' "$identity_file"
rm -rf "$tmp"
}
@test "one key that fails leaves the others and does not read as unreachable" {
tmp=$(mktemp -d)
stage "$tmp"
# One key refused, the rest answering. Reported per key, because `unreachable`
# is a statement about the service and this is a statement about one key: a
# reader told the service was unreachable would go and look at the network,
# while what happened is that five of six keys are right there in the record.
printf '22\n' >"$tmp/imds/faultDomain.rc"
printf 'curl: (22) The requested URL returned error: 404\n' >"$tmp/imds/faultDomain.err"
run_identity
assert_file_contains 'faultDomain: unavailable' "$identity_file"
assert_file_contains 'curl exited 22' "$identity_file"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
assert_file_contains 'shapeConfig.ocpus: 16.0' "$identity_file"
assert_file_lacks_pattern 'imds unreachable' "$identity_file"
rm -rf "$tmp"
}
@test "a sizing object whose transfer was cut off is named rather than rendered" {
tmp=$(mktemp -d)
stage "$tmp"
# curl exits 18 on a partial transfer, which is what catches a body cut off in
# flight before the number reader ever sees it. Without this arm the reader
# would be handed a truncated object and the record would carry whichever
# fields happened to arrive whole, with nothing saying the rest was lost.
printf '18\n' >"$tmp/imds/shapeConfig.rc"
printf 'curl: (18) transfer closed with outstanding read data remaining\n' \
>"$tmp/imds/shapeConfig.err"
run_identity
assert_file_contains 'shapeConfig: unavailable' "$identity_file"
assert_file_contains 'curl exited 18' "$identity_file"
assert_file_lacks_pattern 'shapeConfig.ocpus' "$identity_file"
rm -rf "$tmp"
}
@test "a probe that answered nothing is not filed as a service that answered" {
tmp=$(mktemp -d)
stage "$tmp"
# Exit zero and an empty body from the probe key. Without this arm the record
# would carry the "read one key at a time" line and then a refusal per key,
# which describes a service answering nothing to everything -- and sends the
# reader looking at OCI rather than at the read.
: >"$tmp/imds/shape"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
assert_file_contains 'returned nothing for this key' "$identity_file"
rm -rf "$tmp"
}
@test "a probe answering something that is not a scalar ends the metadata read" {
tmp=$(mktemp -d)
stage "$tmp"
# Something else on this address answering 200: another cloud's metadata
# service, a proxy error page, a redirect target. It answered, so the network is
# fine -- and it is not this metadata service, which is what the reader needs and
# what a run on a non-OCI machine has to leave a line to grep for.
printf 'Not Found\n' >"$tmp/imds/shape"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'imds unreachable' "$identity_file"
# The word is used for a service that DID answer, so the line says which of the
# two happened rather than leaving the reader to assume the network.
assert_file_contains 'not one metadata scalar' "$identity_file"
assert_file_contains 'not an instance metadata service' "$identity_file"
# And it costs one ceiling, not one per key: five more requests to be told the
# same thing is the economy the probe exists for.
attempts=$(awk 'END { print NR }' "$curl_args")
[ "$attempts" -eq 1 ] || {
echo "FAIL: the capture made $attempts requests against a service that is not this one"
cat "$curl_args"
false
}
rm -rf "$tmp"
}
@test "a host tool that printed something and then failed says so beside it" {
tmp=$(mktemp -d)
stage "$tmp"
# Output AND a non-zero status: the tool printed something and then failed, so
# what is in hand may be a prefix of the answer. Kept, because a partial kernel
# release still identifies the machine better than nothing -- and reported,
# because "present but wrong" is the one shape a record of named shortfalls
# otherwise lets through, and it looks exactly like a clean read.
cat >"$tmp/bin/uname" <<'STUB'
#!/bin/sh
printf '6.8.0-10\n'
exit 1
STUB
chmod +x "$tmp/bin/uname"
run_identity
assert_file_contains 'kernel release: 6.8.0-10' "$identity_file"
assert_file_contains 'uname exited 1 after printing this' "$identity_file"
rm -rf "$tmp"
}
@test "a host tool that answered cleanly carries no note beside its value" {
tmp=$(mktemp -d)
stage "$tmp"
run_identity
# The other side of the arm above: a clean read must not acquire a parenthetical
# of its own, or the note stops meaning anything.
assert_file_contains 'kernel release: 6.8.0-1021-oracle' "$identity_file"
assert_file_lacks_pattern 'kernel release:.*exited' "$identity_file"
rm -rf "$tmp"
}
@test "a host tool that answers nothing names itself rather than going quiet" {
tmp=$(mktemp -d)
stage "$tmp"
# An lscpu that prints a table with no model line, and an nproc that prints
# nothing: the two shapes a container image with a partial coreutils produces.
# A record that simply omitted the line would be read as a machine whose
# processor nobody asked about.
printf 'Vendor ID: AuthenticAMD\n' >"$tmp/lscpu.out"
: >"$tmp/nproc.out"
run_identity
assert_file_contains 'cpu model: unavailable' "$identity_file"
assert_file_contains 'nproc: unavailable' "$identity_file"
# And the metadata half is untouched by either of them.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
rm -rf "$tmp"
}
@test "a host read that never returns is cut off instead of holding the step" {
tmp=$(mktemp -d)
stage "$tmp"
# `lscpu` walks sysfs, and a machine wedged enough to be worth identifying is
# one where that walk can block. Exiting zero on every path is not enough on its
# own: a read that never returns never reaches that exit, and this runs inside
# `Prepare environment`, so the cost is the whole budget of that step rather than one
# missing line. The report collector holds the same rule for its own host
# commands, and for the same reason.
printf '#!/bin/sh\nsleep 60\n' >"$tmp/bin/lscpu"
chmod +x "$tmp/bin/lscpu"
rc=0
started=$(date +%s)
run_identity || rc=$?
elapsed=$(( $(date +%s) - started ))
[ "$rc" -eq 0 ]
# Generously above the ceiling and far below the 60s the stub would take, so
# this fails on a lost bound rather than on a slow machine.
[ "$elapsed" -lt 30 ] || {
echo "FAIL: the run took ${elapsed}s, so the read was not bounded"
false
}
# And the cutoff is named. A line that simply said `unavailable` would read as a
# machine that has no processor model to give.
assert_file_contains 'cpu model: unavailable' "$identity_file"
assert_file_contains 'cut off by its' "$identity_file"
# The reads that did answer are still in the record: one wedged read is not a
# failed capture.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$identity_file"
rm -rf "$tmp"
}
@test "reads taken with no ceiling at all are declared rather than passed off" {
# A stripped PATH with no `timeout` in it. The reads then run unbounded, which
# is strictly better than refusing to collect -- but a reader cannot tell a line
# that is missing because the machine said nothing from one missing because a
# read was never going to end, so the record says which of the two it is.
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/out"
for c in sh mkdir date awk uname dirname sed tr cut cat tee rm; do
for d in /bin /usr/bin /usr/local/bin /opt/homebrew/bin /sbin /usr/sbin; do
if [ -x "$d/$c" ]; then
ln -sf "$d/$c" "$tmp/bin/$c"
break
fi
done
if [ ! -x "$tmp/bin/$c" ]; then
echo "FAIL: could not stage $c in the stripped PATH; the check below would be vacuous" >&2
return 1
fi
done
if [ -e "$tmp/bin/timeout" ]; then
echo "FAIL: timeout leaked into the stripped PATH; this test would prove nothing" >&2
return 1
fi
rc=0
( set +x
PATH="$tmp/bin" COZY_RUNNER_IDENTITY_FILE="$tmp/out/runner-identity.txt" \
sh "$SCRIPT" >"$tmp/stdout" 2>"$tmp/stderr"
) || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'timeout is not on PATH here' "$tmp/out/runner-identity.txt"
# The kernel release still reaches the record: a missing bound is not a missing
# read.
assert_file_contains 'kernel release: ' "$tmp/out/runner-identity.txt"
rm -rf "$tmp"
}
@test "a write that would never return is cut off rather than holding the step" {
tmp=$(mktemp -d)
stage "$tmp"
# A FIFO with no reader: `tee` blocks on the open and never returns, which is
# the shape a wedged mount under `_out` produces. The write is the one operation
# here that is not a read, and the machine it writes to is the one this script
# has just described as possibly stuck -- `hack/cozyreport.sh` bounds `df` on
# that same host for exactly this reason.
if ! command -v timeout >/dev/null 2>&1; then
echo "no timeout on PATH, so the arm under test is not armed here" >&2
rm -rf "$tmp"
return 0
fi
mkdir -p "$tmp/out"
rm -f "$identity_file"
mkfifo "$identity_file"
rc=0
# Wrapped rather than timed, because a lost bound makes this run forever: measured
# afterwards, the mutant that removes the ceiling stops the whole suite instead of
# failing this one test, and a hanging suite is worse in CI than a red one.
( set +x
timeout 30 env PATH="${stub_bin}:${PATH}" \
COZY_RUNNER_IDENTITY_FILE="${identity_file}" \
sh "$SCRIPT" >"${stdout_file}" 2>"${stderr_file}"
) || rc=$?
[ "$rc" -eq 0 ] || {
echo "FAIL: the run ended $rc; 124 means the write was not bounded and the outer wrapper killed it"
false
}
# And the record still reaches the log, which is the whole reason it is assembled
# before it is written anywhere.
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -f "$identity_file"
rm -rf "$tmp"
}
@test "a record that could not be written is still in the log and says so" {
tmp=$(mktemp -d)
stage "$tmp"
# A file where the directory has to go, so `mkdir -p` refuses and the write
# after it has nowhere to land. Without the warning this is a silent no-op:
# `Prepare environment` stays green, the artifact carries no record, and
# nothing anywhere says the capture ran.
mkdir -p "$tmp/out"
rm -rf "$tmp/out"
printf 'not a directory\n' >"$tmp/out"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'shape: VM.Standard.E5.Flex' "$stdout_file"
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "a write cut off part way leaves no prefix behind for the artifact" {
tmp=$(mktemp -d)
stage "$tmp"
# A tee killed mid-write -- the bound firing on a wedged mount -- leaves a
# prefix on disk, and a prefix of this record is indistinguishable from a
# whole one to the fold-in. Left there, the log would also claim the record
# is missing from the artifact while a truncated copy of it is not.
cat >"${stub_bin}/tee" <<'STUB'
#!/bin/sh
head -c 40 >"$1"
exit 1
STUB
chmod +x "${stub_bin}/tee"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
[ ! -e "$identity_file" ] || {
echo "FAIL: a partial record was left where the fold-in will pick it up"
cat "$identity_file"
false
}
assert_file_contains 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "a partial record that could not be removed is named as possibly remaining" {
tmp=$(mktemp -d)
stage "$tmp"
cat >"${stub_bin}/tee" <<'STUB'
#!/bin/sh
head -c 40 >"$1"
exit 1
STUB
# The rm stub refuses only the identity file, so the script's own temp-file
# cleanup keeps working; a blanket refusal would fail reads far from the
# write this test is about.
cat >"${stub_bin}/rm" <<'STUB'
#!/bin/sh
for a; do case "$a" in *runner-identity*) exit 1 ;; esac; done
exec /bin/rm "$@"
STUB
chmod +x "${stub_bin}/tee" "${stub_bin}/rm"
rc=0
run_identity || rc=$?
[ "$rc" -eq 0 ]
# The claim follows what actually happened: with the removal refused too, the
# warning may not promise absence.
assert_file_contains 'may remain in cozyreport.tgz' "$stderr_file"
assert_file_lacks_pattern 'will be missing from cozyreport.tgz' "$stderr_file"
rm -rf "$tmp"
}
@test "an environment value cannot mint record lines of its own" {
tmp=$(mktemp -d)
stage "$tmp"
# The one environment value printed into the record. Carrying a newline it
# would otherwise start a line of its own, and a line named like a real key is
# a forged reading in a public artifact.
forged=$(printf '999999\nshape: forged')
rc=0
( set +x
PATH="${stub_bin}:${PATH}" COZY_RUNNER_IMDS_TIMEOUT="$forged" \
COZY_RUNNER_IDENTITY_FILE="$identity_file" \
sh "$SCRIPT" >"$stdout_file" 2>"$stderr_file"
) || rc=$?
[ "$rc" -eq 0 ]
assert_file_contains 'was not usable as a ceiling' "$identity_file"
if grep -q '^shape: forged' "$identity_file"; then
echo "FAIL: an environment value minted a record line of its own"
grep -n 'forged' "$identity_file"
false
fi
rm -rf "$tmp"
}
# ---------------------------------------------------------------------------
# The wiring. Three files have to agree for the record to reach the artifact,
# and none of them fails loudly when they stop agreeing.
# ---------------------------------------------------------------------------
@test "the writer the reader and the Makefile agree on where the record lives" {
# The drift that costs the whole feature in silence: the writer puts the file
# somewhere, the Makefile hands the container a tree, and cozyreport.sh looks
# for it from inside. Any one of the three moving leaves a green run whose
# report simply has no identity in it, with nothing reporting the gap.
default='_out/runner-identity.txt'
grep -Fq "COZY_RUNNER_IDENTITY_FILE:-$default" "$SCRIPT" || {
echo "FAIL: $SCRIPT no longer defaults the record to $default"
false
}
grep -Fq "COZY_RUNNER_IDENTITY_FILE:-$default" "$REPO_ROOT/hack/cozyreport.sh" || {
echo "FAIL: hack/cozyreport.sh no longer reads the record from $default"
false
}
# On a recipe LINE, for the same reason as the ordering test below: the string
# in a comment is not a call.
awk '/^\t/ && $0 !~ /^\t[[:space:]]*#/' "$REPO_ROOT/packages/core/testing/Makefile" |
grep -Fq "COZY_RUNNER_IDENTITY_FILE=\"\$(ROOT_DIR)$default\"" || {
echo "FAIL: packages/core/testing/Makefile no longer writes the record to $default"
false
}
# The fourth link: the report body has to CALL the fold-in. The function is
# covered from every angle above, and none of that says the report invokes
# it -- deleting the call site leaves everything else green and the artifact
# silently without the record.
grep -Eq '^cozyreport_collect_runner_identity "' "$REPO_ROOT/hack/cozyreport.sh" || {
echo "FAIL: hack/cozyreport.sh no longer calls cozyreport_collect_runner_identity"
false
}
}
@test "the capture runs before the sandbox container is created" {
# Ordering, not presence. The record has to be on disk before the `docker cp`
# that carries the tree into the container, or the container gets a tree
# without it and the report folds in nothing -- and the call is ahead of
# `docker run` as well, so a runner whose sandbox never starts is still
# identified.
# Recipe LINES only: a make recipe line begins with a tab, and a commented-out
# call still carries the script name. Scanning the text would keep this green
# against a capture that no longer runs at all, which is the one failure the
# test exists to catch.
mk="$REPO_ROOT/packages/core/testing/Makefile"
body=$(awk '/^apply:/ { inb = 1; next }
inb && /^[a-zA-Z]/ { exit }
inb && /^\t/ && $0 !~ /^\t[[:space:]]*#/ { print }' "$mk")
order=$(printf '%s\n' "$body" |
awk '/e2e-runner-identity\.sh/ { print "identity" }
/docker run/ { print "run" }
/docker cp/ { print "cp" }' |
tr '\n' ' ')
case "$order" in
'identity run cp '*) : ;;
*)
echo "FAIL: the apply recipe runs [$order]; the identity capture must come first"
printf '%s\n' "$body"
false
;;
esac
}
@test "the report folds a record it finds into the sandbox host directory" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
printf '=== e2e runner identity ===\nshape: VM.Standard.E5.Flex\n' >"$tmp/record.txt"
COZY_RUNNER_IDENTITY_FILE="$tmp/record.txt" \
cozyreport_collect_runner_identity "$tmp/report"
assert_file_contains 'shape: VM.Standard.E5.Flex' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report treats a record that arrived empty as no record at all" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
# A write cut off by a full disk leaves the file there and empty. Copied
# through, that is a zero-byte runner-identity.txt in the report holding neither
# the record nor a reason, which is exactly the outcome the one-filename promise
# is supposed to rule out.
: >"$tmp/record.txt"
COZY_RUNNER_IDENTITY_FILE="$tmp/record.txt" \
cozyreport_collect_runner_identity "$tmp/report"
assert_file_contains 'absent, or present and empty' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report names every cause of a missing record rather than picking one" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
COZY_RUNNER_IDENTITY_FILE="$tmp/gone.txt" \
cozyreport_collect_runner_identity "$tmp/report"
# The capture exits 0 whatever happens, so an absent file does not say which of
# the three it was -- and one of them leaves the record in the job log, which is
# where a reader would then go. Asserting a single cause would be this collector
# naming a mechanism it did not observe.
assert_file_contains 'the capture step did not run' "$tmp/report/runner-identity.txt"
assert_file_contains 'could not write its file' "$tmp/report/runner-identity.txt"
assert_file_contains 'in the log of the step that took it' "$tmp/report/runner-identity.txt"
rm -rf "$tmp"
}
@test "the report writes the same filename when there is no record to fold in" {
tmp=$(mktemp -d)
mkdir -p "$tmp/report"
COZY_RUNNER_IDENTITY_FILE="$tmp/does-not-exist.txt" \
cozyreport_collect_runner_identity "$tmp/report"
# One filename, present on every run. A marker file beside an absent one would
# carry the same words and be found by nobody, because the absence is where a
# reader stops.
assert_file_contains 'no runner identity record' "$tmp/report/runner-identity.txt"
# And it says what the absence does not mean. An unidentified runner and an
# unremarkable one leave the same empty space, and only one of them is a
# statement about the run.
assert_file_contains 'NOT a statement that the runner was an ordinary one' \
"$tmp/report/runner-identity.txt"
# In the shared marker form, so one anchored grep over the unpacked tarball
# finds this alongside every other note the collector leaves.
grep -q '^# \[cozyreport\]' "$tmp/report/runner-identity.txt" || {
echo "FAIL: the explanation does not carry the shared marker form"
cat "$tmp/report/runner-identity.txt"
false
}
rm -rf "$tmp"
}