Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
44 changes: 32 additions & 12 deletions src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 ────────────────────────────── */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
59 changes: 59 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading