feat(dirty-content-fingerprint): add --version-only composition with dirty_fingerprint
Adds xce_compose_version_ex() as a sibling of the existing, completely untouched xce_compose_version(): when dirty_fingerprint is non-empty, it strips a trailing -dirty from the commit field (anchored tail-slice strcmp, not a substring search) and appends -<dirty_fingerprint> instead; when empty/NULL, it delegates directly to the original function, so output is structurally guaranteed byte-identical -- including the Requirement 5.2 fallback case (dirty tree, fingerprint computation failed) where commit_id's own pre-existing -dirty suffix already signals "modified" with no new sentinel needed. No main.c call sites touched -- consuming daemons (tasks 3.2/3.3) still call the original xce_compose_version() and will switch to the new function themselves. Verified independently by review, full lib suite on both debug and ASAN profiles: 301 tests, 0 failures, 0 ignored; suffix-anchoring, buffer safety, and the empty-fingerprint delegation path all confirmed correct by hand-tracing the implementation, not just trusting the test results. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc5cba15e1
commit
7ebcba6d49
4 changed files with 178 additions and 5 deletions
|
|
@ -19,11 +19,10 @@
|
|||
- _Boundary: Nix packaging_
|
||||
|
||||
- [ ] 3. Core: C daemon --version fingerprint display
|
||||
- [ ] 3.1 Extend the shared version-composition mechanism with an optional fingerprint field, without touching commit_id
|
||||
- `commit_id`'s own computation and its existing `-dirty` suffix are **not modified** -- this task only changes how the `--version` composition function *displays* it
|
||||
- `xce_compose_version()` (or a sibling function reusing its internals) gains an optional `dirty_fingerprint` parameter, consumed only by the `--version` call path: when non-empty, the composition strips a trailing `-dirty` from `commit_id` (if present) and appends `-<dirty_fingerprint>` instead, producing `<commit_id>-<hash>`; when empty, the composition uses `commit_id` completely as-is (already correctly showing the bare hash on a clean tree, or `<hash>-dirty` on a dirty tree where the fingerprint computation failed -- Requirement 5.2, satisfied for free)
|
||||
- The existing signature/call sites used by startup-log composition are left completely unaffected -- no new parameter reaches them, and `commit_id` is passed to them exactly as before
|
||||
- Observable: new unit tests in `test_utils.c` confirm all three cases (empty field + clean `commit_id` -> `<commit_id>` alone; empty field + `-dirty`-suffixed `commit_id` -> `<commit_id>-dirty` unchanged; non-empty field -> `-dirty` stripped and replaced with `-<hash>`) and confirm the startup-log-facing composition path's existing tests still pass completely unmodified
|
||||
- [x] 3.1 Extend the shared version-composition mechanism with an optional fingerprint field, without touching commit_id
|
||||
- New sibling function `xce_compose_version_ex()` added alongside the completely untouched `xce_compose_version()` (zero diff lines in its body or its 3 pre-existing tests); anchors the `-dirty` strip via a true tail-slice `strcmp` (not `strstr`), so it cannot false-match a `-dirty` substring elsewhere; correctly appends `-<fingerprint>` with exactly one dash even when the commit field has no `-dirty` to strip; delegates via a genuine `return xce_compose_version(...)` call (not a re-implementation) when the fingerprint is empty/NULL, structurally guaranteeing byte-identical output, proven by a dedicated equivalence regression test
|
||||
- No `main.c` call sites were touched by this task (grep-confirmed all still call the original, untouched `xce_compose_version()`) -- that's tasks 3.2/3.3's job
|
||||
- Observable confirmed live (independently re-verified by review, full lib suite on both debug and ASAN profiles): 301 tests, 0 failures, 0 ignored on both profiles; all 3 required cases present with byte-exact assertions, plus 2 meaningful additional regression/defensive tests; buffer handling confirmed snprintf-bounded with no overflow risk
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 3.1, 3.2, 5.2_
|
||||
- _Boundary: utils.h, utils.c_
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,21 @@ extern char* verbose_to_loglevel(int);
|
|||
extern const char* xce_version_field(const char*);
|
||||
extern int xce_compose_version(char*, size_t, const char*, const char*,
|
||||
const char*);
|
||||
/* dirty-content-fingerprint task 3.1: --version-only sibling of
|
||||
* xce_compose_version() above. Same composition (prefix + commit +
|
||||
* " built " + build_time), plus one more optional field: when
|
||||
* dirty_fingerprint is non-empty, a trailing "-dirty" on commit is
|
||||
* stripped and "-<dirty_fingerprint>" is appended in its place, producing
|
||||
* "<commit>-<dirty_fingerprint>". When dirty_fingerprint is NULL or empty,
|
||||
* this function delegates straight to xce_compose_version() and produces
|
||||
* byte-identical output -- commit_id's own computation and its existing
|
||||
* -dirty suffix are never touched by this function. The startup-log
|
||||
* composition call sites (controller/scheduler/worker main.c) must keep
|
||||
* calling xce_compose_version() directly, never this function -- this is
|
||||
* what keeps the startup log's format unchanged per Requirement 3.2 of the
|
||||
* dirty-content-fingerprint spec. */
|
||||
extern int xce_compose_version_ex(char*, size_t, const char*, const char*,
|
||||
const char*, const char*);
|
||||
extern void set_error(char*, char*, int, int);
|
||||
extern ectx_t* get_ectx(void);
|
||||
extern int xce_vprintf(const char*, va_list);
|
||||
|
|
|
|||
|
|
@ -189,6 +189,95 @@ void test_compose_version_null_fields(void)
|
|||
TEST_ASSERT_EQUAL_STRING("xce v0.0.1-unknown built unknown", buf);
|
||||
}
|
||||
|
||||
/* dirty-content-fingerprint task 3.1: xce_compose_version_ex(), the
|
||||
* --version-only sibling of xce_compose_version(). commit_id's own
|
||||
* computation/its "-dirty" suffix are never touched anywhere -- these
|
||||
* tests cover exactly the 3 cases from the task's Observable, plus a
|
||||
* direct regression proof that the startup-log-facing xce_compose_version()
|
||||
* is unaffected. */
|
||||
|
||||
/* Case: non-empty dirty_fingerprint + a "-dirty"-suffixed commit_id -> the
|
||||
* "-dirty" suffix is stripped and replaced with "-<dirty_fingerprint>"
|
||||
* (not "-dirty-<fingerprint>", not "-dirty<fingerprint>"). */
|
||||
void test_compose_version_ex_fingerprint_strips_dirty_suffix(void)
|
||||
{
|
||||
char buf[128];
|
||||
int n;
|
||||
|
||||
n = xce_compose_version_ex(buf, sizeof(buf), "xce v0.0.1-", "a1b2c3d-dirty",
|
||||
"2026-08-20T10:00:00Z", "a1b2c3d4");
|
||||
|
||||
TEST_ASSERT_TRUE(n > 0);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"xce v0.0.1-a1b2c3d-a1b2c3d4 built 2026-08-20T10:00:00Z", buf);
|
||||
}
|
||||
|
||||
/* Case: empty dirty_fingerprint + a clean commit_id (no "-dirty" suffix) ->
|
||||
* output is the commit_id alone, byte-identical to the no-fingerprint
|
||||
* composition. */
|
||||
void test_compose_version_ex_empty_fingerprint_clean_commit(void)
|
||||
{
|
||||
char buf[128];
|
||||
int n;
|
||||
|
||||
n = xce_compose_version_ex(buf, sizeof(buf), "xce v0.0.1-", "a1b2c3d",
|
||||
"2026-08-20T10:00:00Z", "");
|
||||
|
||||
TEST_ASSERT_TRUE(n > 0);
|
||||
TEST_ASSERT_EQUAL_STRING("xce v0.0.1-a1b2c3d built 2026-08-20T10:00:00Z",
|
||||
buf);
|
||||
}
|
||||
|
||||
/* Case: empty dirty_fingerprint + a "-dirty"-suffixed commit_id -> output is
|
||||
* UNCHANGED, i.e. still shows "<commit_id>-dirty" -- Requirement 5.2's
|
||||
* fallback, proving this function does NOT alter this case at all. */
|
||||
void test_compose_version_ex_empty_fingerprint_dirty_commit_unchanged(void)
|
||||
{
|
||||
char buf[128];
|
||||
int n;
|
||||
|
||||
n = xce_compose_version_ex(buf, sizeof(buf), "xce v0.0.1-", "a1b2c3d-dirty",
|
||||
"2026-08-20T10:00:00Z", "");
|
||||
|
||||
TEST_ASSERT_TRUE(n > 0);
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"xce v0.0.1-a1b2c3d-dirty built 2026-08-20T10:00:00Z", buf);
|
||||
}
|
||||
|
||||
/* Regression: a NULL dirty_fingerprint (as well as "") must delegate the
|
||||
* same as an empty one -- defensive NULL-safety, matching this file's
|
||||
* existing test_compose_version_null_fields() convention for the other
|
||||
* fields. */
|
||||
void test_compose_version_ex_null_fingerprint_matches_empty(void)
|
||||
{
|
||||
char buf[128];
|
||||
|
||||
xce_compose_version_ex(buf, sizeof(buf), "xce v0.0.1-", "a1b2c3d-dirty",
|
||||
"2026-08-20T10:00:00Z", NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"xce v0.0.1-a1b2c3d-dirty built 2026-08-20T10:00:00Z", buf);
|
||||
}
|
||||
|
||||
/* Regression: the startup-log-facing xce_compose_version() itself is
|
||||
* unaffected by this task -- for any given inputs, calling it directly
|
||||
* produces byte-identical output to calling xce_compose_version_ex() with
|
||||
* an empty dirty_fingerprint (its documented delegate-through path). This
|
||||
* is the concrete proof that no shared internals were changed underneath
|
||||
* the already-shipped function. */
|
||||
void test_compose_version_ex_matches_original_when_fingerprint_absent(void)
|
||||
{
|
||||
char buf_orig[128];
|
||||
char buf_ex[128];
|
||||
|
||||
xce_compose_version(buf_orig, sizeof(buf_orig), "xce v0.0.1-",
|
||||
"a1b2c3d-dirty", "2026-08-20T10:00:00Z");
|
||||
xce_compose_version_ex(buf_ex, sizeof(buf_ex), "xce v0.0.1-",
|
||||
"a1b2c3d-dirty", "2026-08-20T10:00:00Z", "");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(buf_orig, buf_ex);
|
||||
}
|
||||
|
||||
void test_utils(void)
|
||||
{
|
||||
printf("\n>> utils test\n\n");
|
||||
|
|
@ -204,4 +293,9 @@ void test_utils(void)
|
|||
RUN_TEST(test_compose_version_both_set);
|
||||
RUN_TEST(test_compose_version_both_unset);
|
||||
RUN_TEST(test_compose_version_null_fields);
|
||||
RUN_TEST(test_compose_version_ex_fingerprint_strips_dirty_suffix);
|
||||
RUN_TEST(test_compose_version_ex_empty_fingerprint_clean_commit);
|
||||
RUN_TEST(test_compose_version_ex_empty_fingerprint_dirty_commit_unchanged);
|
||||
RUN_TEST(test_compose_version_ex_null_fingerprint_matches_empty);
|
||||
RUN_TEST(test_compose_version_ex_matches_original_when_fingerprint_absent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,6 +337,71 @@ int xce_compose_version(char* buf, size_t bufsz, const char* prefix,
|
|||
xce_version_field(commit), xce_version_field(build_time));
|
||||
}
|
||||
|
||||
/* dirty-content-fingerprint task 3.1: buffer for the commit-id portion
|
||||
* after an optional "-dirty" strip + "-<dirty_fingerprint>" append, built
|
||||
* by xce_compose_version_ex() below before handing it to
|
||||
* xce_compose_version()'s own "%s%s built %s" formatting. Sized generously
|
||||
* for a full 40-hex-char SHA-1 commit id + "-dirty" (never both present at
|
||||
* once, but sized as if they were) + "-" + a generous fingerprint length. */
|
||||
#define XCE_COMPOSE_COMMIT_BUF_SIZE 128
|
||||
|
||||
static const char XCE_DIRTY_SUFFIX[] = "-dirty";
|
||||
#define XCE_DIRTY_SUFFIX_LEN (sizeof(XCE_DIRTY_SUFFIX) - 1)
|
||||
|
||||
/* dirty-content-fingerprint task 3.1: --version-only sibling of
|
||||
* xce_compose_version() above -- see the doc comment on its extern
|
||||
* declaration in utils.h for the full contract. commit_id's own
|
||||
* computation and its existing "-dirty" suffix (both from the
|
||||
* already-shipped build-time-versioning feature) are NOT modified by this
|
||||
* function or by anything it calls -- it only changes what gets composed
|
||||
* into the returned --version display string. The startup-log composition
|
||||
* call sites must keep calling xce_compose_version() directly and never
|
||||
* this function. */
|
||||
int xce_compose_version_ex(char* buf, size_t bufsz, const char* prefix,
|
||||
const char* commit, const char* build_time,
|
||||
const char* dirty_fingerprint)
|
||||
{
|
||||
const char* commit_field;
|
||||
size_t commit_len;
|
||||
char commit_buf[XCE_COMPOSE_COMMIT_BUF_SIZE];
|
||||
|
||||
if (buf == NULL || bufsz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dirty_fingerprint == NULL || dirty_fingerprint[0] == '\0') {
|
||||
/* No fingerprint available -- delegate straight to the untouched,
|
||||
* already-shipped function. Byte-identical output, including its
|
||||
* own "unknown" fallback for a NULL/empty commit, and including
|
||||
* commit's own "-dirty" suffix (if any) left completely as-is --
|
||||
* this is Requirement 5.2's fallback, satisfied for free. */
|
||||
return xce_compose_version(buf, bufsz, prefix, commit, build_time);
|
||||
}
|
||||
|
||||
/* dirty_fingerprint is non-empty: strip a literal trailing "-dirty"
|
||||
* from the commit field (anchored to the actual suffix position, not a
|
||||
* blind substring removal -- see design.md's Risks section) and append
|
||||
* "-<dirty_fingerprint>" in its place. */
|
||||
commit_field = xce_version_field(commit);
|
||||
commit_len = strlen(commit_field);
|
||||
|
||||
if (commit_len >= XCE_DIRTY_SUFFIX_LEN
|
||||
&& strcmp(commit_field + (commit_len - XCE_DIRTY_SUFFIX_LEN),
|
||||
XCE_DIRTY_SUFFIX)
|
||||
== 0) {
|
||||
snprintf(commit_buf, sizeof(commit_buf), "%.*s-%s",
|
||||
(int)(commit_len - XCE_DIRTY_SUFFIX_LEN), commit_field,
|
||||
dirty_fingerprint);
|
||||
} else {
|
||||
snprintf(commit_buf, sizeof(commit_buf), "%s-%s", commit_field,
|
||||
dirty_fingerprint);
|
||||
}
|
||||
|
||||
return snprintf(buf, bufsz, "%s%s built %s",
|
||||
(prefix != NULL) ? prefix : "", commit_buf,
|
||||
xce_version_field(build_time));
|
||||
}
|
||||
|
||||
static _Thread_local ectx_t ectx;
|
||||
|
||||
void set_error(char* file, char* func, int line, int code)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue