mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-15 12:40:17 +08:00
Merge branch 'main' into fix/test-impact-gate
This commit is contained in:
@@ -467,7 +467,14 @@ jobs:
|
||||
run: scripts/ci/msan-lane.sh build
|
||||
|
||||
- name: MSan suite (exclusions per scripts/msan.sh)
|
||||
run: scripts/ci/msan-lane.sh run
|
||||
# Reduce mmap ASLR entropy on the host first, as the TSan leg does: with
|
||||
# the runner kernel's higher default the MSan runtime tries to re-exec
|
||||
# itself with ADDR_NO_RANDOMIZE, which the container's seccomp profile
|
||||
# refuses, and it aborts ("CHECK failed: msan_linux.cpp:193 ...
|
||||
# personality") before any test runs. The container shares this kernel.
|
||||
run: |
|
||||
sudo sysctl -w vm.mmap_rnd_bits=28 || true
|
||||
scripts/ci/msan-lane.sh run
|
||||
|
||||
# macOS leak coverage. LeakSanitizer runs by default under ASan on Linux, so
|
||||
# test-unix's ubuntu legs have always had it; on macOS it is off by default
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* test_hook_augment.py guards #618 (fixed by #619)
|
||||
* test_ui_drive_listing.py guards #548 (roots field)
|
||||
* test_cli_non_ascii_arg.py guards #423/#20 (wide-argv main())
|
||||
* test_mcp_stdio.py guards #654 (clean stdio stderr)
|
||||
* test_daemon_stability.py guards the daemon parameter
|
||||
surface, crash recovery, busy-stop refusal, and churn
|
||||
* test_windows_update_handoff.py guards that `update`
|
||||
@@ -166,6 +167,7 @@ $guards = @(
|
||||
"tests\windows\test_hook_augment.py",
|
||||
"tests\windows\test_ui_drive_listing.py",
|
||||
"tests\windows\test_cli_non_ascii_arg.py",
|
||||
"tests\windows\test_mcp_stdio.py",
|
||||
"tests\windows\test_windows_update_handoff.py"
|
||||
)
|
||||
|
||||
|
||||
+23
-14
@@ -65,8 +65,8 @@
|
||||
* Observability (#858): a fired deadline is otherwise indistinguishable from
|
||||
* "no matches", so the handler first write()s a pre-formatted breadcrumb to
|
||||
* <cache_dir>/logs/hook-augment-timeouts.log (by default
|
||||
* ~/.cache/codebase-memory-mcp/logs; fd and message prepared at arm time —
|
||||
* only async-signal-safe write/_exit in the handler). */
|
||||
* ~/.cache/codebase-memory-mcp/logs) and to stderr (fd and message prepared at
|
||||
* arm time — only async-signal-safe write/_exit in the handler). */
|
||||
#ifndef _WIN32
|
||||
#define HA_DEADLINE_DEFAULT_MS 2000 /* in-process budget; see ha_deadline_ms() */
|
||||
#define HA_DEADLINE_MIN_MS 50
|
||||
@@ -102,9 +102,16 @@ static size_t g_ha_crumb_len = 0;
|
||||
|
||||
static void ha_deadline_exit(int sig) {
|
||||
(void)sig;
|
||||
if (g_ha_crumb_fd >= 0 && g_ha_crumb_len > 0) {
|
||||
ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len);
|
||||
(void)w;
|
||||
/* async-signal-safe only: write() the prepared breadcrumb to the timeouts
|
||||
* log (when it opened) and ALWAYS to stderr, so a fired deadline is a
|
||||
* visible event, never a silent 0-byte exit(0). */
|
||||
if (g_ha_crumb_len > 0) {
|
||||
if (g_ha_crumb_fd >= 0) {
|
||||
ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len);
|
||||
(void)w;
|
||||
}
|
||||
ssize_t e = write(STDERR_FILENO, g_ha_crumb_msg, g_ha_crumb_len);
|
||||
(void)e;
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
@@ -115,6 +122,16 @@ static bool ha_fits(int written, size_t cap) {
|
||||
}
|
||||
|
||||
static void ha_open_crumb_log(int deadline_ms) {
|
||||
/* Pre-format the breadcrumb unconditionally: the handler may only write() a
|
||||
* prepared buffer, and formatting it before the log path is resolved means
|
||||
* stderr still gets the line even when the file cannot be opened (no HOME,
|
||||
* unwritable cache dir) — the "never silent" guarantee holds regardless. */
|
||||
int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg),
|
||||
"hook-augment: deadline_exceeded ms=%d pid=%ld (raise via "
|
||||
"CBM_HOOK_DEADLINE_MS)\n",
|
||||
deadline_ms, (long)getpid());
|
||||
g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0;
|
||||
|
||||
const char *override = getenv("CBM_HOOK_TIMEOUT_LOG"); /* tests + power users */
|
||||
char path[CBM_SZ_1K];
|
||||
/* A path cut off at the buffer names some other file, and O_CREAT would
|
||||
@@ -136,18 +153,10 @@ static void ha_open_crumb_log(int deadline_ms) {
|
||||
cbm_mkdir_p_ex(dir, 0755, CBM_MKDIR_FOLLOW_OWNED);
|
||||
}
|
||||
/* Startup may re-arm after resolving saved settings. Keep the original
|
||||
* diagnostic descriptor and refresh only the deadline text below. */
|
||||
* diagnostic descriptor; the deadline text above is refreshed on every arm. */
|
||||
if (g_ha_crumb_fd < 0) {
|
||||
g_ha_crumb_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
}
|
||||
if (g_ha_crumb_fd < 0) {
|
||||
return;
|
||||
}
|
||||
int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg),
|
||||
"hook-augment: deadline_exceeded ms=%d pid=%ld (raise via "
|
||||
"CBM_HOOK_DEADLINE_MS)\n",
|
||||
deadline_ms, (long)getpid());
|
||||
g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0;
|
||||
}
|
||||
|
||||
int cbm_hook_augment_deadline_ms_for_testing(void) {
|
||||
|
||||
+11
-1
@@ -121,9 +121,11 @@ static atomic_uint_fast64_t frontend_test_worker_observation_count = ATOMIC_VAR_
|
||||
static atomic_uint_fast64_t frontend_test_worker_idle_count = ATOMIC_VAR_INIT(0);
|
||||
static atomic_uint_fast64_t frontend_test_routed_cancellation_count = ATOMIC_VAR_INIT(0);
|
||||
static atomic_uint_fast32_t frontend_test_replay_budget_ms = ATOMIC_VAR_INIT(0);
|
||||
static atomic_uint_fast32_t frontend_test_monitor_idle_sleep = ATOMIC_VAR_INIT(0);
|
||||
|
||||
void cbm_daemon_frontend_test_observer_reset(bool hold_monitor) {
|
||||
atomic_store_explicit(&frontend_test_monitor_observation_count, 0, memory_order_release);
|
||||
atomic_store_explicit(&frontend_test_monitor_idle_sleep, 0, memory_order_release);
|
||||
atomic_store_explicit(&frontend_test_worker_observation_count, 0, memory_order_release);
|
||||
atomic_store_explicit(&frontend_test_worker_idle_count, 0, memory_order_release);
|
||||
atomic_store_explicit(&frontend_test_monitor_is_waiting, false, memory_order_release);
|
||||
@@ -157,6 +159,10 @@ uint64_t cbm_daemon_frontend_test_routed_cancellations(void) {
|
||||
uint32_t cbm_daemon_frontend_test_replay_timeout_ms(void) {
|
||||
return (uint32_t)atomic_load_explicit(&frontend_test_replay_budget_ms, memory_order_acquire);
|
||||
}
|
||||
|
||||
uint32_t cbm_daemon_frontend_test_monitor_idle_sleep_ms(void) {
|
||||
return (uint32_t)atomic_load_explicit(&frontend_test_monitor_idle_sleep, memory_order_acquire);
|
||||
}
|
||||
#endif
|
||||
|
||||
static cbm_version_cohort_maintenance_presence_t frontend_observe_maintenance(
|
||||
@@ -187,7 +193,11 @@ static void *frontend_maintenance_monitor_worker(void *opaque) {
|
||||
cbm_version_cohort_maintenance_presence_t presence =
|
||||
frontend_observe_maintenance(monitor->manager, true);
|
||||
if (presence == CBM_VERSION_COHORT_MAINTENANCE_ABSENT) {
|
||||
cbm_usleep(FRONTEND_MAINTENANCE_IDLE_POLL_MS * 1000U);
|
||||
uint32_t idle_ms = FRONTEND_MAINTENANCE_IDLE_POLL_MS;
|
||||
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
||||
atomic_store_explicit(&frontend_test_monitor_idle_sleep, idle_ms, memory_order_release);
|
||||
#endif
|
||||
cbm_usleep(idle_ms * 1000U);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,10 @@ uint64_t cbm_daemon_frontend_test_routed_cancellations(void);
|
||||
* settings requests. Recovery tests use distinct connect and startup budgets
|
||||
* and compare. */
|
||||
uint32_t cbm_daemon_frontend_test_replay_timeout_ms(void);
|
||||
/* Milliseconds the maintenance monitor last slept in its no-maintenance
|
||||
* (ABSENT) branch, recorded from the exact value passed to the sleep; zero
|
||||
* until the first idle cycle after reset. Pins the idle probe cadence. */
|
||||
uint32_t cbm_daemon_frontend_test_monitor_idle_sleep_ms(void);
|
||||
#endif
|
||||
|
||||
/* Takes ownership of client and borrows cohort_manager for the complete call.
|
||||
|
||||
+332
-6
@@ -798,6 +798,302 @@ static bool runtime_posix_stat_same_image(const struct stat *first, const struct
|
||||
|
||||
#endif
|
||||
|
||||
/* ── Executable-image fingerprint cache ──────────────────────────────────────
|
||||
* The build-identity fingerprint is a SHA-256 over the entire executable
|
||||
* (cbm_daemon_build_fingerprint_native_file), a full read of a ~300 MB image.
|
||||
* A daemon process otherwise pays it twice for its own image at start (the
|
||||
* supervisor's startup capture, then the runtime service's active-image check),
|
||||
* so the digest of this process's OWN image is cached, keyed by the file
|
||||
* identity the acquire path verifies as stable across the hash: device, inode,
|
||||
* size, mtime and change time (on Windows FILE_BASIC_INFO.ChangeTime). A peer's
|
||||
* image is never cached: it is hashed on every fingerprinting acquire
|
||||
* (re-hashing of a verified peer copy is avoided separately, #1955).
|
||||
*
|
||||
* Limiting the cache to the own image is what makes that key sufficient. On
|
||||
* POSIX the change time moves on every write and cannot be set back. A Windows
|
||||
* change time can be set through SetFileInformationByHandle, but Windows keeps
|
||||
* a running image locked against modification for as long as the process, and
|
||||
* so this cache, exists. A rebuilt or replaced binary rolls the key and misses,
|
||||
* so the cache never returns a stale digest and build-cohort admission keeps
|
||||
* its exact meaning. One entry suffices: a process has one own image.
|
||||
*
|
||||
* The cache is process-local: a fresh process (a hook, a CLI call) still hashes
|
||||
* its own image once. A hit does not weaken the identity proof: the acquire
|
||||
* chain still brackets the held image with its before/after stat and
|
||||
* process-maps checks, so the returned digest is bound to that same verified
|
||||
* image; only the re-read of bytes whose identity is unchanged is skipped. A
|
||||
* freshly hashed digest is committed only after that post-hash check has
|
||||
* passed, under the identity observed after the hash, so an image that changed
|
||||
* under the hash is never cached. */
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || \
|
||||
defined(__NetBSD__)
|
||||
|
||||
/* POSIX carries timespec fields; Windows stores FILETIME ticks in the seconds
|
||||
* fields and leaves the nanoseconds fields zero. */
|
||||
typedef struct {
|
||||
bool valid;
|
||||
uint64_t device;
|
||||
uint64_t inode;
|
||||
uint64_t size;
|
||||
int64_t mtime_seconds;
|
||||
int64_t mtime_nanoseconds;
|
||||
int64_t ctime_seconds;
|
||||
int64_t ctime_nanoseconds;
|
||||
} runtime_fingerprint_key_t;
|
||||
|
||||
/* The single entry: this process's own image. */
|
||||
static atomic_flag runtime_fingerprint_cache_lock = ATOMIC_FLAG_INIT;
|
||||
static runtime_fingerprint_key_t runtime_fingerprint_cache_key;
|
||||
static char runtime_fingerprint_cache_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
||||
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
static atomic_int runtime_fingerprint_hash_call_count;
|
||||
static atomic_bool runtime_fingerprint_hash_stub_active;
|
||||
static char runtime_fingerprint_hash_stub_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
|
||||
#endif
|
||||
|
||||
static bool runtime_fingerprint_key_equal(const runtime_fingerprint_key_t *a,
|
||||
const runtime_fingerprint_key_t *b) {
|
||||
return a->valid && b->valid && a->device == b->device && a->inode == b->inode &&
|
||||
a->size == b->size && a->mtime_seconds == b->mtime_seconds &&
|
||||
a->mtime_nanoseconds == b->mtime_nanoseconds && a->ctime_seconds == b->ctime_seconds &&
|
||||
a->ctime_nanoseconds == b->ctime_nanoseconds;
|
||||
}
|
||||
|
||||
static void runtime_fingerprint_cache_enter(void) {
|
||||
while (
|
||||
atomic_flag_test_and_set_explicit(&runtime_fingerprint_cache_lock, memory_order_acquire)) {}
|
||||
}
|
||||
|
||||
static void runtime_fingerprint_cache_leave(void) {
|
||||
atomic_flag_clear_explicit(&runtime_fingerprint_cache_lock, memory_order_release);
|
||||
}
|
||||
|
||||
static bool runtime_fingerprint_cache_lookup(const runtime_fingerprint_key_t *key,
|
||||
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
if (!key->valid) {
|
||||
return false;
|
||||
}
|
||||
runtime_fingerprint_cache_enter();
|
||||
bool hit = runtime_fingerprint_key_equal(&runtime_fingerprint_cache_key, key);
|
||||
if (hit) {
|
||||
memcpy(out, runtime_fingerprint_cache_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
|
||||
}
|
||||
runtime_fingerprint_cache_leave();
|
||||
return hit;
|
||||
}
|
||||
|
||||
static void runtime_fingerprint_cache_store(
|
||||
const runtime_fingerprint_key_t *key,
|
||||
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
if (!key->valid || fingerprint[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
runtime_fingerprint_cache_enter();
|
||||
runtime_fingerprint_cache_key = *key;
|
||||
memcpy(runtime_fingerprint_cache_digest, fingerprint, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
|
||||
runtime_fingerprint_cache_leave();
|
||||
}
|
||||
|
||||
/* The single point where the expensive image hash is paid; a test seam counts
|
||||
* calls and can substitute a stub digest so the cache's hit/miss behaviour is
|
||||
* observable without a real full-image read. */
|
||||
static bool runtime_fingerprint_hash_native_file(uintptr_t native_file,
|
||||
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
atomic_fetch_add_explicit(&runtime_fingerprint_hash_call_count, 1, memory_order_relaxed);
|
||||
if (atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) {
|
||||
memcpy(out, runtime_fingerprint_hash_stub_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return cbm_daemon_build_fingerprint_native_file(native_file, out);
|
||||
}
|
||||
|
||||
/* Resolve the digest for the image identity `key` of process `process_id`:
|
||||
* from the cache when that process is this one and the cache already holds
|
||||
* that identity, otherwise by hashing the held native image. `cached` reports
|
||||
* which. Every acquire site resolves through here, so this is where the cache
|
||||
* is limited to the own image: a peer's key is marked invalid, which is never
|
||||
* looked up and never committed. A freshly hashed digest is deliberately not
|
||||
* stored here: the acquire site commits it (runtime_fingerprint_cache_commit)
|
||||
* only once its post-hash identity check has proven the hashed bytes belong to
|
||||
* the identity they were keyed on. */
|
||||
static bool runtime_build_fingerprint_resolve(uintptr_t native_file, uint64_t process_id,
|
||||
runtime_fingerprint_key_t *key,
|
||||
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
|
||||
bool *cached) {
|
||||
if (process_id != runtime_current_process_id()) {
|
||||
key->valid = false;
|
||||
}
|
||||
*cached = runtime_fingerprint_cache_lookup(key, fingerprint);
|
||||
return *cached || runtime_fingerprint_hash_native_file(native_file, fingerprint);
|
||||
}
|
||||
|
||||
/* Commit a freshly hashed digest after the acquire chain has passed. The
|
||||
* identity observed after the hash must equal the one the digest was resolved
|
||||
* under; otherwise the bytes read belong to no single identity and are not
|
||||
* cached. A cache hit has nothing to commit, and neither has a peer, whose
|
||||
* resolved key is invalid. */
|
||||
static void runtime_fingerprint_cache_commit(
|
||||
const runtime_fingerprint_key_t *before, const runtime_fingerprint_key_t *after, bool cached,
|
||||
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
if (!cached && runtime_fingerprint_key_equal(before, after)) {
|
||||
runtime_fingerprint_cache_store(after, fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static int64_t runtime_fingerprint_filetime(FILETIME value) {
|
||||
return (int64_t)(((uint64_t)value.dwHighDateTime << 32) | value.dwLowDateTime);
|
||||
}
|
||||
|
||||
/* Key the held image by the snapshot identity the acquire path compares plus
|
||||
* the kernel-maintained change time. An unreadable change time yields an
|
||||
* invalid key, which never hits and is never stored: the image is simply
|
||||
* hashed. */
|
||||
static void runtime_fingerprint_key_windows(HANDLE file,
|
||||
const BY_HANDLE_FILE_INFORMATION *information,
|
||||
const LARGE_INTEGER *size,
|
||||
runtime_fingerprint_key_t *key) {
|
||||
memset(key, 0, sizeof(*key));
|
||||
FILE_BASIC_INFO basic;
|
||||
memset(&basic, 0, sizeof(basic));
|
||||
key->valid = information != NULL && size != NULL && size->QuadPart >= 0 &&
|
||||
file != INVALID_HANDLE_VALUE &&
|
||||
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof(basic)) != 0;
|
||||
if (key->valid) {
|
||||
key->device = information->dwVolumeSerialNumber;
|
||||
key->inode = ((uint64_t)information->nFileIndexHigh << 32) | information->nFileIndexLow;
|
||||
key->size = (uint64_t)size->QuadPart;
|
||||
key->mtime_seconds = runtime_fingerprint_filetime(information->ftLastWriteTime);
|
||||
key->ctime_seconds = basic.ChangeTime.QuadPart;
|
||||
}
|
||||
}
|
||||
|
||||
static bool runtime_build_fingerprint_resolve_windows(
|
||||
HANDLE file, uint64_t process_id, const BY_HANDLE_FILE_INFORMATION *information,
|
||||
const LARGE_INTEGER *size, char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE],
|
||||
runtime_fingerprint_key_t *key, bool *cached) {
|
||||
runtime_fingerprint_key_windows(file, information, size, key);
|
||||
return runtime_build_fingerprint_resolve((uintptr_t)file, process_id, key, fingerprint, cached);
|
||||
}
|
||||
|
||||
static void runtime_fingerprint_cache_commit_windows(
|
||||
HANDLE file, const BY_HANDLE_FILE_INFORMATION *information, const LARGE_INTEGER *size,
|
||||
const runtime_fingerprint_key_t *key, bool cached,
|
||||
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
runtime_fingerprint_key_t key_after;
|
||||
runtime_fingerprint_key_windows(file, information, size, &key_after);
|
||||
runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint);
|
||||
}
|
||||
#else
|
||||
static void runtime_fingerprint_key_posix(const struct stat *status,
|
||||
runtime_fingerprint_key_t *key) {
|
||||
memset(key, 0, sizeof(*key));
|
||||
key->valid = status != NULL && S_ISREG(status->st_mode);
|
||||
if (key->valid) {
|
||||
key->device = (uint64_t)status->st_dev;
|
||||
key->inode = (uint64_t)status->st_ino;
|
||||
key->size = (uint64_t)status->st_size;
|
||||
#ifdef __APPLE__
|
||||
key->mtime_seconds = status->st_mtimespec.tv_sec;
|
||||
key->mtime_nanoseconds = status->st_mtimespec.tv_nsec;
|
||||
key->ctime_seconds = status->st_ctimespec.tv_sec;
|
||||
key->ctime_nanoseconds = status->st_ctimespec.tv_nsec;
|
||||
#else
|
||||
key->mtime_seconds = status->st_mtim.tv_sec;
|
||||
key->mtime_nanoseconds = status->st_mtim.tv_nsec;
|
||||
key->ctime_seconds = status->st_ctim.tv_sec;
|
||||
key->ctime_nanoseconds = status->st_ctim.tv_nsec;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static bool runtime_build_fingerprint_resolve_posix(
|
||||
uintptr_t native_file, uint64_t process_id, const struct stat *status,
|
||||
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE], runtime_fingerprint_key_t *key,
|
||||
bool *cached) {
|
||||
runtime_fingerprint_key_posix(status, key);
|
||||
return runtime_build_fingerprint_resolve(native_file, process_id, key, fingerprint, cached);
|
||||
}
|
||||
|
||||
static void runtime_fingerprint_cache_commit_posix(
|
||||
const struct stat *status, const runtime_fingerprint_key_t *key, bool cached,
|
||||
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
runtime_fingerprint_key_t key_after;
|
||||
runtime_fingerprint_key_posix(status, &key_after);
|
||||
runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void) {
|
||||
runtime_fingerprint_cache_enter();
|
||||
memset(&runtime_fingerprint_cache_key, 0, sizeof(runtime_fingerprint_cache_key));
|
||||
memset(runtime_fingerprint_cache_digest, 0, sizeof(runtime_fingerprint_cache_digest));
|
||||
runtime_fingerprint_cache_leave();
|
||||
atomic_store_explicit(&runtime_fingerprint_hash_call_count, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
|
||||
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
|
||||
}
|
||||
|
||||
void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest) {
|
||||
if (!digest) {
|
||||
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
|
||||
return;
|
||||
}
|
||||
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
|
||||
size_t length = strlen(digest);
|
||||
if (length >= CBM_DAEMON_BUILD_FINGERPRINT_SIZE) {
|
||||
length = CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1;
|
||||
}
|
||||
memcpy(runtime_fingerprint_hash_stub_digest, digest, length);
|
||||
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, true, memory_order_release);
|
||||
}
|
||||
|
||||
int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void) {
|
||||
return atomic_load_explicit(&runtime_fingerprint_hash_call_count, memory_order_relaxed);
|
||||
}
|
||||
|
||||
static runtime_fingerprint_key_t runtime_fingerprint_key_for_testing(
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *identity) {
|
||||
runtime_fingerprint_key_t key = {.valid = true,
|
||||
.device = identity->device,
|
||||
.inode = identity->inode,
|
||||
.size = identity->size,
|
||||
.mtime_seconds = identity->mtime_seconds,
|
||||
.mtime_nanoseconds = identity->mtime_nanoseconds,
|
||||
.ctime_seconds = identity->ctime_seconds,
|
||||
.ctime_nanoseconds = identity->ctime_nanoseconds};
|
||||
return key;
|
||||
}
|
||||
|
||||
bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *before,
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *after,
|
||||
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
|
||||
/* There is no native image behind a synthetic identity: only the stubbed
|
||||
* hash may ever be reached from here. The identity stands for this
|
||||
* process's own image, the only one the cache serves. */
|
||||
if (!before || !after || !out ||
|
||||
!atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
runtime_fingerprint_key_t key_before = runtime_fingerprint_key_for_testing(before);
|
||||
runtime_fingerprint_key_t key_after = runtime_fingerprint_key_for_testing(after);
|
||||
bool cached = false;
|
||||
if (!runtime_build_fingerprint_resolve((uintptr_t)0, runtime_current_process_id(), &key_before,
|
||||
out, &cached)) {
|
||||
return false;
|
||||
}
|
||||
runtime_fingerprint_cache_commit(&key_before, &key_after, cached, out);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* supported platform: executable-image fingerprint cache */
|
||||
|
||||
/* Acquire one process instance's mapped image as a native object. Supplying a
|
||||
* fingerprint hashes that same held object; NULL performs metadata-only
|
||||
* acquisition for the HELLO fast path. Every platform brackets the process
|
||||
@@ -826,6 +1122,8 @@ static bool runtime_process_image_reference_acquire(
|
||||
BY_HANDLE_FILE_INFORMATION file_after;
|
||||
LARGE_INTEGER size_before;
|
||||
LARGE_INTEGER size_after;
|
||||
runtime_fingerprint_key_t fingerprint_key = {0};
|
||||
bool fingerprint_cached = false;
|
||||
bool ok = runtime_windows_process_image_snapshot(process, &process_before);
|
||||
/* QueryFullProcessImageNameW returns a stable identity spelling for the
|
||||
* before/after comparison, but its Win32/DOS form may exceed MAX_PATH.
|
||||
@@ -839,7 +1137,9 @@ static bool runtime_process_image_reference_acquire(
|
||||
: INVALID_HANDLE_VALUE;
|
||||
free(open_path);
|
||||
ok = ok && runtime_windows_file_snapshot(file, &file_before, &size_before) &&
|
||||
(!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)file, fingerprint)) &&
|
||||
(!fingerprint || runtime_build_fingerprint_resolve_windows(
|
||||
file, process_id, &file_before, &size_before, fingerprint,
|
||||
&fingerprint_key, &fingerprint_cached)) &&
|
||||
runtime_windows_file_snapshot(file, &file_after, &size_after) &&
|
||||
runtime_windows_file_snapshot_same(&file_before, &size_before, &file_after, &size_after) &&
|
||||
runtime_windows_process_image_snapshot(process, &process_after) &&
|
||||
@@ -848,6 +1148,10 @@ static bool runtime_process_image_reference_acquire(
|
||||
ok = false;
|
||||
}
|
||||
if (ok) {
|
||||
if (fingerprint) {
|
||||
runtime_fingerprint_cache_commit_windows(
|
||||
file, &file_after, &size_after, &fingerprint_key, fingerprint_cached, fingerprint);
|
||||
}
|
||||
reference->held = true;
|
||||
reference->file = file;
|
||||
reference->information = file_after;
|
||||
@@ -873,14 +1177,22 @@ static bool runtime_process_image_reference_acquire(
|
||||
int fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1;
|
||||
struct stat file_before;
|
||||
struct stat file_after;
|
||||
runtime_fingerprint_key_t fingerprint_key = {0};
|
||||
bool fingerprint_cached = false;
|
||||
ok = ok && fd >= 0 && fstat(fd, &file_before) == 0 && S_ISREG(file_before.st_mode) &&
|
||||
runtime_mac_process_maps_file_executable(pid, &file_before) &&
|
||||
(!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)fd, fingerprint)) &&
|
||||
(!fingerprint || runtime_build_fingerprint_resolve_posix(
|
||||
(uintptr_t)fd, process_id, &file_before, fingerprint,
|
||||
&fingerprint_key, &fingerprint_cached)) &&
|
||||
fstat(fd, &file_after) == 0 && runtime_mac_stat_same(&file_before, &file_after) &&
|
||||
runtime_mac_process_maps_file_executable(pid, &file_after) &&
|
||||
runtime_mac_process_instance(pid, &process_after) &&
|
||||
runtime_mac_process_instance_same(&process_before, &process_after);
|
||||
if (ok) {
|
||||
if (fingerprint) {
|
||||
runtime_fingerprint_cache_commit_posix(&file_after, &fingerprint_key,
|
||||
fingerprint_cached, fingerprint);
|
||||
}
|
||||
reference->held = true;
|
||||
reference->fd = fd;
|
||||
reference->status = file_after;
|
||||
@@ -898,10 +1210,13 @@ static bool runtime_process_image_reference_acquire(
|
||||
int image_fd = process_fd >= 0 ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
|
||||
struct stat image_before;
|
||||
struct stat image_after;
|
||||
runtime_fingerprint_key_t fingerprint_key = {0};
|
||||
bool fingerprint_cached = false;
|
||||
bool ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 &&
|
||||
S_ISREG(image_before.st_mode) &&
|
||||
(!fingerprint ||
|
||||
cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) &&
|
||||
(!fingerprint || runtime_build_fingerprint_resolve_posix(
|
||||
(uintptr_t)image_fd, process_id, &image_before, fingerprint,
|
||||
&fingerprint_key, &fingerprint_cached)) &&
|
||||
fstat(image_fd, &image_after) == 0 &&
|
||||
runtime_posix_stat_same_image(&image_before, &image_after);
|
||||
int verify_fd = ok ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
|
||||
@@ -915,6 +1230,10 @@ static bool runtime_process_image_reference_acquire(
|
||||
ok = false;
|
||||
}
|
||||
if (ok) {
|
||||
if (fingerprint) {
|
||||
runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key,
|
||||
fingerprint_cached, fingerprint);
|
||||
}
|
||||
reference->held = true;
|
||||
reference->fd = image_fd;
|
||||
reference->status = image_after;
|
||||
@@ -940,12 +1259,19 @@ static bool runtime_process_image_reference_acquire(
|
||||
int image_fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1;
|
||||
struct stat image_before;
|
||||
struct stat image_after;
|
||||
runtime_fingerprint_key_t fingerprint_key = {0};
|
||||
bool fingerprint_cached = false;
|
||||
ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) &&
|
||||
(!fingerprint ||
|
||||
cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) &&
|
||||
(!fingerprint || runtime_build_fingerprint_resolve_posix(
|
||||
(uintptr_t)image_fd, process_id, &image_before, fingerprint,
|
||||
&fingerprint_key, &fingerprint_cached)) &&
|
||||
fstat(image_fd, &image_after) == 0 &&
|
||||
runtime_posix_stat_same_image(&image_before, &image_after);
|
||||
if (ok) {
|
||||
if (fingerprint) {
|
||||
runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key,
|
||||
fingerprint_cached, fingerprint);
|
||||
}
|
||||
reference->held = true;
|
||||
reference->fd = image_fd;
|
||||
reference->status = image_after;
|
||||
|
||||
@@ -499,6 +499,31 @@ typedef enum {
|
||||
} cbm_daemon_runtime_hello_point_t;
|
||||
typedef void (*cbm_daemon_runtime_hello_hook_t)(cbm_daemon_runtime_hello_point_t point);
|
||||
void cbm_daemon_runtime_set_hello_hook_for_testing(cbm_daemon_runtime_hello_hook_t hook);
|
||||
|
||||
/* Executable-image fingerprint-cache seams. reset empties the cache, zeroes
|
||||
* the hash counter and disarms the stub; a test that arms the stub must reset
|
||||
* before its first assertion, because an armed stub replaces every later
|
||||
* fingerprint in the process. resolve drives a synthetic file identity through
|
||||
* the exact resolve-then-commit path the acquire sites use: `before` is the
|
||||
* identity the digest is keyed on, `after` the identity observed after the
|
||||
* hash, and the digest is committed only when the two agree. It refuses unless
|
||||
* the stub is armed, since no native image backs a synthetic identity. */
|
||||
typedef struct {
|
||||
uint64_t device;
|
||||
uint64_t inode;
|
||||
uint64_t size;
|
||||
int64_t mtime_seconds;
|
||||
int64_t mtime_nanoseconds;
|
||||
int64_t ctime_seconds;
|
||||
int64_t ctime_nanoseconds;
|
||||
} cbm_daemon_runtime_fingerprint_identity_for_testing_t;
|
||||
void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void);
|
||||
void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest);
|
||||
int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void);
|
||||
bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *before,
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t *after,
|
||||
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]);
|
||||
#endif
|
||||
|
||||
#endif /* CBM_DAEMON_RUNTIME_H */
|
||||
|
||||
@@ -521,13 +521,16 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
|
||||
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
|
||||
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
|
||||
* through the same predicate, so the three registries cannot diverge.
|
||||
* A generated member is registered by qualified name only, at all three. */
|
||||
* A generated member is registered by qualified name only, at all three.
|
||||
* The registry's test verdict is the extractor's OR the test-double path
|
||||
* rule (cbm_registry_test_double_path) on the node's file, at all three. */
|
||||
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
|
||||
if (cbm_def_props_generated(def->extra_props)) {
|
||||
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
|
||||
} else {
|
||||
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
|
||||
def->label, lang, module_prefix_len);
|
||||
def->label, lang, module_prefix_len,
|
||||
def->is_test || cbm_registry_test_double_path(def_rel));
|
||||
}
|
||||
if (def->qn_sig_off) {
|
||||
cbm_registry_set_swift_signature(ctx->registry, def->qualified_name,
|
||||
|
||||
@@ -1816,7 +1816,7 @@ int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
/* `file_node_id` is the defining file's node id (0 when it has none), looked up
|
||||
* once per file by the caller: computing the file QN and finding its node for
|
||||
* every definition was 700 k allocations and lookups on the Go corpus. */
|
||||
static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
|
||||
static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel,
|
||||
int64_t file_node_id, CBMLanguage lang, size_t module_prefix_len,
|
||||
int *reg_entries) {
|
||||
int edges = 0;
|
||||
@@ -1824,14 +1824,17 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
|
||||
return 0;
|
||||
}
|
||||
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
|
||||
* (helpers.c) — see pass_definitions.c for the per-label rationale, and
|
||||
* for the generated member registered by qualified name only. */
|
||||
* (helpers.c) — see pass_definitions.c for the per-label rationale, for
|
||||
* the generated member registered by qualified name only, and for the
|
||||
* test-double path rule, judged on the path the def's node stores. */
|
||||
if (cbm_label_is_registry_symbol(def->label)) {
|
||||
if (cbm_def_props_generated(def->extra_props)) {
|
||||
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
|
||||
} else {
|
||||
const char *def_rel = def->file_path ? def->file_path : rel;
|
||||
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
|
||||
def->label, lang, module_prefix_len);
|
||||
def->label, lang, module_prefix_len,
|
||||
def->is_test || cbm_registry_test_double_path(def_rel));
|
||||
}
|
||||
if (def->qn_sig_off) {
|
||||
cbm_registry_set_swift_signature(ctx->registry, def->qualified_name,
|
||||
@@ -2050,7 +2053,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
: 0;
|
||||
for (int d = 0; d < result->defs.count; d++) {
|
||||
defines_edges +=
|
||||
register_and_link_def(ctx, &result->defs.items[d], file_node_id,
|
||||
register_and_link_def(ctx, &result->defs.items[d], rel, file_node_id,
|
||||
files[i].language, py_prefix_len, ®_entries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ static const char *itoa_log(int val) {
|
||||
}
|
||||
|
||||
/* Check if a node has is_test:true in its properties JSON. */
|
||||
static bool node_is_test(const cbm_gbuf_node_t *n) {
|
||||
bool cbm_node_is_test(const cbm_gbuf_node_t *n) {
|
||||
if (!n || !n->properties_json) {
|
||||
return false;
|
||||
}
|
||||
@@ -270,13 +270,13 @@ static int create_tests_edges(cbm_pipeline_ctx_t *ctx) {
|
||||
}
|
||||
|
||||
bool src_is_test =
|
||||
node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path));
|
||||
cbm_node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path));
|
||||
if (!src_is_test) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool tgt_is_test =
|
||||
node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path));
|
||||
cbm_node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path));
|
||||
if (tgt_is_test) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+19
-4
@@ -283,9 +283,24 @@ cbm_registry_t *cbm_registry_new(void);
|
||||
void cbm_registry_free(cbm_registry_t *r);
|
||||
|
||||
/* Register a function/method/class. All strings are copied. The entry's
|
||||
* language is unknown, so name-based resolution never filters it out. */
|
||||
* language is unknown, so name-based resolution never filters it out.
|
||||
* `is_test` is the extractor's verdict for this symbol (CBMDefinition.is_test:
|
||||
* the file's test-ness OR the definition's own test attributes). The registry
|
||||
* stores it and scores with it; it does not derive test-ness from the QN. */
|
||||
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
|
||||
const char *label);
|
||||
const char *label, bool is_test);
|
||||
|
||||
/* The test-double path rule, for the registry's ranking only: true when the
|
||||
* repo-relative path of a symbol's defining file has a directory segment named
|
||||
* exactly `fake`, `mock`, `mocks`, `testing` or `testutil`, or its file name
|
||||
* matches `mock_*.go` or `fake_*.go`. Go projects keep generated fake
|
||||
* clientsets and mock packages in ordinary files the extractor rightly does
|
||||
* not call tests; a call should still prefer real code over them. The three
|
||||
* registry seeders (pass_definitions.c, pass_parallel.c,
|
||||
* pipeline_incremental.c) pass the extractor's verdict OR this, so full and
|
||||
* incremental indexes rank alike. It never decides what a node is: the stored
|
||||
* is_test property, TESTS edges and importance stay with cbm_is_test_path. */
|
||||
bool cbm_registry_test_double_path(const char *rel_path);
|
||||
|
||||
/* Swift-only metadata and label-compatible overload selection. The returned
|
||||
* QNs are borrowed from the registry and all belong to one base callable.
|
||||
@@ -306,7 +321,7 @@ int cbm_registry_swift_candidates_in_file(const cbm_registry_t *r, const CBMCall
|
||||
* function-local symbols: that needs the defining file's module QN, see
|
||||
* cbm_registry_add_lang_in_module. */
|
||||
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
|
||||
const char *label, CBMLanguage lang);
|
||||
const char *label, CBMLanguage lang, bool is_test);
|
||||
|
||||
/* As cbm_registry_add_lang. A Python symbol declared inside a function body of
|
||||
* its own file (cbm_registry_qn_is_function_local, bounded by
|
||||
@@ -318,7 +333,7 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual
|
||||
* indexes agree. 0 = no local detection. */
|
||||
void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
|
||||
const char *qualified_name, const char *label,
|
||||
CBMLanguage lang, size_t module_prefix_len);
|
||||
CBMLanguage lang, size_t module_prefix_len, bool is_test);
|
||||
|
||||
/* Register a symbol for exact (qualified-name) lookups only: it is never
|
||||
* offered to the short-name strategies (unique_name, suffix_match,
|
||||
|
||||
@@ -623,6 +623,28 @@ static char *delta_generated_props(const char *props, const char *generated) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* `props` (a JSON object) with the extractor's test verdict added as
|
||||
* `"is_test":true` (CBM_MEM_CLASS_OTHER; the caller frees), NULL on allocation
|
||||
* failure. The incremental registry seed reads the verdict back
|
||||
* (cbm_node_is_test) so a re-resolved caller ranks a test definition below
|
||||
* real code exactly as the full build does; an absent key reads as false. */
|
||||
static char *delta_test_props(const char *props) {
|
||||
static const char member[] = "\"is_test\":true";
|
||||
size_t pl = props ? strlen(props) : 0;
|
||||
bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}';
|
||||
size_t cap = pl + sizeof(member) + CBM_SZ_4;
|
||||
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
if (has_members) {
|
||||
snprintf(out, cap, "%.*s,%s}", (int)(pl - SKIP_ONE), props, member);
|
||||
} else {
|
||||
snprintf(out, cap, "{%s}", member);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) {
|
||||
sqlite3 *db = cbm_store_get_db(store);
|
||||
if (!db) {
|
||||
@@ -673,7 +695,13 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
|
||||
* And a generated member's mark (a Java record's implicit accessor, a
|
||||
* Method node): the registry seed reads it to register the member by
|
||||
* qualified name only, as the full build does (cbm_node_props_generated);
|
||||
* without it the proxy came back into the short-name index. */
|
||||
* without it the proxy came back into the short-name index.
|
||||
*
|
||||
* And the extractor's test verdict: the registry seed reads it back
|
||||
* (cbm_node_is_test) to rank a test definition below real code, as the
|
||||
* full build does with CBMDefinition.is_test; without it every proxy
|
||||
* scored as production and a re-resolved caller could bind a test helper
|
||||
* the full build ranks last. */
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(
|
||||
db,
|
||||
@@ -702,7 +730,8 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
|
||||
" THEN json_extract(properties, '$.parent_class') END,"
|
||||
" CASE WHEN label = 'Method'"
|
||||
" AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0"
|
||||
" THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END"
|
||||
" THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END,"
|
||||
" instr(properties, '\"is_test\":true') > 0"
|
||||
" FROM nodes"
|
||||
" WHERE project = ?1 AND label NOT IN"
|
||||
" ('Macro','Comment','Section','Branch','Commit','Tag')"
|
||||
@@ -727,14 +756,27 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
|
||||
int end_line = sqlite3_column_int(stmt, 7);
|
||||
const char *parent = (const char *)sqlite3_column_text(stmt, 8);
|
||||
const char *generated = (const char *)sqlite3_column_text(stmt, 9);
|
||||
bool is_test = sqlite3_column_int(stmt, 10) != 0;
|
||||
char *owner_props =
|
||||
parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL;
|
||||
const char *props = owner_props ? owner_props : (client_props ? client_props : "{}");
|
||||
char *generated_props = delta_generated_props(props, generated);
|
||||
const char *seed_props = generated_props ? generated_props : props;
|
||||
char *test_props = is_test ? delta_test_props(seed_props) : NULL;
|
||||
if (is_test && !test_props) {
|
||||
/* A proxy without its verdict would rank as production code;
|
||||
* the run cannot be trusted, as with an id mismatch below. */
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, generated_props);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, owner_props);
|
||||
sqlite3_finalize(stmt);
|
||||
cbm_log_error("delta.preseed_verdict_alloc_failed", "qn", qn ? qn : "");
|
||||
return -1;
|
||||
}
|
||||
/* Pin the gbuf id to the database id: proxies ARE their rows. */
|
||||
cbm_gbuf_set_next_id(gbuf, id);
|
||||
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line,
|
||||
end_line, generated_props ? generated_props : props);
|
||||
end_line, test_props ? test_props : seed_props);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, test_props);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, generated_props);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, owner_props);
|
||||
if (got != id) {
|
||||
|
||||
@@ -1396,8 +1396,11 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
|
||||
size_t prefix_len = lang == CBM_LANG_PYTHON
|
||||
? cbm_pipeline_fqn_symbol_prefix_len(seed->project, node->file_path)
|
||||
: 0;
|
||||
/* The full index's verdict: the extractor's (stored as is_test) OR the
|
||||
* test-double path rule on the node's file (pass_definitions.c). */
|
||||
bool is_test = cbm_node_is_test(node) || cbm_registry_test_double_path(node->file_path);
|
||||
cbm_registry_add_lang_in_module(seed->registry, node->name, node->qualified_name, node->label,
|
||||
lang, prefix_len);
|
||||
lang, prefix_len, is_test);
|
||||
if (node->properties_json) {
|
||||
const char *mask = strstr(node->properties_json, "\"param_default_mask\":\"");
|
||||
const char *count = strstr(node->properties_json, "\"selector_param_count\":");
|
||||
|
||||
@@ -260,7 +260,8 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char *
|
||||
CBMLanguage lang);
|
||||
/* The CALLS variant: as cbm_registry_resolve_lang, except that a bare C call
|
||||
* (no `.` / `->`) does not consider Field or Method nodes, which it can never
|
||||
* call. */
|
||||
* call, and that in a C# file a built-in type keyword root (`int.TryParse`)
|
||||
* names a type for the receiver chain check (#2121). */
|
||||
cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name,
|
||||
const char *module_qn, const char **import_map_keys,
|
||||
const char **import_map_vals, int import_map_count,
|
||||
@@ -656,6 +657,12 @@ static inline bool cbm_pipeline_test_force_def_modules_null(const cbm_pipeline_c
|
||||
typedef bool (*cbm_result_want_fn)(const CBMFileResult *header);
|
||||
CBMFileResult *cbm_pipeline_result_acquire(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
|
||||
int i, cbm_result_want_fn want, bool *loaded);
|
||||
/* True when a graph-buffer node carries is_test:true in its properties JSON.
|
||||
* The incremental registry seed has only the persisted node, not the
|
||||
* CBMDefinition the definition passes hand to cbm_registry_add, and both must
|
||||
* give the registry the same verdict for the same symbol. */
|
||||
bool cbm_node_is_test(const cbm_gbuf_node_t *n);
|
||||
|
||||
void cbm_pipeline_result_release(CBMFileResult *r, bool loaded);
|
||||
|
||||
/* Log the store counters, close and delete the store, drop the latch. */
|
||||
|
||||
+221
-164
@@ -30,6 +30,7 @@ enum { REG_MAX_CANDIDATES = 256 };
|
||||
#include "foundation/compat.h" /* CBM_TLS */
|
||||
#include "foundation/hash_table.h"
|
||||
#include "foundation/arena.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/dyn_array.h"
|
||||
#include "foundation/mem_core.h"
|
||||
#include "foundation/platform.h"
|
||||
@@ -81,16 +82,16 @@ const char *cbm_confidence_band(double score) {
|
||||
|
||||
/* ── Internal types ──────────────────────────────────────────────── */
|
||||
|
||||
/* Array of QN strings for byName index, with the test/mock verdict of each
|
||||
* name cached beside it: the scorer asked is_test_qn for every candidate of
|
||||
* every unresolved call, and the answer depends only on the name, which never
|
||||
* changes once registered (waste sanitizer scaling lane, 2026-09-17). Field
|
||||
* order and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */
|
||||
/* Array of QN strings for byName index, with each entry's test verdict beside
|
||||
* it. The verdict is the extractor's, handed to cbm_registry_add by the caller
|
||||
* that already computed it from the file path and the definition's attributes;
|
||||
* it is stored rather than derived, so every entry always has one. Field order
|
||||
* and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */
|
||||
typedef struct {
|
||||
char **items;
|
||||
int count;
|
||||
int cap;
|
||||
uint8_t *is_test; /* is_test_qn(items[i]), one byte per entry */
|
||||
uint8_t *is_test; /* the caller's verdict for items[i], one byte per entry */
|
||||
int is_test_cap;
|
||||
/* Language of the file that defined items[i] (a CBMLanguage; CBM_LANG_COUNT
|
||||
* = unknown, compatible with every caller), one byte per entry. Grown in
|
||||
@@ -204,84 +205,64 @@ static int common_prefix_len(const char *a, const char *b) {
|
||||
|
||||
enum { REG_TEST_PENALTY = 1000 };
|
||||
|
||||
/* Check if a qualified name looks like a test/mock path: does it contain any of
|
||||
* Test, test, Mock, mock, Stub, stub, Fake, fake, Fixture, spec? One pass that
|
||||
* dispatches on the first character instead of ten strstr scans of the whole
|
||||
* name (this ran for every candidate of every unresolved call: the scaling
|
||||
* lane's x4.0, waste sanitizer 2026-09-17). */
|
||||
static bool is_test_qn(const char *qn) {
|
||||
if (!qn) {
|
||||
return false;
|
||||
}
|
||||
for (const char *p = qn; *p; p++) {
|
||||
switch (*p) {
|
||||
case 'T':
|
||||
if (strncmp(p, "Test", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 't':
|
||||
if (strncmp(p, "test", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'M':
|
||||
if (strncmp(p, "Mock", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
if (strncmp(p, "mock", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'S':
|
||||
if (strncmp(p, "Stub", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
if (strncmp(p, "stub", 4) == 0 || strncmp(p, "spec", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'F':
|
||||
if (strncmp(p, "Fake", 4) == 0 || strncmp(p, "Fixture", 7) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
if (strncmp(p, "fake", 4) == 0) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
/* A directory segment that, by name alone, holds test doubles: Go's generated
|
||||
* fake clientsets and listers (pkg/client/.../fake), mock packages, and the
|
||||
* testing / testutil helper packages. Whole segments only, compared exactly. */
|
||||
static bool test_double_dir_segment(const char *seg, size_t len) {
|
||||
static const char *const dirs[] = {"fake", "mock", "mocks", "testing", "testutil"};
|
||||
for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
|
||||
if (strlen(dirs[i]) == len && memcmp(seg, dirs[i], len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The cached test verdicts, only when they cover every entry: a failed growth
|
||||
* leaves the cache short, and then the scorer asks per candidate as before. */
|
||||
static const uint8_t *qn_test_flags(const qn_array_t *arr) {
|
||||
return arr->is_test && arr->is_test_cap >= arr->count ? arr->is_test : NULL;
|
||||
/* `base` is a Go file named `prefix`*.go. */
|
||||
static bool go_file_with_prefix(const char *base, const char *prefix) {
|
||||
static const char go_ext[] = ".go";
|
||||
size_t blen = strlen(base);
|
||||
size_t plen = strlen(prefix);
|
||||
size_t elen = sizeof(go_ext) - SKIP_ONE;
|
||||
return blen >= plen + elen && strncmp(base, prefix, plen) == 0 &&
|
||||
strcmp(base + blen - elen, go_ext) == 0;
|
||||
}
|
||||
|
||||
/* The cached candidate languages, under the same coverage rule: both side
|
||||
* arrays grow together and share is_test_cap. */
|
||||
bool cbm_registry_test_double_path(const char *rel_path) {
|
||||
if (!rel_path) {
|
||||
return false;
|
||||
}
|
||||
const char *seg = rel_path;
|
||||
for (const char *slash = strchr(seg, '/'); slash; slash = strchr(seg, '/')) {
|
||||
if (test_double_dir_segment(seg, (size_t)(slash - seg))) {
|
||||
return true;
|
||||
}
|
||||
seg = slash + SKIP_ONE;
|
||||
}
|
||||
/* `seg` is the file name now. */
|
||||
return go_file_with_prefix(seg, "mock_") || go_file_with_prefix(seg, "fake_");
|
||||
}
|
||||
|
||||
/* The stored test verdicts. index_under_name drops an entry it cannot record a
|
||||
* verdict for, so this covers every entry of a non-empty bucket. */
|
||||
static const uint8_t *qn_test_flags(const qn_array_t *arr) {
|
||||
return arr->is_test;
|
||||
}
|
||||
|
||||
/* The stored candidate languages. Both side arrays grow together and share
|
||||
* is_test_cap, and index_under_name drops an entry that does not fit both, so
|
||||
* this covers every entry of a non-empty bucket too. */
|
||||
static const uint8_t *qn_lang_flags(const qn_array_t *arr) {
|
||||
return arr->lang && arr->is_test_cap >= arr->count ? arr->lang : NULL;
|
||||
return arr->lang;
|
||||
}
|
||||
|
||||
/* Score a candidate for tiebreaking. Higher = better.
|
||||
* Layer 1: Non-test code preferred over test code (+1000)
|
||||
* Layer 2: Namespace proximity via common prefix length (+plen)
|
||||
* `is_test` is the cached verdict when the caller has one (-1: ask). */
|
||||
static int candidate_score(const char *candidate_qn, const char *module_qn, int is_test) {
|
||||
* `is_test` is the verdict stored for this candidate at registration. */
|
||||
static int candidate_score(const char *candidate_qn, const char *module_qn, bool is_test) {
|
||||
int score = 0;
|
||||
bool test = is_test >= 0 ? is_test != 0 : is_test_qn(candidate_qn);
|
||||
if (!test) {
|
||||
if (!is_test) {
|
||||
score += REG_TEST_PENALTY;
|
||||
}
|
||||
score += common_prefix_len(candidate_qn, module_qn);
|
||||
@@ -351,9 +332,10 @@ static bool candidate_outranks_on_tie(const cbm_registry_t *r, const char *candi
|
||||
}
|
||||
|
||||
/* Pick candidate with highest composite score (test-deprioritization + namespace
|
||||
* proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` and
|
||||
* `lang_flags` are the candidates' cached test verdicts and languages, or NULL
|
||||
* when the caller has none (a filtered subset carries its own copies). */
|
||||
* proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` are
|
||||
* the candidates' stored test verdicts, always given: every by-name entry has
|
||||
* one, and a filtered subset carries its own copies. `lang_flags` are their
|
||||
* languages, or NULL when the caller has none. */
|
||||
static const char *best_by_import_distance(const cbm_registry_t *r, const char **candidates,
|
||||
const uint8_t *is_test_flags, const uint8_t *lang_flags,
|
||||
int count, const char *module_qn) {
|
||||
@@ -361,8 +343,7 @@ static const char *best_by_import_distance(const cbm_registry_t *r, const char *
|
||||
uint8_t best_lang = (uint8_t)CBM_LANG_COUNT;
|
||||
int best_score = CBM_NOT_FOUND;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int score =
|
||||
candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1);
|
||||
int score = candidate_score(candidates[i], module_qn, is_test_flags[i] != 0);
|
||||
uint8_t lang = lang_flags ? lang_flags[i] : (uint8_t)CBM_LANG_COUNT;
|
||||
if (score > best_score ||
|
||||
(score == best_score && best &&
|
||||
@@ -485,6 +466,7 @@ typedef struct {
|
||||
cbm_resolution_t res;
|
||||
size_t scope_len; /* raw and Python-scoped requests may share one file cache */
|
||||
bool skip_members; /* a C call and a C reference to the same name share it too */
|
||||
bool cs_keywords; /* a C# call and a C# reference to the same name share it too */
|
||||
} resolve_cache_entry_t;
|
||||
|
||||
static CBM_TLS CBMHashTable *_resolve_cache = NULL;
|
||||
@@ -1718,11 +1700,11 @@ void cbm_registry_free(cbm_registry_t *r) {
|
||||
/* ── Registration ────────────────────────────────────────────────── */
|
||||
|
||||
/* Record `owned_qn` in the by-name bucket for `key`, keeping the bucket's
|
||||
* is_test flags in step with its entries.
|
||||
* is_test and lang side arrays in step with its entries.
|
||||
*
|
||||
* No array dedup needed: cbm_registry_add's exact-map check guarantees the QN
|
||||
* is new, and it calls this at most once per distinct key. */
|
||||
static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn,
|
||||
static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn, bool is_test,
|
||||
CBMLanguage lang) {
|
||||
qn_array_t *arr = cbm_ht_get(r->by_name, key);
|
||||
if (!arr) {
|
||||
@@ -1732,7 +1714,7 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own
|
||||
int before = arr->count;
|
||||
cbm_da_push(arr, (char *)owned_qn);
|
||||
if (arr->count == before) {
|
||||
return; /* the name could not be recorded: no verdict to cache */
|
||||
return; /* the name could not be recorded: no verdict to store */
|
||||
}
|
||||
if (arr->count > arr->is_test_cap) {
|
||||
int want = arr->cap > 0 ? arr->cap : arr->count;
|
||||
@@ -1743,19 +1725,25 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own
|
||||
uint8_t *grown_lang =
|
||||
cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->lang, (size_t)want * sizeof(uint8_t));
|
||||
/* Both side arrays share is_test_cap: on failure it stays below
|
||||
* count, so this name serves neither cached test verdicts nor
|
||||
* language filtering, exactly the unfiltered answer. */
|
||||
* count, and the entry is dropped below. */
|
||||
if (grown_lang) {
|
||||
arr->lang = grown_lang;
|
||||
arr->is_test_cap = want;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr->count <= arr->is_test_cap) {
|
||||
arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0;
|
||||
arr->lang[arr->count - SKIP_ONE] =
|
||||
(uint8_t)(lang >= 0 && lang < CBM_LANG_COUNT ? lang : CBM_LANG_COUNT);
|
||||
if (arr->count > arr->is_test_cap) {
|
||||
/* The verdict can no longer be recomputed from the QN, so an entry
|
||||
* without one would be scored as non-test and outrank real code. Drop
|
||||
* it instead: one missing candidate beats a wrong winner. The language
|
||||
* shares is_test_cap, so the drop covers it as well. */
|
||||
arr->count = before;
|
||||
cbm_log_warn("registry.verdict_alloc_failed", "key", key);
|
||||
return;
|
||||
}
|
||||
arr->is_test[arr->count - SKIP_ONE] = is_test ? 1 : 0;
|
||||
arr->lang[arr->count - SKIP_ONE] =
|
||||
(uint8_t)(lang >= 0 && lang < CBM_LANG_COUNT ? lang : CBM_LANG_COUNT);
|
||||
}
|
||||
|
||||
static bool registry_label_is_callable(const char *label) {
|
||||
@@ -1843,13 +1831,13 @@ static const char *registry_add_exact(cbm_registry_t *r, const char *qualified_n
|
||||
}
|
||||
|
||||
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
|
||||
const char *label) {
|
||||
cbm_registry_add_lang_in_module(r, name, qualified_name, label, CBM_LANG_COUNT, 0);
|
||||
const char *label, bool is_test) {
|
||||
cbm_registry_add_lang_in_module(r, name, qualified_name, label, CBM_LANG_COUNT, 0, is_test);
|
||||
}
|
||||
|
||||
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
|
||||
const char *label, CBMLanguage lang) {
|
||||
cbm_registry_add_lang_in_module(r, name, qualified_name, label, lang, 0);
|
||||
const char *label, CBMLanguage lang, bool is_test) {
|
||||
cbm_registry_add_lang_in_module(r, name, qualified_name, label, lang, 0, is_test);
|
||||
}
|
||||
|
||||
/* Python nests defs: a def or class declared in a function body -- a closure,
|
||||
@@ -1868,7 +1856,7 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual
|
||||
* index seconds for nothing. */
|
||||
void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
|
||||
const char *qualified_name, const char *label,
|
||||
CBMLanguage lang, size_t module_prefix_len) {
|
||||
CBMLanguage lang, size_t module_prefix_len, bool is_test) {
|
||||
if (!r || !qualified_name || !label) {
|
||||
return;
|
||||
}
|
||||
@@ -1912,7 +1900,7 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
|
||||
const char *derived = simple_name(qualified_name);
|
||||
const char *primary =
|
||||
name && cbm_qn_callable_base_len_named(owned_qn, name) < strlen(owned_qn) ? name : derived;
|
||||
index_under_name(r, primary, owned_qn, lang);
|
||||
index_under_name(r, primary, owned_qn, is_test, lang);
|
||||
/* '#' is a QN fence; no extractor mints one for a REGISTERED definition
|
||||
* today (Rust cfg twins are variants of one plain QN; a C macro's QN ends in
|
||||
* "#macro", but Macro is no registry label, so it never gets here). A
|
||||
@@ -1924,7 +1912,7 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
|
||||
* signature-qualified callable is filed under `name` already, and its
|
||||
* capped suffix may carry a '#' of its own. */
|
||||
if (name && name[0] && strchr(derived, '#') && strcmp(name, primary) != 0) {
|
||||
index_under_name(r, name, owned_qn, lang);
|
||||
index_under_name(r, name, owned_qn, is_test, lang);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2295,12 +2283,8 @@ static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, cons
|
||||
int fcount = 0;
|
||||
for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) {
|
||||
if (is_import_reachable(arr->items[i], import_vals, import_count)) {
|
||||
if (flags) {
|
||||
filtered_test[fcount] = flags[i];
|
||||
}
|
||||
if (langs) {
|
||||
filtered_lang[fcount] = langs[i];
|
||||
}
|
||||
filtered_test[fcount] = flags[i];
|
||||
filtered_lang[fcount] = langs[i];
|
||||
filtered[fcount] = arr->items[i];
|
||||
fcount++;
|
||||
}
|
||||
@@ -2310,8 +2294,8 @@ static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, cons
|
||||
return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count};
|
||||
}
|
||||
if (fcount > SKIP_ONE) {
|
||||
const char *best = best_by_import_distance(r, filtered, flags ? filtered_test : NULL,
|
||||
langs ? filtered_lang : NULL, fcount, module_qn);
|
||||
const char *best =
|
||||
best_by_import_distance(r, filtered, filtered_test, filtered_lang, fcount, module_qn);
|
||||
if (best) {
|
||||
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount);
|
||||
return (cbm_resolution_t){best, "suffix_match", conf, fcount};
|
||||
@@ -2384,6 +2368,26 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
|
||||
return match;
|
||||
}
|
||||
|
||||
/* True when the first segment of the dotted chain `dotted` is one of C#'s
|
||||
* built-in type keywords (int, string, bool, ...; nint and nuint since C# 9).
|
||||
* Closed list: these words are grammar, not a naming heuristic. `dynamic` is
|
||||
* not on it: the type has no static members, so `dynamic.X` is a variable
|
||||
* named `dynamic` and stays an ordinary lower-case value root. */
|
||||
static bool cs_type_keyword_root(const char *dotted) {
|
||||
static const char *const keywords[] = {
|
||||
"bool", "byte", "char", "decimal", "double", "float", "int", "long", "nint",
|
||||
"nuint", "object", "sbyte", "short", "string", "uint", "ulong", "ushort", "void",
|
||||
};
|
||||
const char *dot = strchr(dotted, '.');
|
||||
size_t len = dot ? (size_t)(dot - dotted) : strlen(dotted);
|
||||
for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
|
||||
if (strlen(keywords[i]) == len && strncmp(dotted, keywords[i], len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A dotted callee whose FIRST segment starts upper-case names a type — URLSession,
|
||||
* Calendar, JSONEncoder. That receiver chain is evidence the bare-name scorers
|
||||
* throw away, and throwing it away binds Foundation's URLSession.shared.data to
|
||||
@@ -2397,9 +2401,17 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
|
||||
* show, so the chain proves nothing there and the call passes through
|
||||
* unchanged. A callee with no separator passes through as well.
|
||||
*
|
||||
* Language agnostic by design: the registry holds no language, and every
|
||||
* language that writes receiver chains gains the same protection. */
|
||||
static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn) {
|
||||
* One exception, for C# CALLS only (#2121): C# spells its built-in types as
|
||||
* lower-case keywords, so in `int.TryParse` or `string.Join` the root names a
|
||||
* type and gets the same check (cs_type_keyword_root). `lang` is the calling
|
||||
* file's language on the CALLS path (cbm_registry_resolve_call_lang) and
|
||||
* CBM_LANG_COUNT on every other resolve. In Python, JS or Go these words are
|
||||
* ordinary identifiers, so a variable named `object` stays a value root there.
|
||||
*
|
||||
* Otherwise language agnostic by design: every language that writes receiver
|
||||
* chains gains the same protection. */
|
||||
static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn,
|
||||
CBMLanguage lang) {
|
||||
/* Normalize "::" -> "." so the chain composes with dotted candidate QNs,
|
||||
* the same way qualified_suffix_match does. */
|
||||
char dotted[CBM_SZ_512];
|
||||
@@ -2418,7 +2430,8 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate
|
||||
if (!last_dot) {
|
||||
return true; /* bare name — no receiver chain to judge */
|
||||
}
|
||||
if (dotted[0] < 'A' || dotted[0] > 'Z') {
|
||||
if ((dotted[0] < 'A' || dotted[0] > 'Z') &&
|
||||
!(lang == CBM_LANG_CSHARP && cs_type_keyword_root(dotted))) {
|
||||
return true; /* lower-case root names a value, not a type */
|
||||
}
|
||||
/* A name written in capitals with underscores is a constant holding a
|
||||
@@ -2477,14 +2490,13 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate
|
||||
|
||||
/* The same-name candidates without the members -- Field and Method nodes --
|
||||
* in `view` (backed by the caller's buffers, at most REG_MAX_CANDIDATES
|
||||
* entries). The side arrays are carried over only when the bucket has them
|
||||
* for every entry. */
|
||||
* entries). Every entry of a non-empty bucket has its stored verdict and
|
||||
* language (index_under_name), and the view carries them over. */
|
||||
static void candidates_without_members(const cbm_registry_t *r, const qn_array_t *arr,
|
||||
qn_array_t *view, char **items, uint8_t *is_test,
|
||||
uint8_t *lang) {
|
||||
const uint8_t *flags = qn_test_flags(arr);
|
||||
const uint8_t *langs = qn_lang_flags(arr);
|
||||
bool side = flags && langs;
|
||||
int n = 0;
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
const char *label = cbm_registry_label_of(r, arr->items[i]);
|
||||
@@ -2492,24 +2504,20 @@ static void candidates_without_members(const cbm_registry_t *r, const qn_array_t
|
||||
continue;
|
||||
}
|
||||
items[n] = arr->items[i];
|
||||
if (side) {
|
||||
is_test[n] = flags[i];
|
||||
lang[n] = langs[i];
|
||||
}
|
||||
is_test[n] = flags[i];
|
||||
lang[n] = langs[i];
|
||||
n++;
|
||||
}
|
||||
*view = (qn_array_t){.items = items,
|
||||
.count = n,
|
||||
.cap = n,
|
||||
.is_test = side ? is_test : NULL,
|
||||
.is_test_cap = side ? n : 0,
|
||||
.lang = side ? lang : NULL};
|
||||
*view = (qn_array_t){
|
||||
.items = items, .count = n, .cap = n, .is_test = is_test, .is_test_cap = n, .lang = lang};
|
||||
}
|
||||
|
||||
/* Strategies 3.5, 3 and 4 over one same-name candidate set. */
|
||||
/* Strategies 3.5, 3 and 4 over one same-name candidate set. `lang` reaches
|
||||
* receiver_chain_admits only (see there). */
|
||||
static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const qn_array_t *arr,
|
||||
const char *callee_name, const char *module_qn,
|
||||
const char **import_vals, int import_count) {
|
||||
const char **import_vals, int import_count,
|
||||
CBMLanguage lang) {
|
||||
/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
|
||||
* candidates by full qualified tail, before bare-name scoring collapses
|
||||
* them onto a single winner. */
|
||||
@@ -2522,7 +2530,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
|
||||
|
||||
/* Strategy 3: unique name */
|
||||
if (arr->count == SKIP_ONE) {
|
||||
if (!receiver_chain_admits(callee_name, arr->items[0])) {
|
||||
if (!receiver_chain_admits(callee_name, arr->items[0], lang)) {
|
||||
return empty_result();
|
||||
}
|
||||
double conf = CONF_UNIQUE_NAME;
|
||||
@@ -2540,7 +2548,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
|
||||
const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr),
|
||||
qn_lang_flags(arr), arr->count, module_qn);
|
||||
if (best) {
|
||||
if (!receiver_chain_admits(callee_name, best)) {
|
||||
if (!receiver_chain_admits(callee_name, best, lang)) {
|
||||
return empty_result();
|
||||
}
|
||||
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
|
||||
@@ -2558,7 +2566,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q
|
||||
* libc `read()` the unique name of a C++ adapter's Method instead. */
|
||||
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
|
||||
const char *module_qn, const char **import_vals,
|
||||
int import_count, bool skip_members) {
|
||||
int import_count, bool skip_members, CBMLanguage lang) {
|
||||
const char *lookup = simple_name(callee_name);
|
||||
const qn_array_t *arr = cbm_ht_get(r->by_name, lookup);
|
||||
if (!arr || arr->count == 0) {
|
||||
@@ -2576,17 +2584,20 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
|
||||
if (view.count == 0) {
|
||||
return empty_result();
|
||||
}
|
||||
return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count);
|
||||
return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count,
|
||||
lang);
|
||||
}
|
||||
return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count);
|
||||
return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count, lang);
|
||||
}
|
||||
|
||||
/* The strategy chain shared by both public resolve variants (no caching here —
|
||||
* cbm_registry_resolve owns the per-file cache). */
|
||||
* cbm_registry_resolve owns the per-file cache). `lang`: see
|
||||
* receiver_chain_admits. */
|
||||
static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const char *callee_name,
|
||||
const char *module_qn, const char **import_map_keys,
|
||||
const char **import_map_vals, int import_map_count,
|
||||
size_t scope_len, bool skip_members) {
|
||||
size_t scope_len, bool skip_members,
|
||||
CBMLanguage lang) {
|
||||
/* Split callee at the first path separator: "pkg.Func" → prefix="pkg",
|
||||
* suffix="Func". Rust/C++ use "::" rather than ".", so honor whichever
|
||||
* separator appears first ("lib::square" → prefix="lib", suffix="square").
|
||||
@@ -2624,7 +2635,7 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch
|
||||
if (!(res.qualified_name && res.qualified_name[0])) {
|
||||
/* Strategy 3+4: name lookup */
|
||||
res = resolve_name_lookup(r, callee_name, module_qn, import_map_vals, import_map_count,
|
||||
skip_members);
|
||||
skip_members, lang);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -2634,7 +2645,7 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch
|
||||
* at most REG_MAX_CANDIDATES long whenever a name strategy produced `qn`. */
|
||||
static CBMLanguage registry_entry_language(const cbm_registry_t *r, const char *qn) {
|
||||
const qn_array_t *arr = cbm_ht_get(r->by_name, simple_name(qn));
|
||||
if (!arr || !arr->lang || arr->is_test_cap < arr->count) {
|
||||
if (!arr) {
|
||||
return CBM_LANG_COUNT;
|
||||
}
|
||||
for (int i = 0; i < arr->count; i++) {
|
||||
@@ -2672,25 +2683,31 @@ bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_
|
||||
registry_entry_language(r, res->qualified_name));
|
||||
}
|
||||
|
||||
/* call_lang: the calling file's language for a CALLS resolve, CBM_LANG_COUNT
|
||||
* for every other one (see receiver_chain_admits). */
|
||||
static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const char *callee_name,
|
||||
const char *module_qn, const char **import_map_keys,
|
||||
const char **import_map_vals, int import_map_count,
|
||||
size_t scope_len, bool skip_members) {
|
||||
size_t scope_len, bool skip_members,
|
||||
CBMLanguage call_lang) {
|
||||
if (!r || !callee_name) {
|
||||
return empty_result();
|
||||
}
|
||||
|
||||
/* Per-file cache: same callee_name in N call sites → 1 chain walk
|
||||
* + N-1 O(1) hash hits. module_qn is constant per file so the
|
||||
* cache key only needs callee_name. */
|
||||
* cache key only needs callee_name. The chain reads call_lang only as
|
||||
* "is it C#", so that one bit is all the entry has to record. */
|
||||
bool cs_keywords = call_lang == CBM_LANG_CSHARP;
|
||||
resolve_cache_entry_t *cached =
|
||||
_resolve_cache ? (resolve_cache_entry_t *)cbm_ht_get(_resolve_cache, callee_name) : NULL;
|
||||
if (cached && cached->scope_len == scope_len && cached->skip_members == skip_members)
|
||||
if (cached && cached->scope_len == scope_len && cached->skip_members == skip_members &&
|
||||
cached->cs_keywords == cs_keywords)
|
||||
return cached->res;
|
||||
|
||||
cbm_resolution_t res =
|
||||
registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals,
|
||||
import_map_count, scope_len, skip_members);
|
||||
import_map_count, scope_len, skip_members, call_lang);
|
||||
|
||||
/* Data relations (Table/View) are lineage-only registry members: common
|
||||
* table names (users, orders, config) collide with code identifiers across
|
||||
@@ -2711,12 +2728,14 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c
|
||||
cached->res = res;
|
||||
cached->scope_len = scope_len;
|
||||
cached->skip_members = skip_members;
|
||||
cached->cs_keywords = cs_keywords;
|
||||
} else if (_resolve_cache) {
|
||||
resolve_cache_entry_t *e = (resolve_cache_entry_t *)malloc(sizeof(*e));
|
||||
if (e) {
|
||||
e->res = res;
|
||||
e->scope_len = scope_len;
|
||||
e->skip_members = skip_members;
|
||||
e->cs_keywords = cs_keywords;
|
||||
char *kdup = strdup(callee_name);
|
||||
if (kdup) {
|
||||
cbm_ht_set(_resolve_cache, kdup, e);
|
||||
@@ -3028,22 +3047,28 @@ static bool swift_signature_matches(const swift_signature_t *meta, const CBMCall
|
||||
}
|
||||
|
||||
/* Keep receiver choice ahead of general import/name choice. Without a module,
|
||||
* this path still uses the deterministic distance tie-break, as before. */
|
||||
static const char *swift_choose_receiver(const cbm_registry_t *r, const char **matches, int matched,
|
||||
const char *name, const CBMCall *call,
|
||||
const char *module_qn, const char **import_vals,
|
||||
int import_count, const char **scratch) {
|
||||
* this path still uses the deterministic distance tie-break, as before.
|
||||
* `match_test` holds the stored test verdict of each of `matches`, and
|
||||
* `scratch_test` is the matching scratch, so every filtered list the scorer
|
||||
* sees carries its own verdicts. */
|
||||
static const char *swift_choose_receiver(const cbm_registry_t *r, const char **matches,
|
||||
const uint8_t *match_test, int matched, const char *name,
|
||||
const CBMCall *call, const char *module_qn,
|
||||
const char **import_vals, int import_count,
|
||||
const char **scratch, uint8_t *scratch_test) {
|
||||
if (!strchr(call->callee_name, '.')) {
|
||||
return NULL;
|
||||
}
|
||||
size_t callee_len = strlen(call->callee_name);
|
||||
const char **receiver_matches = scratch;
|
||||
uint8_t *receiver_test = scratch_test;
|
||||
int receiver_count = 0;
|
||||
for (int i = 0; i < matched; i++) {
|
||||
size_t base = cbm_qn_callable_base_len_named(matches[i], name);
|
||||
if (base >= callee_len &&
|
||||
memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 &&
|
||||
(base == callee_len || matches[i][base - callee_len - SKIP_ONE] == '.')) {
|
||||
receiver_test[receiver_count] = match_test[i];
|
||||
receiver_matches[receiver_count++] = matches[i];
|
||||
}
|
||||
}
|
||||
@@ -3051,45 +3076,53 @@ static const char *swift_choose_receiver(const cbm_registry_t *r, const char **m
|
||||
return NULL;
|
||||
}
|
||||
const char **reachable = scratch + matched;
|
||||
uint8_t *reachable_test = scratch_test + matched;
|
||||
int reachable_count = 0;
|
||||
if (import_vals) {
|
||||
for (int i = 0; i < receiver_count; i++) {
|
||||
if (is_import_reachable(receiver_matches[i], import_vals, import_count)) {
|
||||
reachable_test[reachable_count] = receiver_test[i];
|
||||
reachable[reachable_count++] = receiver_matches[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
const char **preferred = reachable_count > 0 ? reachable : receiver_matches;
|
||||
const uint8_t *preferred_test = reachable_count > 0 ? reachable_test : receiver_test;
|
||||
int preferred_count = reachable_count > 0 ? reachable_count : receiver_count;
|
||||
return best_by_import_distance(r, preferred, NULL, NULL, preferred_count, module_qn);
|
||||
return best_by_import_distance(r, preferred, preferred_test, NULL, preferred_count, module_qn);
|
||||
}
|
||||
|
||||
static const char *swift_choose_candidate(const cbm_registry_t *r, const char **matches,
|
||||
int matched, const char *name, const CBMCall *call,
|
||||
const char *module_qn, const char **import_vals,
|
||||
int import_count, const char **scratch) {
|
||||
const char *chosen = swift_choose_receiver(r, matches, matched, name, call, module_qn,
|
||||
import_vals, import_count, scratch);
|
||||
const uint8_t *match_test, int matched, const char *name,
|
||||
const CBMCall *call, const char *module_qn,
|
||||
const char **import_vals, int import_count,
|
||||
const char **scratch, uint8_t *scratch_test) {
|
||||
const char *chosen =
|
||||
swift_choose_receiver(r, matches, match_test, matched, name, call, module_qn, import_vals,
|
||||
import_count, scratch, scratch_test);
|
||||
if (chosen) {
|
||||
return chosen;
|
||||
}
|
||||
const char **reachable = scratch;
|
||||
uint8_t *reachable_test = scratch_test;
|
||||
int reachable_count = 0;
|
||||
if (import_vals) {
|
||||
for (int i = 0; i < matched; i++) {
|
||||
if (is_import_reachable(matches[i], import_vals, import_count)) {
|
||||
reachable_test[reachable_count] = match_test[i];
|
||||
reachable[reachable_count++] = matches[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reachable_count > 0) {
|
||||
chosen = module_qn
|
||||
? best_by_import_distance(r, reachable, NULL, NULL, reachable_count, module_qn)
|
||||
: reachable[0];
|
||||
chosen = module_qn ? best_by_import_distance(r, reachable, reachable_test, NULL,
|
||||
reachable_count, module_qn)
|
||||
: reachable[0];
|
||||
}
|
||||
if (!chosen) {
|
||||
chosen = module_qn ? best_by_import_distance(r, matches, NULL, NULL, matched, module_qn)
|
||||
: matches[0];
|
||||
chosen = module_qn
|
||||
? best_by_import_distance(r, matches, match_test, NULL, matched, module_qn)
|
||||
: matches[0];
|
||||
}
|
||||
return chosen;
|
||||
}
|
||||
@@ -3719,23 +3752,29 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call,
|
||||
if (!bucket) {
|
||||
return 0;
|
||||
}
|
||||
/* Three QN lists (the matches and the choosers' two scratch lists), each
|
||||
* with a parallel byte per entry for its stored test verdict, in one block. */
|
||||
enum { SWIFT_MATCH_BUFFERS = 3 };
|
||||
if ((size_t)bucket->count > SIZE_MAX / sizeof(const char *) / SWIFT_MATCH_BUFFERS) {
|
||||
const size_t slot = sizeof(const char *) + sizeof(uint8_t);
|
||||
if ((size_t)bucket->count > SIZE_MAX / slot / SWIFT_MATCH_BUFFERS) {
|
||||
return SWIFT_INCOMPATIBLE;
|
||||
}
|
||||
const char **matches = cbm_alloc(
|
||||
CBM_MEM_CLASS_OTHER, (size_t)bucket->count * SWIFT_MATCH_BUFFERS * sizeof(*matches));
|
||||
size_t slots = (size_t)bucket->count * SWIFT_MATCH_BUFFERS;
|
||||
const char **matches = cbm_alloc(CBM_MEM_CLASS_OTHER, slots * slot);
|
||||
if (!matches) {
|
||||
return SWIFT_INCOMPATIBLE;
|
||||
}
|
||||
uint8_t *match_test = (uint8_t *)(matches + slots);
|
||||
const uint8_t *bucket_test = qn_test_flags(bucket);
|
||||
int matched = 0;
|
||||
bool has_swift = false;
|
||||
for (int i = 0; i < bucket->count; i++) {
|
||||
const char *qn = bucket->items[i];
|
||||
swift_signature_t *meta = cbm_ht_get(r->swift_signatures, qn);
|
||||
has_swift = has_swift || meta != NULL;
|
||||
if (meta && receiver_chain_admits(call->callee_name, qn) &&
|
||||
if (meta && receiver_chain_admits(call->callee_name, qn, CBM_LANG_SWIFT) &&
|
||||
swift_signature_matches(meta, call)) {
|
||||
match_test[matched] = bucket_test[i];
|
||||
matches[matched++] = qn;
|
||||
}
|
||||
}
|
||||
@@ -3743,8 +3782,9 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call,
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, matches);
|
||||
return has_swift ? SWIFT_INCOMPATIBLE : 0;
|
||||
}
|
||||
const char *chosen = swift_choose_candidate(r, matches, matched, name, call, module_qn,
|
||||
import_vals, import_count, matches + bucket->count);
|
||||
const char *chosen =
|
||||
swift_choose_candidate(r, matches, match_test, matched, name, call, module_qn, import_vals,
|
||||
import_count, matches + bucket->count, match_test + bucket->count);
|
||||
if (!chosen) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, matches);
|
||||
return 0;
|
||||
@@ -4445,7 +4485,8 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
|
||||
const char *module_qn, const char **import_map_keys,
|
||||
const char **import_map_vals, int import_map_count) {
|
||||
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
|
||||
import_map_count, module_qn ? strlen(module_qn) : 0, false);
|
||||
import_map_count, module_qn ? strlen(module_qn) : 0, false,
|
||||
CBM_LANG_COUNT);
|
||||
}
|
||||
|
||||
static size_t lang_scope_len(const char *module_qn, CBMLanguage lang) {
|
||||
@@ -4458,7 +4499,8 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char *
|
||||
const char **import_map_vals, int import_map_count,
|
||||
CBMLanguage lang) {
|
||||
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
|
||||
import_map_count, lang_scope_len(module_qn, lang), false);
|
||||
import_map_count, lang_scope_len(module_qn, lang), false,
|
||||
CBM_LANG_COUNT);
|
||||
}
|
||||
|
||||
cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name,
|
||||
@@ -4472,7 +4514,8 @@ cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const c
|
||||
bool skip_members = lang == CBM_LANG_C && callee_name && !strchr(callee_name, '.') &&
|
||||
!strstr(callee_name, "->");
|
||||
return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals,
|
||||
import_map_count, lang_scope_len(module_qn, lang), skip_members);
|
||||
import_map_count, lang_scope_len(module_qn, lang), skip_members,
|
||||
lang);
|
||||
}
|
||||
|
||||
/* ── Class-qualified route handlers (PHP, #1146) ─────────────────── */
|
||||
@@ -4640,7 +4683,8 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha
|
||||
* it would poison one variant with the other's semantics. SQL files hold
|
||||
* few distinct relation refs, so the chain walk stays cheap. */
|
||||
return registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals,
|
||||
import_map_count, module_qn ? strlen(module_qn) : 0, false);
|
||||
import_map_count, module_qn ? strlen(module_qn) : 0, false,
|
||||
CBM_LANG_COUNT);
|
||||
}
|
||||
|
||||
/* Python looks a bare name up in the calling def's own scope, then in each
|
||||
@@ -4713,11 +4757,18 @@ cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const
|
||||
/* ── Fuzzy Resolve ──────────────────────────────────────────────── */
|
||||
|
||||
/* Filter candidates by import reachability. Returns count of reachable. */
|
||||
static int filter_import_reachable(const char **candidates, int count, const char **import_vals,
|
||||
int import_count, const char **out, int max_out) {
|
||||
/* Keeps `out_flags` and `out_langs` in step with `out`: the verdicts are
|
||||
* stored per entry and cannot be recomputed from the QN, so a filtered view has
|
||||
* to carry its own, and the languages travel with them. */
|
||||
static int filter_import_reachable(const char **candidates, const uint8_t *flags,
|
||||
const uint8_t *langs, int count, const char **import_vals,
|
||||
int import_count, const char **out, uint8_t *out_flags,
|
||||
uint8_t *out_langs, int max_out) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < count && n < max_out; i++) {
|
||||
if (is_import_reachable(candidates[i], import_vals, import_count)) {
|
||||
out_flags[n] = flags[i];
|
||||
out_langs[n] = langs[i];
|
||||
out[n++] = candidates[i];
|
||||
}
|
||||
}
|
||||
@@ -4757,13 +4808,21 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
|
||||
|
||||
/* Multiple candidates: filter by import reachability */
|
||||
const char *filtered[CBM_SZ_256];
|
||||
uint8_t filtered_flags[CBM_SZ_256];
|
||||
uint8_t filtered_langs[CBM_SZ_256];
|
||||
int fcount = arr->count;
|
||||
const char **fptr = (const char **)arr->items;
|
||||
const uint8_t *fflags = qn_test_flags(arr);
|
||||
const uint8_t *flangs = qn_lang_flags(arr);
|
||||
|
||||
if (have_imports) {
|
||||
fcount = filter_import_reachable((const char **)arr->items, arr->count, import_map_vals,
|
||||
import_map_count, filtered, CBM_SZ_256);
|
||||
fcount = filter_import_reachable((const char **)arr->items, qn_test_flags(arr),
|
||||
qn_lang_flags(arr), arr->count, import_map_vals,
|
||||
import_map_count, filtered, filtered_flags, filtered_langs,
|
||||
CBM_SZ_256);
|
||||
fptr = filtered;
|
||||
fflags = filtered_flags;
|
||||
flangs = filtered_langs;
|
||||
}
|
||||
|
||||
if (fcount == 0) {
|
||||
@@ -4783,9 +4842,7 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha
|
||||
{fptr[0], "fuzzy", candidate_count_penalty(CONF_FUZZY_SINGLE, arr->count), arr->count},
|
||||
true};
|
||||
}
|
||||
const bool all = fptr == (const char **)arr->items;
|
||||
const char *best = best_by_import_distance(r, fptr, all ? qn_test_flags(arr) : NULL,
|
||||
all ? qn_lang_flags(arr) : NULL, fcount, module_qn);
|
||||
const char *best = best_by_import_distance(r, fptr, fflags, flangs, fcount, module_qn);
|
||||
if (!best) {
|
||||
return no_match;
|
||||
}
|
||||
|
||||
@@ -434,9 +434,9 @@ TEST(callable_sig_leaf_splitters_skip_suffix) {
|
||||
* keeps its historical key (last '.' or "::" segment). */
|
||||
TEST(callable_sig_registry_by_name_uses_base_leaf) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method");
|
||||
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method");
|
||||
cbm_registry_add(r, "square", "p.Math::square", "Function");
|
||||
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method", false);
|
||||
cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method", false);
|
||||
cbm_registry_add(r, "square", "p.Math::square", "Function", false);
|
||||
const char **out = NULL;
|
||||
int count = 0;
|
||||
cbm_registry_find_by_name(r, "upload", &out, &count);
|
||||
|
||||
+31
-3
@@ -18664,19 +18664,31 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) {
|
||||
snprintf(logpath, sizeof(logpath), "%s/timeouts.log", tmpdir);
|
||||
|
||||
int fds[2];
|
||||
int errfds[2];
|
||||
if (pipe(fds) != 0) {
|
||||
test_rmdir_r(tmpdir);
|
||||
FAIL("pipe failed");
|
||||
}
|
||||
if (pipe(errfds) != 0) {
|
||||
close(fds[0]);
|
||||
close(fds[1]);
|
||||
test_rmdir_r(tmpdir);
|
||||
FAIL("stderr pipe failed");
|
||||
}
|
||||
|
||||
fflush(NULL);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
/* Child: hook-augment with a 60ms deadline and stdin that blocks
|
||||
* forever (parent keeps the write end open, sends nothing). */
|
||||
* forever (parent keeps the write end open, sends nothing). Its stderr
|
||||
* is redirected so the parent can prove the deadline is also announced
|
||||
* there, not only in the timeouts log. */
|
||||
close(fds[1]);
|
||||
dup2(fds[0], 0);
|
||||
close(fds[0]);
|
||||
close(errfds[0]);
|
||||
dup2(errfds[1], 2);
|
||||
close(errfds[1]);
|
||||
setenv("CBM_HOOK_DEADLINE_MS", "60", 1);
|
||||
setenv("CBM_HOOK_TIMEOUT_LOG", logpath, 1);
|
||||
alarm(10); /* backstop: never hang the suite */
|
||||
@@ -18684,6 +18696,19 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) {
|
||||
}
|
||||
ASSERT_GT(pid, 0);
|
||||
close(fds[0]);
|
||||
close(errfds[1]);
|
||||
|
||||
/* Drain the child's stderr before reaping, so a full pipe can never wedge
|
||||
* it. EOF arrives once the child's deadline handler has run and _exit()ed. */
|
||||
char errbuf[4096] = "";
|
||||
size_t errlen = 0;
|
||||
ssize_t rd;
|
||||
while (errlen + 1 < sizeof(errbuf) &&
|
||||
(rd = read(errfds[0], errbuf + errlen, sizeof(errbuf) - 1 - errlen)) > 0) {
|
||||
errlen += (size_t)rd;
|
||||
}
|
||||
errbuf[errlen] = '\0';
|
||||
close(errfds[0]);
|
||||
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
@@ -18694,8 +18719,8 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) {
|
||||
ASSERT_EQ(WEXITSTATUS(status), 0);
|
||||
|
||||
/* RED before the fix: no breadcrumb existed — a fired deadline was
|
||||
* indistinguishable from a no-match run. GREEN: the log names the
|
||||
* deadline and the knob. */
|
||||
* indistinguishable from a no-match run. GREEN: it is announced on BOTH
|
||||
* the timeouts log and stderr, each naming the deadline and the knob. */
|
||||
FILE *f = fopen(logpath, "r");
|
||||
if (!f) {
|
||||
fprintf(stderr, " [858] FAIL no timeout breadcrumb written to %s\n", logpath);
|
||||
@@ -18708,6 +18733,9 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) {
|
||||
ASSERT(strstr(line, "deadline_exceeded") != NULL);
|
||||
ASSERT(strstr(line, "CBM_HOOK_DEADLINE_MS") != NULL);
|
||||
|
||||
ASSERT(strstr(errbuf, "deadline_exceeded") != NULL);
|
||||
ASSERT(strstr(errbuf, "CBM_HOOK_DEADLINE_MS") != NULL);
|
||||
|
||||
cbm_unsetenv("CBM_HOOK_DEADLINE_MS");
|
||||
cbm_unsetenv("CBM_HOOK_TIMEOUT_LOG");
|
||||
test_rmdir_r(tmpdir);
|
||||
|
||||
@@ -1641,6 +1641,32 @@ TEST(daemon_frontend_idle_uses_one_maintenance_observer) {
|
||||
ASSERT_TRUE(fixture_closed);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An idle monitor must not repeatedly perform the expensive presence probe
|
||||
* while no maintenance request exists (#2093). The monitor records the exact
|
||||
* value it sleeps on in its no-maintenance branch; once recorded that value is
|
||||
* stable, so the verdict depends on neither elapsed time nor scheduler cadence.
|
||||
* The deadline below is only a hang guard. */
|
||||
TEST(daemon_frontend_idle_maintenance_probe_is_bounded) {
|
||||
frontend_maintenance_fixture_t fixture;
|
||||
ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "idle-cadence"));
|
||||
|
||||
cbm_daemon_frontend_test_observer_reset(false);
|
||||
cbm_daemon_maintenance_monitor_t *monitor = cbm_daemon_maintenance_monitor_start(
|
||||
fixture.manager, NULL, NULL, EXIT_SUCCESS, "idle cadence test");
|
||||
ASSERT_TRUE(monitor != NULL);
|
||||
uint64_t deadline = cbm_now_ms() + 10000U;
|
||||
while (cbm_now_ms() < deadline && cbm_daemon_frontend_test_monitor_idle_sleep_ms() == 0) {
|
||||
cbm_usleep(1000);
|
||||
}
|
||||
uint32_t idle_sleep_ms = cbm_daemon_frontend_test_monitor_idle_sleep_ms();
|
||||
ASSERT_TRUE(cbm_daemon_maintenance_monitor_stop(&monitor));
|
||||
frontend_maintenance_fixture_finish(&fixture);
|
||||
|
||||
ASSERT_TRUE(idle_sleep_ms != 0);
|
||||
ASSERT_GTE(idle_sleep_ms, 250);
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
@@ -1982,6 +2008,7 @@ SUITE(daemon_frontend) {
|
||||
RUN_TEST(daemon_frontend_rejects_non_notification_cancellation_shapes);
|
||||
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
||||
RUN_TEST(daemon_frontend_idle_uses_one_maintenance_observer);
|
||||
RUN_TEST(daemon_frontend_idle_maintenance_probe_is_bounded);
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(daemon_frontend_maintenance_exits_while_stdio_reader_is_blocked);
|
||||
|
||||
@@ -5237,6 +5237,196 @@ TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The executable-image fingerprint cache short-circuits the full-image hash for
|
||||
* a repeated file identity, and rolls its key when the binary changes so a
|
||||
* rebuild or atomic replacement never returns a stale digest. Results are
|
||||
* gathered first and asserted only after the stub is disarmed: an assertion
|
||||
* returns early, and an armed stub would replace every later fingerprint in
|
||||
* this process. */
|
||||
TEST(daemon_runtime_fingerprint_cache_hit_miss_key_roll) {
|
||||
/* 64-hex placeholders, distinct enough to tell "cached" from "recomputed". */
|
||||
static const char digest_a[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] =
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
static const char digest_b[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] =
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t image = {1, 2, 3, 100,
|
||||
200, 300, 400};
|
||||
cbm_daemon_runtime_fingerprint_identity_for_testing_t rolled = image;
|
||||
char miss[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char hit[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char mtime_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char ctime_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char inode_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
/* Miss: first sight of this identity pays the hash and stores digest_a. */
|
||||
cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest_a);
|
||||
bool miss_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, miss);
|
||||
int hashes_after_miss = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
/* Hit: the same identity returns the cached digest without calling the
|
||||
* hash, even though the stub would now yield a different value. */
|
||||
cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest_b);
|
||||
bool hit_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, hit);
|
||||
int hashes_after_hit = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
/* Key roll: a changed mtime (a rebuilt binary) misses and recomputes. */
|
||||
rolled = image;
|
||||
rolled.mtime_seconds = 101;
|
||||
bool mtime_ok =
|
||||
cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, mtime_rolled);
|
||||
int hashes_after_mtime_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
/* A changed change time (an in-place write, a metadata edit) rolls it too. */
|
||||
rolled = image;
|
||||
rolled.ctime_seconds = 301;
|
||||
bool ctime_ok =
|
||||
cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, ctime_rolled);
|
||||
int hashes_after_ctime_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
/* A changed inode (atomic replacement) likewise rolls the key. */
|
||||
rolled = image;
|
||||
rolled.inode = 9;
|
||||
bool inode_ok =
|
||||
cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, inode_rolled);
|
||||
int hashes_after_inode_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
|
||||
ASSERT_TRUE(miss_ok);
|
||||
ASSERT_STR_EQ(miss, digest_a);
|
||||
ASSERT_EQ(hashes_after_miss, 1);
|
||||
ASSERT_TRUE(hit_ok);
|
||||
ASSERT_STR_EQ(hit, digest_a);
|
||||
ASSERT_EQ(hashes_after_hit, 1);
|
||||
ASSERT_TRUE(mtime_ok);
|
||||
ASSERT_STR_EQ(mtime_rolled, digest_b);
|
||||
ASSERT_EQ(hashes_after_mtime_roll, 2);
|
||||
ASSERT_TRUE(ctime_ok);
|
||||
ASSERT_STR_EQ(ctime_rolled, digest_b);
|
||||
ASSERT_EQ(hashes_after_ctime_roll, 3);
|
||||
ASSERT_TRUE(inode_ok);
|
||||
ASSERT_STR_EQ(inode_rolled, digest_b);
|
||||
ASSERT_EQ(hashes_after_inode_roll, 4);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A digest is committed only when the identity observed after the hash is the
|
||||
* one it was keyed on. An image that changed under the hash still yields the
|
||||
* bytes read for that acquisition (the acquire chain's own post-hash stat check
|
||||
* is what rejects it) but is never cached, so the next sight of the original
|
||||
* identity hashes again. */
|
||||
TEST(daemon_runtime_fingerprint_cache_skips_image_changed_under_hash) {
|
||||
static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] =
|
||||
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
|
||||
const cbm_daemon_runtime_fingerprint_identity_for_testing_t image = {1, 2, 3, 100,
|
||||
200, 300, 400};
|
||||
cbm_daemon_runtime_fingerprint_identity_for_testing_t moved = image;
|
||||
moved.ctime_seconds = 301;
|
||||
char first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest);
|
||||
bool first_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &moved, first);
|
||||
int hashes_after_first = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
bool second_ok =
|
||||
cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, second);
|
||||
int hashes_after_second = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
|
||||
ASSERT_TRUE(first_ok);
|
||||
ASSERT_STR_EQ(first, digest);
|
||||
ASSERT_EQ(hashes_after_first, 1);
|
||||
ASSERT_TRUE(second_ok);
|
||||
ASSERT_STR_EQ(second, digest);
|
||||
ASSERT_EQ(hashes_after_second, 2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The real acquire path: fingerprinting this process twice pays the image hash
|
||||
* once, and the second call is served from the cache inside the same bracketed
|
||||
* identity checks. This drives cbm_daemon_runtime_process_build_fingerprint,
|
||||
* not the seam, so a cache wired into the seam but not into the platform's
|
||||
* acquire site fails here. */
|
||||
TEST(daemon_runtime_process_fingerprint_hashes_own_image_once_per_process) {
|
||||
static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] =
|
||||
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
||||
char first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest);
|
||||
bool first_ok = cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), first);
|
||||
int hashes_after_first = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
bool second_ok =
|
||||
cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), second);
|
||||
int hashes_after_second = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
|
||||
ASSERT_TRUE(first_ok);
|
||||
ASSERT_TRUE(second_ok);
|
||||
ASSERT_STR_EQ(first, digest);
|
||||
ASSERT_STR_EQ(second, digest);
|
||||
ASSERT_EQ(hashes_after_first, 1);
|
||||
ASSERT_EQ(hashes_after_second, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
/* Only this process's own image is cached. The peer here is this very runner
|
||||
* spawned in holder mode, so its image has exactly the file identity the own
|
||||
* image is cached under: a cache keyed on that identity alone would answer for
|
||||
* the peer. A peer must be hashed on every fingerprint, before and after the
|
||||
* own image is cached, while the own image still costs one hash. */
|
||||
TEST(daemon_runtime_process_fingerprint_caches_only_own_image) {
|
||||
static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] =
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
|
||||
char image_path[RUNTIME_TEST_PATH_CAP] = {0};
|
||||
char peer_first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char peer_second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char own_first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char own_second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
char peer_after_own[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0};
|
||||
int release_fd = -1;
|
||||
bool setup = runtime_test_self_image_path(image_path);
|
||||
pid_t child = setup ? runtime_test_spawn_blocked_executable(image_path, &release_fd) : -1;
|
||||
setup = setup && child > 0;
|
||||
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest);
|
||||
bool peer_first_ok =
|
||||
setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_first);
|
||||
bool peer_second_ok =
|
||||
setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_second);
|
||||
int hashes_after_peer = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
bool own_first_ok =
|
||||
cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), own_first);
|
||||
bool own_second_ok =
|
||||
cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), own_second);
|
||||
int hashes_after_own = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
bool peer_after_own_ok =
|
||||
setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_after_own);
|
||||
int hashes_after_peer_again = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing();
|
||||
cbm_daemon_runtime_fingerprint_cache_reset_for_testing();
|
||||
runtime_test_stop_blocked_executable(child, release_fd);
|
||||
|
||||
ASSERT_TRUE(setup);
|
||||
ASSERT_TRUE(peer_first_ok);
|
||||
ASSERT_TRUE(peer_second_ok);
|
||||
ASSERT_TRUE(own_first_ok);
|
||||
ASSERT_TRUE(own_second_ok);
|
||||
ASSERT_TRUE(peer_after_own_ok);
|
||||
ASSERT_STR_EQ(peer_first, digest);
|
||||
ASSERT_STR_EQ(peer_second, digest);
|
||||
ASSERT_STR_EQ(own_first, digest);
|
||||
ASSERT_STR_EQ(own_second, digest);
|
||||
ASSERT_STR_EQ(peer_after_own, digest);
|
||||
/* Each peer fingerprint pays its own hash: a peer is never cached. */
|
||||
ASSERT_EQ(hashes_after_peer, 2);
|
||||
/* The own image is hashed once, then served from the cache. */
|
||||
ASSERT_EQ(hashes_after_own, 3);
|
||||
/* The cached own entry never answers for the peer's identical file. */
|
||||
ASSERT_EQ(hashes_after_peer_again, 4);
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) {
|
||||
static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'};
|
||||
cbm_daemon_build_identity_t identity =
|
||||
@@ -5430,6 +5620,12 @@ SUITE(daemon_runtime) {
|
||||
RUN_TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed);
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
RUN_TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path);
|
||||
#endif
|
||||
RUN_TEST(daemon_runtime_fingerprint_cache_hit_miss_key_roll);
|
||||
RUN_TEST(daemon_runtime_fingerprint_cache_skips_image_changed_under_hash);
|
||||
RUN_TEST(daemon_runtime_process_fingerprint_hashes_own_image_once_per_process);
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
RUN_TEST(daemon_runtime_process_fingerprint_caches_only_own_image);
|
||||
#endif
|
||||
RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard);
|
||||
RUN_TEST(daemon_runtime_host_loop_stops_on_coordination_file_loss);
|
||||
|
||||
@@ -630,6 +630,23 @@ TEST(project_name_encodes_unicode_segments_issue571) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_unicode_segments_do_not_collide_issue571) {
|
||||
char *first = cbm_project_name_from_path("/Users/dev/\xe5\xbc\x80\xe5\x8f\x91");
|
||||
char *second = cbm_project_name_from_path("/Users/dev/\xe5\x90\x8e\xe7\xab\xaf");
|
||||
ASSERT_NOT_NULL(first);
|
||||
ASSERT_NOT_NULL(second);
|
||||
ASSERT_STR_NEQ(first, second);
|
||||
/* Distinct is not enough: each name must carry the hex image of its own
|
||||
* non-ASCII segment, so the segment is preserved rather than replaced. */
|
||||
ASSERT_NOT_NULL(strstr(first, "e5bc80e58f91"));
|
||||
ASSERT_NOT_NULL(strstr(second, "e5908ee7abaf"));
|
||||
ASSERT_TRUE(cbm_validate_project_name(first));
|
||||
ASSERT_TRUE(cbm_validate_project_name(second));
|
||||
free(first);
|
||||
free(second);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #624: #571 preserves non-ASCII path segments by hex-encoding each byte
|
||||
* (1 byte -> 2 hex chars), so a DEEP non-ASCII path triples in length and can
|
||||
* blow past the filesystem's 255-byte filename-component limit. Then
|
||||
@@ -802,6 +819,7 @@ SUITE(fqn) {
|
||||
RUN_TEST(project_name_deep_path);
|
||||
RUN_TEST(project_name_always_validator_safe_issue349);
|
||||
RUN_TEST(project_name_encodes_unicode_segments_issue571);
|
||||
RUN_TEST(project_name_unicode_segments_do_not_collide_issue571);
|
||||
RUN_TEST(project_name_length_capped_issue624);
|
||||
RUN_TEST(project_name_colon_only);
|
||||
RUN_TEST(project_name_backslash_only);
|
||||
|
||||
@@ -3331,6 +3331,100 @@ TEST(parallel_csharp_parse_damaged_namespace_is_not_global) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2121, the issue's own two files: C# spells its built-in types as lower-case
|
||||
* keywords, so `int.TryParse(...)` calls the BCL, never the project's only
|
||||
* TryParse. The LSP's answer (System.Int32.TryParse) has no graph node, so the
|
||||
* call falls through to the registry, whose receiver chain check used to skip
|
||||
* every lower-case root. Both resolvers must refuse the name guess (also for
|
||||
* the C# 9 `nint`), and a real call to the project type must still bind. */
|
||||
TEST(parallel_csharp_keyword_receiver_no_name_guess_issue2121) {
|
||||
static const char decoys_src[] =
|
||||
"namespace Contoso.App;\n"
|
||||
"\n"
|
||||
"public class TsidNode\n"
|
||||
"{\n"
|
||||
" public static bool TryParse(string text, out TsidNode n)\n"
|
||||
" {\n"
|
||||
" n = null;\n"
|
||||
" return false;\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
static const char demo_src[] = "using Microsoft.Extensions.Options;\n"
|
||||
"\n"
|
||||
"namespace Contoso.App;\n"
|
||||
"\n"
|
||||
"public class Demo\n"
|
||||
"{\n"
|
||||
" public string M(string input)\n"
|
||||
" {\n"
|
||||
" _ = int.TryParse(input, out _);\n"
|
||||
" _ = nint.TryParse(input, out _);\n"
|
||||
" return input;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" public string Control(string input)\n"
|
||||
" {\n"
|
||||
" _ = TsidNode.TryParse(input, out _);\n"
|
||||
" return input;\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
const char *project = "cbm_cs_2121";
|
||||
static const char *rels[] = {"src/Decoys.cs", "src/Demo.cs"};
|
||||
const char *srcs[] = {decoys_src, demo_src};
|
||||
enum { N_FILES = 2 };
|
||||
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_2121_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir)) {
|
||||
FAIL("mkdtemp failed");
|
||||
}
|
||||
char paths[N_FILES][512];
|
||||
cbm_file_info_t files[N_FILES] = {0};
|
||||
for (int i = 0; i < N_FILES; i++) {
|
||||
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]);
|
||||
if (th_write_file(paths[i], srcs[i]) != 0) {
|
||||
th_rmtree(tmpdir);
|
||||
FAIL("failed to write #2121 fixture");
|
||||
}
|
||||
files[i].path = paths[i];
|
||||
files[i].rel_path = (char *)rels[i];
|
||||
files[i].language = CBM_LANG_CSHARP;
|
||||
}
|
||||
|
||||
cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL};
|
||||
g_harness_cs_registry = true;
|
||||
graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2);
|
||||
g_harness_cs_registry = false;
|
||||
ASSERT_NOT_NULL(graphs[0]);
|
||||
ASSERT_NOT_NULL(graphs[1]);
|
||||
|
||||
const char *tsid = "cbm_cs_2121.src.Decoys.TsidNode.TryParse";
|
||||
bool ok[2][4];
|
||||
bool all = true;
|
||||
for (int g = 0; g < 2; g++) {
|
||||
cbm_gbuf_t *gb = graphs[g];
|
||||
/* The negative below must not pass because a node went missing. */
|
||||
ok[g][0] = cbm_gbuf_find_by_qn(gb, tsid) != NULL;
|
||||
ok[g][1] = find_unique_callable_node_by_tail(gb, "Demo.M") != NULL;
|
||||
ok[g][2] = !has_edge_from_callable_to_qn(gb, "Demo.M", tsid, "CALLS");
|
||||
ok[g][3] = has_edge_from_callable_to_qn(gb, "Demo.Control", tsid, "CALLS");
|
||||
for (int k = 0; k < 4; k++) {
|
||||
all = all && ok[g][k];
|
||||
}
|
||||
}
|
||||
if (!all) {
|
||||
for (int g = 0; g < 2; g++) {
|
||||
printf(" #2121 diagnostic %s: target=%d caller=%d no_keyword_guess=%d control=%d\n",
|
||||
g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3]);
|
||||
}
|
||||
}
|
||||
cbm_gbuf_free(graphs[0]);
|
||||
cbm_gbuf_free(graphs[1]);
|
||||
th_rmtree(tmpdir);
|
||||
ASSERT_TRUE(all);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* External Kotlin protocol targets must not borrow a project method merely
|
||||
* because the final Class.method segments agree. Raw assertions prove that the
|
||||
* iterator/destructuring carriers and their stdlib semantic rows really exist;
|
||||
@@ -6031,6 +6125,7 @@ SUITE(parallel) {
|
||||
RUN_TEST(parallel_csharp_same_name_class_binds_by_namespace);
|
||||
RUN_TEST(parallel_csharp_nested_type_and_global_namespace_binding);
|
||||
RUN_TEST(parallel_csharp_parse_damaged_namespace_is_not_global);
|
||||
RUN_TEST(parallel_csharp_keyword_receiver_no_name_guess_issue2121);
|
||||
RUN_TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail);
|
||||
RUN_TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph);
|
||||
RUN_TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest);
|
||||
|
||||
+261
-40
@@ -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
|
||||
@@ -21834,7 +21960,7 @@ static int python_typed_field_scope_case(bool external) {
|
||||
if (result && reg && gbuf && result->field_types.count == 1) {
|
||||
for (int i = 0; i < result->defs.count; i++) {
|
||||
const CBMDefinition *def = &result->defs.items[i];
|
||||
cbm_registry_add(reg, def->name, def->qualified_name, def->label);
|
||||
cbm_registry_add(reg, def->name, def->qualified_name, def->label, false);
|
||||
}
|
||||
const char **keys = NULL;
|
||||
const char **vals = NULL;
|
||||
@@ -27801,8 +27927,8 @@ TEST(helm_parse_chart_many_dependencies) {
|
||||
|
||||
TEST(registry_resolve_single_candidate) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
||||
cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function");
|
||||
cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false);
|
||||
cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false);
|
||||
|
||||
/* Normal resolve unique name */
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "CreateOrder", "svcC.caller", NULL, NULL, 0);
|
||||
@@ -27820,7 +27946,7 @@ TEST(registry_resolve_single_candidate) {
|
||||
|
||||
TEST(registry_fuzzy_nonexistent) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
||||
cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(reg, "NonExistent", "svcC.caller", NULL, NULL, 0);
|
||||
@@ -27832,8 +27958,8 @@ TEST(registry_fuzzy_nonexistent) {
|
||||
|
||||
TEST(registry_fuzzy_multiple_best_by_distance) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function", false);
|
||||
cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function", false);
|
||||
|
||||
/* Caller in svcA → prefer svcA */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -27852,7 +27978,7 @@ TEST(registry_fuzzy_multiple_best_by_distance) {
|
||||
|
||||
TEST(registry_fuzzy_simple_name_extraction) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function");
|
||||
cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function", false);
|
||||
|
||||
/* Deeply qualified name → extract "DoWork" */
|
||||
cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "some.deep.module.DoWork",
|
||||
@@ -27877,8 +28003,8 @@ TEST(registry_fuzzy_empty) {
|
||||
|
||||
TEST(registry_exists) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function");
|
||||
cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method");
|
||||
cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function", false);
|
||||
cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method", false);
|
||||
|
||||
ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Foo"));
|
||||
ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Bar"));
|
||||
@@ -27891,7 +28017,7 @@ TEST(registry_exists) {
|
||||
|
||||
TEST(registry_confidence_import_map) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function");
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false);
|
||||
|
||||
const char *keys[] = {"other"};
|
||||
const char *vals[] = {"proj.other"};
|
||||
@@ -27906,7 +28032,7 @@ TEST(registry_confidence_import_map) {
|
||||
|
||||
TEST(registry_confidence_import_map_suffix) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function");
|
||||
cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function", false);
|
||||
|
||||
const char *keys[] = {"other"};
|
||||
const char *vals[] = {"proj.other"};
|
||||
@@ -27921,7 +28047,7 @@ TEST(registry_confidence_import_map_suffix) {
|
||||
|
||||
TEST(registry_confidence_same_module) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function");
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo");
|
||||
@@ -27934,7 +28060,7 @@ TEST(registry_confidence_same_module) {
|
||||
|
||||
TEST(registry_confidence_unique_name) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function");
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar");
|
||||
@@ -27947,8 +28073,8 @@ TEST(registry_confidence_unique_name) {
|
||||
|
||||
TEST(registry_confidence_suffix_match) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function", false);
|
||||
cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.svcA.caller", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "proj.svcA.Process");
|
||||
@@ -27964,7 +28090,7 @@ TEST(registry_confidence_suffix_match) {
|
||||
* wrong target — and with one candidate it won the top name-only confidence. */
|
||||
TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false);
|
||||
|
||||
cbm_resolution_t r =
|
||||
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
|
||||
@@ -27978,8 +28104,8 @@ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) {
|
||||
* the final name and import distance picks the winner. */
|
||||
TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable");
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable");
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false);
|
||||
cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable", false);
|
||||
|
||||
cbm_resolution_t r =
|
||||
cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0);
|
||||
@@ -27993,7 +28119,7 @@ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) {
|
||||
* so Calendar really is in the receiver chain. */
|
||||
TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method");
|
||||
cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC",
|
||||
"HomeboxUI.Stats", NULL, NULL, 0);
|
||||
@@ -28013,7 +28139,7 @@ TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
|
||||
TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "putList", "org.elasticsearch.common.settings.Settings.Builder.putList",
|
||||
"Method");
|
||||
"Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(
|
||||
reg, "Settings.builder().putList", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0);
|
||||
@@ -28024,7 +28150,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) {
|
||||
* .toBuilder().setWarningsHandler -> RequestOptions.Builder.setWarningsHandler. */
|
||||
cbm_registry_add(reg, "setWarningsHandler",
|
||||
"org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler",
|
||||
"Method");
|
||||
"Method", false);
|
||||
r = cbm_registry_resolve(reg, "RequestOptions.DEFAULT.toBuilder().setWarningsHandler",
|
||||
"org.elasticsearch.client.Rest", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name,
|
||||
@@ -28040,7 +28166,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) {
|
||||
TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "getEncoder", "org.elasticsearch.index.codec.DocOffsetsCodec.getEncoder",
|
||||
"Method");
|
||||
"Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(
|
||||
reg, "Base64.getEncoder", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0);
|
||||
@@ -28050,7 +28176,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) {
|
||||
* org.elasticsearch.common.Math.toIntExact through "Math" appearing as an
|
||||
* ancestry segment ONLY IF that segment really is the type — here it is, and
|
||||
* that is the correct outcome (the project's own Math.toIntExact). */
|
||||
cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method");
|
||||
cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method", false);
|
||||
r = cbm_registry_resolve(reg, "Math.toIntExact", "org.elasticsearch.index.IndexSettings", NULL,
|
||||
NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.Math.toIntExact");
|
||||
@@ -28063,7 +28189,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) {
|
||||
* must not look at it, or every ordinary vm.load style call would be refused. */
|
||||
TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method");
|
||||
cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load");
|
||||
@@ -28073,10 +28199,99 @@ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #2121: unlike vm.load's lower-case value root above, C#'s built-in
|
||||
* type keywords (int, string, bool, nint, ...) DO name a type and must get the
|
||||
* same chain-consistency check an upper-case root gets. The check belongs to
|
||||
* the CALLS entry point both resolvers share, for a C# file. */
|
||||
TEST(registry_receiver_chain_refuses_lowercase_type_keyword_issue2121) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "TryParse", "tmp.Decoys.TsidNode.TryParse", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "int.TryParse", "tmp.Demo", NULL, NULL,
|
||||
0, CBM_LANG_CSHARP);
|
||||
ASSERT_NULL(r.qualified_name);
|
||||
/* C# 9 native-sized integers are keywords in receiver position too. */
|
||||
r = cbm_registry_resolve_call_lang(reg, "nint.TryParse", "tmp.Demo", NULL, NULL, 0,
|
||||
CBM_LANG_CSHARP);
|
||||
ASSERT_NULL(r.qualified_name);
|
||||
|
||||
cbm_registry_free(reg);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The true positive the new keyword check must not eat: the project really
|
||||
* does define a type literally named "int" (or similar) and the chain shows
|
||||
* it, so the match must still go through. Resolved as a C# call, so this
|
||||
* exercises the keyword branch's ancestry walk, not the non-C# early return
|
||||
* (which would pass through for the wrong reason). */
|
||||
TEST(registry_receiver_chain_keeps_type_keyword_when_chain_matches_issue2121) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "TryParse", "tmp.int.TryParse", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "int.TryParse", "tmp.Demo", NULL, NULL,
|
||||
0, CBM_LANG_CSHARP);
|
||||
ASSERT_STR_EQ(r.qualified_name, "tmp.int.TryParse");
|
||||
|
||||
cbm_registry_free(reg);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The type-keyword branch above is a C#-only rule (closed list, gated on the
|
||||
* calling file's language) and must not trade away recall in a language where
|
||||
* the same spellings are ordinary identifiers. `object` and `string` are
|
||||
* ordinary lower-case variable names in Python/Go; a variable named `object`
|
||||
* calling a method (`object.TryParse`, shaped exactly like the refusal test
|
||||
* above) must still resolve as a value root there: the negative control
|
||||
* pinning the other side of the gate. */
|
||||
TEST(registry_receiver_chain_keyword_list_gated_to_csharp_issue2121) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "TryParse", "tmp.Decoys.TsidNode.TryParse", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "object.TryParse", "tmp.Demo", NULL,
|
||||
NULL, 0, CBM_LANG_PYTHON);
|
||||
ASSERT_STR_EQ(r.qualified_name, "tmp.Decoys.TsidNode.TryParse");
|
||||
ASSERT_STR_EQ(r.strategy, "unique_name");
|
||||
|
||||
/* A Go call and the language-blind cbm_registry_resolve read a second
|
||||
* keyword spelling as a value root as well. */
|
||||
cbm_registry_t *reg2 = cbm_registry_new();
|
||||
cbm_registry_add(reg2, "Join", "tmp.Decoys.Joiner.Join", "Method", false);
|
||||
cbm_resolution_t r2 =
|
||||
cbm_registry_resolve_call_lang(reg2, "string.Join", "tmp.Demo", NULL, NULL, 0, CBM_LANG_GO);
|
||||
ASSERT_STR_EQ(r2.qualified_name, "tmp.Decoys.Joiner.Join");
|
||||
ASSERT_STR_EQ(r2.strategy, "unique_name");
|
||||
r2 = cbm_registry_resolve(reg2, "string.Join", "tmp.Demo", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r2.qualified_name, "tmp.Decoys.Joiner.Join");
|
||||
ASSERT_STR_EQ(r2.strategy, "unique_name");
|
||||
|
||||
cbm_registry_free(reg);
|
||||
cbm_registry_free(reg2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `dynamic` is a contextual keyword in C#, not a reserved type keyword like
|
||||
* `int`: in receiver position (`dynamic.Invoke(...)`) it is a variable that
|
||||
* happens to be called `dynamic`, whose type the chain does not show. In a C#
|
||||
* file it must stay an ordinary lower-case value root, like vm.load above, so
|
||||
* the call still binds: the control that keeps the keyword list closed to
|
||||
* words that can only name a type. */
|
||||
TEST(registry_receiver_chain_csharp_dynamic_variable_still_binds_issue2121) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Invoke", "tmp.Handlers.Handler.Invoke", "Method", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "dynamic.Invoke", "tmp.Demo", NULL,
|
||||
NULL, 0, CBM_LANG_CSHARP);
|
||||
ASSERT_STR_EQ(r.qualified_name, "tmp.Handlers.Handler.Invoke");
|
||||
ASSERT_STR_EQ(r.strategy, "unique_name");
|
||||
|
||||
cbm_registry_free(reg);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An unqualified callee has no chain at all and must pass through unchanged. */
|
||||
TEST(registry_receiver_chain_ignores_bare_name_issue1893) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function");
|
||||
cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function", false);
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper");
|
||||
@@ -28088,7 +28303,7 @@ TEST(registry_receiver_chain_ignores_bare_name_issue1893) {
|
||||
|
||||
TEST(registry_fuzzy_confidence_single) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function");
|
||||
cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(reg, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0);
|
||||
@@ -28102,8 +28317,8 @@ TEST(registry_fuzzy_confidence_single) {
|
||||
|
||||
TEST(registry_fuzzy_confidence_distance) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function", false);
|
||||
cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(reg, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0);
|
||||
@@ -28117,8 +28332,8 @@ TEST(registry_fuzzy_confidence_distance) {
|
||||
|
||||
TEST(registry_negative_import_rejects) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Process", "proj.billing.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.handler.Process", "Function");
|
||||
cbm_registry_add(reg, "Process", "proj.billing.Process", "Function", false);
|
||||
cbm_registry_add(reg, "Process", "proj.handler.Process", "Function", false);
|
||||
|
||||
/* Import only handler's module → should prefer handler */
|
||||
const char *keys[] = {"handler"};
|
||||
@@ -28132,7 +28347,7 @@ TEST(registry_negative_import_rejects) {
|
||||
|
||||
TEST(registry_fuzzy_import_penalty) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function");
|
||||
cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false);
|
||||
|
||||
/* Has imports but billing not imported → confidence halved */
|
||||
const char *keys[] = {"other"};
|
||||
@@ -28149,7 +28364,7 @@ TEST(registry_fuzzy_import_penalty) {
|
||||
|
||||
TEST(registry_fuzzy_no_import_map_passthrough) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function");
|
||||
cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false);
|
||||
|
||||
/* NULL import map → no penalty, full fuzzy confidence */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -28163,10 +28378,10 @@ TEST(registry_fuzzy_no_import_map_passthrough) {
|
||||
|
||||
TEST(registry_find_by_name) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function");
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function");
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function");
|
||||
cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method");
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false);
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false);
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false);
|
||||
cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false);
|
||||
|
||||
/* FindByName returns all entries for "Foo" */
|
||||
const char **foos = NULL;
|
||||
@@ -29126,10 +29341,10 @@ TEST(registry_is_import_reachable) {
|
||||
/* Port of FindEndingWith portion from Go TestFunctionRegistry in pipeline_test.go */
|
||||
TEST(registry_find_ending_with) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function");
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function");
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function");
|
||||
cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method");
|
||||
cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false);
|
||||
cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false);
|
||||
cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false);
|
||||
cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false);
|
||||
|
||||
/* FindEndingWith "DataProcessor.transform" → 1 match */
|
||||
const char **matches = NULL;
|
||||
@@ -34397,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
|
||||
@@ -34687,6 +34904,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(registry_receiver_chain_admits_factory_chain_into_nested_type);
|
||||
RUN_TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule);
|
||||
RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893);
|
||||
RUN_TEST(registry_receiver_chain_refuses_lowercase_type_keyword_issue2121);
|
||||
RUN_TEST(registry_receiver_chain_keeps_type_keyword_when_chain_matches_issue2121);
|
||||
RUN_TEST(registry_receiver_chain_keyword_list_gated_to_csharp_issue2121);
|
||||
RUN_TEST(registry_receiver_chain_csharp_dynamic_variable_still_binds_issue2121);
|
||||
RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893);
|
||||
RUN_TEST(registry_fuzzy_confidence_single);
|
||||
RUN_TEST(registry_fuzzy_confidence_distance);
|
||||
|
||||
+271
-157
@@ -193,7 +193,7 @@ TEST(registry_free_null) {
|
||||
|
||||
TEST(registry_add_and_exists) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function", false);
|
||||
ASSERT_EQ(cbm_registry_size(r), 1);
|
||||
ASSERT_TRUE(cbm_registry_exists(r, "proj.cmd.main"));
|
||||
ASSERT_FALSE(cbm_registry_exists(r, "proj.cmd.other"));
|
||||
@@ -201,10 +201,114 @@ TEST(registry_add_and_exists) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* REG_TEST_PENALTY had no direct coverage. Of the registry call sites in this
|
||||
* suite, none registered a symbol the scorer would treat as a test, so the
|
||||
* layer that keeps production code ahead of test code was never exercised.
|
||||
* These three pin it, and pin where the verdict comes from. */
|
||||
TEST(registry_prefers_non_test_over_test_candidate) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "helper", "proj.app.helper", "Function", false);
|
||||
cbm_registry_add(r, "helper", "proj.harness.helper", "Function", true);
|
||||
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.other", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(res.qualified_name, "proj.app.helper");
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The verdict is the caller's, not the QN's spelling. "latest" contains "test",
|
||||
* so the scan this replaces read ordinary production code as a test and pushed
|
||||
* it below a real one; "harness" spells nothing, so the same scan read a test
|
||||
* as production. Both verdicts here are the extractor's, and both are right. */
|
||||
TEST(registry_test_verdict_comes_from_the_caller_not_the_qn) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "parse", "proj.latest.parse", "Function", false);
|
||||
cbm_registry_add(r, "parse", "proj.harness.parse", "Function", true);
|
||||
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "parse", "proj.other", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(res.qualified_name, "proj.latest.parse");
|
||||
cbm_registry_free(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A bucket of one is returned whatever its verdict: the penalty orders
|
||||
* candidates, it does not veto them. */
|
||||
TEST(registry_sole_test_candidate_still_resolves) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "only", "proj.harness.only", "Function", true);
|
||||
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "only", "proj.other", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(res.qualified_name, "proj.harness.only");
|
||||
cbm_registry_free(r);
|
||||
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");
|
||||
cbm_registry_add(r, "bar", "proj.pkg.bar", "Function");
|
||||
cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class", false);
|
||||
cbm_registry_add(r, "bar", "proj.pkg.bar", "Function", false);
|
||||
|
||||
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.Foo"), "Class");
|
||||
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.bar"), "Function");
|
||||
@@ -218,10 +322,10 @@ TEST(registry_label_of) {
|
||||
* method's fenced `#field` twin when the class has one; nothing else moves. */
|
||||
TEST(registry_value_target_field_twin) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "count", "p.C.count", "Method");
|
||||
cbm_registry_add(r, "count", "p.C.count#field", "Field");
|
||||
cbm_registry_add(r, "size", "p.C.size", "Method");
|
||||
cbm_registry_add(r, "name", "p.C.name", "Field");
|
||||
cbm_registry_add(r, "count", "p.C.count", "Method", false);
|
||||
cbm_registry_add(r, "count", "p.C.count#field", "Field", false);
|
||||
cbm_registry_add(r, "size", "p.C.size", "Method", false);
|
||||
cbm_registry_add(r, "name", "p.C.name", "Field", false);
|
||||
ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.count"), "p.C.count#field");
|
||||
ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.size"), "p.C.size");
|
||||
ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.name"), "p.C.name");
|
||||
@@ -237,9 +341,9 @@ TEST(registry_value_target_field_twin) {
|
||||
|
||||
TEST(registry_find_by_name) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
||||
cbm_registry_add(r, "main", "proj.srv.main", "Function");
|
||||
cbm_registry_add(r, "helper", "proj.util.helper", "Function");
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function", false);
|
||||
cbm_registry_add(r, "main", "proj.srv.main", "Function", false);
|
||||
cbm_registry_add(r, "helper", "proj.util.helper", "Function", false);
|
||||
|
||||
const char **out = NULL;
|
||||
int count = 0;
|
||||
@@ -258,8 +362,8 @@ TEST(registry_find_by_name) {
|
||||
|
||||
TEST(registry_no_duplicates) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function"); /* duplicate */
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function", false);
|
||||
cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); /* duplicate */
|
||||
ASSERT_EQ(cbm_registry_size(r), 1);
|
||||
|
||||
const char **out = NULL;
|
||||
@@ -275,7 +379,7 @@ TEST(registry_no_duplicates) {
|
||||
|
||||
TEST(resolve_same_module) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function");
|
||||
cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function", false);
|
||||
|
||||
/* Call "helper" from the same module → should resolve */
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.pkg.service", NULL, NULL, 0);
|
||||
@@ -297,7 +401,7 @@ TEST(resolve_same_module) {
|
||||
* (false only for a this/super receiver). */
|
||||
TEST(resolve_same_module_only_on_self_receiver) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "get", "proj.pkg.service.get", "Function");
|
||||
cbm_registry_add(r, "get", "proj.pkg.service.get", "Function", false);
|
||||
const CBMLanguage js = CBM_LANG_JAVASCRIPT;
|
||||
const char *mod = "proj.pkg.service";
|
||||
CBMImport axios_import = {.local_name = "axios", .module_path = "axios", .is_default = true};
|
||||
@@ -457,11 +561,11 @@ TEST(resolve_qualified_receiver_awareness) {
|
||||
const CBMLanguage js = CBM_LANG_JAVASCRIPT;
|
||||
|
||||
/* 1. Register a Method candidate under Class Service: proj.pkg.Service.get */
|
||||
cbm_registry_add(r, "Service", "proj.pkg.Service", "Class");
|
||||
cbm_registry_add(r, "get", "proj.pkg.Service.get", "Method");
|
||||
cbm_registry_add(r, "Service", "proj.pkg.Service", "Class", false);
|
||||
cbm_registry_add(r, "get", "proj.pkg.Service.get", "Method", false);
|
||||
|
||||
/* 2. Register a Module axios */
|
||||
cbm_registry_add(r, "axios", "proj.axios", "Module");
|
||||
cbm_registry_add(r, "axios", "proj.axios", "Module", false);
|
||||
|
||||
/* Test Case A: Unrelated receiver that is a known module/type -> name-only
|
||||
* guess, dropped by the member guard */
|
||||
@@ -513,7 +617,7 @@ TEST(resolve_qualified_receiver_awareness) {
|
||||
/* Test Case F: Function candidate behind an unrelated receiver -> name-only
|
||||
* guess, dropped by the member guard */
|
||||
/* Register unique Function helper.compute */
|
||||
cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function");
|
||||
cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function", false);
|
||||
{
|
||||
cbm_resolution_t res =
|
||||
cbm_registry_resolve(r, "unrelated.compute", "proj.app", NULL, NULL, 0);
|
||||
@@ -530,9 +634,9 @@ TEST(resolve_qualified_receiver_awareness) {
|
||||
* Foo::Bar::sub()) where the same sub name exists in multiple packages. */
|
||||
TEST(resolve_qualified_disambiguates_same_name) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function");
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function");
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function");
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function", false);
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function", false);
|
||||
cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function", false);
|
||||
|
||||
/* Each fully-qualified call routes to its own package. */
|
||||
cbm_resolution_t a =
|
||||
@@ -573,8 +677,8 @@ TEST(resolve_qualified_disambiguates_same_name) {
|
||||
* pick arbitrarily under the high-confidence qualified_suffix strategy. */
|
||||
TEST(resolve_qualified_ambiguous_tail_falls_through) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function");
|
||||
cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function");
|
||||
cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function", false);
|
||||
cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function", false);
|
||||
|
||||
/* "Foo::Bar::run" tail matches BOTH candidates → not unique → fall through. */
|
||||
cbm_resolution_t res =
|
||||
@@ -592,7 +696,7 @@ TEST(resolve_qualified_ambiguous_tail_falls_through) {
|
||||
* `add` callee could ever reach it. */
|
||||
TEST(registry_indexes_by_passed_name_not_qn_tail) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function");
|
||||
cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function", false);
|
||||
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "add", "proj.lib.caller", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(res.qualified_name, "proj.lib.add#cfg(test)");
|
||||
@@ -617,9 +721,9 @@ TEST(registry_indexes_by_passed_name_not_qn_tail) {
|
||||
* lookup a bare module reference needs. */
|
||||
TEST(registry_indexes_a_dotted_name_under_its_tail_too) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web",
|
||||
"Class");
|
||||
cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module");
|
||||
cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web", "Class",
|
||||
false);
|
||||
cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module", false);
|
||||
|
||||
cbm_resolution_t tail = cbm_registry_resolve(r, "web", "proj.main", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(tail.qualified_name, "proj.main.resource.aws_instance.web");
|
||||
@@ -637,7 +741,7 @@ TEST(registry_indexes_a_dotted_name_under_its_tail_too) {
|
||||
|
||||
TEST(resolve_import_map) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function", false);
|
||||
|
||||
/* Import map: "worker" → "proj.pkg.worker" */
|
||||
const char *keys[] = {"worker"};
|
||||
@@ -660,9 +764,9 @@ TEST(resolve_import_map) {
|
||||
* file. Regression for the @/lib/auth-style import case. */
|
||||
TEST(resolve_import_map_bare_function) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function");
|
||||
cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function", false);
|
||||
/* Same name in another module — without the fix this is what gets picked. */
|
||||
cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function");
|
||||
cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function", false);
|
||||
|
||||
const char *keys[] = {"requireAdmin"};
|
||||
const char *vals[] = {"proj.lib.authorization"};
|
||||
@@ -683,7 +787,7 @@ TEST(resolve_import_map_bare_function) {
|
||||
* Regression for #875. */
|
||||
TEST(resolve_import_map_bare_alias) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function");
|
||||
cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function", false);
|
||||
/* Import map: alias "_scan_bash" → FULL SYMBOL QN (not the module). */
|
||||
const char *keys[] = {"_scan_bash"};
|
||||
const char *vals[] = {"proj.security_scan.scan_bash"};
|
||||
@@ -700,10 +804,10 @@ TEST(resolve_import_map_bare_alias) {
|
||||
* already on main via #875/#979; this locks the Yui G1 shape. */
|
||||
TEST(resolve_import_map_aliased_from_import) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function");
|
||||
cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function", false);
|
||||
/* Alias ghost must not win if somehow registered. */
|
||||
cbm_registry_add(r, "bridge_execute", "proj.services.satori_bridge.gate.bridge_execute",
|
||||
"Function");
|
||||
"Function", false);
|
||||
|
||||
const char *keys[] = {"bridge_execute"};
|
||||
const char *vals[] = {"proj.services.satori_bridge.gate.execute"};
|
||||
@@ -719,7 +823,7 @@ TEST(resolve_import_map_aliased_from_import) {
|
||||
|
||||
TEST(resolve_unique_name) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function");
|
||||
cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function", false);
|
||||
|
||||
/* Call "UniqueFunc" — only one candidate project-wide */
|
||||
cbm_resolution_t res =
|
||||
@@ -733,7 +837,7 @@ TEST(resolve_unique_name) {
|
||||
|
||||
TEST(resolve_unresolved) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "foo", "proj.pkg.foo", "Function");
|
||||
cbm_registry_add(r, "foo", "proj.pkg.foo", "Function", false);
|
||||
|
||||
/* Call "nonexistent" — not in registry */
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "nonexistent", "proj.other", NULL, NULL, 0);
|
||||
@@ -750,7 +854,7 @@ TEST(resolve_many_nodes) {
|
||||
char name[32], qn[64];
|
||||
snprintf(name, sizeof(name), "func_%d", i);
|
||||
snprintf(qn, sizeof(qn), "proj.pkg.func_%d", i);
|
||||
cbm_registry_add(r, name, qn, "Function");
|
||||
cbm_registry_add(r, name, qn, "Function", false);
|
||||
}
|
||||
ASSERT_EQ(cbm_registry_size(r), 500);
|
||||
|
||||
@@ -788,8 +892,8 @@ TEST(confidence_band_speculative) {
|
||||
|
||||
TEST(resolve_suffix_match) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function", false);
|
||||
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false);
|
||||
|
||||
/* Caller in svcA — should prefer svcA via import distance */
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.svcA.caller", NULL, NULL, 0);
|
||||
@@ -812,7 +916,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) {
|
||||
for (int i = 0; i < 300; i++) {
|
||||
char qn[64];
|
||||
snprintf(qn, sizeof(qn), "proj.mod%d.flags", i);
|
||||
cbm_registry_add(r, "flags", qn, "Variable");
|
||||
cbm_registry_add(r, "flags", qn, "Variable", false);
|
||||
}
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "flags", "proj.other.caller", NULL, NULL, 0);
|
||||
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
|
||||
@@ -830,7 +934,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) {
|
||||
|
||||
TEST(resolve_import_map_suffix) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function");
|
||||
cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function", false);
|
||||
|
||||
const char *keys[] = {"other"};
|
||||
const char *vals[] = {"proj.other"};
|
||||
@@ -852,7 +956,7 @@ TEST(resolve_is_import_reachable) {
|
||||
/* Test import reachability through unique_name confidence penalty.
|
||||
* is_import_reachable is static, so we test it indirectly. */
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function");
|
||||
cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function", false);
|
||||
|
||||
/* With import covering the module → full confidence */
|
||||
const char *keys1[] = {"utils"};
|
||||
@@ -875,7 +979,7 @@ TEST(resolve_is_import_reachable) {
|
||||
TEST(resolve_import_reachable_prefix) {
|
||||
/* "proj.handler.sub.Process" should be reachable via import "proj.handler" */
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function", false);
|
||||
|
||||
const char *keys[] = {"handler"};
|
||||
const char *vals[] = {"proj.handler"};
|
||||
@@ -929,8 +1033,8 @@ TEST(reach_cache_memo_matches_uncached_across_files) {
|
||||
|
||||
TEST(negative_import_rejects_unimported) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.billing.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.handler.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.billing.Process", "Function", false);
|
||||
cbm_registry_add(r, "Process", "proj.handler.Process", "Function", false);
|
||||
|
||||
/* Import only handler's module — suffix_match should prefer handler */
|
||||
const char *keys[] = {"handler"};
|
||||
@@ -946,8 +1050,8 @@ TEST(negative_import_rejects_unimported) {
|
||||
|
||||
TEST(fuzzy_resolve_single_candidate) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
||||
cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function");
|
||||
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false);
|
||||
cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false);
|
||||
|
||||
/* FuzzyResolve should find by simple name even with unknown prefix */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -961,7 +1065,7 @@ TEST(fuzzy_resolve_single_candidate) {
|
||||
|
||||
TEST(fuzzy_resolve_nonexistent) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
||||
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(r, "NonExistent", "svcC.caller", NULL, NULL, 0);
|
||||
@@ -973,8 +1077,8 @@ TEST(fuzzy_resolve_nonexistent) {
|
||||
|
||||
TEST(fuzzy_resolve_multiple_best_by_distance) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function", false);
|
||||
cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function", false);
|
||||
|
||||
/* Caller in svcA — should prefer svcA */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -993,7 +1097,7 @@ TEST(fuzzy_resolve_multiple_best_by_distance) {
|
||||
|
||||
TEST(fuzzy_resolve_deep_name_extraction) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function");
|
||||
cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function", false);
|
||||
|
||||
/* Deeply qualified callee — should extract "DoWork" */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -1018,7 +1122,7 @@ TEST(fuzzy_resolve_empty_registry) {
|
||||
|
||||
TEST(fuzzy_resolve_confidence_single) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function");
|
||||
cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(r, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0);
|
||||
@@ -1032,8 +1136,8 @@ TEST(fuzzy_resolve_confidence_single) {
|
||||
|
||||
TEST(fuzzy_resolve_confidence_distance) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function");
|
||||
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function", false);
|
||||
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false);
|
||||
|
||||
cbm_fuzzy_result_t fr =
|
||||
cbm_registry_fuzzy_resolve(r, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0);
|
||||
@@ -1047,7 +1151,7 @@ TEST(fuzzy_resolve_confidence_distance) {
|
||||
|
||||
TEST(fuzzy_penalty_unreachable_import) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function");
|
||||
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false);
|
||||
|
||||
/* Import for different module → confidence halved */
|
||||
const char *keys[] = {"other"};
|
||||
@@ -1064,7 +1168,7 @@ TEST(fuzzy_penalty_unreachable_import) {
|
||||
|
||||
TEST(fuzzy_no_import_map_passthrough) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function");
|
||||
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false);
|
||||
|
||||
/* nil import map → full confidence */
|
||||
cbm_fuzzy_result_t fr =
|
||||
@@ -1173,8 +1277,8 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
cbm_registry_t *forward = cbm_registry_new();
|
||||
cbm_registry_t *backward = cbm_registry_new();
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_registry_add(forward, "dev_name", cands[i], labels[i]);
|
||||
cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i]);
|
||||
cbm_registry_add(forward, "dev_name", cands[i], labels[i], false);
|
||||
cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i], false);
|
||||
}
|
||||
|
||||
/* A far-away caller with no imports: every candidate scores the same. */
|
||||
@@ -1192,10 +1296,10 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
/* Equal kind: the smaller QN, from either order. */
|
||||
cbm_registry_t *lex_a = cbm_registry_new();
|
||||
cbm_registry_t *lex_b = cbm_registry_new();
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false);
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false);
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false);
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false);
|
||||
cbm_resolution_t la =
|
||||
cbm_registry_resolve(lex_a, "sg_next", "proj.drivers.scsi.arm_scsi", NULL, NULL, 0);
|
||||
cbm_resolution_t lb =
|
||||
@@ -1205,9 +1309,9 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
|
||||
/* Proximity outranks the tie order: the sibling wins over a shallower stranger. */
|
||||
cbm_registry_t *proximity = cbm_registry_new();
|
||||
cbm_registry_add(proximity, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function");
|
||||
cbm_registry_add(proximity, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function", false);
|
||||
cbm_registry_add(proximity, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free",
|
||||
"Function");
|
||||
"Function", false);
|
||||
cbm_resolution_t nr = cbm_registry_resolve(proximity, "vnic_rq_free",
|
||||
"proj.drivers.scsi.fnic.fnic_res", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(nr.qualified_name, "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free");
|
||||
@@ -1231,13 +1335,14 @@ TEST(registry_tie_prefers_callable_over_property) {
|
||||
cbm_registry_add(
|
||||
r, "where",
|
||||
"proj.exposed-core.src.main.kotlin.org.jetbrains.exposed.v1.core.AbstractQuery.where",
|
||||
"Variable");
|
||||
"Variable", false);
|
||||
cbm_registry_add(r, "where",
|
||||
"proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where",
|
||||
"Method");
|
||||
"Method", false);
|
||||
/* A deeper method still beats a shallower field: kind decides before order. */
|
||||
cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field");
|
||||
cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method");
|
||||
cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field", false);
|
||||
cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method",
|
||||
false);
|
||||
|
||||
const char *caller =
|
||||
"proj.exposed-tests.src.test.kotlin.org.jetbrains.exposed.v1.tests.SelectTests";
|
||||
@@ -1258,8 +1363,8 @@ TEST(registry_tie_prefers_callable_over_property) {
|
||||
* order alone, which is still a pure function of the candidate set (O9). */
|
||||
TEST(registry_tie_ignores_nesting_depth) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method");
|
||||
cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method");
|
||||
cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method", false);
|
||||
cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method", false);
|
||||
cbm_resolution_t f =
|
||||
cbm_registry_resolve(r, "filter", "proj.tests.lookup.tests", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(f.qualified_name, "proj.django.db.models.query.QuerySet.filter");
|
||||
@@ -1281,23 +1386,25 @@ TEST(registry_tie_prefers_csharp_implementation_over_ref_stub) {
|
||||
cbm_registry_add_lang(
|
||||
r, "MemoryStream",
|
||||
"proj.src.libraries.System.Runtime.ref.System.Runtime.MemoryStream.MemoryStream", "Method",
|
||||
CBM_LANG_CSHARP);
|
||||
CBM_LANG_CSHARP, false);
|
||||
cbm_registry_add_lang(
|
||||
r, "MemoryStream",
|
||||
"proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
"Method", CBM_LANG_CSHARP, false);
|
||||
/* Equal depth, "ref" sorts before "src" (WithCancellation). */
|
||||
cbm_registry_add_lang(r, "WithCancellation",
|
||||
"proj.src.libraries.System.Linq.Parallel.ref.System.Linq."
|
||||
"ParallelEnumerable.WithCancellation",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
"Method", CBM_LANG_CSHARP, false);
|
||||
cbm_registry_add_lang(r, "WithCancellation",
|
||||
"proj.src.libraries.System.Linq.Parallel.src.System.Linq."
|
||||
"ParallelEnumerable.WithCancellation",
|
||||
"Method", CBM_LANG_CSHARP);
|
||||
"Method", CBM_LANG_CSHARP, false);
|
||||
/* Not C#: a `ref` directory gets no special treatment. */
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA);
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA);
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA,
|
||||
false);
|
||||
cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA,
|
||||
false);
|
||||
|
||||
const char *caller = "proj.src.libraries.System.Security.Cryptography.src.CapiHelper";
|
||||
cbm_resolution_t ms = cbm_registry_resolve(r, "MemoryStream", caller, NULL, NULL, 0);
|
||||
@@ -1353,10 +1460,10 @@ static cbm_resolution_t xl_resolve(const cbm_registry_t *r, CBMLanguage lang, co
|
||||
|
||||
TEST(name_guess_vetoed_across_languages) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add_lang(r, "get", "builtins.dict.get", "Method", CBM_LANG_PYTHON);
|
||||
cbm_registry_add_lang(r, "patch", "proj.web.Panel.patch", "Function", CBM_LANG_TSX);
|
||||
cbm_registry_add_lang(r, "patch", "proj.api.client.patch", "Function", CBM_LANG_PYTHON);
|
||||
cbm_registry_add(r, "legacy", "proj.x.legacy", "Function"); /* unknown language */
|
||||
cbm_registry_add_lang(r, "get", "builtins.dict.get", "Method", CBM_LANG_PYTHON, false);
|
||||
cbm_registry_add_lang(r, "patch", "proj.web.Panel.patch", "Function", CBM_LANG_TSX, false);
|
||||
cbm_registry_add_lang(r, "patch", "proj.api.client.patch", "Function", CBM_LANG_PYTHON, false);
|
||||
cbm_registry_add(r, "legacy", "proj.x.legacy", "Function", false); /* unknown language */
|
||||
|
||||
/* JS `app.get`: the only "get" is Python's -> EMPTY, not unique_name. */
|
||||
cbm_resolution_t res = xl_resolve(r, CBM_LANG_JAVASCRIPT, "app.get", "proj.app");
|
||||
@@ -1402,10 +1509,11 @@ TEST(name_guess_vetoed_across_languages) {
|
||||
TEST(same_module_vetoed_across_languages) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add_lang(r, "reducer_worker", "proj.tests.cov.reducer_worker", "Function",
|
||||
CBM_LANG_PYTHON);
|
||||
CBM_LANG_PYTHON, false);
|
||||
cbm_registry_add_lang(r, "reducer_worker", "proj.other.reducer_worker", "Function",
|
||||
CBM_LANG_PYTHON);
|
||||
cbm_registry_add_lang(r, "stat_child", "proj.tests.cov.stat_child", "Function", CBM_LANG_C);
|
||||
CBM_LANG_PYTHON, false);
|
||||
cbm_registry_add_lang(r, "stat_child", "proj.tests.cov.stat_child", "Function", CBM_LANG_C,
|
||||
false);
|
||||
|
||||
cbm_resolution_t res =
|
||||
cbm_registry_resolve(r, "reducer_worker", "proj.tests.cov", NULL, NULL, 0);
|
||||
@@ -1562,11 +1670,11 @@ TEST(c_bare_call_does_not_weigh_field_namesakes) {
|
||||
* A bare C call never names a member, so its Field namesakes do not
|
||||
* compete: the function is the unique name it was in v0.11.0. */
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add_lang(r, "put_page", "proj.include.linux.mm.put_page", "Function",
|
||||
CBM_LANG_CPP);
|
||||
cbm_registry_add_lang(r, "put_page", "proj.include.linux.mm.put_page", "Function", CBM_LANG_CPP,
|
||||
false);
|
||||
cbm_registry_add_lang(r, "put_page",
|
||||
"proj.arch.arm64.include.asm.kvm_pgtable.kvm_pgtable_mm_ops.put_page",
|
||||
"Field", CBM_LANG_CPP);
|
||||
"Field", CBM_LANG_CPP, false);
|
||||
const char *mod = "proj.arch.arm64.kernel.elfcore";
|
||||
cbm_resolution_t res =
|
||||
cbm_registry_resolve_call_lang(r, "put_page", mod, NULL, NULL, 0, CBM_LANG_C);
|
||||
@@ -1581,13 +1689,14 @@ TEST(c_bare_call_does_not_weigh_field_namesakes) {
|
||||
/* A name only members carry is no bare C call target at all -- neither a
|
||||
* struct member nor a C++ method: redis's libc `read()` became the unique
|
||||
* name of a Qt adapter's `read` Method once only Fields were set aside. */
|
||||
cbm_registry_add_lang(r, "on_close", "proj.net.sock.sock_ops.on_close", "Field", CBM_LANG_C);
|
||||
cbm_registry_add_lang(r, "on_close", "proj.net.sock.sock_ops.on_close", "Field", CBM_LANG_C,
|
||||
false);
|
||||
res = cbm_registry_resolve_call_lang(r, "on_close", mod, NULL, NULL, 0, CBM_LANG_C);
|
||||
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
|
||||
cbm_registry_add_lang(r, "read", "proj.deps.hiredis.adapters.qt.RedisQtAdapter.read", "Method",
|
||||
CBM_LANG_CPP);
|
||||
cbm_registry_add_lang(r, "read", "proj.src.connection.ConnectionType.read", "Field",
|
||||
CBM_LANG_C);
|
||||
CBM_LANG_CPP, false);
|
||||
cbm_registry_add_lang(r, "read", "proj.src.connection.ConnectionType.read", "Field", CBM_LANG_C,
|
||||
false);
|
||||
res = cbm_registry_resolve_call_lang(r, "read", mod, NULL, NULL, 0, CBM_LANG_C);
|
||||
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
|
||||
/* C++ still reaches the method by name. */
|
||||
@@ -1652,12 +1761,12 @@ TEST(unique_field_qn_needs_exactly_one_field_of_that_name) {
|
||||
* the same name is not a candidate for a member at all. */
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "only_here", "proj.a.Alpha.only_here", "Field");
|
||||
cbm_registry_add(r, "count", "proj.a.Alpha.count", "Field");
|
||||
cbm_registry_add(r, "count", "proj.b.Beta.count", "Field");
|
||||
cbm_registry_add(r, "close", "proj.cb.Callbacks.close", "Field");
|
||||
cbm_registry_add(r, "close", "proj.io.close", "Function");
|
||||
cbm_registry_add(r, "lonely_fn", "proj.io.lonely_fn", "Function");
|
||||
cbm_registry_add(r, "only_here", "proj.a.Alpha.only_here", "Field", false);
|
||||
cbm_registry_add(r, "count", "proj.a.Alpha.count", "Field", false);
|
||||
cbm_registry_add(r, "count", "proj.b.Beta.count", "Field", false);
|
||||
cbm_registry_add(r, "close", "proj.cb.Callbacks.close", "Field", false);
|
||||
cbm_registry_add(r, "close", "proj.io.close", "Function", false);
|
||||
cbm_registry_add(r, "lonely_fn", "proj.io.lonely_fn", "Function", false);
|
||||
|
||||
const char *only = cbm_registry_unique_field_qn(r, "only_here");
|
||||
ASSERT_NOT_NULL(only);
|
||||
@@ -1746,7 +1855,7 @@ TEST(generated_member_registers_by_qn_only) {
|
||||
ASSERT_EQ(n, 0);
|
||||
cbm_resolution_t res = cbm_registry_resolve(r, "v.y", "proj.app.Caller", NULL, NULL, 0);
|
||||
ASSERT_NULL(res.qualified_name);
|
||||
cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method");
|
||||
cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method", false);
|
||||
res = cbm_registry_resolve(r, "v.x", "proj.app.Caller", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(res.qualified_name, "proj.geo.Point.Point.x");
|
||||
/* Degenerate input registers nothing. */
|
||||
@@ -2403,8 +2512,8 @@ TEST(go_suppress_keeps_typed_and_import_aware_matches) {
|
||||
* Signal.send). Regression guard for #1000. */
|
||||
TEST(resolve_import_map_alias_with_suffix_hits_method) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable");
|
||||
cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method");
|
||||
cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable", false);
|
||||
cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method", false);
|
||||
const char *keys[] = {"user_logged_in"};
|
||||
const char *vals[] = {"proj.auth.signals.user_logged_in"};
|
||||
cbm_resolution_t res =
|
||||
@@ -2418,7 +2527,7 @@ TEST(resolve_import_map_alias_with_suffix_hits_method) {
|
||||
TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *qns[] = {
|
||||
"proj.Service.work(flag:Bool)",
|
||||
"proj.Service.work(name:String)",
|
||||
@@ -2449,7 +2558,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
"combine", "repeated", "repeated", "unlabeled"};
|
||||
const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2};
|
||||
for (int i = 0; i < 22; i++) {
|
||||
cbm_registry_add(r, names[i], qns[i], "Method");
|
||||
cbm_registry_add(r, names[i], qns[i], "Method", false);
|
||||
uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1
|
||||
: i == 17 || i >= 19 ? UINT64_C(1)
|
||||
: 0;
|
||||
@@ -2460,7 +2569,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
const char *generic_qns[] = {"proj.Service.constrained<T;where T==()=>Void>(_:T)",
|
||||
"proj.Service.constrained<T>(_:T)async"};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cbm_registry_add(r, "constrained", generic_qns[i], "Method");
|
||||
cbm_registry_add(r, "constrained", generic_qns[i], "Method", false);
|
||||
cbm_registry_set_swift_signature(r, generic_qns[i], 0, 1, NULL);
|
||||
}
|
||||
CBMCallArg generic_arg = {0};
|
||||
@@ -2468,7 +2577,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
.callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1};
|
||||
ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2);
|
||||
const char *nested = "proj.Service.nested<T,U;where T==Box<[Int]>,U==()=>Void>(_:T)=>Int";
|
||||
cbm_registry_add(r, "nested", nested, "Method");
|
||||
cbm_registry_add(r, "nested", nested, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, nested, 0, 1, NULL);
|
||||
generic_call.callee_name = "Service.nested";
|
||||
ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 1);
|
||||
@@ -2476,7 +2585,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
const char *returns[] = {"proj.Service.pick()=>Int",
|
||||
"proj.Service.pick()async=>(A/Item)=>B/Item"};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
cbm_registry_add(r, "pick", returns[i], "Method");
|
||||
cbm_registry_add(r, "pick", returns[i], "Method", false);
|
||||
cbm_registry_set_swift_signature(r, returns[i], 0, 0, NULL);
|
||||
}
|
||||
CBMCall returned = {.callee_name = "Service.pick"};
|
||||
@@ -2484,7 +2593,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
const char *malformed[] = {"proj.Service.bad()=>", "proj.Service.bad()=>[Int",
|
||||
"proj.Service.bad()=>Int)"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
cbm_registry_add(r, "bad", malformed[i], "Method");
|
||||
cbm_registry_add(r, "bad", malformed[i], "Method", false);
|
||||
cbm_registry_set_swift_signature(r, malformed[i], 0, 0, NULL);
|
||||
}
|
||||
returned.callee_name = "Service.bad";
|
||||
@@ -2493,7 +2602,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char qn[128];
|
||||
snprintf(qn, sizeof(qn), "proj.Service.%s(_:A,_:A)=>Bool", operators[i]);
|
||||
cbm_registry_add(r, operators[i], qn, "Method");
|
||||
cbm_registry_add(r, operators[i], qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, qn, 0, 2, NULL);
|
||||
CBMCallArg operands[2] = {{0}};
|
||||
CBMCall operation = {.callee_name = operators[i], .args = operands, .arg_count = 2};
|
||||
@@ -2657,14 +2766,14 @@ TEST(swift_manifest_root_targets_respect_nested_package_boundaries) {
|
||||
const char *methods[] = {"proj.Root.Ext.Service.run()", "proj.Nested.Ext.Service.run()",
|
||||
"proj.Unknown.Ext.Service.run()"};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
cbm_registry_add(r, "Service", qns[i], "Class");
|
||||
cbm_registry_add(r, "Service", qns[i], "Class", false);
|
||||
ASSERT_GT(cbm_gbuf_upsert_node(graph, "Class", "Service", qns[i], files[i], 1, 3,
|
||||
i == 0 || i == 2 ? "{\"swift_type_path\":\"Service\"}"
|
||||
: "{\"swift_extension\":\"Service\"}"),
|
||||
0);
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
cbm_registry_add(r, "run", methods[i], "Method");
|
||||
cbm_registry_add(r, "run", methods[i], "Method", false);
|
||||
cbm_registry_set_swift_signature(r, methods[i], 0, 0, "()");
|
||||
}
|
||||
cbm_registry_build_swift_owners(r, graph, root, NULL, 0);
|
||||
@@ -2701,12 +2810,12 @@ TEST(swift_receiver_resolution_is_independent_of_registration_order) {
|
||||
for (int reverse = 0; reverse < 2; reverse++) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.alpha.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.beta.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.alpha.Service", "Class", false);
|
||||
cbm_registry_add(r, "Service", "proj.beta.Service", "Class", false);
|
||||
const char *first = reverse ? beta : alpha;
|
||||
const char *second = reverse ? alpha : beta;
|
||||
cbm_registry_add(r, "work", first, "Method");
|
||||
cbm_registry_add(r, "work", second, "Method");
|
||||
cbm_registry_add(r, "work", first, "Method", false);
|
||||
cbm_registry_add(r, "work", second, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, first, 0, 1, NULL);
|
||||
cbm_registry_set_swift_signature(r, second, 0, 1, NULL);
|
||||
|
||||
@@ -2725,11 +2834,11 @@ TEST(swift_receiver_resolution_is_independent_of_registration_order) {
|
||||
TEST(swift_multiple_trailing_closures_match_labels) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *one_qn = "proj.Service.handle(completion:()=>Void)";
|
||||
const char *two_qn = "proj.Service.handle(completion:()=>Void,onError:()=>Void)";
|
||||
cbm_registry_add(r, "handle", one_qn, "Method");
|
||||
cbm_registry_add(r, "handle", two_qn, "Method");
|
||||
cbm_registry_add(r, "handle", one_qn, "Method", false);
|
||||
cbm_registry_add(r, "handle", two_qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, one_qn, 0, 1, NULL);
|
||||
cbm_registry_set_swift_signature(r, two_qn, 0, 2, NULL);
|
||||
|
||||
@@ -2762,7 +2871,7 @@ TEST(swift_multiple_trailing_closures_match_labels) {
|
||||
* closure can skip an earlier defaulted closure and bind the required one. */
|
||||
const char *delayed_qn =
|
||||
"proj.Service.delayed(onDismiss:(()=>Void)?,content:()=>Void,onError:(()=>Void)?)";
|
||||
cbm_registry_add(r, "delayed", delayed_qn, "Method");
|
||||
cbm_registry_add(r, "delayed", delayed_qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, delayed_qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 2), 3,
|
||||
NULL);
|
||||
call.callee_name = "Service.delayed";
|
||||
@@ -2779,9 +2888,9 @@ TEST(swift_multiple_trailing_closures_match_labels) {
|
||||
TEST(swift_repeated_labels_skip_default_before_next_trailing_label) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)";
|
||||
cbm_registry_add(r, "repeated", qn, "Method");
|
||||
cbm_registry_add(r, "repeated", qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 3), 4, NULL);
|
||||
const char *labels[3] = {NULL, "a", "b"};
|
||||
CBMCall call = {.callee_name = "Service.repeated",
|
||||
@@ -2800,9 +2909,9 @@ TEST(swift_repeated_labels_skip_default_before_next_trailing_label) {
|
||||
TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)";
|
||||
cbm_registry_add(r, "repeated", qn, "Method");
|
||||
cbm_registry_add(r, "repeated", qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 1), 4, NULL);
|
||||
const char *labels[2] = {NULL, "b"};
|
||||
CBMCall call = {.callee_name = "Service.repeated",
|
||||
@@ -2822,9 +2931,9 @@ TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) {
|
||||
TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *qn = "proj.Service.f(callbacks:(()=>Void)~,flag:Bool,done:()=>Void)";
|
||||
cbm_registry_add(r, "f", qn, "Method");
|
||||
cbm_registry_add(r, "f", qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, qn, UINT64_C(1) << 1, 3, NULL);
|
||||
CBMCallArg arg = {.keyword = "flag"};
|
||||
const char *labels[2] = {NULL, "done"};
|
||||
@@ -2845,17 +2954,17 @@ TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) {
|
||||
TEST(swift_overloaded_bucket_fails_closed) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class");
|
||||
cbm_registry_add(r, "Service", "proj.Service", "Class", false);
|
||||
const char *first_qn = "proj.Service.work(flag:Bool)";
|
||||
cbm_registry_add(r, "work", first_qn, "Method");
|
||||
cbm_registry_add(r, "work", first_qn, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, first_qn, 0, 1, NULL);
|
||||
const char *other = "proj.Other.work(flag:Bool)";
|
||||
cbm_registry_add(r, "work", other, "Method");
|
||||
cbm_registry_add(r, "work", other, "Method", false);
|
||||
cbm_registry_set_swift_signature(r, other, 0, 1, NULL);
|
||||
char qns[256][64];
|
||||
for (int i = 1; i < 256; i++) {
|
||||
snprintf(qns[i], sizeof(qns[i]), "proj.Other%d.work(flag:Bool)", i);
|
||||
cbm_registry_add(r, "work", qns[i], "Method");
|
||||
cbm_registry_add(r, "work", qns[i], "Method", false);
|
||||
cbm_registry_set_swift_signature(r, qns[i], 0, 1, NULL);
|
||||
}
|
||||
CBMCall call = {.callee_name = "Service.work"};
|
||||
@@ -2921,11 +3030,11 @@ TEST(python_symbol_scope_len_preserves_file_identity) {
|
||||
TEST(registry_python_package_scope_is_explicit) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Field", "proj.forms.fields.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.db.fields.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.db.fields.__init__.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.db.plain.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.db.index.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.forms.fields.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.db.fields.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.db.fields.__init__.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.db.plain.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.db.index.Field", "Class", false);
|
||||
const char *keys[] = {"forms"};
|
||||
const char *vals[] = {"proj.forms"};
|
||||
const char *module = "proj.db.fields.__init__";
|
||||
@@ -2957,11 +3066,11 @@ TEST(registry_python_package_scope_is_explicit) {
|
||||
TEST(registry_python_scope_keeps_import_precedence_and_raw_scoring) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Field", "proj.vendor.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.pkg.Field", "Class");
|
||||
cbm_registry_add(r, "Field", "proj.pkg.__init__.Field", "Class");
|
||||
cbm_registry_add(r, "choose", "proj.pkg.__init__.child.choose", "Function");
|
||||
cbm_registry_add(r, "choose", "proj.pkg.peer.choose", "Function");
|
||||
cbm_registry_add(r, "Field", "proj.vendor.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.pkg.Field", "Class", false);
|
||||
cbm_registry_add(r, "Field", "proj.pkg.__init__.Field", "Class", false);
|
||||
cbm_registry_add(r, "choose", "proj.pkg.__init__.child.choose", "Function", false);
|
||||
cbm_registry_add(r, "choose", "proj.pkg.peer.choose", "Function", false);
|
||||
const char *keys[] = {"Field"};
|
||||
const char *vals[] = {"proj.vendor.Field"};
|
||||
bool seeded = cbm_registry_size(r) == 5;
|
||||
@@ -2985,14 +3094,14 @@ TEST(registry_python_scope_keeps_import_precedence_and_raw_scoring) {
|
||||
TEST(registry_python_scope_cache_modes_and_raw_lineage) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
ASSERT_NOT_NULL(r);
|
||||
cbm_registry_add(r, "Entry", "proj.pkg.Entry", "Function");
|
||||
cbm_registry_add(r, "Entry", "proj.pkg.__init__.Entry", "Function");
|
||||
cbm_registry_add(r, "Reject", "proj.pkg.Reject", "Table");
|
||||
cbm_registry_add(r, "Reject", "proj.pkg.__init__.Reject", "Function");
|
||||
cbm_registry_add(r, "Accept", "proj.pkg.Accept", "Function");
|
||||
cbm_registry_add(r, "Accept", "proj.pkg.__init__.Accept", "Table");
|
||||
cbm_registry_add(r, "Relation", "proj.pkg.Relation", "View");
|
||||
cbm_registry_add(r, "Relation", "proj.pkg.__init__.Relation", "Table");
|
||||
cbm_registry_add(r, "Entry", "proj.pkg.Entry", "Function", false);
|
||||
cbm_registry_add(r, "Entry", "proj.pkg.__init__.Entry", "Function", false);
|
||||
cbm_registry_add(r, "Reject", "proj.pkg.Reject", "Table", false);
|
||||
cbm_registry_add(r, "Reject", "proj.pkg.__init__.Reject", "Function", false);
|
||||
cbm_registry_add(r, "Accept", "proj.pkg.Accept", "Function", false);
|
||||
cbm_registry_add(r, "Accept", "proj.pkg.__init__.Accept", "Table", false);
|
||||
cbm_registry_add(r, "Relation", "proj.pkg.Relation", "View", false);
|
||||
cbm_registry_add(r, "Relation", "proj.pkg.__init__.Relation", "Table", false);
|
||||
bool seeded = cbm_registry_size(r) == 8;
|
||||
bool correct = true;
|
||||
/* Each cache lifetime has one fixed module/import map. Alternate modes in
|
||||
@@ -3070,12 +3179,12 @@ TEST(registry_python_scope_complete_candidate_boundaries) {
|
||||
int tn = snprintf(target, sizeof(target), "%s.%s", scope, name);
|
||||
bool shape = mn > 0 && (size_t)mn < sizeof(module) && tn > 0 && (size_t)tn == lengths[i] &&
|
||||
(size_t)tn < sizeof(target);
|
||||
cbm_registry_add(r, name, target, "Function");
|
||||
cbm_registry_add(r, name, target, "Function", false);
|
||||
if (lengths[i] > 511) {
|
||||
char truncated[512];
|
||||
memcpy(truncated, target, sizeof(truncated) - 1);
|
||||
truncated[sizeof(truncated) - 1] = '\0';
|
||||
cbm_registry_add(r, "truncated_decoy", truncated, "Function");
|
||||
cbm_registry_add(r, "truncated_decoy", truncated, "Function", false);
|
||||
}
|
||||
const char *canonical = registry_scope_canonical(r, name, target);
|
||||
cbm_resolution_t python =
|
||||
@@ -3102,7 +3211,7 @@ TEST(registry_python_scope_qualified_terminal_bucket_uncapped) {
|
||||
char qn[80];
|
||||
int n = snprintf(qn, sizeof(qn), "proj.noise%d.perform", i);
|
||||
shape = n > 0 && (size_t)n < sizeof(qn) && shape;
|
||||
cbm_registry_add(r, "perform", qn, "Method");
|
||||
cbm_registry_add(r, "perform", qn, "Method", false);
|
||||
}
|
||||
char scope[1024];
|
||||
memcpy(scope, "proj.", 5);
|
||||
@@ -3120,7 +3229,7 @@ TEST(registry_python_scope_qualified_terminal_bucket_uncapped) {
|
||||
int pn = snprintf(prefixed, sizeof(prefixed), "receiver.%s", names[i]);
|
||||
shape = tn > 0 && (size_t)tn < sizeof(target) && pn > 0 && (size_t)pn < sizeof(prefixed) &&
|
||||
shape;
|
||||
cbm_registry_add(r, "perform", target, "Method");
|
||||
cbm_registry_add(r, "perform", target, "Method", false);
|
||||
const char *canonical = registry_scope_canonical(r, "perform", target);
|
||||
cbm_resolution_t direct =
|
||||
cbm_registry_resolve_lang(r, names[i], module, NULL, NULL, 0, CBM_LANG_PYTHON);
|
||||
@@ -3154,13 +3263,13 @@ TEST(registry_python_function_local_is_exact_only_issue1548) {
|
||||
{"Inner", "proj.web.Outer.Inner", "Class"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) {
|
||||
cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON,
|
||||
web);
|
||||
cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON, web,
|
||||
false);
|
||||
}
|
||||
cbm_registry_add_lang_in_module(r, "f", "proj.ts.f", "Function", CBM_LANG_TYPESCRIPT,
|
||||
strlen("proj.ts"));
|
||||
strlen("proj.ts"), false);
|
||||
cbm_registry_add_lang_in_module(r, "g", "proj.ts.f.g", "Function", CBM_LANG_TYPESCRIPT,
|
||||
strlen("proj.ts"));
|
||||
strlen("proj.ts"), false);
|
||||
|
||||
static const char *const local[] = {"proj.web.create_app.index", "proj.web.create_app.Settings",
|
||||
"proj.web.create_app.Settings.value"};
|
||||
@@ -3212,7 +3321,7 @@ TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548) {
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) {
|
||||
cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON,
|
||||
strlen("proj.m"));
|
||||
strlen("proj.m"), false);
|
||||
}
|
||||
|
||||
/* In method Tree.visit, `visit` is its nested def; so it is inside that
|
||||
@@ -3254,17 +3363,17 @@ TEST(registry_python_local_bound_at_own_file_issue1548) {
|
||||
cbm_registry_t *r = cbm_registry_new();
|
||||
if (order == 0) {
|
||||
cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function",
|
||||
CBM_LANG_PYTHON, init_prefix);
|
||||
CBM_LANG_PYTHON, init_prefix, false);
|
||||
}
|
||||
cbm_registry_add_lang_in_module(r, "Layout", "proj.pkg.layout.Layout", "Class",
|
||||
CBM_LANG_PYTHON, mod_prefix);
|
||||
CBM_LANG_PYTHON, mod_prefix, false);
|
||||
cbm_registry_add_lang_in_module(r, "create", "proj.pkg.layout.Layout.create", "Method",
|
||||
CBM_LANG_PYTHON, mod_prefix);
|
||||
CBM_LANG_PYTHON, mod_prefix, false);
|
||||
cbm_registry_add_lang_in_module(r, "make", "proj.pkg.layout.make", "Function",
|
||||
CBM_LANG_PYTHON, mod_prefix);
|
||||
CBM_LANG_PYTHON, mod_prefix, false);
|
||||
if (order == 1) {
|
||||
cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function",
|
||||
CBM_LANG_PYTHON, init_prefix);
|
||||
CBM_LANG_PYTHON, init_prefix, false);
|
||||
}
|
||||
static const char *const nameable[][2] = {{"Layout", "proj.pkg.layout.Layout"},
|
||||
{"create", "proj.pkg.layout.Layout.create"},
|
||||
@@ -3332,6 +3441,11 @@ SUITE(registry) {
|
||||
RUN_TEST(registry_create_free);
|
||||
RUN_TEST(registry_free_null);
|
||||
RUN_TEST(registry_add_and_exists);
|
||||
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);
|
||||
|
||||
@@ -25,6 +25,8 @@ class McpServer:
|
||||
self._id = 0
|
||||
self.proc = None
|
||||
self._stderr = []
|
||||
self._stderr_thread = None
|
||||
self._stderr_eof = False
|
||||
env = dict(os.environ)
|
||||
if cache_dir:
|
||||
env["CBM_CACHE_DIR"] = cache_dir # isolate the graph DB location
|
||||
@@ -52,12 +54,27 @@ class McpServer:
|
||||
try:
|
||||
for line in self.proc.stderr:
|
||||
self._stderr.append(line.decode("utf-8", "replace"))
|
||||
self._stderr_eof = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stderr_text(self):
|
||||
return "".join(self._stderr)
|
||||
|
||||
def stderr_complete(self, timeout):
|
||||
"""Wait for the stderr reader to reach EOF; True once it has.
|
||||
|
||||
Only then is stderr_text() the whole capture. False means the reader
|
||||
is still running (some process still holds the pipe) or stopped on a
|
||||
read error, so the text may be partial: a caller that asserts on
|
||||
stderr must treat False as a setup failure, never as a verdict. The
|
||||
timeout is a liveness backstop, not part of any verdict.
|
||||
"""
|
||||
if self._stderr_thread is None:
|
||||
return False
|
||||
self._stderr_thread.join(timeout)
|
||||
return not self._stderr_thread.is_alive() and self._stderr_eof
|
||||
|
||||
def _send(self, obj):
|
||||
data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||
self.proc.stdin.write(data + b"\n")
|
||||
@@ -148,6 +165,7 @@ class McpServer:
|
||||
self.proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self._stderr_thread.join(timeout=10)
|
||||
|
||||
|
||||
def wait_projects_with_stats(server, timeout=90.0, poll=1.0):
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Regression guard for clean Windows MCP stdio startup (#654).
|
||||
|
||||
A successful initialize + tools/list exchange must leave nothing on stderr but
|
||||
the server's own structured log lines. #654 reported the OS "path not found"
|
||||
message leaking to stderr on startup, in English and on a Spanish Windows. Those
|
||||
two messages are named cases; any other non-empty line that is not a structured
|
||||
`level=<name> msg=<event>` record fails as well, so the same leak on a machine
|
||||
in any other language is caught without listing its translation.
|
||||
|
||||
The verdict is read only after the stderr reader has reached EOF. A reader that
|
||||
is still running after the server exited means the capture may be partial, and
|
||||
that is a setup failure, never a pass.
|
||||
|
||||
Exit codes follow the guard contract of scripts/test-windows.ps1:
|
||||
0 = GREEN, 1 = RED (regression), 2 = SETUP / precondition.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from mcp_stdio import McpServer
|
||||
|
||||
# The OS path-not-found message as reported in #654 (English and Spanish
|
||||
# Windows). Named so a RED says exactly which known leak came back.
|
||||
KNOWN_PATH_ERRORS = (
|
||||
"The system cannot find the path specified.",
|
||||
"El sistema no puede encontrar la ruta especificada.",
|
||||
)
|
||||
|
||||
# The text-format record cbm_log writes (src/foundation/log.c):
|
||||
# "level=<debug|info|warn|error> msg=<event> key=value ...".
|
||||
STRUCTURED_LOG_LINE = re.compile(r"level=[a-z]+ msg=\S")
|
||||
|
||||
# Liveness backstop for the stderr reader. The server has already exited when
|
||||
# this is waited on, so the reader is at EOF unless another process still holds
|
||||
# the pipe. Expiry is a SETUP failure; it never decides PASS or FAIL.
|
||||
STDERR_EOF_BACKSTOP_S = 60
|
||||
|
||||
|
||||
def classify_stderr(text):
|
||||
"""Return (known, raw): the named #654 messages found, and every other
|
||||
non-empty stderr line that is not a structured log record."""
|
||||
known = [message for message in KNOWN_PATH_ERRORS if message in text]
|
||||
raw = []
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or STRUCTURED_LOG_LINE.match(line):
|
||||
continue
|
||||
if any(message in line for message in KNOWN_PATH_ERRORS):
|
||||
continue
|
||||
raw.append(line)
|
||||
return known, raw
|
||||
|
||||
|
||||
def main():
|
||||
if os.name != "nt":
|
||||
print("SKIP: Windows-only MCP stdio guard")
|
||||
return 2
|
||||
|
||||
binary = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("CBM_TEST_BINARY")
|
||||
if not binary:
|
||||
print("SETUP FAIL: CBM_TEST_BINARY is required", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="cbm-mcp-stdio-") as work:
|
||||
cache = os.path.join(work, "cache")
|
||||
runtime = os.path.join(work, "runtime")
|
||||
os.makedirs(cache)
|
||||
os.makedirs(runtime)
|
||||
# The verdict reads the text log shape, so select it explicitly: an
|
||||
# inherited CBM_LOG_FORMAT=json must not turn every record into a raw
|
||||
# line.
|
||||
server = McpServer(
|
||||
binary,
|
||||
cache_dir=cache,
|
||||
extra_env={"CBM_RUNTIME_DIR": runtime, "CBM_LOG_FORMAT": "text"},
|
||||
)
|
||||
try:
|
||||
with server:
|
||||
server.initialize(timeout=30)
|
||||
server.tools_list(timeout=30)
|
||||
finally:
|
||||
stop = subprocess.run(
|
||||
[binary, "daemon", "stop"],
|
||||
env=server.env,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
if stop.returncode != 0:
|
||||
detail = (stop.stdout + stop.stderr).decode("utf-8", "replace")
|
||||
raise RuntimeError(
|
||||
"daemon cleanup failed (%d): %s" % (stop.returncode, detail.strip())
|
||||
)
|
||||
|
||||
if not server.stderr_complete(STDERR_EOF_BACKSTOP_S):
|
||||
print(
|
||||
"SETUP FAIL: the stderr reader did not reach EOF after the server "
|
||||
"exited; refusing to judge a possibly partial capture",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
known, raw = classify_stderr(server.stderr_text())
|
||||
if known or raw:
|
||||
if known:
|
||||
print(
|
||||
"FAIL: startup leaked an OS path error to stderr: " + ", ".join(known),
|
||||
file=sys.stderr,
|
||||
)
|
||||
if raw:
|
||||
print(
|
||||
"FAIL: startup wrote %d stderr line(s) that are not structured "
|
||||
"level= log records:" % len(raw),
|
||||
file=sys.stderr,
|
||||
)
|
||||
for line in raw:
|
||||
print(" " + line, file=sys.stderr)
|
||||
return 1
|
||||
print("PASS: successful MCP startup emitted only structured log lines on stderr")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user