diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 7b6713810..db3a0bc24 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -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)); diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index aac5b1042..43287f736 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -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; diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 70ce0b2df..b6a692271 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -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) ─────────────────────────────────── */ diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 4447568bc..b07936060 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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);