mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
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>
1453 lines
59 KiB
Bash
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"
|
|
}
|