mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-03 02:27:01 +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>
955 lines
40 KiB
Bash
955 lines
40 KiB
Bash
#!/usr/bin/env bats
|
|
# Regression coverage for the tenant-worker per-thread CPU capture in
|
|
# hack/e2e-chainsaw/_lib/run-kubernetes.sh. cozytest.sh ends an @test block at
|
|
# the first bare closing brace, so command mocks stay at top level.
|
|
#
|
|
# What this capture answers, and why the collector beside it cannot: the
|
|
# throttling capture reads the compute container's cgroup, and that cgroup holds
|
|
# every thread QEMU runs -- the guest's vCPUs, the emulator thread, and the IO
|
|
# helpers. A container burning most of its quota while the guest reports no
|
|
# progress is therefore two different findings wearing the same number, and the
|
|
# container-level counter cannot separate them. Only the per-thread split can,
|
|
# and it exists nowhere else in this tree. These tests pin the split and the
|
|
# separation between an unread container and a quiet one, not the reading.
|
|
|
|
load test_helper
|
|
|
|
kubectl_calls=/dev/null
|
|
kubectl_pod_names=
|
|
kubectl_list_rc=0
|
|
kubectl_list_stderr=
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_stderr=
|
|
# Two stat lines from one process, in the shape /proc writes them. The second
|
|
# is the one every field-counting reader gets wrong: a KVM vCPU thread is named
|
|
# "CPU 0/KVM", the space sits inside the parentheses, and every field after it
|
|
# shifts by one.
|
|
kubectl_exec_output='clock ticks per second: 100
|
|
process 41 comm qemu-kvm
|
|
41 (qemu-kvm) S 1 41 41 0 -1 4194560 8100 0 0 0 4210 1180 0 0 20 0 9 0 5512
|
|
57 (CPU 0/KVM) R 1 41 41 0 -1 4194368 120 0 0 0 39120 2210 0 0 20 0 9 0 5530'
|
|
timeout_calls=/dev/null
|
|
timeout_fail_pod=
|
|
|
|
kubectl() {
|
|
printf '%s\n' "$*" >>"${kubectl_calls}"
|
|
|
|
if [ "${3:-}" = get ] && [ "${4:-}" = pods ]; then
|
|
[ -z "${kubectl_list_stderr}" ] || printf '%s\n' "${kubectl_list_stderr}" >&2
|
|
[ "${kubectl_list_rc}" -eq 0 ] || return "${kubectl_list_rc}"
|
|
[ -z "${kubectl_pod_names}" ] || printf '%s\n' ${kubectl_pod_names}
|
|
return 0
|
|
fi
|
|
|
|
if [ "${3:-}" = exec ]; then
|
|
[ -z "${kubectl_exec_stderr}" ] || printf '%s\n' "${kubectl_exec_stderr}" >&2
|
|
# Output before the status, so the branch that labels a partial capture is
|
|
# reachable: a read cut off part way through has already written what it
|
|
# managed to read, and a stub that returns first can only ever stage
|
|
# empty-plus-failure.
|
|
[ -z "${kubectl_exec_output}" ] || printf '%s\n' "${kubectl_exec_output}"
|
|
return "${kubectl_exec_rc}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
timeout() {
|
|
local command_rc=0
|
|
printf '%s\n' "$*" >>"${timeout_calls}"
|
|
# Return 97 rather than running the command when the wrapper is not the
|
|
# bounded form: a read that lost its ceiling must not pass as a read.
|
|
[ "${1:-}" = -k ] || return 97
|
|
shift 3
|
|
"$@" || command_rc=$?
|
|
if [ -n "${timeout_fail_pod}" ]; then
|
|
case " $* " in
|
|
*"${timeout_fail_pod}"*) return 124 ;;
|
|
esac
|
|
fi
|
|
return "${command_rc}"
|
|
}
|
|
|
|
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
|
|
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 label".
|
|
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 into an `if`, both land in the passing
|
|
# branch and the assertion "this pattern is absent" is satisfied by the
|
|
# matcher giving up. Every claim in this file about something NOT appearing
|
|
# rests on the three-way split below.
|
|
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
|
|
return 1
|
|
;;
|
|
1) return 0 ;;
|
|
*)
|
|
printf 'awk could not evaluate pattern %s against %s\n' "${pattern}" "${file}" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The function under test redirects a command's stderr into a report file and
|
|
# decides which artifact to write from it. cozytest.sh runs under `set -x`, so
|
|
# the tracer's own line for that command lands on the very stderr being
|
|
# redirected, and every assertion about those files would be reading the
|
|
# tracer. Call through this wrapper so the sinks hold what production writes.
|
|
run_capture() {
|
|
local _rc=0
|
|
( set +x; cozy_capture_tenant_worker_thread_cpu 1 ) || _rc=$?
|
|
return "${_rc}"
|
|
}
|
|
|
|
@test "a negative assertion fails when its own matcher cannot evaluate" {
|
|
# The helper below every "must not appear" claim in this file. What rides on
|
|
# it here is the collector's contract stated negatively -- that an unreachable
|
|
# container is not written up as one that answered, that an incomplete read
|
|
# carries no pairing instruction -- so a matcher that fails open turns each of
|
|
# them into a sentence nobody checked.
|
|
tmp=$(mktemp -d)
|
|
printf 'anything\n' >"$tmp/subject"
|
|
|
|
if assert_file_lacks_pattern '*(' "$tmp/subject" 2>/dev/null; then
|
|
echo "FAIL: an unparseable pattern was reported as absent" >&2
|
|
rm -rf "$tmp"
|
|
return 1
|
|
fi
|
|
assert_file_lacks_pattern 'nothinglikethis' "$tmp/subject"
|
|
if assert_file_lacks_pattern 'anything' "$tmp/subject" 2>/dev/null; then
|
|
echo "FAIL: a present pattern was reported as absent" >&2
|
|
rm -rf "$tmp"
|
|
return 1
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the threads are read from inside the compute container of each worker Pod" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
# The container has to be named. virt-launcher runs several, and kubectl's
|
|
# default is the first one in the spec rather than the one running QEMU, so an
|
|
# unnamed exec reads a stranger's threads or fails on a container with no shell.
|
|
assert_file_contains 'exec virt-launcher-a -c compute' "$kubectl_calls"
|
|
# The projection is a parameter now, and the two callers of that listing pass
|
|
# different ones from twelve hundred lines apart. Swapped, this collector
|
|
# would exec into Pods named after nodes and the cAdvisor one would ask
|
|
# kubelets named after Pods: both dead, neither with an error, and every
|
|
# artifact full of sentences saying the subject was not reached. Nothing else
|
|
# in either suite sees the argument, because the mocks answer any listing
|
|
# regardless of what was asked for.
|
|
assert_file_contains 'jsonpath={range .items[?(@.status.phase=="Running")]}{.metadata.name}' "$kubectl_calls"
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# The vCPU line and the emulator line both, because the whole question is
|
|
# which of them carries the time: a capture that kept only one of the two
|
|
# would answer it by construction.
|
|
assert_file_contains '(CPU 0/KVM)' "$capture"
|
|
assert_file_contains '(qemu-kvm)' "$capture"
|
|
assert_file_lacks_pattern 'incomplete' "$capture"
|
|
assert_file_lacks_pattern 'unknown' "$capture"
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
[ ! -f "$dir/virt-launcher-a.exec-error.log" ]
|
|
[ ! -f "$dir/COLLECTION-FAILED.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "each read is bounded and keeps its exit code" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
# An unbounded exec would hold the whole diagnostics block against the wedged
|
|
# kubelet this collector exists to study, and the phase budget bounds when a
|
|
# read may START, never how long one already running may take.
|
|
# Named by their verbs: both reads carry the same bound, so a bare '-k 5 20'
|
|
# would be satisfied twice by whichever one survived.
|
|
assert_file_contains '-k 5 20 kubectl -n tenant-test get pods -l kubevirt.io=virt-launcher' "$timeout_calls"
|
|
assert_file_contains '-k 5 20 kubectl -n tenant-test exec' "$timeout_calls"
|
|
# The listing is a request and carries the second bound too; the exec is a
|
|
# streamed connection and the tree's other exec captures bound it with the
|
|
# wrapper alone.
|
|
assert_file_contains '--request-timeout=20s' "$kubectl_calls"
|
|
assert_file_contains '[capture exit code: 0]' \
|
|
"$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the sample number decides which reading a file belongs to" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
( set +x; cozy_capture_tenant_worker_thread_cpu 2 )
|
|
|
|
# utime and stime are cumulative since the thread started, so one reading is
|
|
# an average over an uptime and says nothing about which thread burned the
|
|
# window the deadline covered. A second reading written over the first leaves
|
|
# one file and no interval.
|
|
assert_file_contains '(CPU 0/KVM)' \
|
|
"$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-2/virt-launcher-a.txt"
|
|
[ ! -d "$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "each reading records when it was taken" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# The wait between the two passes is not the interval this subject's two
|
|
# readings span: the other two subjects' passes fall between them, and on the
|
|
# run this collector exists for those passes are the slow part. The stamps are
|
|
# what make the real interval readable off the pair.
|
|
assert_file_contains 'read attempted from' "$capture"
|
|
stamp=$(sed -n 's/.*read attempted from \([0-9][0-9]*\) to \([0-9][0-9]*\) epoch seconds.*/\1 \2/p' "$capture")
|
|
if [ -z "$stamp" ]; then
|
|
echo "expected a bare epoch stamp in $capture" >&2
|
|
cat "$capture" >&2
|
|
return 1
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the legend names the field positions and the thread name that shifts them" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# Raw lines with no legend are a trap rather than data. The two numbers a
|
|
# reader wants sit at fixed positions in /proc/<pid>/task/<tid>/stat, and the
|
|
# thread this capture exists to identify is the one whose name breaks the
|
|
# count: awk over whitespace reads the wrong column for exactly the vCPU
|
|
# threads and the right one for every other thread, which is the shape of a
|
|
# wrong answer that looks right.
|
|
assert_file_contains 'utime' "$capture"
|
|
assert_file_contains 'stime' "$capture"
|
|
assert_file_contains 'CPU 0/KVM' "$capture"
|
|
# The escape from the trap, not just a warning about it. A legend that names
|
|
# the hazard without naming the parse leaves the next reader to invent one.
|
|
assert_file_contains 'last )' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the legend names which thread each comm belongs to" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# The whole discriminator is which of four kinds of thread holds the time, so
|
|
# a capture that does not say which name is which kind leaves the reader to
|
|
# guess the answer it was collected to settle.
|
|
assert_file_contains 'emulator' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the capture tells the reader how to pair it with its sibling sample" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
assert_file_contains 'subtract this Pod file from its sibling under the other sample directory' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a read that did not finish is not told to pair with a sibling" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
# Lines arrived and the read still failed, which is a stream cut off part way
|
|
# through the thread list.
|
|
kubectl_exec_rc=124
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
assert_file_contains 'these thread counters are incomplete' "$capture"
|
|
# Telling a reader to subtract two files asserts that both hold a whole
|
|
# reading. A difference taken against a truncated one understates whichever
|
|
# threads the read did not reach, and the threads it did not reach are the
|
|
# ones listed last -- so the understated set is not even random.
|
|
assert_file_lacks_pattern 'subtract this Pod file from its sibling' "$capture"
|
|
# And a legend that explains a column layout the file may not carry in full
|
|
# is the same overclaim one line down.
|
|
assert_file_lacks_pattern 'last \)' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a compute container that was not reached is recorded as unknown, not as an idle guest" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=1
|
|
kubectl_exec_output=
|
|
kubectl_exec_stderr='Error from server: error dialing backend: remote error: tls: internal error'
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
capture="$dir/virt-launcher-a.txt"
|
|
# An empty file here reads as a guest whose threads were all idle, which is
|
|
# the one conclusion this collector must never manufacture: the container
|
|
# dying is a routine outcome on this failure path and it is not a reading.
|
|
assert_file_contains 'was not reached' "$capture"
|
|
assert_file_contains 'exec-error.log' "$capture"
|
|
assert_file_lacks_pattern 'subtract this Pod file from its sibling' "$capture"
|
|
assert_file_contains 'error dialing backend' "$dir/virt-launcher-a.exec-error.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "an exec that died without a word is not reported as a container that answered" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
# timeout kills its child without a word and kubectl dies on SIGTERM the same
|
|
# way, so the dominant failure here arrives non-zero with an empty error log.
|
|
# Keyed on stderr rather than on the status, the artifact would say the
|
|
# opposite of what happened.
|
|
kubectl_exec_rc=137
|
|
kubectl_exec_output=
|
|
kubectl_exec_stderr=
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
capture="$dir/virt-launcher-a.txt"
|
|
assert_file_contains 'was not reached' "$capture"
|
|
assert_file_contains 'without a word on either stream' "$capture"
|
|
# Naming a file that was never written sends the reader looking for evidence
|
|
# that does not exist.
|
|
[ ! -f "$dir/virt-launcher-a.exec-error.log" ]
|
|
assert_file_contains '[exit 137:' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a container that answered and said nothing is not reported as unreachable" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_output=
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# Two different problems that leave the same empty file: an exec that never
|
|
# landed, and a shell that ran and printed nothing. They are fixed by looking
|
|
# in different places, so they must not share a sentence.
|
|
assert_file_contains 'answered and returned nothing' "$capture"
|
|
assert_file_lacks_pattern 'was not reached' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a container whose QEMU is gone carries no legend for columns it does not have" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_output='clock ticks per second: 100
|
|
NO-QEMU-PROCESS: no process whose name starts with qemu- exists in this container, so it had no threads to read'
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# A clean read that found nothing to read is a finding about the failure, and
|
|
# a different one from every arm around it.
|
|
assert_file_contains 'nothing to split' "$capture"
|
|
# A legend explaining a column layout over a file with no columns is the same
|
|
# overclaim as a pairing instruction over a partial read, and it would send
|
|
# the reader looking for stat lines that are not there.
|
|
assert_file_lacks_pattern 'last \)' "$capture"
|
|
assert_file_lacks_pattern 'subtract this Pod file from its sibling' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a warning beside a healthy listing is not filed as a failure" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
kubectl_list_stderr='Warning: v1 Pod is deprecated in this cluster'
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# kubectl writes deprecation and partial-result warnings with a zero exit, so
|
|
# a sink chosen by "something was written" files a healthy read as a failed
|
|
# one and the capture beside it reads as salvage.
|
|
[ ! -f "$dir/COLLECTION-FAILED.txt" ]
|
|
assert_file_contains 'deprecated' "$dir/READ-WARNINGS.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a warning beside a healthy exec is not filed as a failure" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_stderr='Defaulted container "compute" out of: compute, volumecontainerdisk'
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# kubectl writes deprecation and defaulted-container notices on stderr with a
|
|
# zero exit, so the sink is chosen by the status rather than by something
|
|
# having been written. Filed as an error, a healthy exec would look failed and
|
|
# the capture beside it would read as salvage; discarded, a real notice about
|
|
# the read would be gone. The two sibling collectors pin this branch, and the
|
|
# round that suppressed the probe's own stderr rests on it.
|
|
assert_file_contains 'Defaulted container' "$dir/virt-launcher-a.READ-WARNINGS.txt"
|
|
[ ! -f "$dir/virt-launcher-a.exec-error.log" ]
|
|
assert_file_contains '(CPU 0/KVM)' "$dir/virt-launcher-a.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a Pod list that never returned is not reported as no workers" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_list_rc=1
|
|
kubectl_list_stderr='Unable to connect to the server: dial tcp: i/o timeout'
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture || true
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# An apiserver that never answered and a namespace with no worker in it are
|
|
# different findings, and an empty directory is how they become one.
|
|
assert_file_contains 'failed to list' "$dir/COLLECTION-FAILED.txt"
|
|
assert_file_contains 'i/o timeout' "$dir/COLLECTION-FAILED.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a namespace with no worker Pod says so instead of leaving an empty tree" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture || true
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# Named for the filter the query carries. A namespace whose launchers are all
|
|
# Pending is a different finding from one with no launcher at all, and the
|
|
# sentence has to be true of the listing that was actually issued.
|
|
assert_file_contains 'no Running virt-launcher Pod' "$dir/COLLECTION-FAILED.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the walk is capped and names both counts when it stops" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a virt-launcher-b virt-launcher-c virt-launcher-d virt-launcher-e"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# A short listing otherwise reads as a small pool rather than a truncated
|
|
# walk, and the two counts are what separate them. Asserted as the whole
|
|
# sentence, the way the sibling suites assert theirs: the marker's claim is
|
|
# that the two numbers DIFFER, and a pair of bare digit checks is satisfied by
|
|
# a marker reading "stopped after 4 Pods; 4 matched in total".
|
|
assert_file_contains 'capture stopped after 4 Pods; 5 matched in total' \
|
|
"$dir/COLLECTION-TRUNCATED.txt"
|
|
# And the cap's VALUE, not just its wording. The marker prints max_pods rather
|
|
# than what the walk read, so an off-by-one in the boundary test leaves the
|
|
# sentence byte-identical while a Pod goes unread -- and max_pods is the term
|
|
# the node-join budget guard multiplies, so the arithmetic and the marker
|
|
# would misstate the same run together.
|
|
[ -f "$dir/virt-launcher-d.txt" ]
|
|
[ ! -f "$dir/virt-launcher-e.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a pool below the cap leaves no truncation marker" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a virt-launcher-b"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# A marker on every run is a marker nobody reads, and this one is the only
|
|
# thing that says a capture answered for part of the pool.
|
|
[ ! -f "$dir/COLLECTION-TRUNCATED.txt" ]
|
|
[ -f "$dir/virt-launcher-b.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a Pod whose read times out does not stop the next one" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a virt-launcher-b"
|
|
timeout_fail_pod=virt-launcher-a
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
|
|
run_capture
|
|
|
|
dir="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1"
|
|
# The two workers of one cluster are the comparison this capture is for, so a
|
|
# walk that abandons the rest after one wedged container answers half the
|
|
# question while looking like it answered all of it.
|
|
assert_file_contains '[exit 124:' "$dir/virt-launcher-a.txt"
|
|
assert_file_contains '(CPU 0/KVM)' "$dir/virt-launcher-b.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe finds the qemu process and reads every one of its threads" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# A /proc the size of the one inside a virt-launcher: the QEMU process, a
|
|
# neighbour that is not it, and two threads under QEMU.
|
|
mkdir -p "$tmp/proc/41/task/41" "$tmp/proc/41/task/57" "$tmp/proc/9/task/9"
|
|
printf 'qemu-kvm\n' >"$tmp/proc/41/comm"
|
|
printf 'virt-launcher\n' >"$tmp/proc/9/comm"
|
|
printf '41 (qemu-kvm) S 1 41 41 0 -1 4194560 8100 0 0 0 4210 1180\n' >"$tmp/proc/41/task/41/stat"
|
|
printf '57 (CPU 0/KVM) R 1 41 41 0 -1 4194368 120 0 0 0 39120 2210\n' >"$tmp/proc/41/task/57/stat"
|
|
printf '9 (virt-launcher) S 0 9 9 0 -1 4194304 900 0 0 0 12 4\n' >"$tmp/proc/9/task/9/stat"
|
|
|
|
out=$(sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc")
|
|
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
# Both threads of the QEMU process, or the split this capture exists for is
|
|
# taken over a subset chosen by the probe rather than by the guest.
|
|
assert_file_contains '(qemu-kvm)' "$tmp/out"
|
|
assert_file_contains '(CPU 0/KVM)' "$tmp/out"
|
|
# And nothing from the neighbour: virt-launcher's own threads are outside the
|
|
# question and would be read as QEMU's under a heading that says QEMU's.
|
|
assert_file_lacks_pattern 'virt-launcher\) S' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a process that vanished mid-walk leaves nothing on the probe's stderr" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/proc/41/task/41" "$tmp/proc/99"
|
|
printf 'qemu-kvm\n' >"$tmp/proc/41/comm"
|
|
printf '41 (qemu-kvm) S 1 41 41 0 -1 0 0 0 0 0 4210 1180\n' >"$tmp/proc/41/task/41/stat"
|
|
# An entry the glob finds and the read cannot open, which is what a thread
|
|
# exiting between the two looks like. A dangling symlink stages it the same
|
|
# way whatever uid the suite runs as, where a chmod would not.
|
|
ln -s /definitely-not-here "$tmp/proc/99/comm"
|
|
|
|
sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc" >"$tmp/out" 2>"$tmp/err"
|
|
|
|
# The probe's stderr is the collector's error artifact. A shell complaint
|
|
# about a process that had already exited lands there, and on a clean read the
|
|
# collector files it as a read warning -- a healthy capture shipping a warning
|
|
# about something that did not affect the reading, which is the misfiling
|
|
# every branch in this collector is written against.
|
|
if [ -s "$tmp/err" ]; then
|
|
echo "expected the probe to say nothing on stderr; it wrote:" >&2
|
|
cat "$tmp/err" >&2
|
|
return 1
|
|
fi
|
|
# And the suppression did not cost the reading it was protecting.
|
|
assert_file_contains '(qemu-kvm)' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe says so when no qemu process exists rather than returning nothing" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/proc/9/task/9"
|
|
printf 'virt-launcher\n' >"$tmp/proc/9/comm"
|
|
printf '9 (virt-launcher) S 0 9 9 0 -1 4194304 900 0 0 0 12 4\n' >"$tmp/proc/9/task/9/stat"
|
|
|
|
out=$(sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc")
|
|
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
# A container whose QEMU has already exited is a finding about the failure,
|
|
# and silence from the probe would reach the artifact as a container that
|
|
# answered and said nothing -- a different problem, in a different place.
|
|
assert_file_contains 'NO-QEMU-PROCESS' "$tmp/out"
|
|
# And it reports having read the neighbour, because that is what separates
|
|
# this from a probe that could read nothing at all.
|
|
assert_file_lacks_pattern 'NO-PROC-READ' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe says so when QEMU is named and its threads are already gone" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# The race the probe runs into by construction: the comm read succeeds and
|
|
# the task directory is gone by the time it is opened. Staged as a process
|
|
# with no task tree at all, which is what the walk sees either way.
|
|
mkdir -p "$tmp/proc/41"
|
|
printf 'qemu-kvm\n' >"$tmp/proc/41/comm"
|
|
|
|
out=$(sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc")
|
|
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
# The heading alone makes the capture non-empty, so nothing downstream can
|
|
# tell this from a reading unless the probe says it. Reported as its own
|
|
# outcome rather than as "no QEMU": QEMU was there, which is a different
|
|
# sentence about the guest.
|
|
assert_file_contains 'NO-THREAD-LINES' "$tmp/out"
|
|
assert_file_lacks_pattern 'NO-QEMU-PROCESS' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a capture carrying only a process heading is not given a column legend" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_output='clock ticks per second: 100
|
|
process 41 comm qemu-kvm
|
|
NO-THREAD-LINES: a qemu-* process was named and not one of its task stat files could be read, so it exited between the two reads'
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
assert_file_contains 'threads were gone before they could be read' "$capture"
|
|
# The whole hazard: a heading is enough to make the file non-empty, so every
|
|
# "was it read" arm above passes and the legend would describe columns this
|
|
# capture does not have.
|
|
assert_file_lacks_pattern 'last \)' "$capture"
|
|
assert_file_lacks_pattern 'subtract this Pod file from its sibling' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe reports a proc it could not read apart from a container without QEMU" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# A proc mount with nothing readable under it. The two outcomes are fixed in
|
|
# different places, and only one of them is about the guest: reported as the
|
|
# other, the artifact would state that QEMU had exited on the strength of a
|
|
# read that never happened.
|
|
mkdir -p "$tmp/proc"
|
|
|
|
out=$(sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc")
|
|
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
assert_file_contains 'NO-PROC-READ' "$tmp/out"
|
|
assert_file_lacks_pattern 'NO-QEMU-PROCESS' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a proc the probe could not read is not written up as a guest without QEMU" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=thread-smoke
|
|
kubectl_exec_rc=0
|
|
kubectl_exec_output='clock ticks per second: 100
|
|
NO-PROC-READ: not one process could be read under the proc mount, so this says nothing about what the container was running'
|
|
|
|
run_capture
|
|
|
|
capture="$COZY_REPORT_DIR/snapshots/thread-smoke/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
# The collector has to carry the distinction the probe drew, or drawing it
|
|
# bought nothing: a statement about the probe filed as a statement about the
|
|
# guest is the conflation this whole collector is built against.
|
|
assert_file_contains 'the thread split is unknown' "$capture"
|
|
assert_file_lacks_pattern 'nothing to split' "$capture"
|
|
assert_file_lacks_pattern 'last \)' "$capture"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe reports the tick rate its two numbers are counted in" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/proc/9/task/9"
|
|
printf 'virt-launcher\n' >"$tmp/proc/9/comm"
|
|
|
|
out=$(sh -c "$(_cozy_thread_cpu_probe)" probe "$tmp/proc")
|
|
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
# utime and stime are clock ticks, and the figure this capture is read
|
|
# against is cores. Without the rate on the same page the reader supplies one
|
|
# from memory, which is where a factor of ten enters a measurement.
|
|
assert_file_contains 'clock ticks per second' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "with timeout off PATH the exec still runs instead of exiting 127" {
|
|
# The fallback arm is what the phase-start warning promises for this
|
|
# collector: on a runner without the binary it runs UNBOUNDED rather than
|
|
# exiting 127 and leaving an empty directory. A promise the code does not keep
|
|
# is what this collector's every other branch is against, so the arm is
|
|
# entered here rather than inspected.
|
|
#
|
|
# A subprocess with a stripped PATH, because this file mocks `timeout` as a
|
|
# shell function and `command -v` finds functions -- the condition cannot be
|
|
# reached from inside a test body.
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/bin"
|
|
# Everything the collector shells out to, minus `timeout` itself. Missing one
|
|
# would fail this test for the wrong reason and read as the arm being broken.
|
|
for c in mkdir sort grep mv rm wc tr cat date; do
|
|
for d in /bin /usr/bin /usr/local/bin /opt/homebrew/bin; do
|
|
if [ -x "$d/$c" ]; then
|
|
ln -sf "$d/$c" "$tmp/bin/$c"
|
|
break
|
|
fi
|
|
done
|
|
done
|
|
for c in mkdir sort grep mv rm wc tr cat date; do
|
|
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
|
|
# Only the staged directory is checked. `command -v timeout` is always true
|
|
# here -- this file defines it as a shell function, which is the very fact the
|
|
# subprocess exists to escape.
|
|
if [ -e "$tmp/bin/timeout" ]; then
|
|
echo "FAIL: timeout leaked into the stripped PATH; this test would prove nothing" >&2
|
|
return 1
|
|
fi
|
|
|
|
out=$(bash -c '
|
|
set -eu
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
kubectl() {
|
|
# To a file, not to stderr. The collector redirects the stderr of every
|
|
# read into its own artifact, so a stub that announces itself there is
|
|
# captured by the code under test and never reaches this output.
|
|
printf "KUBECTL %s\n" "$*" >>"$CALLS"
|
|
if [ "${3:-}" = get ] && [ "${4:-}" = pods ]; then printf "virt-launcher-a\n"; return 0; fi
|
|
if [ "${3:-}" = exec ]; then printf "41 (qemu-kvm) S 1 41 41 0 -1 0 0 0 0 0 4210 1180\n"; return 0; fi
|
|
return 0
|
|
}
|
|
CALLS='"$tmp"'/calls
|
|
COZY_REPORT_DIR='"$tmp"'/report
|
|
COZY_SNAPSHOT_NAME=fallback
|
|
PATH='"$tmp"'/bin
|
|
cozy_capture_tenant_worker_thread_cpu 1
|
|
' 2>&1) || true
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
|
|
# The read ran at all -- bounded into exit 127 would leave nothing here.
|
|
assert_file_contains 'KUBECTL -n tenant-test exec virt-launcher-a -c compute' "$tmp/calls"
|
|
# And produced thread lines rather than an "unknown" label.
|
|
assert_file_contains '(qemu-kvm)' \
|
|
"$tmp/report/snapshots/fallback/tenant-thread-cpu/sample-1/virt-launcher-a.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the collector's own caps stay what its comments claim" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# The Pod listing lives in the body this collector shares with the cAdvisor
|
|
# captures, so the subject here is the code the collector runs rather than the
|
|
# function that carries its name; reading only the named function would leave
|
|
# the listing assertions vacuously true against a body it does not contain.
|
|
fn=$(awk '/^cozy_capture_tenant_worker_thread_cpu\(\)/,/^}/' "$lib"
|
|
awk '/^_cozy_virt_launcher_listing\(\)/,/^}/' "$lib")
|
|
if [ -z "$fn" ]; then
|
|
echo "expected to find the collector in $lib" >&2
|
|
return 1
|
|
fi
|
|
case "$fn" in
|
|
*'max_pods=4'*) ;;
|
|
*) echo "expected the collector to cap the walk at the 4 Pods the comments state" >&2; return 1 ;;
|
|
esac
|
|
# The knob, not a literal that equals it today. A hardcoded 20 keeps its value
|
|
# when the block's budget is lowered, so the one read that ignores the knob is
|
|
# the one that outlives it.
|
|
case "$fn" in
|
|
*'timeout -k "${COZY_DIAG_READ_GRACE}" "${COZY_DIAG_READ_TIMEOUT}"'*) ;;
|
|
*) echo "expected each read to take the block's read-budget knob" >&2; return 1 ;;
|
|
esac
|
|
case "$fn" in
|
|
*'timeout -k 5 20'*) echo "expected no hardcoded bound to survive beside the knob" >&2; return 1 ;;
|
|
esac
|
|
# The listing is an apiserver request and carries the HTTP bound as well as
|
|
# the wall-clock one; without it a client retrying against a wedged apiserver
|
|
# is bounded only by the wrapper.
|
|
reads=$(printf '%s\n' "$fn" | grep -c 'kubectl -n tenant-test get pods' || true)
|
|
bounded=$(printf '%s\n' "$fn" | grep -A3 'kubectl -n tenant-test get pods' | grep -c -- '--request-timeout=' || true)
|
|
if [ "$reads" -eq 0 ] || [ "$reads" -ne "$bounded" ]; then
|
|
echo "every Pod listing must carry --request-timeout; $bounded of $reads do" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the capture is declined by the phase budget rather than running unconditionally" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
gate=$(grep -n "^ *if cozy_diag_phase_has_time '(d) tenant worker CPU counters, sandbox node CPU time and worker per-thread CPU time'; then$" \
|
|
"$lib" | head -n 1 | cut -d: -f1)
|
|
call=$(grep -n '^ *cozy_capture_tenant_worker_thread_cpu "${_sample}" || true$' "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$gate" ] || [ -z "$call" ]; then
|
|
echo "expected the capture to sit behind a cozy_diag_phase_has_time gate" >&2
|
|
return 1
|
|
fi
|
|
# THIS gate, not just some gate above the call. A line comparison alone is
|
|
# satisfied by any earlier gate in the block, which would leave the capture
|
|
# guarded by a decision taken about something else.
|
|
next=$(awk -v g="$gate" 'NR > g && /cozy_diag_phase_has_time/ { print NR; exit }' "$lib")
|
|
if [ -z "$next" ]; then
|
|
echo "expected another phase gate after line $gate, so this test could bound the section" >&2
|
|
return 1
|
|
fi
|
|
if [ "$call" -le "$gate" ] || [ "$call" -ge "$next" ]; then
|
|
echo "the capture (line $call) must sit between its gate ($gate) and the next one ($next)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the capture shares the pass the other two subjects already wait for" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# A second wait would be paid on every failing run for a second rate over the
|
|
# same window, and the window is already the scarce term: the phase declines
|
|
# whatever has not started, so an added pause is taken out of the collectors
|
|
# below it rather than out of slack.
|
|
waits=$(grep -c '^ *sleep "\${COZY_DIAG_RATE_INTERVAL}"$' "$lib" || true)
|
|
if [ "$waits" -ne 1 ]; then
|
|
echo "expected exactly one measurement wait in the block, found $waits" >&2
|
|
return 1
|
|
fi
|
|
loop=$(grep -n '^ *for _sample in 1 2; do$' "$lib" | head -n 1 | cut -d: -f1)
|
|
done_line=$(awk -v l="$loop" 'NR > l && /^ *done$/ { print NR; exit }' "$lib")
|
|
call=$(grep -n '^ *cozy_capture_tenant_worker_thread_cpu "${_sample}" || true$' "$lib" | head -n 1 | cut -d: -f1)
|
|
for v in loop done_line call; do
|
|
eval "n=\$$v"
|
|
if [ -z "$n" ]; then
|
|
echo "expected to locate $v in $lib" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
if [ "$call" -le "$loop" ] || [ "$call" -ge "$done_line" ]; then
|
|
echo "the capture (line $call) must run inside the existing sampling loop ($loop..$done_line)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the capture runs behind the console it could otherwise starve" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
call=$(grep -n '^ *cozy_capture_tenant_worker_thread_cpu "${_sample}" || true$' "$lib" | head -n 1 | cut -d: -f1)
|
|
console=$(grep -n "cozy_capture_tenant_serial_console 'node-join failed" "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$call" ] || [ -z "$console" ]; then
|
|
echo "expected the failure path to call both the console and this capture" >&2
|
|
return 1
|
|
fi
|
|
# Admission gates only when a collector may START, so a collector read twice
|
|
# and placed ahead of the console can leave the console never started rather
|
|
# than cut short -- and the console is the only surface that survives a worker
|
|
# which never reached apid, the shape this failure usually takes.
|
|
if [ "$call" -le "$console" ]; then
|
|
echo "the thread capture (line $call) must run after the console capture (line $console)" >&2
|
|
return 1
|
|
fi
|
|
}
|