fleet-sensor-baseline 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/.githooks/commit-msg +10 -5
- fleet_sensor_baseline-0.2.3/.githooks/pre-commit +82 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/.github/workflows/checks.yml +48 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/NOTICE +1 -1
- fleet_sensor_baseline-0.2.2/README.md → fleet_sensor_baseline-0.2.3/PKG-INFO +36 -4
- fleet_sensor_baseline-0.2.2/PKG-INFO → fleet_sensor_baseline-0.2.3/README.md +17 -23
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/pyproject.toml +11 -1
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/__init__.py +1 -1
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/cli.py +90 -4
- fleet_sensor_baseline-0.2.3/tests/test_closed_pipe.py +194 -0
- fleet_sensor_baseline-0.2.3/tests/test_commit_msg_check.py +145 -0
- fleet_sensor_baseline-0.2.3/tests/test_hygiene_check.py +239 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_seam.py +31 -2
- fleet_sensor_baseline-0.2.3/tools/commit_msg_check.py +145 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tools/hygiene_check.py +94 -8
- fleet_sensor_baseline-0.2.2/.githooks/pre-commit +0 -23
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/.gitignore +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/CONTRIBUTING.md +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/LICENSE +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/SECURITY.md +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/docs/formats.md +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/docs/upstream-asks.md +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/baseline.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/collect/__init__.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/collect/backends/__init__.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/collect/backends/mock.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/collect/backends/subprocess_backend.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/collect/collector.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/compare.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/drift.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/exits.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/for_referee.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/formats.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/outliers.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/report.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/store.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/verdict.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/walk.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/conftest.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_baseline.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_boundary.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_collector.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_compare.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_etag_collect.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_exits.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_for_referee.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_formats.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_hygiene.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_readme.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_readme_counts.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_referee_floor.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_scenarios.py +0 -0
- {fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/tests/test_store.py +0 -0
|
@@ -14,12 +14,17 @@
|
|
|
14
14
|
# while the code did something else entirely. Nothing checked the claim, because it
|
|
15
15
|
# was a claim in a comment.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
# Two checkers now, because they answer two different questions:
|
|
18
|
+
# hygiene_check.py --message must this text never be published?
|
|
19
|
+
# commit_msg_check.py is this message shaped like the others?
|
|
19
20
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
21
|
+
# Publication first: it is the one that cannot be undone after a push.
|
|
22
|
+
#
|
|
23
|
+
# To commit past both knowingly: git commit --no-verify
|
|
24
|
+
# Note that --no-verify also disables the pre-commit hygiene sweep. That is why
|
|
25
|
+
# these refuse only on objective problems and merely note the heuristic ones.
|
|
22
26
|
set -e
|
|
23
27
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
python3 "$ROOT/tools/hygiene_check.py" --message "$1"
|
|
30
|
+
exec python3 "$ROOT/tools/commit_msg_check.py" "$1"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Refuse a commit carrying something that should not be published, and one whose
|
|
3
|
+
# README no longer describes the suite.
|
|
4
|
+
#
|
|
5
|
+
# Activate once per clone: git config core.hooksPath .githooks
|
|
6
|
+
#
|
|
7
|
+
# Versioned deliberately. A hook that lives only in .git/hooks exists on one
|
|
8
|
+
# machine, is invisible in review, and is absent from every fresh clone -- which
|
|
9
|
+
# for a repository authored in public means the safety check is missing exactly
|
|
10
|
+
# when a new contributor needs it most.
|
|
11
|
+
#
|
|
12
|
+
# To commit past it knowingly: git commit --no-verify
|
|
13
|
+
# That is a real escape hatch and it should be rare enough to notice.
|
|
14
|
+
set -e
|
|
15
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
16
|
+
|
|
17
|
+
# FIRST, because it decides whether anything below it can speak for this commit.
|
|
18
|
+
# Every other check here reads the WORKING TREE; the commit records the INDEX.
|
|
19
|
+
# When they differ, a green hook is green about files that are not being
|
|
20
|
+
# committed -- which is how this repository shipped a README test count that was
|
|
21
|
+
# six short, twice in one day, with this hook passing both times.
|
|
22
|
+
python3 "$ROOT/tools/hygiene_check.py" --staged-is-what-ships
|
|
23
|
+
|
|
24
|
+
python3 "$ROOT/tools/hygiene_check.py"
|
|
25
|
+
|
|
26
|
+
# The README's test count, checked where the drift is created rather than where it
|
|
27
|
+
# is discovered. Dependency-free and about 0.2s.
|
|
28
|
+
#
|
|
29
|
+
# The count it checks is taken from what git TRACKS, not from what the directory
|
|
30
|
+
# holds, and that is the whole reason it can run here at all. The published 289 was
|
|
31
|
+
# measured on a working tree carrying a test file that was never committed: true of
|
|
32
|
+
# that disk, false of this repository. A disk-derived count would make this hook
|
|
33
|
+
# red on any tree with an untracked test in it -- red every run, for a reason that
|
|
34
|
+
# is not a defect, which is how a check teaches people to pass --no-verify. That
|
|
35
|
+
# flag also switches off the hygiene sweep above, so a check nobody can satisfy
|
|
36
|
+
# costs more than the one it was added to provide.
|
|
37
|
+
#
|
|
38
|
+
# The identity this commit will carry.
|
|
39
|
+
#
|
|
40
|
+
# A commit's AUTHOR is neither its message nor a file in it, so no text scanner
|
|
41
|
+
# reaches it -- the hygiene rules ran over both surfaces and a personal address
|
|
42
|
+
# still reached 24 published commits, because a local `user.email` quietly beat
|
|
43
|
+
# the global one and nothing read it. Checked here, before the commit exists,
|
|
44
|
+
# because after it is pushed a force-push changes the SHAs and removes nothing:
|
|
45
|
+
# the old objects stay fetchable and the address stays published.
|
|
46
|
+
AUTHOR_EMAIL=$(git config user.email || true)
|
|
47
|
+
case "$AUTHOR_EMAIL" in
|
|
48
|
+
*@users.noreply.github.com|*@noreply.github.com) ;;
|
|
49
|
+
"")
|
|
50
|
+
echo "pre-commit: no user.email is set, so this commit would carry a guess." >&2
|
|
51
|
+
exit 1 ;;
|
|
52
|
+
*)
|
|
53
|
+
echo "pre-commit: user.email is a real address, and a commit publishes it." >&2
|
|
54
|
+
echo " Set the GitHub noreply address for this repository:" >&2
|
|
55
|
+
echo " git config user.email <you>@users.noreply.github.com" >&2
|
|
56
|
+
echo " A local setting beats the global one; that is how this" >&2
|
|
57
|
+
echo " reached 24 published commits before anyone looked." >&2
|
|
58
|
+
exit 1 ;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
# A missing pytest fails rather than skips. A gate that converts its own red into a
|
|
62
|
+
# skip removes the failure and the evidence together.
|
|
63
|
+
if ! python3 -c "import pytest" 2>/dev/null; then
|
|
64
|
+
echo "pre-commit: pytest is not installed, so the README count check cannot run." >&2
|
|
65
|
+
echo " python3 -m pip install pytest (or commit with --no-verify)" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# The README-count check, where a repository has one.
|
|
70
|
+
#
|
|
71
|
+
# This step used to name `tests/test_readme_commands.py` outright. That file is
|
|
72
|
+
# this repository's; the same hook copied into four others refused EVERY commit,
|
|
73
|
+
# because pytest exits non-zero when it collects nothing and the hook could not
|
|
74
|
+
# tell "no such test" from "the test failed". The hooks are meant to be one file
|
|
75
|
+
# across the repositories, so the step has to find its target by SHAPE rather
|
|
76
|
+
# than be told a name that is only true here.
|
|
77
|
+
README_TESTS=$(ls "$ROOT"/tests/test_readme*.py 2>/dev/null || true)
|
|
78
|
+
if [ -z "$README_TESTS" ]; then
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
exec python3 -m pytest $README_TESTS -q -p no:cacheprovider
|
|
@@ -113,6 +113,54 @@ jobs:
|
|
|
113
113
|
- name: The publication vocabulary, over the tree
|
|
114
114
|
run: python3 tools/hygiene_check.py --all
|
|
115
115
|
|
|
116
|
+
- name: Commit messages
|
|
117
|
+
# The commit-msg hook's backstop, and the reason this is not optional: a
|
|
118
|
+
# message is the one published surface no later commit can correct.
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
BASE="${{ github.event.pull_request.base.sha || github.event.before || '' }}"
|
|
122
|
+
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \
|
|
123
|
+
|| ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
|
|
124
|
+
echo "no usable base ref; checking HEAD only"
|
|
125
|
+
LIST=$(git rev-parse HEAD)
|
|
126
|
+
else
|
|
127
|
+
LIST=$(git rev-list "$BASE..${{ github.sha }}")
|
|
128
|
+
echo "checking $(echo "$LIST" | grep -c . ) commit message(s) in $BASE..${{ github.sha }}"
|
|
129
|
+
fi
|
|
130
|
+
FAILED=0
|
|
131
|
+
for SHA in $LIST; do
|
|
132
|
+
git log -1 --format=%B "$SHA" > /tmp/msg.txt
|
|
133
|
+
echo "--- $SHA"
|
|
134
|
+
python3 tools/commit_msg_check.py /tmp/msg.txt || FAILED=1
|
|
135
|
+
done
|
|
136
|
+
exit $FAILED
|
|
137
|
+
|
|
138
|
+
- name: Authorship
|
|
139
|
+
# A commit's author is neither its message nor a file in it, so neither
|
|
140
|
+
# check above can see it. A local `user.email` quietly beat the global
|
|
141
|
+
# one and a personal address reached 24 published commits. The hook
|
|
142
|
+
# checks the setting before the commit; this checks the RESULT after it,
|
|
143
|
+
# which is the thing that actually gets published.
|
|
144
|
+
run: |
|
|
145
|
+
set -euo pipefail
|
|
146
|
+
BASE="${{ github.event.pull_request.base.sha || github.event.before || '' }}"
|
|
147
|
+
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \
|
|
148
|
+
|| ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
|
|
149
|
+
LIST=$(git rev-parse HEAD)
|
|
150
|
+
else
|
|
151
|
+
LIST=$(git rev-list "$BASE..${{ github.sha }}")
|
|
152
|
+
fi
|
|
153
|
+
FAILED=0
|
|
154
|
+
for SHA in $LIST; do
|
|
155
|
+
for WHO in $(git log -1 --format='%ae%n%ce' "$SHA"); do
|
|
156
|
+
case "$WHO" in
|
|
157
|
+
*@users.noreply.github.com|*@noreply.github.com) ;;
|
|
158
|
+
*) echo "::error::$SHA carries a real address in its authorship"; FAILED=1 ;;
|
|
159
|
+
esac
|
|
160
|
+
done
|
|
161
|
+
done
|
|
162
|
+
exit $FAILED
|
|
163
|
+
|
|
116
164
|
- name: A mock rack, end to end
|
|
117
165
|
# 0.3's exit criterion, stated as a command: a verdict produced from
|
|
118
166
|
# nothing but BMC endpoints. It runs against fake machines, which is the
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fleet-sensor-baseline
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Which units differ from their cohort, and what changed across time.
|
|
5
|
+
Project-URL: Homepage, https://github.com/james-sheen/fleet-sensor-baseline
|
|
6
|
+
Project-URL: Source, https://github.com/james-sheen/fleet-sensor-baseline
|
|
7
|
+
Project-URL: Issues, https://github.com/james-sheen/fleet-sensor-baseline/issues
|
|
8
|
+
Author: James Sheen
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Keywords: baseline,bmc,drift,fleet,hardware,openbmc,redfish,sensors
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Provides-Extra: collect
|
|
15
|
+
Requires-Dist: bmc-sensor-audit<0.4,>=0.2.0; extra == 'collect'
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
# fleet-sensor-baseline
|
|
2
21
|
|
|
3
22
|
**Which units differ from their cohort, and what changed on this unit across time.**
|
|
@@ -6,9 +25,18 @@
|
|
|
6
25
|
list of machines and the history of captures, and answers the two questions the
|
|
7
26
|
referee cannot.
|
|
8
27
|
|
|
9
|
-
**Released — 0.2.
|
|
28
|
+
**Released — 0.2.3**, tagged `v0.2.3`, Apache-2.0, on PyPI as
|
|
10
29
|
`fleet-sensor-baseline`.
|
|
11
30
|
|
|
31
|
+
**0.2.3 admits the referee's 0.3 line, and stops a closed pipe reading as a
|
|
32
|
+
finding.** The `collect` extra held `bmc-sensor-audit` below 0.3 for eighteen
|
|
33
|
+
days after 0.3.0, and nothing here needed it: two tests read what 0.2 kept
|
|
34
|
+
where. They read the referee as installed now, and the suite passes on
|
|
35
|
+
referees 0.2.7, 0.3.0 and 0.3.5 -- which also lets this extra sit beside
|
|
36
|
+
`odm-cert-generator`, whose releases from 0.2.2 require the 0.3 line. And every
|
|
37
|
+
subcommand, `--help` included, raised `BrokenPipeError` when its reader closed
|
|
38
|
+
the pipe, exiting 1, which this family reads as findings about a rack.
|
|
39
|
+
|
|
12
40
|
**0.2.2 adds `compare`, the threshold audit across time.** `drift` reads records
|
|
13
41
|
and never opens a walk, so a threshold edited on a sensor that stayed present was
|
|
14
42
|
invisible to it — and invisible to `collect --etag-cache` too, which is why a
|
|
@@ -350,7 +378,7 @@ first would pass by finding nothing.
|
|
|
350
378
|
|
|
351
379
|
| | count |
|
|
352
380
|
|---|---|
|
|
353
|
-
| tests collected |
|
|
381
|
+
| tests collected | 447 |
|
|
354
382
|
| of those, requiring `bmc-sensor-audit` | 40 |
|
|
355
383
|
|
|
356
384
|
**The predicate**: `pytest --collect-only` over the test files git tracks, and
|
|
@@ -377,8 +405,12 @@ a walk with the referee's own reader and asserts the fixture still matches.
|
|
|
377
405
|
|
|
378
406
|
## Upstream
|
|
379
407
|
|
|
380
|
-
Pinned at `bmc-sensor-audit>=0.2.0,<0.
|
|
381
|
-
|
|
408
|
+
Pinned at `bmc-sensor-audit>=0.2.0,<0.4`. **The ceiling was `<0.3` until
|
|
409
|
+
2026-09-26**, eighteen days into the referee's 0.3 line, and what held it there
|
|
410
|
+
was this repository's own tests reading where 0.2 kept things -- the prefix
|
|
411
|
+
parser, which moved to `presence-audit` at 0.3.0, and a ceiling typed into the
|
|
412
|
+
pin guard. Both now read the referee as installed, and the suite passes on 0.2.7,
|
|
413
|
+
0.3.0 and 0.3.5. The floor is **derived, not chosen**. It has moved three times, each time to consume something reported from
|
|
382
414
|
here:
|
|
383
415
|
|
|
384
416
|
- `>=0.1.2` for `--password-env`, so a credential never crosses argv, and for
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: fleet-sensor-baseline
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Which units differ from their cohort, and what changed across time.
|
|
5
|
-
Project-URL: Homepage, https://github.com/james-sheen/fleet-sensor-baseline
|
|
6
|
-
Project-URL: Source, https://github.com/james-sheen/fleet-sensor-baseline
|
|
7
|
-
Project-URL: Issues, https://github.com/james-sheen/fleet-sensor-baseline/issues
|
|
8
|
-
Author: James Sheen
|
|
9
|
-
License-Expression: Apache-2.0
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
License-File: NOTICE
|
|
12
|
-
Keywords: baseline,bmc,drift,fleet,hardware,openbmc,redfish,sensors
|
|
13
|
-
Requires-Python: >=3.10
|
|
14
|
-
Provides-Extra: collect
|
|
15
|
-
Requires-Dist: bmc-sensor-audit<0.3,>=0.2.0; extra == 'collect'
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: pytest>=7; extra == 'dev'
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
1
|
# fleet-sensor-baseline
|
|
21
2
|
|
|
22
3
|
**Which units differ from their cohort, and what changed on this unit across time.**
|
|
@@ -25,9 +6,18 @@ Description-Content-Type: text/markdown
|
|
|
25
6
|
list of machines and the history of captures, and answers the two questions the
|
|
26
7
|
referee cannot.
|
|
27
8
|
|
|
28
|
-
**Released — 0.2.
|
|
9
|
+
**Released — 0.2.3**, tagged `v0.2.3`, Apache-2.0, on PyPI as
|
|
29
10
|
`fleet-sensor-baseline`.
|
|
30
11
|
|
|
12
|
+
**0.2.3 admits the referee's 0.3 line, and stops a closed pipe reading as a
|
|
13
|
+
finding.** The `collect` extra held `bmc-sensor-audit` below 0.3 for eighteen
|
|
14
|
+
days after 0.3.0, and nothing here needed it: two tests read what 0.2 kept
|
|
15
|
+
where. They read the referee as installed now, and the suite passes on
|
|
16
|
+
referees 0.2.7, 0.3.0 and 0.3.5 -- which also lets this extra sit beside
|
|
17
|
+
`odm-cert-generator`, whose releases from 0.2.2 require the 0.3 line. And every
|
|
18
|
+
subcommand, `--help` included, raised `BrokenPipeError` when its reader closed
|
|
19
|
+
the pipe, exiting 1, which this family reads as findings about a rack.
|
|
20
|
+
|
|
31
21
|
**0.2.2 adds `compare`, the threshold audit across time.** `drift` reads records
|
|
32
22
|
and never opens a walk, so a threshold edited on a sensor that stayed present was
|
|
33
23
|
invisible to it — and invisible to `collect --etag-cache` too, which is why a
|
|
@@ -369,7 +359,7 @@ first would pass by finding nothing.
|
|
|
369
359
|
|
|
370
360
|
| | count |
|
|
371
361
|
|---|---|
|
|
372
|
-
| tests collected |
|
|
362
|
+
| tests collected | 447 |
|
|
373
363
|
| of those, requiring `bmc-sensor-audit` | 40 |
|
|
374
364
|
|
|
375
365
|
**The predicate**: `pytest --collect-only` over the test files git tracks, and
|
|
@@ -396,8 +386,12 @@ a walk with the referee's own reader and asserts the fixture still matches.
|
|
|
396
386
|
|
|
397
387
|
## Upstream
|
|
398
388
|
|
|
399
|
-
Pinned at `bmc-sensor-audit>=0.2.0,<0.
|
|
400
|
-
|
|
389
|
+
Pinned at `bmc-sensor-audit>=0.2.0,<0.4`. **The ceiling was `<0.3` until
|
|
390
|
+
2026-09-26**, eighteen days into the referee's 0.3 line, and what held it there
|
|
391
|
+
was this repository's own tests reading where 0.2 kept things -- the prefix
|
|
392
|
+
parser, which moved to `presence-audit` at 0.3.0, and a ceiling typed into the
|
|
393
|
+
pin guard. Both now read the referee as installed, and the suite passes on 0.2.7,
|
|
394
|
+
0.3.0 and 0.3.5. The floor is **derived, not chosen**. It has moved three times, each time to consume something reported from
|
|
401
395
|
here:
|
|
402
396
|
|
|
403
397
|
- `>=0.1.2` for `--password-env`, so a credential never crosses argv, and for
|
|
@@ -75,7 +75,17 @@ dependencies = []
|
|
|
75
75
|
# caller's install line.** `compare` needs the referee too -- it hands two
|
|
76
76
|
# stored walks to `regression` -- so this extra now gates two commands. Both
|
|
77
77
|
# were verified against every release the range admits: 0.2.0, 0.2.1, 0.2.2.
|
|
78
|
-
|
|
78
|
+
#
|
|
79
|
+
# **The ceiling rose to <0.4 on 2026-09-26**, eighteen days after the referee
|
|
80
|
+
# reached 0.3.0. Nothing here was wrong with the referee's 0.3 line: the only
|
|
81
|
+
# reds were two tests reading what 0.2 happened to keep where -- the prefix
|
|
82
|
+
# parser, which moved to `presence-audit` with the 0.3.0 split, and a ceiling
|
|
83
|
+
# typed into the pin guard. Both read the referee as it is now. Measured
|
|
84
|
+
# before the move: 446 passed on 0.2.7 and 8 failed on 0.3.5, all eight those
|
|
85
|
+
# two; after it, the suite passes on 0.2.7, 0.3.0 and 0.3.5. Meanwhile
|
|
86
|
+
# `odm-cert-generator` 0.2.2 already required the referee at >=0.3.0, so this
|
|
87
|
+
# extra could not be installed beside the certificate renderer at all.
|
|
88
|
+
collect = ["bmc-sensor-audit>=0.2.0,<0.4"]
|
|
79
89
|
dev = ["pytest>=7"]
|
|
80
90
|
|
|
81
91
|
[project.scripts]
|
{fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/__init__.py
RENAMED
|
@@ -15,6 +15,6 @@ from __future__ import annotations
|
|
|
15
15
|
# read this to decide whether the README may name a tag at all: an unreleased
|
|
16
16
|
# tree that announces `v0.1.0` hands a reader a tag to check out that does not
|
|
17
17
|
# exist. Bump this at step 1 of a release, not at step 9.
|
|
18
|
-
__version__ = "0.2.
|
|
18
|
+
__version__ = "0.2.3"
|
|
19
19
|
|
|
20
20
|
__all__ = ["__version__"]
|
{fleet_sensor_baseline-0.2.2 → fleet_sensor_baseline-0.2.3}/src/fleet_sensor_baseline/cli.py
RENAMED
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
14
|
import json
|
|
15
|
+
import os
|
|
15
16
|
import sys
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Iterable
|
|
@@ -714,13 +715,98 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
714
715
|
return parser
|
|
715
716
|
|
|
716
717
|
|
|
718
|
+
class _StdoutThatOutlivesItsReader:
|
|
719
|
+
"""`sys.stdout`, for a program whose exit code is a claim about a fleet.
|
|
720
|
+
|
|
721
|
+
**A reader that stops reading has said something about itself, not about
|
|
722
|
+
the rack.** `fleet-sensor-baseline baseline ... | head` is an ordinary
|
|
723
|
+
thing to do, and before this every subcommand died of it -- including
|
|
724
|
+
`--help`, which is the likeliest thing anyone pipes. The failure arrived
|
|
725
|
+
two ways, and neither is in the vocabulary `exits.py` defines: a report
|
|
726
|
+
long enough to fill the pipe buffer raised out of `print` and the
|
|
727
|
+
interpreter exited `1`, which this tool means as FINDINGS; a shorter one
|
|
728
|
+
survived to the shutdown flush, printed `Exception ignored` to stderr and
|
|
729
|
+
exited `120`, which is not a verdict at all.
|
|
730
|
+
|
|
731
|
+
Both replace a statement about the fleet with a statement about the
|
|
732
|
+
terminal. Absorbing rather than refusing is the point: `INCOMPLETE` says
|
|
733
|
+
the fleet could not be assessed, and a baseline whose reader walked away
|
|
734
|
+
was assessed perfectly well.
|
|
735
|
+
|
|
736
|
+
The sibling package has carried this since its own two occurrences. This
|
|
737
|
+
is the third in the family, and it shipped in 0.2.2.
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
def __init__(self, stream: Any) -> None:
|
|
741
|
+
self._stream = stream
|
|
742
|
+
self.reader_left = False
|
|
743
|
+
|
|
744
|
+
def _abandon(self) -> None:
|
|
745
|
+
"""Point the descriptor at nowhere, then stop trying.
|
|
746
|
+
|
|
747
|
+
The interpreter flushes `stdout` again on its way out, on bytes this
|
|
748
|
+
stream may still hold. Without the redirect that second flush raises
|
|
749
|
+
where no `except` can reach it -- which is the `Exception ignored`
|
|
750
|
+
line, and the `120`.
|
|
751
|
+
"""
|
|
752
|
+
self.reader_left = True
|
|
753
|
+
try:
|
|
754
|
+
fileno = self._stream.fileno()
|
|
755
|
+
except (AttributeError, ValueError, OSError):
|
|
756
|
+
return # captured by a harness rather than piped; nothing to point
|
|
757
|
+
try:
|
|
758
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), fileno)
|
|
759
|
+
except OSError:
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
def write(self, text: str) -> int:
|
|
763
|
+
if self.reader_left:
|
|
764
|
+
return len(text)
|
|
765
|
+
try:
|
|
766
|
+
return self._stream.write(text)
|
|
767
|
+
except BrokenPipeError:
|
|
768
|
+
self._abandon()
|
|
769
|
+
return len(text)
|
|
770
|
+
|
|
771
|
+
def flush(self) -> None:
|
|
772
|
+
if self.reader_left:
|
|
773
|
+
return
|
|
774
|
+
try:
|
|
775
|
+
self._stream.flush()
|
|
776
|
+
except BrokenPipeError:
|
|
777
|
+
self._abandon()
|
|
778
|
+
|
|
779
|
+
def __getattr__(self, name: str) -> Any:
|
|
780
|
+
return getattr(self._stream, name)
|
|
781
|
+
|
|
782
|
+
|
|
717
783
|
def main(argv: list[str] | None = None) -> int:
|
|
718
|
-
|
|
784
|
+
# Installed before the parser runs, because `--help` prints through it too
|
|
785
|
+
# and argparse exits from inside `parse_args`.
|
|
786
|
+
stdout = _StdoutThatOutlivesItsReader(sys.stdout)
|
|
787
|
+
sys.stdout = stdout
|
|
719
788
|
try:
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
789
|
+
args = build_parser().parse_args(argv)
|
|
790
|
+
try:
|
|
791
|
+
return args.func(args)
|
|
792
|
+
except StoreError as exc:
|
|
793
|
+
print(f"{args.command}: {exc}", file=sys.stderr)
|
|
794
|
+
return INCOMPLETE
|
|
795
|
+
except BrokenPipeError:
|
|
796
|
+
# A pipe that broke somewhere the wrapper does not cover is a failure
|
|
797
|
+
# to deliver, and this tool says so with the code that means it.
|
|
798
|
+
print("the output could not be written: the pipe closed",
|
|
799
|
+
file=sys.stderr)
|
|
723
800
|
return INCOMPLETE
|
|
801
|
+
finally:
|
|
802
|
+
# **Flush through the wrapper, before handing the stream back.** A
|
|
803
|
+
# report short enough to sit in the buffer is not written until the
|
|
804
|
+
# interpreter flushes on its way out -- by which point this wrapper is
|
|
805
|
+
# gone and the failure lands where no `except` can reach it.
|
|
806
|
+
try:
|
|
807
|
+
stdout.flush()
|
|
808
|
+
finally:
|
|
809
|
+
sys.stdout = stdout._stream
|
|
724
810
|
|
|
725
811
|
|
|
726
812
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""`| head` is not a fault in the fleet.
|
|
2
|
+
|
|
3
|
+
**The exit code is the claim, and the claim is about a rack.** A reader that
|
|
4
|
+
stops reading has said something about itself, not about the machines, so the
|
|
5
|
+
code this tool returns must not move when somebody pipes a report into `head`.
|
|
6
|
+
|
|
7
|
+
Until this was guarded it moved two different ways, and 0.2.2 shipped with both.
|
|
8
|
+
A report long enough to fill the pipe buffer raised `BrokenPipeError` out of
|
|
9
|
+
`print`, escaped `main`, and left Python to exit `1` -- and `1` in this
|
|
10
|
+
vocabulary means FINDINGS, so a truncated report was indistinguishable from a
|
|
11
|
+
complete one to anything reading the code. A short report failed later instead,
|
|
12
|
+
at the interpreter's shutdown flush, printing `Exception ignored` and exiting
|
|
13
|
+
`120`, which `exits.normalise` reads as INCOMPLETE. Either way an aggregator
|
|
14
|
+
files a rack on the strength of where the operator's terminal stopped.
|
|
15
|
+
|
|
16
|
+
`--help` was in the second class, which is the part worth stating plainly: the
|
|
17
|
+
likeliest thing anybody pipes was the likeliest thing to fail.
|
|
18
|
+
|
|
19
|
+
The sibling package guards this and has since its own two occurrences. This is
|
|
20
|
+
the third in the family. The rule was written down after the second; writing it
|
|
21
|
+
down is not a mechanism, and a test is, which is why this file exists rather
|
|
22
|
+
than a line in a document.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import pytest
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
35
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
36
|
+
|
|
37
|
+
from conftest import record # noqa: E402
|
|
38
|
+
|
|
39
|
+
#: Enough one-line results to carry the rendered output comfortably past a 64KB
|
|
40
|
+
#: pipe buffer, so the failure lands in `print` rather than at the shutdown
|
|
41
|
+
#: flush. Both paths are exercised below; non-vacuity is asserted, not assumed.
|
|
42
|
+
WIDE = 1200
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _write_records(where: Path, count: int, *, valid: bool) -> list[str]:
|
|
46
|
+
paths = []
|
|
47
|
+
for index in range(count):
|
|
48
|
+
path = where / f"r{index:05d}.json"
|
|
49
|
+
if valid:
|
|
50
|
+
payload = record(f"rack-01/unit{index:05d}",
|
|
51
|
+
captured_at="2026-08-20T00:00:00Z",
|
|
52
|
+
digest="sha256:" + f"{index:064x}")
|
|
53
|
+
else:
|
|
54
|
+
payload = {"format": "fleet-sensor-baseline/fleet-record/1"}
|
|
55
|
+
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
56
|
+
paths.append(str(path))
|
|
57
|
+
return paths
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture(scope="module")
|
|
61
|
+
def wide_clean(tmp_path_factory):
|
|
62
|
+
"""A long report whose verdict is CLEAN, and the reason it must be.
|
|
63
|
+
|
|
64
|
+
**An unhandled `BrokenPipeError` makes Python exit `1`.** A fixture whose
|
|
65
|
+
honest verdict is also `1` cannot tell a preserved verdict from a crash --
|
|
66
|
+
the two agree by coincidence and the assertion passes against the unguarded
|
|
67
|
+
code. `validate` answers only `0` or `2`, so every fixture here is already
|
|
68
|
+
clear of the crash code; this one is pinned anyway, because the day
|
|
69
|
+
`validate` grows a FINDINGS path is the day these tests go quiet without
|
|
70
|
+
going red.
|
|
71
|
+
"""
|
|
72
|
+
where = tmp_path_factory.mktemp("wide_clean")
|
|
73
|
+
return ["validate", *_write_records(where, WIDE, valid=True)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture(scope="module")
|
|
77
|
+
def wide_incomplete(tmp_path_factory):
|
|
78
|
+
"""The same length, verdict INCOMPLETE.
|
|
79
|
+
|
|
80
|
+
Kept because `2` is the verdict an operator most often pipes into `head` --
|
|
81
|
+
the run that found something wrong is the run worth skimming.
|
|
82
|
+
"""
|
|
83
|
+
where = tmp_path_factory.mktemp("wide_incomplete")
|
|
84
|
+
return ["validate", *_write_records(where, WIDE, valid=False)]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture(scope="module")
|
|
88
|
+
def narrow(tmp_path_factory):
|
|
89
|
+
"""A run whose report is short, and whose verdict is CLEAN.
|
|
90
|
+
|
|
91
|
+
Too little output to fail during `print`, so it reaches the shutdown flush
|
|
92
|
+
instead -- the path that produced `Exception ignored` and `120`.
|
|
93
|
+
"""
|
|
94
|
+
where = tmp_path_factory.mktemp("narrow")
|
|
95
|
+
return ["validate", *_write_records(where, 2, valid=True)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _environment() -> dict:
|
|
99
|
+
return {"PYTHONPATH": str(ROOT / "src"), "PATH": "/usr/bin:/bin"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _unpiped(argv) -> subprocess.CompletedProcess:
|
|
103
|
+
return subprocess.run([sys.executable, "-m", "fleet_sensor_baseline.cli", *argv],
|
|
104
|
+
capture_output=True, text=True, env=_environment())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _through_head(argv, lines: int) -> tuple[int, str]:
|
|
108
|
+
"""`(writer exit code, writer stderr)` -- the writer's, never the pipe's.
|
|
109
|
+
|
|
110
|
+
A shell pipeline reports the LAST command's status, which here is `head`,
|
|
111
|
+
which is always `0`. Measuring that would make every assertion below
|
|
112
|
+
vacuously true; this project has three recorded instances of exactly that
|
|
113
|
+
mistake, so the writer is held open and asked directly.
|
|
114
|
+
"""
|
|
115
|
+
writer = subprocess.Popen(
|
|
116
|
+
[sys.executable, "-m", "fleet_sensor_baseline.cli", *argv],
|
|
117
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
118
|
+
env=_environment())
|
|
119
|
+
reader = subprocess.Popen(["head", "-n", str(lines)], stdin=writer.stdout,
|
|
120
|
+
stdout=subprocess.DEVNULL)
|
|
121
|
+
writer.stdout.close()
|
|
122
|
+
reader.wait()
|
|
123
|
+
stderr = writer.stderr.read()
|
|
124
|
+
writer.stderr.close()
|
|
125
|
+
writer.wait()
|
|
126
|
+
return writer.returncode, stderr
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestTheReportIsLongEnoughToReachTheDefect:
|
|
130
|
+
def test_the_wide_fixture_would_fill_a_pipe_buffer(self, wide_clean):
|
|
131
|
+
"""Non-vacuity for everything below. A report that fits inside the 64KB
|
|
132
|
+
buffer never makes the writer notice the reader has gone, so a suite
|
|
133
|
+
built on a short report passes against the unguarded code."""
|
|
134
|
+
rendered = _unpiped(wide_clean)
|
|
135
|
+
assert len(rendered.stdout) > 65536, (
|
|
136
|
+
f"the report is {len(rendered.stdout)} bytes and cannot close a "
|
|
137
|
+
f"pipe before the writer finishes")
|
|
138
|
+
|
|
139
|
+
def test_the_incomplete_fixture_is_long_too(self, wide_incomplete):
|
|
140
|
+
assert len(_unpiped(wide_incomplete).stdout) > 65536
|
|
141
|
+
|
|
142
|
+
def test_no_fixture_shares_the_crash_code(self, wide_clean, wide_incomplete,
|
|
143
|
+
narrow):
|
|
144
|
+
"""`1` is what an unhandled exception exits with. If any fixture ever
|
|
145
|
+
returns it, the assertions below agree with a crash by coincidence."""
|
|
146
|
+
for argv in (wide_clean, wide_incomplete, narrow):
|
|
147
|
+
assert _unpiped(argv).returncode != 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestAClosedPipeDoesNotMoveTheVerdict:
|
|
151
|
+
def test_a_long_clean_report_keeps_its_exit_code(self, wide_clean):
|
|
152
|
+
"""**The assertion the whole guard exists for.** Not *no traceback* --
|
|
153
|
+
a run that printed nothing and exited 120 would satisfy that."""
|
|
154
|
+
expected = _unpiped(wide_clean).returncode
|
|
155
|
+
assert expected == 0
|
|
156
|
+
code, _ = _through_head(wide_clean, 10)
|
|
157
|
+
assert code == expected, (
|
|
158
|
+
f"piping the report changed the verdict from {expected} to {code}")
|
|
159
|
+
|
|
160
|
+
def test_a_long_incomplete_report_keeps_its_exit_code(self, wide_incomplete):
|
|
161
|
+
expected = _unpiped(wide_incomplete).returncode
|
|
162
|
+
assert expected == 2
|
|
163
|
+
code, _ = _through_head(wide_incomplete, 10)
|
|
164
|
+
assert code == expected
|
|
165
|
+
|
|
166
|
+
def test_a_clean_run_stays_clean(self, narrow):
|
|
167
|
+
"""The direction a refusal-shaped fix would break. Mapping a closed pipe
|
|
168
|
+
to INCOMPLETE would file a healthy rack as unwalked."""
|
|
169
|
+
assert _unpiped(narrow).returncode == 0
|
|
170
|
+
code, _ = _through_head(narrow, 1)
|
|
171
|
+
assert code == 0
|
|
172
|
+
|
|
173
|
+
def test_a_reader_that_leaves_immediately_is_also_clean(self, narrow):
|
|
174
|
+
"""The shutdown-flush path, which is where `--help` failed."""
|
|
175
|
+
code, stderr = _through_head(narrow, 0)
|
|
176
|
+
assert code == 0, f"exit {code} for a clean run whose reader left"
|
|
177
|
+
assert "Exception ignored" not in stderr
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestNothingIsPrintedAboutIt:
|
|
181
|
+
@pytest.mark.parametrize("lines", [0, 1, 10])
|
|
182
|
+
def test_no_traceback_reaches_the_operator(self, wide_clean, lines):
|
|
183
|
+
_, stderr = _through_head(wide_clean, lines)
|
|
184
|
+
assert stderr == "", stderr
|
|
185
|
+
|
|
186
|
+
@pytest.mark.parametrize("argv", ["--help", "validate --help",
|
|
187
|
+
"baseline --help", "collect --help"])
|
|
188
|
+
def test_help_survives_its_likeliest_reader(self, argv):
|
|
189
|
+
"""`--help | head` is the commonest pipe there is, and it was broken on
|
|
190
|
+
every subcommand. Parametrised over four because argparse exits from
|
|
191
|
+
inside `parse_args`, before anything this program wrote could run."""
|
|
192
|
+
code, stderr = _through_head(argv.split(), 0)
|
|
193
|
+
assert stderr == "", stderr
|
|
194
|
+
assert code == 0
|