diff --git a/.kiro/specs/dirty-content-fingerprint/tasks.md b/.kiro/specs/dirty-content-fingerprint/tasks.md index 468dceca..3886d451 100644 --- a/.kiro/specs/dirty-content-fingerprint/tasks.md +++ b/.kiro/specs/dirty-content-fingerprint/tasks.md @@ -77,7 +77,7 @@ - _Boundary: xcecli/cmd/main.go_ - _Depends: 4.1_ -- [ ] 5. Core: xceadmin fingerprint display +- [x] 5. Core: xceadmin fingerprint display - [x] 5.1 (P) meson-build.sh generates the fingerprint field in the build-info module - `dirty_fingerprint` threaded through as an 8th positional arg (`$8`), correctly numbered and consistent between `meson.build` and `meson-build.sh`; `_build_info.py` gains `DIRTY_FINGERPRINT` with an independent empty-string fallback (not `"unknown"`); `GIT_COMMIT`/`BUILD_TIME`'s existing generation and fallback confirmed byte-for-byte untouched - Observable confirmed live (independently re-verified by review, full real build+test+wheel cycles across 3 scenarios): dirty tree -> real, non-empty `DIRTY_FINGERPRINT` present identically in the mirrored tree, the build/lib copy, an installed venv, and the packaged wheel, cross-checked byte-for-byte against that same build's `compile_commands.json`; explicit-empty fingerprint -> genuinely `""` while `GIT_COMMIT`/`BUILD_TIME` pass through real values; all-empty -> `GIT_COMMIT`/`BUILD_TIME` still fall back to `"unknown"` while `DIRTY_FINGERPRINT` stays `""`; 261 passed/3 skipped in all three scenarios @@ -85,10 +85,10 @@ - _Boundary: xceadmin/meson.build, xceadmin/meson-build.sh_ - _Depends: 1_ -- [ ] 5.2 (P) Committed fallback stub and CLI fingerprint wiring - - The committed fallback stub (`src/xceadmin/src/xceadmin/_build_info.py`) gains `DIRTY_FINGERPRINT = ""` alongside its existing two `"unknown"` fields - - `cli.py`'s `_compose_version()` applies the same strip-and-append rule as the other components: if `DIRTY_FINGERPRINT` is non-empty, strip a trailing `-dirty` from `GIT_COMMIT` and append `-DIRTY_FINGERPRINT`; if empty, use `GIT_COMMIT` as-is - - Observable confirmed live (both install paths): an editable install (`pip install -e .`) shows no fingerprint suffix (empty field, `GIT_COMMIT` unaffected, no import error); a wheel built from a dirty tree shows `-`, not `-dirty-` +- [x] 5.2 (P) Committed fallback stub and CLI fingerprint wiring + - `_build_info.py`'s fallback stub gains `DIRTY_FINGERPRINT = ""`; `cli.py`'s new `_compose_version_display()` applies the strip-and-append rule via a genuine anchored `removesuffix("-dirty")` (independently confirmed via a second, differently-constructed mutation test); both fallback paths (`_build_info.py`'s module stub and `cli.py`'s separate `ImportError` fallback class) correctly carry the field + - A pre-existing test's hard-coded literal-`GIT_COMMIT` assertion was updated to compute its expected value via the production `_compose_version_display()` function instead -- independently assessed by review as a legitimate strengthening (the old assertion's premise no longer holds once a fingerprint is present), not a weakened/gutted check; the 4 new dedicated unit tests independently guard the transform itself with hard-coded literal assertions + - Observable confirmed live (independently re-verified by review, both install paths via a full real `/xce-build xceadmin` cycle): editable install shows `0.1.0 (unknown, unknown)`, no import error; wheel built from the dirty tree shows `-`, never `-dirty-`; full suite 267 passed/3 skipped (261 baseline + 6 new) - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 4.1_ - _Boundary: xceadmin/src/xceadmin_ - _Depends: 5.1_ diff --git a/src/xceadmin/src/xceadmin/_build_info.py b/src/xceadmin/src/xceadmin/_build_info.py index 82f3e305..e1ff3993 100644 --- a/src/xceadmin/src/xceadmin/_build_info.py +++ b/src/xceadmin/src/xceadmin/_build_info.py @@ -1,14 +1,20 @@ -"""Committed fallback build-info stub (build-time-versioning task 5.2). +"""Committed fallback build-info stub (build-time-versioning task 5.2; +extended by dirty-content-fingerprint task 5.2 with DIRTY_FINGERPRINT). -Both fields are always "unknown" here. This file ships checked into git so -that a non-meson `pip install -e .` (the documented fast-iteration path, -`build-deployment.md`) still imports `xceadmin._build_info` cleanly without -ever having run the meson build -- Requirement 1.4's "record as unknown -rather than fail" applies here too. +GIT_COMMIT/BUILD_TIME are always "unknown" here; DIRTY_FINGERPRINT is always +"" (empty, not "unknown" -- matching that field's convention everywhere else +in the dirty-content-fingerprint feature: empty means "no fingerprint +available", distinct from the other two fields' "unknown" sentinel for "not +injected at all"). This file ships checked into git so that a non-meson `pip +install -e .` (the documented fast-iteration path, `build-deployment.md`) +still imports `xceadmin._build_info` cleanly without ever having run the +meson build -- Requirement 1.4's "record as unknown rather than fail" +applies here too. When built via the meson custom_target (`src/xceadmin/meson-build.sh`, task -5.1), this exact filename is regenerated with real `GIT_COMMIT`/`BUILD_TIME` -values -- but only in meson's own out-of-tree MIRRORED copy of this package +5.1), this exact filename is regenerated with real `GIT_COMMIT`/`BUILD_TIME`/ +`DIRTY_FINGERPRINT` values -- but only in meson's own out-of-tree MIRRORED +copy of this package (`$BUILD_DIR/repo-mirror/src/xceadmin/src/xceadmin/_build_info.py`). This checked-in copy, under the live source tree, is never overwritten. """ @@ -17,3 +23,4 @@ from __future__ import annotations GIT_COMMIT = "unknown" BUILD_TIME = "unknown" +DIRTY_FINGERPRINT = "" diff --git a/src/xceadmin/src/xceadmin/cli.py b/src/xceadmin/src/xceadmin/cli.py index 3451f3eb..5a9c3997 100644 --- a/src/xceadmin/src/xceadmin/cli.py +++ b/src/xceadmin/src/xceadmin/cli.py @@ -53,16 +53,38 @@ except ImportError: # pragma: no cover - defensive only: a committed class _build_info: # type: ignore[no-redef] GIT_COMMIT = "unknown" BUILD_TIME = "unknown" + DIRTY_FINGERPRINT = "" + + +def _compose_version_display(git_commit: str, dirty_fingerprint: str) -> str: + """Applies the dirty-content-fingerprint feature's strip-and-append rule + to ``git_commit``: if ``dirty_fingerprint`` is non-empty, a trailing + ``-dirty`` suffix on ``git_commit`` is stripped (an anchored suffix + check via ``str.removesuffix``, not a blind substring replace) and + ``-`` is appended in its place; otherwise + ``git_commit`` is returned exactly as-is (already correctly showing + ``-dirty`` or not, per the original build-time-versioning feature). + Mirrors ``xce_compose_version_ex()`` in ``src/lib/utils.c`` (task 3.1) + and ``composeVersionDisplay()`` in ``apiserver/cmd/main.go`` / + ``xcecli/cmd/main.go`` (tasks 4.4/4.5) -- the equivalent implementations + already approved for the other version-displaying components + (Requirements 1.1, 1.2, 1.3, 1.4, 2.1, 4.1). + """ + if not dirty_fingerprint: + return git_commit + return git_commit.removesuffix("-dirty") + "-" + dirty_fingerprint def _compose_version() -> str: """Composes the string `xceadmin --version` prints: pyproject.toml's static semantic version -- read via ``importlib.metadata`` (the standard way to read an *installed* package's own version, rather than - hand-parsing pyproject.toml) -- plus the two build-info fields, which are + hand-parsing pyproject.toml) -- plus the build-info fields, which are either task 5.1's meson-generated real values or task 5.2's committed - "unknown" fallback, whichever this install currently carries - (Requirements 1.1, 1.2, 2.1). + "unknown"/"" fallback, whichever this install currently carries + (Requirements 1.1, 1.2, 2.1). ``GIT_COMMIT`` has + ``_compose_version_display``'s strip-and-append rule applied against + ``DIRTY_FINGERPRINT`` before being embedded here. """ try: pkg_version = _package_version("xceadmin") @@ -71,7 +93,8 @@ def _compose_version() -> str: # entry point already running as `xceadmin`; fails soft to # "unknown" rather than raising, matching Requirement 1.4. pkg_version = "unknown" - return f"{pkg_version} ({_build_info.GIT_COMMIT}, {_build_info.BUILD_TIME})" + commit_display = _compose_version_display(_build_info.GIT_COMMIT, _build_info.DIRTY_FINGERPRINT) + return f"{pkg_version} ({commit_display}, {_build_info.BUILD_TIME})" _DEFAULT_OUTPUT = "cluster.bin" diff --git a/src/xceadmin/tests/test_cli.py b/src/xceadmin/tests/test_cli.py index 0ef72634..6b89430c 100644 --- a/src/xceadmin/tests/test_cli.py +++ b/src/xceadmin/tests/test_cli.py @@ -93,18 +93,31 @@ def test_cli_version_flag_exits_zero_and_shows_package_version() -> None: def test_cli_version_flag_includes_build_info_fields() -> None: - """The printed --version string must include BOTH _build_info fields - (whichever variant -- "unknown" fallback or meson-generated real values - -- is currently installed), not just the bare package version, so - --version and the eventual startup log line (out of this task's scope) - can never disagree (Requirement 2.2).""" + """The printed --version string must include BUILD_TIME verbatim, plus + the commit display -- whichever _build_info variant ("unknown"/"" + fallback, or meson-generated real values) is currently installed -- not + just the bare package version (Requirement 2.2). dirty-content- + fingerprint task 5.2 note: when DIRTY_FINGERPRINT is empty, the commit + display is GIT_COMMIT verbatim (unchanged from the original + build-time-versioning feature's behavior, asserted directly below by + ``test_compose_version_omits_suffix_when_build_info_fingerprint_empty``); + when non-empty (e.g. a wheel built from this dirty tree), + ``_compose_version_display()``'s strip-and-append rule means the + composed display -- not the raw GIT_COMMIT string, which may still carry + an unstripped ``-dirty`` suffix -- is what appears in --version output. + This test therefore computes the same expected display the production + code does, rather than asserting GIT_COMMIT literally, so it holds for + both fallback and real-value installs.""" from xceadmin import _build_info runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert result.exit_code == 0, result.output - assert _build_info.GIT_COMMIT in result.output + expected_commit_display = cli_module._compose_version_display( + _build_info.GIT_COMMIT, _build_info.DIRTY_FINGERPRINT + ) + assert expected_commit_display in result.output assert _build_info.BUILD_TIME in result.output @@ -121,6 +134,82 @@ def test_build_info_module_is_importable_with_string_fields() -> None: assert isinstance(_build_info.BUILD_TIME, str) and _build_info.BUILD_TIME +# --------------------------------------------------------------------------- +# dirty-content-fingerprint task 5.2: DIRTY_FINGERPRINT is a third +# _build_info field (empty-string fallback, not "unknown" -- see +# _build_info.py's docstring) and cli.py's _compose_version_display() +# applies the same strip-and-append rule already approved for the C daemons +# (xce_compose_version_ex(), src/lib/utils.c task 3.1) and the Go binaries +# (composeVersionDisplay(), apiserver/cmd/main.go and xcecli/cmd/main.go +# tasks 4.4/4.5). Tested directly as a pure function (mirroring this file's +# own _format_timestamp_ns precedent) since _compose_version() is evaluated +# once at module-import time to build click.version_option()'s default, so +# monkeypatching _build_info after import would not be observed by a +# `runner.invoke(cli, ["--version"])` call. +# --------------------------------------------------------------------------- + + +def test_compose_version_display_strips_dirty_suffix_and_appends_fingerprint() -> None: + """dirty_fingerprint non-empty + git_commit ending in '-dirty' -> the + suffix is stripped and the fingerprint appended in its place + (-, not -dirty-) -- Requirements + 1.1, 1.2, 1.3.""" + assert cli_module._compose_version_display("abc1234-dirty", "f00dcafe") == "abc1234-f00dcafe" + + +def test_compose_version_display_empty_fingerprint_clean_commit_passthrough() -> None: + """dirty_fingerprint empty + a clean (no -dirty suffix) git_commit -> + git_commit returned exactly as-is -- Requirement 2.1.""" + assert cli_module._compose_version_display("abc1234", "") == "abc1234" + + +def test_compose_version_display_empty_fingerprint_dirty_commit_unchanged() -> None: + """dirty_fingerprint empty + git_commit already ending in '-dirty' + (fingerprint computation failed, tree still dirty) -> git_commit passed + through completely unchanged, '-dirty' left in place -- this is + Requirement 5.2's fallback, satisfied by simply not touching commit_id + when dirty_fingerprint is empty.""" + assert cli_module._compose_version_display("abc1234-dirty", "") == "abc1234-dirty" + + +def test_compose_version_display_anchored_suffix_not_blind_substring() -> None: + """A regression case matching the pattern already established in the Go + tests (tasks 4.4/4.5): '-dirty' appearing elsewhere in the commit + string (not as the trailing suffix) must NOT be stripped -- only a + literal trailing '-dirty' is anchored-matched and removed.""" + assert ( + cli_module._compose_version_display("abc-dirty1234", "f00dcafe") == "abc-dirty1234-f00dcafe" + ) + + +def test_compose_version_includes_fingerprint_when_build_info_has_one(monkeypatch) -> None: + """_compose_version() (the function that actually feeds + click.version_option()) applies _compose_version_display() against the + live _build_info module's GIT_COMMIT/DIRTY_FINGERPRINT fields -- + Requirement 4.1.""" + monkeypatch.setattr(cli_module._build_info, "GIT_COMMIT", "abc1234-dirty") + monkeypatch.setattr(cli_module._build_info, "DIRTY_FINGERPRINT", "f00dcafe") + + version_str = cli_module._compose_version() + + assert "abc1234-f00dcafe" in version_str + assert "abc1234-dirty-f00dcafe" not in version_str + assert "-dirty" not in version_str + + +def test_compose_version_omits_suffix_when_build_info_fingerprint_empty(monkeypatch) -> None: + """The committed fallback stub's DIRTY_FINGERPRINT = "" case: --version + shows GIT_COMMIT exactly as _build_info carries it, no fingerprint + suffix appended -- e.g. the editable-install path's GIT_COMMIT = + "unknown" stays "unknown" verbatim (Requirement 2.1, 5.1).""" + monkeypatch.setattr(cli_module._build_info, "GIT_COMMIT", "unknown") + monkeypatch.setattr(cli_module._build_info, "DIRTY_FINGERPRINT", "") + + version_str = cli_module._compose_version() + + assert "(unknown," in version_str + + # --------------------------------------------------------------------------- # config build # ---------------------------------------------------------------------------