diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 55a4ac617..baf2e34b7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6619,6 +6619,132 @@ TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full) { PASS(); } +/* #2427 rank probes: one bare call, two same-named candidates, and the + * registry's test ranking alone decides which one the call binds. Three runs + * on one database: a full index, then a comment-only edit of the caller on + * the closure-repair route, then another on the forced legacy-partial route. + * Both incremental routes re-resolve the caller against a registry seeded from + * stored nodes rather than the extractor's definitions, which is where a + * seeder that loses the verdict shows. */ +typedef struct { + int want[3]; + int other[3]; + cbm_incremental_route_t route[3]; +} rank_probe_t; + +static int rank_probe_count(const char *db, const char *project, const char *source, + const char *target, const char *file) { + cbm_store_t *s = cbm_store_open_path(db); + if (!s) { + return -1; + } + int n = named_edge_to_file_count(s, project, "CALLS", source, target, file); + cbm_store_close(s); + return n; +} + +static void rank_probe(const char *tmp, const char *caller_path, const char *caller_src, + const char *source, const char *target, const char *want_file, + const char *other_file, rank_probe_t *out) { + char db[512]; + snprintf(db, sizeof(db), "%s/rank.db", tmp); + for (int run = 0; run < 3; run++) { + out->want[run] = -1; + out->other[run] = -1; + out->route[run] = CBM_INCREMENTAL_ROUTE_NONE; + if (run > 0) { + char edited[512]; + snprintf(edited, sizeof(edited), "%s// edit %d\n", caller_src, run); + write_temp_file(tmp, caller_path, edited); + } + cbm_pipeline_incremental_test_reset_faults(); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + if (run == 2) { + cbm_pipeline_incremental_test_force_legacy_partial_once(); + } +#endif + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + if (!p) { + return; + } + int rc = cbm_pipeline_run(p); + out->route[run] = cbm_pipeline_incremental_test_last_route(); + char project[256]; + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_pipeline_free(p); + if (rc != 0) { + return; + } + out->want[run] = rank_probe_count(db, project, source, target, want_file); + out->other[run] = rank_probe_count(db, project, source, target, other_file); + } +} + +/* "latest" spells "test", so the QN scan the extractor's verdict replaced + * ranked the production Parse below a helper in a _test.go file, whose Go QN + * names only its package and so spells nothing. The verdict ranks them the + * right way round, and both incremental routes must read the same verdict + * back from the stored nodes: the closure-repair proxies once carried no + * is_test, every candidate scored as production, and the smaller QN -- the + * test helper -- won. */ +TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_verdict_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package app\n\nfunc Run() int { return Parse() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "latest/parse.go", "package latest\n\nfunc Parse() int { return 1 }\n"); + write_temp_file(tmp, "harness/parse_test.go", + "package harness\n\nfunc Parse() int { return 2 }\n"); + write_temp_file(tmp, "app/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "app/run.go", caller, "Run", "Parse", "latest/parse.go", + "harness/parse_test.go", &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + +/* A generated fake beside its callers: client/fake shares more of the caller's + * package path than the real store does, so on the extractor's verdict alone + * (a fake is not a test file) the fake wins on proximity. The test-double path + * rule ranks it last in the full index and, through the incremental seed's own + * use of the rule, on both incremental routes. */ +TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_double_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package client\n\nfunc Run() int { return Load() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "store/load.go", "package store\n\nfunc Load() int { return 1 }\n"); + write_temp_file(tmp, "client/fake/load.go", "package fake\n\nfunc Load() int { return 2 }\n"); + write_temp_file(tmp, "client/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "client/run.go", caller, "Run", "Load", "store/load.go", "client/fake/load.go", + &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + /* A semantic control file can change an unchanged source file's exact target. * Both target modules deliberately export the same short name: without the * tsconfig path mapping, project-wide unique-name fallback is ambiguous and @@ -34486,6 +34612,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity); RUN_TEST(pipeline_complexity_props_independent_of_worker_order); RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full); + RUN_TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental); + RUN_TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental); RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference); RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API diff --git a/tests/test_registry.c b/tests/test_registry.c index 99bb0b523..10c9ef8e5 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -243,6 +243,68 @@ TEST(registry_sole_test_candidate_still_resolves) { PASS(); } +/* The test-double path rule: whole directory segments named fake, mock, mocks, + * testing or testutil, and Go files named mock_*.go or fake_*.go. Anchored, so + * the substrings the old QN scan tripped on (inspect, latest, contest) and + * near-miss names (fakeroot, mockery, testingutil) stay ordinary code. */ +TEST(registry_test_double_path_table) { + static const char *const doubles[] = { + "pkg/client/fake/clientset.go", + "fake/x.go", + "internal/mocks/store.go", + "pkg/mock/store.go", + "pkg/testing/fixtures.go", + "internal/testutil/helpers.go", + "pkg/mock_store.go", + "pkg/fake_clock.go", + "mock_store.go", + "src/fake/deep/nested/util.py", + }; + static const char *const ordinary[] = { + "pkg/inspect/x.go", "pkg/latest/x.go", "pkg/contest/x.go", "pkg/fakeroot/x.go", + "pkg/mockery/x.go", "pkg/testingutil/x.go", "pkg/mocking.go", "pkg/mock_store.py", + "pkg/fake_clock.golang", "pkg/fake.go", "pkg/x/fake", "pkg/Fake/x.go", + "pkg/myfake/x.go", "pkg/fake-client/x.go", "testing.go", "", + }; + char why[256]; + for (size_t i = 0; i < sizeof(doubles) / sizeof(doubles[0]); i++) { + if (!cbm_registry_test_double_path(doubles[i])) { + snprintf(why, sizeof(why), "expected a test-double path: %s", doubles[i]); + FAIL(why); + } + } + for (size_t i = 0; i < sizeof(ordinary) / sizeof(ordinary[0]); i++) { + if (cbm_registry_test_double_path(ordinary[i])) { + snprintf(why, sizeof(why), "expected an ordinary path: %s", ordinary[i]); + FAIL(why); + } + } + ASSERT_FALSE(cbm_registry_test_double_path(NULL)); + PASS(); +} + +/* The Swift overload choosers score with the verdicts stored at registration, + * carried through their own filtered lists. Both overloads fit the call and tie + * on proximity; without the verdicts the smaller QN (the test double) would win + * the tie. */ +TEST(registry_swift_overload_choice_uses_stored_verdicts) { + cbm_registry_t *r = cbm_registry_new(); + const char *double_qn = "proj.Fixtures.Store.load(path:String)"; + const char *real_qn = "proj.Zeta.Store.load(path:String)"; + cbm_registry_add(r, "load", double_qn, "Method", true); + cbm_registry_add(r, "load", real_qn, "Method", false); + cbm_registry_set_swift_signature(r, double_qn, 0, 1, NULL); + cbm_registry_set_swift_signature(r, real_qn, 0, 1, NULL); + + CBMCallArg arg = {.keyword = "path"}; + CBMCall call = {.callee_name = "load", .args = &arg, .arg_count = 1}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], real_qn); + cbm_registry_free(r); + PASS(); +} + TEST(registry_label_of) { cbm_registry_t *r = cbm_registry_new(); cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class", false); @@ -3382,6 +3444,8 @@ SUITE(registry) { RUN_TEST(registry_prefers_non_test_over_test_candidate); RUN_TEST(registry_test_verdict_comes_from_the_caller_not_the_qn); RUN_TEST(registry_sole_test_candidate_still_resolves); + RUN_TEST(registry_test_double_path_table); + RUN_TEST(registry_swift_overload_choice_uses_stored_verdicts); RUN_TEST(registry_label_of); RUN_TEST(registry_value_target_field_twin); RUN_TEST(registry_find_by_name);