-
Notifications
You must be signed in to change notification settings - Fork 33
ci: make a CI failure legible - crash frames, budget overruns, shared memory, and a coverage job that fits its cap #648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bburda
wants to merge
8
commits into
main
Choose a base branch
from
ci/harden-visibility-and-shm
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3d38df6
ci: name a budget overrun as one, and stop stranding shared memory
bburda d9fe062
ci: shard the coverage job and merge the tracefiles
bburda f566f00
ci: install curl in the coverage merge job
bburda 7bd176a
test(integration): report the stack when a demo node dies on a fatal …
bburda 7e5d5fe
fix: correct the overrun guard, the coverage merge and the crash handler
bburda 5683400
fix: repair the missing-result detector and make the crash tests asse…
bburda 22514ba
fix: two regressions this branch put into CI, both mine
bburda 78da681
fix: keep the catch-all shard green and report a crash from every dem…
bburda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| name: Record the test margin and the DDS environment | ||
| description: >- | ||
| Says plainly whether a test step ran out of time, and records what /dev/shm and | ||
| the installed middleware looked like afterwards. Both are invisible once a run | ||
| has finished, and a step killed by its cap is reported by the runner as an | ||
| ordinary failure naming whichever test was in flight. | ||
|
|
||
| inputs: | ||
| start: | ||
| description: Epoch seconds recorded immediately before the test step. | ||
| required: true | ||
| cap-minutes: | ||
| description: >- | ||
| The cap the guard measures against. Pass the step's timeout-minutes where | ||
| the step has one; where only the job is capped, pass the job's - the | ||
| overrun branch cannot fire there, because a job-level kill stops every | ||
| remaining step, but the margin warning still arrives in time to act on. | ||
| required: true | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - shell: bash | ||
| run: | | ||
| # The start is missing only when an earlier step failed before it ran. | ||
| # Say so and carry on to the census, rather than dying on a usage error | ||
| # and taking the environment evidence down with it. | ||
| if [ -n "${{ inputs.start }}" ]; then | ||
| ./scripts/ci_test_completeness.sh "${{ inputs.start }}" "${{ inputs.cap-minutes }}" || guard_rc=$? | ||
| else | ||
| echo "::notice::no test step start was recorded - the run failed before the tests" | ||
| fi | ||
| # A DDS participant killed rather than shut down leaves its segments | ||
| # behind for the rest of the job, and the installed middleware decides | ||
| # which transport those segments belong to. | ||
| df -h /dev/shm | ||
| echo "shm segments: $(ls /dev/shm | wc -l)" | ||
| # No `head` in this pipeline: it closes the pipe early, SIGPIPEs `ls`, and | ||
| # the step's `pipefail` would then kill the census exactly when there are | ||
| # enough segments to be worth listing. | ||
| ls -la /dev/shm | tail -n +1 | ||
| dpkg -l | grep -E 'rmw-fastrtps|fastdds|cyclonedds' || true | ||
| exit "${guard_rc:-0}" |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| #!/usr/bin/env bash | ||
| # Say plainly when a test step ran out of time, instead of letting it read as a | ||
| # test failure. | ||
| # | ||
| # A step killed by its timeout is marked FAILED by the runner, not cancelled, and | ||
| # the in-flight test leaves no result file. `colcon test-result` then reports that | ||
| # as one erroring test named `<test>.xunit.missing_result`: a test that never | ||
| # failed, among thousands that never ran. On its own that is indistinguishable | ||
| # from a single flake, which is how a budget overrun gets filed as a test problem | ||
| # while every package queued behind it disappears without trace. | ||
| # | ||
| # The step's own log is not readable from inside the job, so the overrun is | ||
| # detected by the clock instead: the caller records a start timestamp before the | ||
| # test step and passes the step's cap here. Elapsed at or above the cap means the | ||
| # runner killed it. | ||
| # | ||
| # What this deliberately does NOT do is count registered tests against result | ||
| # files. Some tests legitimately write none - `test_dds_domain_allocation` is | ||
| # registered by ros2_medkit_cmake in every package and produces no xunit - so | ||
| # that comparison reports missing results on a run where everything passed. | ||
| # | ||
| # Usage: ci_test_completeness.sh <start-epoch-seconds> <cap-minutes> | ||
| set -euo pipefail | ||
|
|
||
| START_EPOCH="${1:-}" | ||
| CAP_MINUTES="${2:-}" | ||
|
|
||
| usage() { | ||
| echo "usage: ci_test_completeness.sh <start-epoch-seconds> <cap-minutes>" >&2 | ||
| echo " both are positive integers; the start is seconds since the epoch, as" >&2 | ||
| echo " written by date +%s before the step being measured" >&2 | ||
| } | ||
|
|
||
| # Validated rather than trusted: a start value from a different clock, or a cap | ||
| # of zero, otherwise turns this from a diagnostic into a source of false | ||
| # verdicts - a monotonic start reads as an enormous overrun, and a zero cap | ||
| # divides by zero while still printing an overrun. | ||
| if [[ ! "${START_EPOCH}" =~ ^[0-9]+$ || ! "${CAP_MINUTES}" =~ ^[0-9]+$ ]]; then | ||
| usage | ||
| exit 2 | ||
| fi | ||
| if [[ "${START_EPOCH}" -eq 0 || "${CAP_MINUTES}" -eq 0 ]]; then | ||
| usage | ||
| exit 2 | ||
| fi | ||
|
|
||
| now=$(date +%s) | ||
| if [[ "${START_EPOCH}" -gt "${now}" ]]; then | ||
| echo "::error::the recorded start (${START_EPOCH}) is in the future - the clock moved, so no margin can be computed" | ||
| exit 2 | ||
| fi | ||
|
|
||
| elapsed=$(( now - START_EPOCH )) | ||
| cap_seconds=$(( CAP_MINUTES * 60 )) | ||
|
|
||
| empty=0 | ||
| found=0 | ||
| while IFS= read -r xml; do | ||
| found=$((found + 1)) | ||
| if [[ ! -s "${xml}" ]]; then | ||
| echo "::error::empty result file: ${xml}" | ||
| empty=$((empty + 1)) | ||
| fi | ||
| done < <(find build -path '*/test_results/*' -name '*.xml' 2>/dev/null) | ||
|
|
||
| # `missing_result` is not a filename. ament writes it as the name attribute of a | ||
| # <testcase> INSIDE the ordinary xunit file, before running the command, so the | ||
| # file exists and is not empty. Measured: across five build trees, zero files are | ||
| # named that, while one tree carried two genuine missing results in its XML. | ||
| # `|| true` inside the braces, not after the pipe: grep exits 1 when it matches | ||
| # nothing, which is the healthy case, and under `set -e` with `pipefail` that | ||
| # killed this script before it printed anything at all - so a clean run reported | ||
| # nothing and failed the step. | ||
| missing=$( { grep -l 'missing_result' -r build --include='*.xml' 2>/dev/null || true; } | wc -l) | ||
|
|
||
| printf 'test step: %ds elapsed of a %ds cap (%d%%); results: %d written, %d empty, %d missing_result\n' \ | ||
| "${elapsed}" "${cap_seconds}" "$(( elapsed * 100 / cap_seconds ))" "${found}" "${empty}" "${missing}" | ||
|
|
||
| status=0 | ||
|
|
||
| if [[ "${found}" -eq 0 ]]; then | ||
| echo "::error::no test result files at all - the run never reached the tests" | ||
| status=1 | ||
| fi | ||
|
|
||
| if [[ "${empty}" -gt 0 ]]; then | ||
| status=1 | ||
| fi | ||
|
|
||
| # The runner kills the step AT its cap and this check runs afterwards, so an | ||
| # overrun is simply elapsed at or past the cap. An earlier version subtracted a | ||
| # fixed minute, which made every cap below five minutes report an overrun on a | ||
| # run that had barely started. | ||
| if [[ "${elapsed}" -ge "${cap_seconds}" ]]; then | ||
| echo "::error::the test step reached its ${CAP_MINUTES} minute cap. Any missing_result above belongs to the test that was interrupted, and the packages queued behind it did not run at all. This is a budget overrun, not a test verdict." | ||
| status=1 | ||
| elif [[ "${elapsed}" -ge $(( cap_seconds * 8 / 10 )) ]]; then | ||
| echo "::warning::the test step used $(( elapsed * 100 / cap_seconds ))% of its ${CAP_MINUTES} minute cap - it will start being killed before anyone decides to raise it" | ||
| fi | ||
|
|
||
| if [[ "${missing}" -gt 0 && "${elapsed}" -lt "${cap_seconds}" ]]; then | ||
| echo "::warning::${missing} test(s) produced no result file without the step running out of time - investigate the test, not the budget" | ||
| fi | ||
|
|
||
| exit "${status}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| #!/usr/bin/env bash | ||
| # Reclaim shared-memory segments left behind by DDS participants that were killed | ||
| # rather than shut down. | ||
| # | ||
| # A participant that exits cleanly frees its segments; one that dies on SIGKILL | ||
| # does not, and nothing reclaims them afterwards. Measured on Fast DDS: about | ||
| # 0.65 MB per participant, and 125 killed participants filled 63 MB of a 64 MB | ||
| # /dev/shm. Test runs that kill nodes on purpose - the aggregation suites do - | ||
| # accumulate this across every run on a developer machine, and the failure it | ||
| # eventually produces looks like anything but a full tmpfs. | ||
| # | ||
| # The reclaiming is done by `fastdds shm clean`, which is the vendor's own tool | ||
| # and ships with every distro we support. It decides a segment is stale by taking | ||
| # an exclusive non-blocking flock on the segment's lock file; the kernel drops | ||
| # that lock when a process dies, SIGKILL included, so a segment is removed only | ||
| # when nothing holds it. Each distro's copy matches its own file naming | ||
| # (fastrtps_* on 2.x, fastdds_* on 3.x), so the tool from the sourced | ||
| # distribution is the one to call - there is nothing for us to match on. | ||
| # | ||
| # Refuses to run while ROS processes are alive: the flock test protects live | ||
| # segments, but a sweep in the middle of a test run is still a lie about what the | ||
| # run measured. | ||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| echo "usage: sweep_shm.sh [--force]" >&2 | ||
| echo " --force sweep even if ROS processes are running" >&2 | ||
| } | ||
|
|
||
| FORCE=0 | ||
| case "${1:-}" in | ||
| --force) FORCE=1 ;; | ||
| "") ;; | ||
| *) usage; exit 2 ;; | ||
| esac | ||
|
|
||
| shm_bytes() { | ||
| # One value or nothing. An earlier form piped du into cut with `|| echo 0`, | ||
| # which under pipefail could emit cut's partial output AND the fallback, so the | ||
| # arithmetic below got a two-line operand and died with a bad math expression. | ||
| local out | ||
| if ! out=$(du -sb /dev/shm 2>/dev/null); then | ||
| echo unknown | ||
| return | ||
| fi | ||
| printf '%s' "${out}" | head -n 1 | cut -f1 | ||
| } | ||
|
|
||
| if [[ "${FORCE}" -eq 0 ]]; then | ||
| # Match on the command line rather than the process name: colcon runs as | ||
| # python3, and a pattern that also matches this script would match itself. | ||
| # demo_ covers the launch fixtures the documented demo workflow starts, which | ||
| # an earlier pattern missed entirely - a developer running only demo nodes got | ||
| # a sweep in the middle of live work. | ||
| # | ||
| # Our own process and our caller are excluded, because the caller's command | ||
| # line is often a match by itself: `./scripts/test.sh test_demo_lifecycle` | ||
| # contains demo_, so every single-test preset refused to sweep and the only | ||
| # symptom was a puzzling message. | ||
| if pgrep -f '[g]ateway_node|[f]ault_manager_node|[d]emo_|[c]test|[l]aunch_test|[c]olcon test' 2>/dev/null | | ||
| grep -qvE "^($$|${PPID})$"; then | ||
| echo "sweep_shm: ROS processes are running - refusing to sweep (use --force if you mean it)" >&2 | ||
| exit 1 | ||
| fi | ||
| fi | ||
|
|
||
| before="$(shm_bytes)" | ||
|
|
||
| if command -v fastdds >/dev/null 2>&1; then | ||
| fastdds shm clean || echo "sweep_shm: 'fastdds shm clean' reported a problem, continuing" >&2 | ||
| else | ||
| echo "sweep_shm: no 'fastdds' on PATH - source a ROS distribution first" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| after="$(shm_bytes)" | ||
|
|
||
| if [[ "${before}" == unknown || "${after}" == unknown ]]; then | ||
| echo "sweep_shm: cleaned, but /dev/shm could not be measured (du declined)" >&2 | ||
| exit 0 | ||
| fi | ||
|
|
||
| printf 'sweep_shm: /dev/shm %s -> %s bytes (reclaimed %s)\n' "${before}" "${after}" "$((before - after))" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.