diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 9f363922a..f0e7d25a2 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -263,6 +263,10 @@ const char *cbm_pipeline_project_name(const cbm_pipeline_t *p) { return p ? p->project_name : NULL; } +bool cbm_pipeline_persistence(const cbm_pipeline_t *p) { + return p && p->persistence; +} + const char *cbm_pipeline_repo_path(const cbm_pipeline_t *p) { return p ? p->repo_path : NULL; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 67182329e..6c46d2041 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -65,6 +65,9 @@ void cbm_pipeline_cancel(cbm_pipeline_t *p); * owned by the pipeline. Valid until cbm_pipeline_free(). */ const char *cbm_pipeline_project_name(const cbm_pipeline_t *p); +/* Return whether persistent artifact export is enabled for this pipeline. */ +bool cbm_pipeline_persistence(const cbm_pipeline_t *p); + /* Override the derived project name with a sanitized user-provided label. */ bool cbm_pipeline_set_project_name(cbm_pipeline_t *p, const char *name); diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 8b5766978..27c76aa44 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -643,11 +643,11 @@ static void run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_fil * Mode-skipped hash rows are preserved across the rebuild so subsequent * reindexes can correctly distinguish "never indexed" from "indexed but * not visited this pass". */ -static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project, - cbm_file_info_t *files, int file_count, - const cbm_file_hash_t *mode_skipped, int mode_skipped_count, - const char *repo_path, const cbm_coverage_row_t *cov, int cov_count, - const cbm_coverage_meta_t *meta_template) { +static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project, + cbm_file_info_t *files, int file_count, + const cbm_file_hash_t *mode_skipped, int mode_skipped_count, + const char *repo_path, bool persistence, const cbm_coverage_row_t *cov, + int cov_count, const cbm_coverage_meta_t *meta_template) { struct timespec t; cbm_clock_gettime(CLOCK_MONOTONIC, &t); @@ -701,10 +701,19 @@ static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char * cbm_store_close(hash_store); } - /* Auto-update artifact if one already exists (persistence was enabled previously) */ - if (repo_path && cbm_artifact_exists(repo_path)) { - cbm_artifact_export(db_path, repo_path, project, CBM_ARTIFACT_FAST); + /* Create a requested artifact, or cheaply refresh one that already exists. */ + bool artifact_exists = repo_path && cbm_artifact_exists(repo_path); + if (repo_path && (persistence || artifact_exists)) { + int arc = cbm_artifact_export(db_path, repo_path, project, + artifact_exists ? CBM_ARTIFACT_FAST : CBM_ARTIFACT_BEST); + if (arc != 0) { + const char *err = cbm_artifact_export_last_error(); + cbm_log_error("pipeline.err", "phase", "artifact_export", "err", err ? err : "unknown"); + return arc; + } } + + return 0; } /* ── Incremental pipeline entry point ────────────────────────────── */ @@ -752,12 +761,22 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil * that were already preserved by an earlier run) remain intact. */ if (n_changed == 0 && deleted_count == 0) { cbm_log_info("incremental.noop", "reason", "no_changes"); + int arc = 0; + const char *repo_path = cbm_pipeline_repo_path(p); + if (cbm_pipeline_persistence(p) && repo_path && !cbm_artifact_exists(repo_path)) { + arc = cbm_artifact_export(db_path, repo_path, project, CBM_ARTIFACT_BEST); + if (arc != 0) { + const char *err = cbm_artifact_export_last_error(); + cbm_log_error("pipeline.err", "phase", "artifact_export", "err", + err ? err : "unknown"); + } + } free(is_changed); free(deleted); free_mode_skipped(mode_skipped, mode_skipped_count); cbm_store_free_file_hashes(stored, stored_count); cbm_store_close(store); - return 0; + return arc; } cbm_store_free_file_hashes(stored, stored_count); @@ -1010,13 +1029,14 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil .ignored_files_total = run_ignored_total, .coverage_version = 1, }; - dump_and_persist(existing, db_path, project, files, file_count, mode_skipped, - mode_skipped_count, cbm_pipeline_repo_path(p), cov, cov_n, &coverage_meta); + int persist_rc = dump_and_persist(existing, db_path, project, files, file_count, mode_skipped, + mode_skipped_count, cbm_pipeline_repo_path(p), + cbm_pipeline_persistence(p), cov, cov_n, &coverage_meta); free(cov); cbm_store_free_coverage(old_cov, old_cov_count); free_mode_skipped(mode_skipped, mode_skipped_count); cbm_gbuf_free(existing); cbm_log_info("incremental.done", "elapsed_ms", itoa_buf((int)elapsed_ms(t0))); - return 0; + return persist_rc; } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 4f095f385..bb5d3b964 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -11,6 +11,7 @@ #include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test) #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" +#include "pipeline/artifact.h" #include "store/store.h" #include "git/git_context.h" #include "foundation/dump_verify.h" @@ -5646,6 +5647,62 @@ TEST(incremental_full_then_noop) { PASS(); } +TEST(incremental_persistence_creates_first_artifact) { + /* Populate the local DB without persistence, then change a file and + * enable persistence on the incremental path. The first artifact must be + * created here rather than waiting for a later reindex. */ + if (setup_incremental_repo() != 0) { + FAIL("setup failed"); + } + + cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + cbm_pipeline_free(p); + + ASSERT_FALSE(cbm_artifact_exists(g_incr_tmpdir)); + + char path[512]; + snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); + ASSERT_EQ(th_append_file(path, "\n// persistence regression marker\n"), 0); + + p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + cbm_pipeline_set_persistence(p, true); + ASSERT_EQ(cbm_pipeline_run(p), 0); + ASSERT_TRUE(cbm_artifact_exists(g_incr_tmpdir)); + cbm_pipeline_free(p); + + cleanup_incremental_repo(); + PASS(); +} + +TEST(incremental_noop_persistence_creates_first_artifact) { + /* Populate the local DB without persistence, then request persistence + * without changing any files. The incremental no-op path must still + * create the missing artifact. */ + if (setup_incremental_repo() != 0) { + FAIL("setup failed"); + } + + cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + cbm_pipeline_free(p); + + ASSERT_FALSE(cbm_artifact_exists(g_incr_tmpdir)); + + p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + cbm_pipeline_set_persistence(p, true); + ASSERT_EQ(cbm_pipeline_run(p), 0); + ASSERT_TRUE(cbm_artifact_exists(g_incr_tmpdir)); + cbm_pipeline_free(p); + + cleanup_incremental_repo(); + PASS(); +} + TEST(incremental_detects_changed_file) { /* Full index, modify one file, re-index → changed file re-parsed */ if (setup_incremental_repo() != 0) { @@ -7091,6 +7148,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_fastapi_depends_edges); /* Incremental */ RUN_TEST(incremental_full_then_noop); + RUN_TEST(incremental_persistence_creates_first_artifact); + RUN_TEST(incremental_noop_persistence_creates_first_artifact); RUN_TEST(incremental_detects_changed_file); RUN_TEST(incremental_aborts_when_previous_coverage_is_unreadable); RUN_TEST(incremental_detects_deleted_file);