test(pipeline): cover overlay publish failure

Add a CBM_TEST_FAIL_INCREMENTAL_PHASE value for the overlay producer boundary and share the existing phase-failure helper between incremental and delta code. When injected after overlay generation reservation, the producer marks the overlay generation failed and returns an error.

Add a focused pipeline canary proving opt-in overlay publish failure falls back to canonical exact publish: the changed symbol is present canonically, the failed overlay generation is recorded, no ready overlay generation or reserved canonical generation remains, and dirty rows are cleared after canonical repair.

Validation: initial build failed because the helper included the wrong declaration header; corrected to foundation/platform.h. Final gates: make -f Makefile.cbm build/c/test-runner -j8; CBM_ONLY_SUITE=pipeline build/c/test-runner; bash scripts/check-source-safety.sh; git diff --check; make -f Makefile.cbm cbm -j8.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-04 02:21:53 -04:00
parent fcce029177
commit d9d1d20542
4 changed files with 104 additions and 16 deletions
+5
View File
@@ -1489,6 +1489,11 @@ int cbm_pipeline_publish_overlay_file_delta_batch(cbm_store_t *store,
if (rc != CBM_STORE_OK) {
return rc;
}
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_OVERLAY_PUBLISH)) {
(void)cbm_store_set_overlay_generation_status(store, project, overlay_generation,
CBM_STORE_OVERLAY_STATUS_FAILED);
return CBM_STORE_ERR;
}
const cbm_store_file_delta_t **store_deltas =
malloc((size_t)delta_count * sizeof(*store_deltas));
+6 -16
View File
@@ -38,8 +38,6 @@ enum { INCR_RING_BUF = 4, INCR_RING_MASK = 3, INCR_TS_BUF = 24 };
/* ── Timing helper (same as pipeline.c) ──────────────────────────── */
static const char cbm_incr_test_env_disabled[] = "0";
/* Fork renames this elapsed_ms_incr (vs pipeline.c's elapsed_ms) to avoid an
* ODR/duplicate-symbol collision when both TUs are linked into the same binary. */
static double elapsed_ms_incr(struct timespec start) {
@@ -64,14 +62,6 @@ static const char *itoa_buf_incr(int v) {
static void free_mode_skipped(cbm_file_hash_t *ms, int count);
static void free_deleted_paths(char **deleted, int count);
static bool incr_test_fail_phase_enabled(const char *phase) {
char buf[CBM_SZ_64];
const char *val =
cbm_safe_getenv(CBM_TEST_FAIL_INCREMENTAL_PHASE, buf, sizeof(buf), NULL);
return val && val[0] != '\0' && strcmp(val, cbm_incr_test_env_disabled) != 0 &&
phase && strcmp(val, phase) == 0;
}
static bool incr_is_c_family_header(CBMLanguage lang, const char *rel_path) {
if (!rel_path) {
return false;
@@ -270,7 +260,7 @@ static int find_deleted_files(const char *repo_path, cbm_file_info_t *files, int
}
int rc = CBM_STORE_OK;
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_CLASSIFY_DELETED)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_CLASSIFY_DELETED)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_CLASSIFY_DELETED,
"rc", itoa_buf_incr(CBM_STORE_ERR));
rc = CBM_STORE_ERR;
@@ -720,7 +710,7 @@ static int persist_hashes(cbm_store_t *store, const char *project, cbm_file_info
int current_failed = 0;
int ms_failed = 0;
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_HASH_PERSIST)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_HASH_PERSIST)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_HASH_PERSIST, "rc",
itoa_buf_incr(CBM_STORE_ERR));
return CBM_STORE_ERR;
@@ -840,7 +830,7 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
CBMFileResult **cache = (CBMFileResult **)calloc(ci, sizeof(CBMFileResult *));
if (cache) {
int rc = 0;
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_EXTRACT)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_EXTRACT)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_EXTRACT, "rc",
itoa_buf_incr(CBM_NOT_FOUND));
incr_free_result_cache(cache, ci);
@@ -857,7 +847,7 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
return rc;
}
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_REGISTRY)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_REGISTRY)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_REGISTRY, "rc",
itoa_buf_incr(CBM_NOT_FOUND));
incr_free_result_cache(cache, ci);
@@ -885,7 +875,7 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
* still fires; cross-file resolution is deferred to the
* next full re-index. Pass NULL/0/NULL to make the fused
* step in resolve_worker a no-op. */
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_RESOLVE)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_RESOLVE)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_RESOLVE, "rc",
itoa_buf_incr(CBM_NOT_FOUND));
incr_free_result_cache(cache, ci);
@@ -2162,7 +2152,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
}
}
if (pipeline_rc == 0) {
if (incr_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_POSTPASS)) {
if (cbm_pipeline_test_fail_phase_enabled(CBM_TEST_FAIL_INCREMENTAL_POSTPASS)) {
cbm_log_error("incremental.err", "phase", CBM_TEST_FAIL_INCREMENTAL_POSTPASS, "rc",
itoa_buf_incr(CBM_NOT_FOUND));
pipeline_rc = CBM_NOT_FOUND;
+12
View File
@@ -15,6 +15,7 @@
#include "discover/discover.h"
#include "foundation/hash_table.h"
#include "foundation/str_util.h"
#include "foundation/platform.h"
#include "cbm.h"
#include "service_patterns.h"
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
@@ -189,12 +190,23 @@ static inline void cbm_pipeline_close_call_edge_props(char *props, size_t props_
/* Test-only incremental fault injection. Values name internal phases and are
* intentionally not user configuration. */
#define CBM_TEST_FAIL_INCREMENTAL_PHASE "CBM_TEST_FAIL_INCREMENTAL_PHASE"
#define CBM_TEST_FAIL_INCREMENTAL_DISABLED "0"
#define CBM_TEST_FAIL_INCREMENTAL_EXTRACT "incr_extract"
#define CBM_TEST_FAIL_INCREMENTAL_REGISTRY "incr_registry"
#define CBM_TEST_FAIL_INCREMENTAL_RESOLVE "incr_resolve"
#define CBM_TEST_FAIL_INCREMENTAL_CLASSIFY_DELETED "classify_deleted"
#define CBM_TEST_FAIL_INCREMENTAL_POSTPASS "postpass"
#define CBM_TEST_FAIL_INCREMENTAL_HASH_PERSIST "hash_persist"
#define CBM_TEST_FAIL_INCREMENTAL_OVERLAY_PUBLISH "overlay_publish"
static inline bool cbm_pipeline_test_fail_phase_enabled(const char *phase) {
char buf[CBM_SZ_64];
const char *val =
cbm_safe_getenv(CBM_TEST_FAIL_INCREMENTAL_PHASE, buf, sizeof(buf), NULL);
return val && val[0] != '\0' &&
strcmp(val, CBM_TEST_FAIL_INCREMENTAL_DISABLED) != 0 && phase &&
strcmp(val, phase) == 0;
}
/* ── Pipeline context (internal) ─────────────────────────────────── */
+81
View File
@@ -8568,6 +8568,21 @@ static int pipeline_store_completed_generation_count(const char *db_path, const
CBM_STORE_INDEX_STATUS_COMPLETE);
}
static int pipeline_store_overlay_generation_status_count(const char *db_path,
const char *project,
const char *status) {
cbm_store_t *s = cbm_store_open_path_query(db_path);
if (!s) {
return CBM_STORE_ERR;
}
int count = CBM_STORE_ERR;
if (cbm_store_count_overlay_generations(s, project, status, &count) != CBM_STORE_OK) {
count = CBM_STORE_ERR;
}
cbm_store_close(s);
return count;
}
static int pipeline_store_count_file_rows_sql(const char *db_path, const char *project,
const char *rel_path, const char *sql,
int *out_count) {
@@ -10814,6 +10829,71 @@ TEST(incremental_overlay_publish_repeated_update_keeps_active_view_idempotent) {
PASS();
}
TEST(incremental_overlay_publish_failure_falls_back_to_canonical_exact) {
pipeline_env_snapshot_t fail_env = pipeline_env_save(CBM_TEST_FAIL_INCREMENTAL_PHASE);
if (setup_incremental_repo() != 0) {
FAIL("setup failed");
}
cbm_config_t *cfg = incremental_test_config(g_incr_tmpdir);
ASSERT_NOT_NULL(cfg);
ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_OVERLAY_PUBLISH,
CBM_CONFIG_OVERLAY_PUBLISH_SMALL_DELTAS),
0);
cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
char *project = cbm_strdup(cbm_pipeline_project_name(p));
cbm_pipeline_free(p);
ASSERT_NOT_NULL(project);
ASSERT(!pipeline_store_has_function_name(g_incr_dbpath, project, "OverlayFailureOnly"));
char path[CBM_PATH_MAX];
int n = snprintf(path, sizeof(path), "%s/leaf.go", g_incr_tmpdir);
ASSERT(n >= 0 && (size_t)n < sizeof(path));
ASSERT_EQ(th_write_file(path,
"package main\n\n"
"func Leaf() int {\n\treturn 2\n}\n\n"
"func OverlayFailureOnly() int {\n\treturn 88\n}\n"),
0);
p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
cbm_setenv(CBM_TEST_FAIL_INCREMENTAL_PHASE, CBM_TEST_FAIL_INCREMENTAL_OVERLAY_PUBLISH, 1);
int run_rc = cbm_pipeline_run(p);
pipeline_env_restore(&fail_env);
ASSERT_EQ(run_rc, 0);
ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT);
ASSERT(cbm_pipeline_graph_changed(p));
cbm_pipeline_free(p);
ASSERT_EQ(pipeline_store_overlay_generation_status_count(
g_incr_dbpath, project, CBM_STORE_OVERLAY_STATUS_FAILED),
1);
ASSERT_EQ(pipeline_store_overlay_generation_status_count(
g_incr_dbpath, project, CBM_STORE_OVERLAY_STATUS_READY),
0);
ASSERT_EQ(pipeline_store_generation_status_count(g_incr_dbpath, project,
CBM_STORE_INDEX_STATUS_RESERVED),
0);
ASSERT(pipeline_store_has_function_name(g_incr_dbpath, project, "OverlayFailureOnly"));
int dirty_pending = -1;
int dirty_overlay_ready = -1;
ASSERT_EQ(pipeline_store_dirty_counts(g_incr_dbpath, project, &dirty_pending,
&dirty_overlay_ready),
CBM_STORE_OK);
ASSERT_EQ(dirty_pending, 0);
ASSERT_EQ(dirty_overlay_ready, 0);
free(project);
cbm_config_close(cfg);
cleanup_incremental_repo();
PASS();
}
TEST(incremental_full_mode_keeps_exact_upsert_disabled) {
if (setup_incremental_repo() != 0) {
FAIL("setup failed");
@@ -12980,6 +13060,7 @@ SUITE(pipeline) {
RUN_TEST(incremental_overlay_publish_small_deltas_keeps_canonical_base_visible);
RUN_TEST(incremental_overlay_publish_delete_keeps_canonical_base_visible);
RUN_TEST(incremental_overlay_publish_repeated_update_keeps_active_view_idempotent);
RUN_TEST(incremental_overlay_publish_failure_falls_back_to_canonical_exact);
RUN_TEST(incremental_full_mode_keeps_exact_upsert_disabled);
RUN_TEST(incremental_detects_same_size_rewrite_with_preserved_mtime);
RUN_TEST(incremental_missing_file_state_keeps_legacy_metadata_path);