Merge pull request #1230 from DeusData/fix/dry-run-determinism

Deterministic QA gates, dry-run fixes, and graph-quality repairs
This commit is contained in:
Martin Vogel
2026-07-23 20:06:31 +02:00
committed by GitHub
23 changed files with 727 additions and 137 deletions
+12 -2
View File
@@ -67,8 +67,18 @@ EDITOR_TEST_DEFINES = -DCBM_JSON_LIKE_ENABLE_TEST_API=1 \
-DCBM_TOML_EDIT_ENABLE_TEST_API=1 -DCBM_YAML_ENABLE_TEST_API=1 \
-DCBM_TEXT_EDIT_ENABLE_TEST_API=1 -DCBM_CLI_ENABLE_TEST_API=1 \
-DCBM_DIAGNOSTICS_ENABLE_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) -g -O1 $(SANITIZE)
# The build system is the single source of truth for "is this binary
# instrumented": compiler-specific probes (__SANITIZE_ADDRESS__) miss
# clang's feature-check spelling and every non-ASan sanitizer, so the
# trap-UBSan leg once ran NATIVE timing budgets on an instrumented
# binary. Any non-empty SANITIZE ⇒ sanitized budgets everywhere.
ifneq ($(strip $(SANITIZE)),)
SANITIZED_DEFINE = -DCBM_SANITIZED_BUILD=1
else
SANITIZED_DEFINE =
endif
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE)
# TSan (can't combine with ASan)
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
+19 -2
View File
@@ -54,9 +54,17 @@ stamp_windows_build_dir() {
MINGW* | MSYS*) ;;
*) return 0 ;;
esac
local runner_dir_w me stamp_out reset_out
local runner_dir_w me norm_out stamp_out reset_out
runner_dir_w="$(cygpath -w "$(dirname "$RUNNER")")"
me="$(whoami | tr -d '\r')"
# Normalize FIRST: some runner images stamp EXPLICIT (non-inherited)
# Authenticated-Users ACEs onto the workspace tree, which /inheritance:r
# cannot strip and /grant:r does not touch (it replaces only the granted
# SIDs' own entries). /reset drops every explicit ACE and restores pure
# inheritance, so the protect-and-grant below starts from a known shape
# regardless of image provisioning.
norm_out=$(MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" /reset /Q 2>&1) ||
echo "WARN: build-dir DACL normalize ($when) failed: $norm_out"
stamp_out=$(MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" /inheritance:r \
/grant:r "${me}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' \
/Q 2>&1) || echo "WARN: build-dir DACL stamp ($when) failed (user=$me dir=$runner_dir_w): $stamp_out"
@@ -67,8 +75,9 @@ stamp_windows_build_dir() {
# even see WHETHER it had run.
if MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" 2>/dev/null |
grep -qE 'Authenticated Users|CREATOR OWNER'; then
echo "WARN: build-dir DACL still grants cross-account mutation after $when stamp:"
echo "FAIL: build-dir DACL still grants cross-account mutation after $when stamp:"
MSYS2_ARG_CONV_EXCL='*' icacls "$runner_dir_w" 2>&1 | head -8
exit 1
else
echo "build-dir DACL stamped clean ($when, $runner_dir_w, user=$me)"
fi
@@ -215,6 +224,14 @@ run_one() {
fi
secs=$((SECONDS - t0))
summary=$(grep -E '^ [0-9]+ passed' "$LOGDIR/$s.log" | tail -1)
# A suite that exits 0 WITHOUT printing its completion summary ran zero
# tests as far as anyone can prove (mis-parsed argv, drifted registration
# macro, early return) — that is a failure, not a green with pass=0.
if [ "$rc" -eq 0 ] && [ -z "$summary" ]; then
rc=97
echo " FAIL: suite '$s' exited 0 without a completion summary (ran nothing?)" \
>> "$LOGDIR/$s.log"
fi
pass=$(printf '%s' "$summary" | sed -n 's/^ \([0-9]*\) passed.*/\1/p')
failn=$(printf '%s' "$summary" | sed -n 's/.* \([0-9]*\) failed.*/\1/p')
skip=$(printf '%s' "$summary" | sed -n 's/.* \([0-9]*\) skipped.*/\1/p')
+24 -6
View File
@@ -1,6 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
# The fuzzer IS python3; without it this gate would run zero mutations while
# still reporting green. Fail loudly instead.
command -v python3 >/dev/null 2>&1 || {
echo "FAIL: python3 missing — the fuzz gate cannot generate payloads" >&2
exit 1
}
# Reproducibility: every payload derives from a logged seed, so any crash is
# re-runnable by construction (CBM_FUZZ_SEED overrides for replay).
FUZZ_SEED="${CBM_FUZZ_SEED:-$$}"
echo "fuzz seed: $FUZZ_SEED (replay: CBM_FUZZ_SEED=$FUZZ_SEED)"
# Fuzz testing: feeds random/mutated inputs to the MCP server and CLI
# to find crashes, hangs, and memory errors. Runs for a limited time.
#
@@ -25,11 +36,15 @@ END_TIME=$((SECONDS + DURATION))
# Portable timeout
run_with_timeout() {
# PRESERVES the child's exit status: the whole gate exists to observe
# crash codes (139/134/136), and an `|| true` here once made EC
# unconditionally 0 — the crash checks were dead code and a SIGSEGV
# read green. Timeout reports 124, which the checks correctly ignore.
local secs="$1"; shift
if command -v timeout &>/dev/null; then
timeout "$secs" "$@" || true
timeout "$secs" "$@"
else
perl -e "alarm($secs); exec @ARGV" -- "$@" || true
perl -e "alarm($secs); exec @ARGV" -- "$@"
fi
}
@@ -43,6 +58,7 @@ while [ $SECONDS -lt $END_TIME ]; do
# Generate a random mutated JSON-RPC payload
PAYLOAD=$(python3 -c "
import random, string, json
random.seed($FUZZ_SEED + $ITERATIONS)
# Base valid request
base = {
@@ -111,8 +127,8 @@ except:
printf '%s\n%s\n' "$INIT" "$PAYLOAD" > "$FUZZ_TMPDIR/input.jsonl"
# Run and check for crashes (not exit code — we expect errors, just not crashes)
run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1
EC=$?
EC=0
run_with_timeout 5 "$BINARY" < "$FUZZ_TMPDIR/input.jsonl" > /dev/null 2>&1 || EC=$?
# 139 = SIGSEGV, 134 = SIGABRT, 136 = SIGFPE — real crashes
if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
@@ -135,7 +151,8 @@ while [ $SECONDS -lt $CLI_END ]; do
CLI_ITERATIONS=$((CLI_ITERATIONS + 1))
QUERY=$(python3 -c "
import random, string
import random
random.seed($FUZZ_SEED + 100000 + $CLI_ITERATIONS), string
parts = ['MATCH', 'WHERE', 'RETURN', '(', ')', '-[', ']->', '<-[', ']-',
'n', 'r', '.name', '.label', '=', '\"', \"'\", ';', '--', 'DROP',
'DELETE', 'ATTACH', 'DETACH', '*', 'COUNT', 'LIMIT', 'ORDER BY']
@@ -146,7 +163,8 @@ if random.random() > 0.5:
print(q)
" 2>/dev/null || echo "MATCH (n) RETURN n")
run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1
EC=0
run_with_timeout 3 "$BINARY" cli query_graph "{\"query\":\"$QUERY\"}" > /dev/null 2>&1 || EC=$?
EC=$?
if [ $EC -eq 139 ] || [ $EC -eq 134 ] || [ $EC -eq 136 ]; then
+64 -13
View File
@@ -82,6 +82,21 @@ retire_account_daemon() {
fi
}
# Tolerate ERRORS, never CRASHES: the adversarial phases assert "doesn't
# crash", and a bare `|| true` discards exactly the crash exit the claim is
# about. rc >= 128 means killed by signal / fatal status (SIGSEGV=139,
# SIGABRT=134 — ASan aborts land here too).
run_no_crash() {
local label="$1"; shift
local rc=0
"$@" > /dev/null 2>&1 || rc=$?
if [ "$rc" -ge 128 ]; then
echo "FAIL $label: crashed (rc=$rc)"
exit 1
fi
return 0
}
TMPDIR=$(smoke_mktemp_dir)
DRYRUN_HOME=""
# On MSYS2/Windows, convert POSIX path to native Windows path for the binary
@@ -2596,7 +2611,12 @@ echo "--- Phase 9b: adversarial install/uninstall tests ---"
# completes without crash and prints "Detected agents:" line.
EMPTY_HOME=$(smoke_mktemp_dir)
mkdir -p "$EMPTY_HOME/.local/bin"
INSTALL_OUT=$(HOME="$EMPTY_HOME" LOCALAPPDATA="$EMPTY_HOME/AppData/Local" "$BINARY" install -y 2>&1) || true
INSTALL_RC=0
INSTALL_OUT=$(HOME="$EMPTY_HOME" LOCALAPPDATA="$EMPTY_HOME/AppData/Local" "$BINARY" install -y 2>&1) || INSTALL_RC=$?
if [ "$INSTALL_RC" -ge 128 ]; then
echo "FAIL 9b-1: install crashed (rc=$INSTALL_RC)"
exit 1
fi
if ! echo "$INSTALL_OUT" | grep -qi 'detected agents'; then
echo "FAIL 9b-1: install output missing 'Detected agents' line"
exit 1
@@ -2609,12 +2629,12 @@ rm -rf "$EMPTY_HOME"
IDEM_HOME=$(smoke_mktemp_dir)
mkdir -p "$IDEM_HOME/.claude" "$IDEM_HOME/.local/bin"
copy_smoke_binary "$IDEM_HOME/.local/bin/codebase-memory-mcp"
HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-2 env HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$BINARY" install -y
IDEM_INSTALLER="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
IDEM_INSTALLER="$IDEM_HOME/.local/bin/codebase-memory-mcp.exe"
fi
HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$IDEM_INSTALLER" install -y 2>&1 > /dev/null || true
run_no_crash 9b-2-second env HOME="$IDEM_HOME" LOCALAPPDATA="$IDEM_HOME/AppData/Local" "$IDEM_INSTALLER" install -y
# Count MCP entries — should be exactly 1
COUNT=$(cat "$IDEM_HOME/.claude.json" 2>/dev/null | python3 -c "
import json, sys
@@ -2632,7 +2652,12 @@ rm -rf "$IDEM_HOME"
# 9b-3: Uninstall without prior install
CLEAN_HOME=$(smoke_mktemp_dir)
mkdir -p "$CLEAN_HOME/.claude" "$CLEAN_HOME/.local/bin"
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$BINARY" uninstall -y -n 2>&1) || true
UNINSTALL_RC=0
UNINSTALL_OUT=$(HOME="$CLEAN_HOME" "$BINARY" uninstall -y -n 2>&1) || UNINSTALL_RC=$?
if [ "$UNINSTALL_RC" -ge 128 ]; then
echo "FAIL 9b-3: uninstall crashed (rc=$UNINSTALL_RC)"
exit 1
fi
echo "OK 9b-3: uninstall without install doesn't crash"
retire_account_daemon "9b-3-cleanup"
rm -rf "$CLEAN_HOME"
@@ -2642,7 +2667,7 @@ CORRUPT_HOME=$(smoke_mktemp_dir)
mkdir -p "$CORRUPT_HOME/.claude" "$CORRUPT_HOME/.local/bin"
copy_smoke_binary "$CORRUPT_HOME/.local/bin/codebase-memory-mcp"
echo '{invalid json here' > "$CORRUPT_HOME/.claude.json"
HOME="$CORRUPT_HOME" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-4 env HOME="$CORRUPT_HOME" "$BINARY" install -y
# Should either fix it or handle gracefully — not crash
echo "OK 9b-4: install over corrupt JSON doesn't crash"
retire_account_daemon "9b-4-cleanup"
@@ -2652,13 +2677,13 @@ rm -rf "$CORRUPT_HOME"
DBL_HOME=$(smoke_mktemp_dir)
mkdir -p "$DBL_HOME/.claude" "$DBL_HOME/.local/bin"
copy_smoke_binary "$DBL_HOME/.local/bin/codebase-memory-mcp"
HOME="$DBL_HOME" "$BINARY" install -y 2>&1 > /dev/null || true
run_no_crash 9b-8-install env HOME="$DBL_HOME" "$BINARY" install -y
DBL_UNINSTALLER="$BINARY"
if [[ "$BINARY" == *.exe ]]; then
DBL_UNINSTALLER="$DBL_HOME/.local/bin/codebase-memory-mcp.exe"
fi
HOME="$DBL_HOME" "$DBL_UNINSTALLER" uninstall -y -n 2>&1 > /dev/null || true
HOME="$DBL_HOME" "$BINARY" uninstall -y -n 2>&1 > /dev/null || true
run_no_crash 9b-8-first env HOME="$DBL_HOME" "$DBL_UNINSTALLER" uninstall -y -n
run_no_crash 9b-8-second env HOME="$DBL_HOME" "$BINARY" uninstall -y -n
echo "OK 9b-8: double uninstall doesn't crash"
retire_account_daemon "9b-8-cleanup"
rm -rf "$DBL_HOME"
@@ -2783,7 +2808,8 @@ echo "=== Phase 11: process kill E2E ==="
# Start MCP server in background
MCP_KILL_INPUT=$(smoke_mktemp_file)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"kill-test","version":"1.0"}}}' > "$MCP_KILL_INPUT"
"$BINARY" < "$MCP_KILL_INPUT" > /dev/null 2>&1 &
KILL_OUT=$(smoke_mktemp_file)
"$BINARY" < "$MCP_KILL_INPUT" > "$KILL_OUT" 2>&1 &
KILL_PID=$!
sleep 1
@@ -2798,8 +2824,20 @@ if kill -0 "$KILL_PID" 2>/dev/null; then
fi
echo "OK 11c-d: process killed successfully"
else
echo "OK 11: MCP server already exited (clean shutdown on EOF)"
# The server ended before the probe: distinguish clean shutdown-on-EOF
# from an instant crash — the old branch treated both as OK with the
# output discarded, so a startup segfault read as a clean pass on any
# fast machine. A clean run must have exited 0 AND produced the
# initialize response.
KILL_RC=0
wait "$KILL_PID" 2>/dev/null || KILL_RC=$?
if [ "$KILL_RC" -ge 128 ] || ! grep -q '"jsonrpc"' "$KILL_OUT"; then
echo "FAIL 11: server exited before probe with rc=$KILL_RC and $(wc -c < "$KILL_OUT") bytes of output — crash, not clean EOF shutdown"
exit 1
fi
echo "OK 11: MCP server already exited (clean shutdown on EOF, initialize answered)"
fi
rm -f "$KILL_OUT"
rm -f "$MCP_KILL_INPUT"
@@ -3228,13 +3266,24 @@ fi
echo ""
echo "=== Phase 15: UI HTTP server ==="
UI_PORT=19876
# A kernel-assigned free port instead of a fixed one: a squatter on a fixed
# port made the whole phase read as SKIP. The tiny TOCTOU window between
# probe and bind is the residual risk, not the common case.
UI_PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')
UI_INPUT=$(smoke_mktemp_file)
"$BINARY" --port "$UI_PORT" < "$UI_INPUT" > /dev/null 2>&1 &
UI_PID=$!
sleep 1
# Readiness poll instead of a fixed sleep: SKIP is legitimate ONLY when the
# process exited (the documented no-embedded-assets case); a slow start on a
# loaded runner must not masquerade as it.
UI_READY=0
for _ in $(seq 1 100); do
if ! kill -0 "$UI_PID" 2>/dev/null; then break; fi
if curl -sf "http://127.0.0.1:$UI_PORT/" -o /dev/null 2>/dev/null; then UI_READY=1; break; fi
sleep 0.1
done
if kill -0 "$UI_PID" 2>/dev/null; then
if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then
# 15a: GET / returns 200 with HTML content
UI_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/" 2>/dev/null || echo "")
if echo "$UI_BODY" | grep -qi "<html"; then
@@ -3258,6 +3307,8 @@ if kill -0 "$UI_PID" 2>/dev/null; then
echo "SKIP 15b: /rpc not reachable"
else
echo "FAIL 15b: /rpc did not return JSON-RPC"
kill "$UI_PID" 2>/dev/null || true
exit 1
fi
kill "$UI_PID" 2>/dev/null || true
+25
View File
@@ -340,6 +340,19 @@ mcp_call() {
MCP_LAST_RESPONSE="$resp"
if json_rpc_response_ok "$id" "$resp"; then
exit_code=0
else
# A stale (previous, late) response desyncs every later
# round-trip: drain queued lines until the matching id shows up
# or the stream runs dry, so one slow reply costs one failed op
# instead of corrupting the whole run's verdicts.
local drain=""
while read -r -t 2 drain <&4 2>/dev/null; do
MCP_LAST_RESPONSE="$drain"
if json_rpc_response_ok "$id" "$drain"; then
exit_code=0
break
fi
done
fi
fi
local t1
@@ -426,7 +439,9 @@ wait_for_diagnostics_snapshot() {
return 1
}
SNAPSHOT_ATTEMPTS=0
collect_snapshot() {
SNAPSHOT_ATTEMPTS=$((SNAPSHOT_ATTEMPTS + 1))
refresh_diagnostics_paths || return 0
if [ -f "$DIAG_FILE" ]; then
python3 -c "
@@ -681,6 +696,16 @@ fi
# ── Analysis ─────────────────────────────────────────────────────
# Check 0: the analysis must have DATA. Snapshot collection silently
# tolerates transient misses, so a run where diagnostics never materialized
# used to sail through every check vacuously — the leak/FD/latency verdicts
# below are only meaningful if most sampling attempts actually landed.
SNAPSHOT_ROWS=$(($(wc -l < "$METRICS_CSV") - 1))
if [ "$SNAPSHOT_ATTEMPTS" -gt 0 ] && [ $((SNAPSHOT_ROWS * 10)) -lt $((SNAPSHOT_ATTEMPTS * 6)) ]; then
echo "FAIL: only ${SNAPSHOT_ROWS}/${SNAPSHOT_ATTEMPTS} snapshots collected — analysis would be vacuous" | tee -a "$SUMMARY"
PASS=false
fi
# Check 1: Memory leak detection via RSS trend
# This is the primary leak detector on ALL platforms (including Windows
# where LeakSanitizer is unavailable). Catches both linear leaks (slope)
+13 -2
View File
@@ -227,9 +227,16 @@ foreach ($t in $guards) {
} elseif ($code -eq 1 -or $t -eq "tests\windows\test_windows_launcher.py") {
Write-Host "RED ($t) - REGRESSION: a fixed Windows bug is broken again" -ForegroundColor Red
$guardFailures += $t
} else {
Write-Host "PRECONDITION ($t) exit=$code - skipped (see message above)" -ForegroundColor Yellow
} elseif ($code -eq 2) {
# Exit 2 is the guards' DOCUMENTED precondition-skip contract; every
# other unexpected code (a crashed python, an access-violation status,
# a mistyped guard) is a FAILURE - an uncontracted exit once let a
# crashing guard read as an invisible skip under a green banner.
Write-Host "PRECONDITION ($t) exit=2 - skipped (see message above)" -ForegroundColor Yellow
$guardSkips += $t
} else {
Write-Host "FAILED ($t) exit=$code - crashed or exited outside the guard contract 0/1/2" -ForegroundColor Red
$guardFailures += $t
}
}
@@ -266,6 +273,10 @@ if ($guardSkips.Count -gt 0) {
if ($fixedKeepers.Count -gt 0) {
Write-Host ("Known-red repros that are now GREEN (promote to guards): {0}" -f ($fixedKeepers -join ", ")) -ForegroundColor Green
}
if ($guardSkips.Count -eq $guards.Count -and $guards.Count -gt 0) {
Write-Host "FAIL: every guard skipped - nothing was actually verified" -ForegroundColor Red
exit 1
}
if ($guardFailures.Count -gt 0) {
Write-Host ("REGRESSION: {0} green guard(s) went red: {1}" -f $guardFailures.Count, ($guardFailures -join ", ")) -ForegroundColor Red
Write-Host "A previously-fixed Windows bug is broken again (see the guard's docstring and its referenced issue)." -ForegroundColor Red
+70 -8
View File
@@ -211,6 +211,35 @@ void cbm_daemon_application_fail_next_job_thread_start_for_test(void) {
memory_order_release);
}
static atomic_bool g_application_hold_job_before_start_for_test = ATOMIC_VAR_INIT(false);
/* Counts completed background-initialize passes (the request-handler tail
* that evaluates auto-index admission). The client can observe its
* initialize RESPONSE before this tail finishes, so a test asserting the
* tail's NEGATIVE outcome ("no job was admitted") needs this positive
* completion signal — a fixed sleep is a lottery in both directions. */
static atomic_int g_application_background_initializes_for_test = ATOMIC_VAR_INIT(0);
int cbm_daemon_application_background_initializes_for_test(void) {
return atomic_load_explicit(&g_application_background_initializes_for_test,
memory_order_acquire);
}
/* Counts iterations of the explicit-index busy-queue wait loop. A test that
* needs "the request is parked behind the physical job limit" waits for a
* DELTA here — the positive signal that queueing (not an error return)
* happened — before releasing the slot. */
static atomic_int g_application_busy_queue_waits_for_test = ATOMIC_VAR_INIT(0);
int cbm_daemon_application_busy_queue_waits_for_test(void) {
return atomic_load_explicit(&g_application_busy_queue_waits_for_test, memory_order_acquire);
}
void cbm_daemon_application_hold_job_before_start_for_test(bool hold) {
atomic_store_explicit(&g_application_hold_job_before_start_for_test, hold,
memory_order_release);
}
static int application_job_thread_create(cbm_thread_t *thread, void *context) {
if (atomic_exchange_explicit(&g_application_fail_next_job_thread_start_for_test, false,
memory_order_acq_rel)) {
@@ -1541,6 +1570,15 @@ static void *application_job_thread(void *opaque) {
cbm_daemon_application_job_t *job = opaque;
application_job_execution_t execution;
application_job_execution_init(&execution);
/* Deterministic-interleaving seam: a held gate parks this thread before
* its first pre-start cancel check, so tests can force the
* cancel-wins-before-worker-start ordering that otherwise needs a
* descheduled thread on a loaded runner (CI is never a lottery: the
* interleaving must be reproducible by construction). */
while (
atomic_load_explicit(&g_application_hold_job_before_start_for_test, memory_order_acquire)) {
cbm_usleep(1000);
}
/* The linked non-terminal job is the daemon-internal reservation: it
* coalesces identical requests and blocks same-project daemon mutations.
@@ -1982,7 +2020,7 @@ static char *application_auto_index_args(const char *root_path) {
return args;
}
static void application_background_initialize(cbm_daemon_application_session_t *session) {
static void application_background_initialize_impl(cbm_daemon_application_session_t *session) {
if (!session || !session->application || !session->context_set ||
session->tool_profile != CBM_MCP_TOOL_PROFILE_ALL || session->hook_event ||
session->hook_dialect) {
@@ -2061,6 +2099,12 @@ static void application_background_initialize(cbm_daemon_application_session_t *
application_refresh_watch(session);
}
static void application_background_initialize(cbm_daemon_application_session_t *session) {
application_background_initialize_impl(session);
atomic_fetch_add_explicit(&g_application_background_initializes_for_test, 1,
memory_order_release);
}
static bool application_jsonrpc_success(const char *response) {
yyjson_doc *document = response ? yyjson_read(response, strlen(response), 0) : NULL;
yyjson_val *root = document ? yyjson_doc_get_root(document) : NULL;
@@ -2244,18 +2288,36 @@ static char *application_index_execute(void *context, const char *root_path,
return cbm_mcp_text_result("failed to derive index project identity", true);
}
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
cbm_daemon_application_job_t *job = application_job_subscribe(
session->application, project_key, root_path, args_json, &subscribe_status);
cbm_daemon_application_job_t *job = NULL;
for (;;) {
job = application_job_subscribe(session->application, project_key, root_path, args_json,
&subscribe_status);
if (job || (subscribe_status != APPLICATION_JOB_SUBSCRIBE_BUSY &&
subscribe_status != APPLICATION_JOB_SUBSCRIBE_CANCELLING)) {
break;
}
/* Physical job limit reached (or a same-project cancel still
* draining): QUEUE instead of surfacing a raw busy error. The
* request thread blocks for the whole index anyway, so waiting for
* a slot is the same contract — and the wait stays cancellable and
* shutdown-aware (a stopping coordinator answers UNAVAILABLE, which
* exits this loop with the error below). */
atomic_fetch_add_explicit(&g_application_busy_queue_waits_for_test, 1,
memory_order_release);
cbm_mutex_lock(&session->application->mutex);
bool queued_cancelled = application_request_cancelled_locked(session);
cbm_mutex_unlock(&session->application->mutex);
if (queued_cancelled) {
free(project_key);
return cbm_mcp_text_result("index operation cancelled for this session", true);
}
cbm_usleep(APPLICATION_JOB_POLL_US);
}
free(project_key);
if (!job) {
const char *message = "daemon index coordinator is stopping or unavailable";
if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_OPTIONS_CONFLICT) {
message = "another index operation for this project is active with different options";
} else if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_BUSY) {
message =
"daemon index coordinator is busy: physical index job limit reached; retry later";
} else if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_CANCELLING) {
message = "another index operation for this project is cancelling; retry shortly";
} else if (subscribe_status == APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED) {
message = "daemon index coordinator could not allocate an index job";
}
+16
View File
@@ -19,4 +19,20 @@ bool cbm_daemon_application_session_retains_store_for_test(
* back rather than retained as terminal background state. */
void cbm_daemon_application_fail_next_job_thread_start_for_test(void);
/* Park every new job thread before its first pre-start cancel check while
* held. This makes the cancel-wins-before-worker-start interleaving — which
* otherwise needs a descheduled thread on a loaded runner — reproducible by
* construction: hold, subscribe, cancel, observe CANCELLED, release, then
* assert the worker was never started and its ops were never invoked. */
void cbm_daemon_application_hold_job_before_start_for_test(bool hold);
/* Completed background-initialize passes (auto-index admission tail).
* Waiting for a delta here replaces sleep-then-assert negatives. */
int cbm_daemon_application_background_initializes_for_test(void);
/* Iterations of the explicit-index busy-queue wait (request parked behind
* the physical job limit). Waiting for a delta here is the positive signal
* that a request QUEUED rather than erroring or starting. */
int cbm_daemon_application_busy_queue_waits_for_test(void);
#endif /* CBM_DAEMON_APPLICATION_INTERNAL_H */
+43 -25
View File
@@ -764,7 +764,17 @@ typedef struct {
cbm_gitignore_t *local_gi; /* nested .gitignore for this subtree */
char local_gi_prefix[CBM_SZ_4K]; /* rel_prefix when local_gi was loaded */
} walk_frame_t;
/* Initial capacity only — the stack grows on demand. A single directory can
* hold more pending sibling frames than any fixed cap (dotnet/runtime has 855
* subdirs in one JIT regression dir), so a hard cap here means whole-repo
* discovery failure, not a depth guard. */
#define WALK_STACK_CAP 512
typedef struct {
walk_frame_t *frames;
int top;
int cap;
} walk_stack_t;
/* Build abs/rel paths and process one directory entry. */
/* Try to load a nested .gitignore from this directory. Returns owned pointer or NULL. */
static cbm_gitignore_t *try_load_nested_gitignore(const walk_frame_t *frame) {
@@ -780,35 +790,44 @@ static cbm_gitignore_t *try_load_nested_gitignore(const walk_frame_t *frame) {
return NULL;
}
/* Push a subdirectory onto the walk stack, inheriting local gitignore context. */
static void walk_push_subdir(walk_frame_t *stack, int *top, const char *abs_path,
const char *rel_path, const walk_frame_t *parent, file_list_t *out) {
if (*top >= WALK_STACK_CAP) {
out->failed = true;
return;
/* Push a subdirectory onto the walk stack, inheriting local gitignore
* context. Grows the stack geometrically; the caller's `parent` must not
* point into the stack array (walk_dir pops into a local copy). */
static void walk_push_subdir(walk_stack_t *ws, const char *abs_path, const char *rel_path,
const walk_frame_t *parent, file_list_t *out) {
if (ws->top >= ws->cap) {
int new_cap = ws->cap * 2;
walk_frame_t *grown = realloc(ws->frames, (size_t)new_cap * sizeof(*grown));
if (!grown) {
out->failed = true;
return;
}
ws->frames = grown;
ws->cap = new_cap;
}
int directory_length = snprintf(stack[*top].dir, CBM_SZ_4K, "%s", abs_path);
int prefix_length = snprintf(stack[*top].prefix, CBM_SZ_4K, "%s", rel_path);
walk_frame_t *slot = &ws->frames[ws->top];
int directory_length = snprintf(slot->dir, CBM_SZ_4K, "%s", abs_path);
int prefix_length = snprintf(slot->prefix, CBM_SZ_4K, "%s", rel_path);
if (directory_length <= 0 || directory_length >= CBM_SZ_4K || prefix_length < 0 ||
prefix_length >= CBM_SZ_4K) {
out->failed = true;
return;
}
stack[*top].local_gi = parent->local_gi;
slot->local_gi = parent->local_gi;
int local_prefix_length =
snprintf(stack[*top].local_gi_prefix, CBM_SZ_4K, "%s", parent->local_gi_prefix);
snprintf(slot->local_gi_prefix, CBM_SZ_4K, "%s", parent->local_gi_prefix);
if (local_prefix_length < 0 || local_prefix_length >= CBM_SZ_4K) {
out->failed = true;
return;
}
(*top)++;
ws->top++;
}
static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *frame,
const cbm_discover_opts_t *opts,
const cbm_gitignore_t *gitignore,
const cbm_gitignore_t *global_gi,
const cbm_gitignore_t *cbmignore, walk_frame_t *stack, int *top,
const cbm_gitignore_t *cbmignore, walk_stack_t *ws,
file_list_t *out) {
char abs_path[CBM_SZ_4K];
char rel_path[CBM_SZ_4K];
@@ -836,7 +855,7 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
if (S_ISDIR(st.st_mode)) {
if (!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore,
frame->local_gi, frame->local_gi_prefix)) {
walk_push_subdir(stack, top, abs_path, rel_path, frame, out);
walk_push_subdir(ws, abs_path, rel_path, frame, out);
} else {
/* Record the excluded subtree root so callers can report it (#411). */
file_list_add_excluded(out, rel_path);
@@ -871,8 +890,9 @@ static bool walk_owned_gitignore_append(cbm_gitignore_t ***owned, size_t *count,
static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_discover_opts_t *opts,
const cbm_gitignore_t *gitignore, const cbm_gitignore_t *global_gi,
const cbm_gitignore_t *cbmignore, file_list_t *out) {
walk_frame_t *stack = calloc(WALK_STACK_CAP, sizeof(walk_frame_t));
if (!stack) {
walk_stack_t ws = {
.frames = calloc(WALK_STACK_CAP, sizeof(walk_frame_t)), .top = 0, .cap = WALK_STACK_CAP};
if (!ws.frames) {
out->failed = true;
return;
}
@@ -882,19 +902,18 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis
size_t owned_count = 0;
size_t owned_capacity = 0;
int top = 0;
int initial_directory_length = snprintf(stack[top].dir, CBM_SZ_4K, "%s", dir_path);
int initial_prefix_length = snprintf(stack[top].prefix, CBM_SZ_4K, "%s", rel_prefix);
int initial_directory_length = snprintf(ws.frames[0].dir, CBM_SZ_4K, "%s", dir_path);
int initial_prefix_length = snprintf(ws.frames[0].prefix, CBM_SZ_4K, "%s", rel_prefix);
if (initial_directory_length <= 0 || initial_directory_length >= CBM_SZ_4K ||
initial_prefix_length < 0 || initial_prefix_length >= CBM_SZ_4K) {
out->failed = true;
free(stack);
free(ws.frames);
return;
}
top++;
ws.top++;
while (top > 0 && !file_list_should_stop(out)) {
walk_frame_t frame = stack[--top];
while (ws.top > 0 && !file_list_should_stop(out)) {
walk_frame_t frame = ws.frames[--ws.top];
cbm_gitignore_t *loaded = try_load_nested_gitignore(&frame);
if (loaded) {
@@ -920,8 +939,7 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis
cbm_dirent_t *entry;
while (!file_list_should_stop(out) && (entry = cbm_readdir(d)) != NULL) {
walk_dir_process_entry(entry, &frame, opts, gitignore, global_gi, cbmignore, stack,
&top, out);
walk_dir_process_entry(entry, &frame, opts, gitignore, global_gi, cbmignore, &ws, out);
}
cbm_closedir(d);
}
@@ -929,7 +947,7 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis
cbm_gitignore_free(owned_gis[i]);
}
free(owned_gis);
free(stack);
free(ws.frames);
}
/* ── Public API ───────────────────────────────── */
+9 -1
View File
@@ -2539,7 +2539,11 @@ static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
}
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, bqn);
if (bn && node->id != bn->id) {
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, bn->id, "INHERITS", "{}");
/* Same Interface-label split as the sequential semantic pass —
* hardcoding INHERITS here demoted every explicit `implements`
* on large (parallel-path) corpora. */
cbm_gbuf_insert_edge(ws->local_edge_buf, node->id, bn->id,
cbm_semantic_base_edge_type(bn), "{}");
ws->semantic_resolved++;
}
}
@@ -3010,6 +3014,10 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
/* Go-style implicit interface satisfaction (needs full graph, serial) */
int go_impl = cbm_pipeline_implements_go(ctx);
/* Explicit-language override detection (same serial full-graph tail the
* sequential pipeline runs — the two venues must emit identical graphs). */
total_lsp_overrides += cbm_pipeline_override_explicit(ctx);
if (atomic_load(ctx->cancelled)) {
return CBM_NOT_FOUND;
}
+78 -8
View File
@@ -454,12 +454,13 @@ static void sem_process_def_edges(cbm_pipeline_ctx_t *ctx, const CBMDefinition *
if (base_node && node->id != base_node->id) {
/* A base that resolves to an Interface is an IMPLEMENTS relation
* (Java `implements`, C# `: IFace`, TS `implements`); a Class/
* Type/Enum base is plain INHERITS. */
const char *base_label = cbm_registry_label_of(ctx->registry, base_qn);
const char *edge_type = (base_label && strcmp(base_label, "Interface") == 0)
? "IMPLEMENTS"
: "INHERITS";
cbm_gbuf_insert_edge(ctx->gbuf, node->id, base_node->id, edge_type, "{}");
* Type/Enum base is plain INHERITS. Keyed off the target NODE's
* label — the graph truth the edge attaches to — so the
* sequential and parallel venues cannot diverge (the parallel
* path once hardcoded INHERITS and demoted every explicit
* implements at scale). */
cbm_gbuf_insert_edge(ctx->gbuf, node->id, base_node->id,
cbm_semantic_base_edge_type(base_node), "{}");
(*inherits_count)++;
}
}
@@ -576,8 +577,77 @@ int cbm_pipeline_pass_semantic(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
int go_impl = cbm_pipeline_implements_go(ctx);
implements_count += go_impl;
/* ── Explicit-language override detection (full graph, serial) ── */
int overrides = cbm_pipeline_override_explicit(ctx);
cbm_log_info("pass.done", "pass", "semantic", "inherits", itoa_log(inherits_count), "decorates",
itoa_log(decorates_count), "implements", itoa_log(implements_count), "errors",
itoa_log(errors));
itoa_log(decorates_count), "implements", itoa_log(implements_count), "overrides",
itoa_log(overrides), "errors", itoa_log(errors));
return 0;
}
const char *cbm_semantic_base_edge_type(const cbm_gbuf_node_t *base_node) {
return (base_node && base_node->label && strcmp(base_node->label, "Interface") == 0)
? "IMPLEMENTS"
: "INHERITS";
}
/* Create OVERRIDE edges from one class's methods to same-named methods of one
* explicit base (interface or superclass). */
static int override_match_methods(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *cls,
const cbm_gbuf_node_t *base) {
const cbm_gbuf_edge_t **cls_dm = NULL;
int cls_dm_count = 0;
cbm_gbuf_find_edges_by_source_type(ctx->gbuf, cls->id, "DEFINES_METHOD", &cls_dm,
&cls_dm_count);
if (cls_dm_count == 0) {
return 0;
}
const cbm_gbuf_edge_t **base_dm = NULL;
int base_dm_count = 0;
cbm_gbuf_find_edges_by_source_type(ctx->gbuf, base->id, "DEFINES_METHOD", &base_dm,
&base_dm_count);
int created = 0;
for (int c = 0; c < cls_dm_count; c++) {
const cbm_gbuf_node_t *cm = cbm_gbuf_find_by_id(ctx->gbuf, cls_dm[c]->target_id);
if (!cm || !cm->name) {
continue;
}
for (int b = 0; b < base_dm_count; b++) {
const cbm_gbuf_node_t *bm = cbm_gbuf_find_by_id(ctx->gbuf, base_dm[b]->target_id);
if (bm && bm->name && cm->id != bm->id && strcmp(cm->name, bm->name) == 0) {
cbm_gbuf_insert_edge(ctx->gbuf, cm->id, bm->id, "OVERRIDE", "{}");
created++;
break;
}
}
}
return created;
}
int cbm_pipeline_override_explicit(cbm_pipeline_ctx_t *ctx) {
if (!ctx || !ctx->gbuf) {
return 0;
}
int created = 0;
static const char *base_edge_types[] = {"IMPLEMENTS", "INHERITS"};
for (size_t t = 0; t < sizeof(base_edge_types) / sizeof(base_edge_types[0]); t++) {
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
cbm_gbuf_find_edges_by_type(ctx->gbuf, base_edge_types[t], &edges, &edge_count);
for (int e = 0; e < edge_count; e++) {
const cbm_gbuf_node_t *cls = cbm_gbuf_find_by_id(ctx->gbuf, edges[e]->source_id);
const cbm_gbuf_node_t *base = cbm_gbuf_find_by_id(ctx->gbuf, edges[e]->target_id);
if (!cls || !base) {
continue;
}
/* Go's implicit satisfaction already emits OVERRIDE with interface
* semantics; running both would double-cover .go sources. */
if (cls->file_path && fp_ends_with(cls->file_path, ".go")) {
continue;
}
created += override_match_methods(ctx, cls, base);
}
}
return created;
}
+14
View File
@@ -259,6 +259,20 @@ int cbm_compute_change_coupling(const cbm_commit_files_t *commits, int commit_co
* creates IMPLEMENTS + OVERRIDE edges. Returns edge count created. */
int cbm_pipeline_implements_go(cbm_pipeline_ctx_t *ctx);
/* Edge type for an explicit base-class relation, keyed off the resolved
* TARGET node's label: Interface → IMPLEMENTS, anything else → INHERITS.
* The single decision point for BOTH the sequential semantic pass and the
* parallel per-file resolve — the two venues must never diverge. */
const char *cbm_semantic_base_edge_type(const cbm_gbuf_node_t *base_node);
/* Explicit-language override detection on the full graph (serial tail).
* For every IMPLEMENTS/INHERITS edge whose source is a non-Go class, matches
* the class's DEFINES_METHOD children by name against the base's and creates
* Method→Method OVERRIDE edges (Java @Override, TS/C#/Kotlin override, PHP
* redefinition). Go is excluded: implicit satisfaction already covers it.
* Returns edge count created. */
int cbm_pipeline_override_explicit(cbm_pipeline_ctx_t *ctx);
/* ── Git diff helpers (pass_gitdiff.c) ───────────────────────────── */
typedef struct {
+14 -1
View File
@@ -63,6 +63,7 @@ struct cbm_httpd {
struct cbm_http_conn *active;
bool interrupted;
int send_buffer_for_test;
int send_deadline_for_test_ms;
};
struct cbm_http_conn {
@@ -311,6 +312,17 @@ bool cbm_httpd_close(cbm_httpd_t *d) {
return true;
}
void cbm_httpd_set_send_deadline_for_test(cbm_httpd_t *d, int ms) {
/* Lets a test pin the send deadline far above (or below) the default so
* an observed abort has exactly ONE possible cause — the interruption
* tests once discriminated interrupt-abort from deadline-abort by wall
* clock, which sanitizer slowdown turns into a lottery. */
if (!d) {
return;
}
d->send_deadline_for_test_ms = ms;
}
void cbm_httpd_set_send_buffer_for_test(cbm_httpd_t *d, int bytes) {
if (!d)
return;
@@ -374,7 +386,8 @@ cbm_http_conn_t *cbm_httpd_accept(cbm_httpd_t *d, int timeout_ms) {
}
c->fd = cfd;
c->owner = d;
c->send_deadline_ms = CBM_HTTP_SEND_DEADLINE_MS;
c->send_deadline_ms = d->send_deadline_for_test_ms > 0 ? d->send_deadline_for_test_ms
: CBM_HTTP_SEND_DEADLINE_MS;
atomic_init(&c->response_started, false);
cbm_mutex_lock(&d->active_mutex);
+1
View File
@@ -90,6 +90,7 @@ cbm_httpd_activity_t cbm_httpd_activity_for_test(cbm_httpd_t *d);
/* Caps SO_SNDBUF on subsequently accepted sockets so a non-reading peer
* produces a deterministic backpressure point on every platform. */
void cbm_httpd_set_send_buffer_for_test(cbm_httpd_t *d, int bytes);
void cbm_httpd_set_send_deadline_for_test(cbm_httpd_t *d, int ms);
/* ── Connection handling ──────────────────────────────────────── */
+3 -1
View File
@@ -6,7 +6,9 @@
# Build: docker build -t cbm-test test-infrastructure/
# Run: docker run --rm -v $(pwd):/src cbm-test
FROM ubuntu:noble
# Pinned by digest (supply-chain: no floating tags) — multi-arch manifest-list
# digest of ubuntu:noble as of 2026-07-23; bump deliberately, never to a tag.
FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
# Minimal: gcc + zlib only. sqlite3 is vendored (compiled from source with ASan).
# curl + zsh mirror the GitHub runner images: the self-update tests shell out
+3 -1
View File
@@ -5,7 +5,9 @@
# Build: docker build -t cbm-lint -f test-infrastructure/Dockerfile.lint test-infrastructure/
# Run: docker run --rm -v $(pwd):/src cbm-lint
FROM ubuntu:noble
# Pinned by digest (supply-chain: no floating tags) — multi-arch manifest-list
# digest of ubuntu:noble as of 2026-07-23; bump deliberately, never to a tag.
FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ make cmake \
+12 -3
View File
@@ -7,7 +7,10 @@
# docker compose -f test-infrastructure/docker-compose.yml run --rm build-windows
# docker compose -f test-infrastructure/docker-compose.yml run --rm test-windows
FROM mstorsjo/llvm-mingw:latest
# Pinned by digest (supply-chain: no floating tags). This is the multi-arch
# manifest-list digest of mstorsjo/llvm-mingw:latest as of 2026-07-23; bump
# deliberately by re-resolving the digest, never back to a bare tag.
FROM mstorsjo/llvm-mingw:latest@sha256:5cbf421906f889e01c8d4005787474e302df6daa197286ebf212d419024966af
# curl+zstd are required (zlib sysroot fetch below). Wine is BEST-EFFORT: it
# only runs the .exe, and its i386 packages are unavailable on an arm64 Docker
@@ -19,10 +22,16 @@ RUN (dpkg --add-architecture i386 && apt-get update && \
apt-get install -y --no-install-recommends wine64 && rm -rf /var/lib/apt/lists/*) \
|| echo "WARN: Wine unavailable on this host arch -> compile-only mode"
# Install zlib into the llvm-mingw sysroot (from MSYS2 CLANG64 repo)
# Install zlib into the llvm-mingw sysroot (from MSYS2 CLANG64 repo).
# The package is verified against its pinned sha256 before extraction —
# a bare curl|tar would execute whatever the mirror serves.
RUN mkdir -p /tmp/zlib-pkg && \
curl -sL "https://mirror.msys2.org/mingw/clang64/mingw-w64-clang-x86_64-zlib-1.3.1-1-any.pkg.tar.zst" \
| zstd -d | tar xf - -C /tmp/zlib-pkg && \
-o /tmp/zlib.pkg.tar.zst && \
echo "b7f0c06e6d48128209a3a441f96b92351e565a0d031278c56a8db8a9b5ec291a /tmp/zlib.pkg.tar.zst" \
| sha256sum -c - && \
zstd -d < /tmp/zlib.pkg.tar.zst | tar xf - -C /tmp/zlib-pkg && \
rm /tmp/zlib.pkg.tar.zst && \
cp -rn /tmp/zlib-pkg/clang64/include/* /opt/llvm-mingw/x86_64-w64-mingw32/include/ && \
cp -rn /tmp/zlib-pkg/clang64/lib/* /opt/llvm-mingw/x86_64-w64-mingw32/lib/ && \
rm -rf /tmp/zlib-pkg
+1 -1
View File
@@ -238,7 +238,7 @@ TEST(cslsp_bench_resolution_ratio) {
/* Time budget. ASan+UBSan instrumentation slows the parse ~5-10×, so
* scale the budget when a sanitizer is active. Native: 200 ms for a
* ~260-line fixture; sanitized: 2000 ms. */
#ifdef __SANITIZE_ADDRESS__
#if defined(CBM_SANITIZED_BUILD) || defined(__SANITIZE_ADDRESS__)
ASSERT(ms < 2000.0);
#else
ASSERT(ms < 200.0);
+183 -35
View File
@@ -1425,6 +1425,21 @@ static bool app_wait_for_atomic_int_at_least(atomic_int *value, int minimum) {
return false;
}
static bool app_wait_for_background_initializes(int minimum) {
/* Positive completion signal for the request-handler's background tail:
* negatives about auto-index admission ("nothing was started") are only
* meaningful once the admission pass has RUN — a fixed sleep loses that
* race in both directions. */
uint64_t deadline = cbm_now_ms() + APP_TEST_TIMEOUT_MS;
while (cbm_now_ms() < deadline) {
if (cbm_daemon_application_background_initializes_for_test() >= minimum) {
return true;
}
cbm_usleep(1000);
}
return false;
}
static bool app_wait_for_atomic_bool(atomic_bool *value, bool expected) {
uint64_t deadline = cbm_now_ms() + APP_TEST_TIMEOUT_MS;
while (cbm_now_ms() < deadline) {
@@ -1856,13 +1871,14 @@ TEST(daemon_application_initialize_coalesces_auto_index_for_full_sessions) {
sessions[i] = app_test_open(&callbacks, (cbm_daemon_client_id_t)(4110U + i));
}
int bg_baseline = cbm_daemon_application_background_initializes_for_test();
bool scout_initialized = app_test_initialize_profile(&callbacks, sessions[2], root,
CBM_MCP_TOOL_PROFILE_SCOUT, NULL, NULL);
bool hook_initialized = app_test_initialize_profile(
&callbacks, sessions[3], root, CBM_MCP_TOOL_PROFILE_ALL, "SessionStart", "copilot");
cbm_usleep(50000);
bool admissions_evaluated = app_wait_for_background_initializes(bg_baseline + 2);
bool restricted_started_nothing =
atomic_load(&fake.starts) == 0 && application && project &&
admissions_evaluated && atomic_load(&fake.starts) == 0 && application && project &&
cbm_daemon_application_job_subscribers(application, project) == 0 &&
atomic_load(&update.starts) == 0;
@@ -1990,10 +2006,12 @@ TEST(daemon_application_auto_index_honors_tracked_file_limit) {
cbm_daemon_application_runtime_callbacks(application);
cbm_daemon_runtime_application_session_t *session =
application ? app_test_open(&callbacks, 4190) : NULL;
int bg_baseline = cbm_daemon_application_background_initializes_for_test();
bool initialized = app_test_initialize_profile(&callbacks, session, root,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
cbm_usleep(50000);
bool limit_prevented_admission = initialized && atomic_load(&fake.starts) == 0 &&
bool admission_evaluated = app_wait_for_background_initializes(bg_baseline + 1);
bool limit_prevented_admission = initialized && admission_evaluated &&
atomic_load(&fake.starts) == 0 &&
cbm_daemon_application_active_jobs(application) == 0;
if (session) {
@@ -2126,11 +2144,12 @@ TEST(daemon_application_auto_index_retries_transient_busy_admission) {
cbm_daemon_application_runtime_callbacks(application);
cbm_daemon_runtime_application_session_t *session =
application ? app_test_open(&callbacks, 4191) : NULL;
int bg_baseline = cbm_daemon_application_background_initializes_for_test();
bool initialized =
occupied_admitted && app_test_initialize_profile(&callbacks, session, auto_root,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
cbm_usleep(20000);
bool initially_deferred = initialized && atomic_load(&fake.starts) == 1;
bool admission_evaluated = app_wait_for_background_initializes(bg_baseline + 1);
bool initially_deferred = initialized && admission_evaluated && atomic_load(&fake.starts) == 1;
atomic_store(&fake.allow_completion, true);
if (occupied_started) {
@@ -3002,11 +3021,17 @@ TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session) {
encoded && cbm_thread_create(&request_thread, 0, app_request_thread, &request) == 0;
bool subscribed =
thread_started && app_wait_for_subscribers(fixture.application, fixture.project, 1);
if (subscribed) {
/* Cancel only after the WORKER exists: subscription precedes worker
* start, and a cancel that wins that window legally aborts the job with
* no worker (and no worker-cancel op) ever invoked — that interleaving
* has its own deterministic test below. From worker start onward, cancel
* delivery to the worker is guaranteed. */
bool worker_started = subscribed && app_wait_for_atomic_int_at_least(&fixture.fake.starts, 1);
if (worker_started) {
fixture.callbacks.request_cancel(fixture.callbacks.context, fixture.session,
request.request_token);
}
bool request_returned = subscribed && app_wait_for_atomic_bool(&request.done, true);
bool request_returned = worker_started && app_wait_for_atomic_bool(&request.done, true);
if (!request_returned) {
/* Keep cleanup bounded if the RED path fails to detach the request. */
atomic_store(&fixture.fake.allow_completion, true);
@@ -3038,6 +3063,7 @@ TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session) {
ASSERT_TRUE(encoded);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(subscribed);
ASSERT_TRUE(worker_started);
ASSERT_TRUE(request_returned);
ASSERT_TRUE(thread_joined);
ASSERT_EQ(request.status, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
@@ -3055,6 +3081,67 @@ TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session) {
PASS();
}
/* Deterministic sibling of the test above: with every job thread parked
* before its first pre-start cancel check, the cancel WINS the pre-start
* window by construction — the request must still report CANCELLED with the
* watch preserved, while the worker is never started and none of its ops
* are ever invoked. A loaded CI runner produced this interleaving by
* chance; here it is forced (CI is never a lottery). */
TEST(daemon_application_cancel_before_worker_start_skips_worker) {
app_watch_race_fixture_t fixture;
bool fixture_ready = app_watch_race_fixture_init(&fixture, 3022);
char args[APP_TEST_PATH_CAP + 32];
(void)snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", fixture.root);
uint8_t *tool = NULL;
uint32_t tool_length = 0;
bool encoded =
fixture_ready && app_test_tool_request("index_repository", args, &tool, &tool_length);
cbm_daemon_application_hold_job_before_start_for_test(true);
app_request_thread_t request = {
.callbacks = fixture.callbacks,
.session = fixture.session,
.request_token = UINT64_C(8301),
.request = tool,
.request_length = tool_length,
.status = CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR,
};
atomic_init(&request.done, false);
cbm_thread_t request_thread;
bool thread_started =
encoded && cbm_thread_create(&request_thread, 0, app_request_thread, &request) == 0;
bool subscribed =
thread_started && app_wait_for_subscribers(fixture.application, fixture.project, 1);
if (subscribed) {
fixture.callbacks.request_cancel(fixture.callbacks.context, fixture.session,
request.request_token);
}
bool request_returned = subscribed && app_wait_for_atomic_bool(&request.done, true);
cbm_daemon_application_hold_job_before_start_for_test(false);
bool thread_joined = thread_started && cbm_thread_join(&request_thread) == 0;
int watch_count_after_cancel = fixture.watcher ? cbm_watcher_watch_count(fixture.watcher) : -1;
bool cleaned = app_watch_race_fixture_finish(&fixture);
int worker_starts = atomic_load(&fixture.fake.starts);
int worker_cancels = atomic_load(&fixture.fake.cancels);
int worker_destroys = atomic_load(&fixture.fake.destroys);
free(request.response);
free(tool);
ASSERT_TRUE(fixture_ready);
ASSERT_TRUE(encoded);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(subscribed);
ASSERT_TRUE(request_returned);
ASSERT_TRUE(thread_joined);
ASSERT_EQ(request.status, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
ASSERT_NULL(request.response);
ASSERT_EQ(watch_count_after_cancel, 1);
ASSERT_EQ(worker_starts, 0);
ASSERT_EQ(worker_cancels, 0);
ASSERT_EQ(worker_destroys, 0);
ASSERT_TRUE(cleaned);
PASS();
}
TEST(daemon_application_cancel_drops_watch_before_inflight_request_returns) {
const char *old_cache = getenv("CBM_CACHE_DIR");
bool had_cache = old_cache != NULL;
@@ -3973,10 +4060,9 @@ TEST(daemon_application_worker_lock_serializes_external_mutation) {
}
cbm_project_lock_lease_t *terminal_other_probe = NULL;
cbm_private_file_lock_status_t terminal_other_status =
other_worker_acquired
? app_project_lock_try_acquire_until_released(locks.probe_locks, other_project,
&terminal_other_probe)
: CBM_PRIVATE_FILE_LOCK_IO;
other_worker_acquired ? app_project_lock_try_acquire_until_released(
locks.probe_locks, other_project, &terminal_other_probe)
: CBM_PRIVATE_FILE_LOCK_IO;
bool other_lock_released_at_terminal =
terminal_other_status == CBM_PRIVATE_FILE_LOCK_OK && terminal_other_probe;
bool terminal_other_probe_released = app_project_lock_release(&terminal_other_probe);
@@ -3993,10 +4079,9 @@ TEST(daemon_application_worker_lock_serializes_external_mutation) {
}
cbm_project_lock_lease_t *blocked_probe = NULL;
cbm_private_file_lock_status_t terminal_blocked_status =
blocked_worker_acquired
? app_project_lock_try_acquire_until_released(locks.probe_locks, blocked_project,
&blocked_probe)
: CBM_PRIVATE_FILE_LOCK_IO;
blocked_worker_acquired ? app_project_lock_try_acquire_until_released(
locks.probe_locks, blocked_project, &blocked_probe)
: CBM_PRIVATE_FILE_LOCK_IO;
bool blocked_lock_released_at_terminal =
terminal_blocked_status == CBM_PRIVATE_FILE_LOCK_OK && blocked_probe;
bool blocked_probe_released = app_project_lock_release(&blocked_probe);
@@ -4507,7 +4592,32 @@ TEST(daemon_application_thread_start_failure_rolls_back_job_reservation) {
PASS();
}
TEST(daemon_application_enforces_global_physical_job_limit) {
/* Session-request worker: issues one runtime request and records the raw
* response, so a request that legitimately BLOCKS (queued behind the
* physical job limit) can run off the test thread. */
typedef struct {
cbm_daemon_runtime_application_callbacks_t *callbacks;
cbm_daemon_runtime_application_session_t *session;
uint8_t *payload;
uint32_t payload_length;
uint8_t *response;
uint32_t response_length;
int status;
} app_session_request_thread_t;
static void *app_session_request_thread(void *opaque) {
app_session_request_thread_t *request = opaque;
request->status =
app_test_request(request->callbacks, request->session, request->payload,
request->payload_length, &request->response, &request->response_length);
return NULL;
}
/* An explicit index request that hits the physical job limit QUEUES (parked,
* observable via the busy-queue-wait counter) instead of erroring, and runs
* to completion once the occupying job finishes. The non-session coordinator
* API keeps its non-blocking busy answer. */
TEST(daemon_application_queues_explicit_index_behind_physical_job_limit) {
app_fake_worker_context_t fake;
app_fake_worker_context_init(&fake);
cbm_daemon_application_worker_ops_t worker_ops = {
@@ -4539,7 +4649,10 @@ TEST(daemon_application_enforces_global_physical_job_limit) {
application && roots_ok && cbm_thread_create(&thread, 0, app_index_thread, &first) == 0;
bool admitted = started && app_wait_for_subscribers(application, "cap-first", 1) &&
app_wait_for_atomic_int(&fake.starts, 1);
/* The programmatic coordinator API stays non-blocking: callers with their
* own retry policy (watcher, auto-index) still get the busy answer. */
int busy = admitted ? cbm_daemon_application_index(application, "cap-second", second_root) : -1;
cbm_daemon_runtime_application_callbacks_t callbacks =
cbm_daemon_application_runtime_callbacks(application);
cbm_daemon_runtime_application_session_t *session = app_test_open(&callbacks, 51);
@@ -4553,18 +4666,49 @@ TEST(daemon_application_enforces_global_physical_job_limit) {
app_test_context_request(second_root, second_root, &session_context,
&session_context_length) &&
app_test_tool_request("index_repository", args, &tool, &tool_length);
uint8_t *response = NULL;
uint32_t response_length = 0;
bool session_busy =
session_encoded &&
app_test_request(&callbacks, session, session_context, session_context_length, &response,
&response_length) == CBM_DAEMON_RUNTIME_APPLICATION_OK;
free(response);
response = NULL;
session_busy = session_busy &&
app_test_request(&callbacks, session, tool, tool_length, &response,
&response_length) == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
response && strstr((char *)response, "physical index job limit reached");
uint8_t *context_response = NULL;
uint32_t context_response_length = 0;
bool context_ok = session_encoded &&
app_test_request(&callbacks, session, session_context, session_context_length,
&context_response, &context_response_length) ==
CBM_DAEMON_RUNTIME_APPLICATION_OK;
free(context_response);
int queue_waits_baseline = cbm_daemon_application_busy_queue_waits_for_test();
app_session_request_thread_t queued = {
.callbacks = &callbacks,
.session = session,
.payload = tool,
.payload_length = tool_length,
.status = -1,
};
cbm_thread_t request_thread;
bool request_started =
context_ok &&
cbm_thread_create(&request_thread, 0, app_session_request_thread, &queued) == 0;
/* Positive queue signal: the request entered the busy-wait loop instead
* of erroring — and no second worker was started while parked. */
bool queued_parked = false;
if (request_started) {
uint64_t deadline = cbm_now_ms() + APP_TEST_TIMEOUT_MS;
while (cbm_now_ms() < deadline) {
if (cbm_daemon_application_busy_queue_waits_for_test() > queue_waits_baseline) {
queued_parked = true;
break;
}
cbm_usleep(1000);
}
}
bool no_second_start_while_parked = queued_parked && atomic_load(&fake.starts) == 1;
/* Release the slot: the first job completes, the queued request must be
* admitted and complete on its own. */
atomic_store(&fake.allow_completion, true);
bool request_joined = request_started && cbm_thread_join(&request_thread) == 0;
bool queued_succeeded = request_joined && queued.status == CBM_DAEMON_RUNTIME_APPLICATION_OK &&
queued.response && strstr((char *)queued.response, "indexed") != NULL &&
strstr((char *)queued.response, "physical index job limit") == NULL;
callbacks.session_close(callbacks.context, session);
bool stopped = application && cbm_daemon_application_shutdown(application, APP_TEST_TIMEOUT_MS);
if (started) {
@@ -4573,17 +4717,20 @@ TEST(daemon_application_enforces_global_physical_job_limit) {
cbm_daemon_application_free(application);
free(session_context);
free(tool);
free(response);
free(queued.response);
ASSERT_TRUE(roots_ok);
ASSERT_TRUE(started);
ASSERT_TRUE(admitted);
ASSERT_EQ(busy, 1);
ASSERT_TRUE(session_busy);
ASSERT_TRUE(context_ok);
ASSERT_TRUE(request_started);
ASSERT_TRUE(queued_parked);
ASSERT_TRUE(no_second_start_while_parked);
ASSERT_TRUE(queued_succeeded);
ASSERT_TRUE(stopped);
ASSERT_EQ(atomic_load(&fake.starts), 1);
ASSERT_EQ(atomic_load(&fake.cancels), 1);
ASSERT_EQ(atomic_load(&fake.destroys), 1);
ASSERT_EQ(atomic_load(&fake.starts), 2);
ASSERT_EQ(atomic_load(&fake.destroys), 2);
(void)cbm_rmdir(first_root);
(void)cbm_rmdir(second_root);
@@ -4750,6 +4897,7 @@ SUITE(daemon_application) {
RUN_TEST(daemon_application_cancels_physical_job_only_after_final_session);
RUN_TEST(daemon_application_disconnect_before_request_callback_is_sticky);
RUN_TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session);
RUN_TEST(daemon_application_cancel_before_worker_start_skips_worker);
RUN_TEST(daemon_application_cancel_drops_watch_before_inflight_request_returns);
RUN_TEST(daemon_application_stale_watcher_callback_is_rejected_at_job_admission);
RUN_TEST(daemon_application_final_cancel_drains_admitted_watcher_job);
@@ -4766,7 +4914,7 @@ SUITE(daemon_application) {
RUN_TEST(daemon_application_recovers_with_unique_per_job_quarantine_files);
RUN_TEST(daemon_application_cancellation_between_recovery_attempts_stops_retry);
RUN_TEST(daemon_application_thread_start_failure_rolls_back_job_reservation);
RUN_TEST(daemon_application_enforces_global_physical_job_limit);
RUN_TEST(daemon_application_queues_explicit_index_behind_physical_job_limit);
RUN_TEST(daemon_application_default_limit_admits_four_and_rejects_fifth);
RUN_TEST(daemon_application_free_reports_retained_live_ownership);
RUN_TEST(daemon_application_rejects_clean_exit_when_process_tree_is_not_contained);
+50 -23
View File
@@ -328,6 +328,35 @@ TEST(discover_simple) {
PASS();
}
/* A directory with more immediate subdirectories than the walk stack's
* initial capacity must still be fully discovered — dotnet/runtime's
* src/tests/JIT/Regression/JitBlue holds 855 sibling dirs and made the whole
* pipeline hard-fail with files=0 (walk_push_subdir treated the fixed cap as
* a hard error instead of growing). */
TEST(discover_wide_sibling_fanout_exceeds_initial_walk_stack) {
char *base = th_mktempdir("cbm_disc_wide");
ASSERT(base != NULL);
enum { WIDE_SIBLINGS = 600 };
for (int i = 0; i < WIDE_SIBLINGS; i++) {
char rel[64];
(void)snprintf(rel, sizeof(rel), "wide/d%03d/f.py", i);
th_write_file(TH_PATH(base, rel), "x = 1\n");
}
cbm_discover_opts_t opts = {0};
cbm_file_info_t *files = NULL;
int count = 0;
int rc = cbm_discover(base, &opts, &files, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, WIDE_SIBLINGS);
cbm_discover_free(files, count);
th_cleanup(base);
PASS();
}
TEST(discover_bounded_count_is_allocation_free_and_limit_exact) {
char *base = th_mktempdir("cbm_disc_bounded");
ASSERT(base != NULL);
@@ -337,11 +366,11 @@ TEST(discover_bounded_count_is_allocation_free_and_limit_exact) {
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
int limited_count = -1;
cbm_discover_status_t limited = cbm_discover_count_bounded(
base, &opts, 1, cbm_now_ms() + 2000, &limited_count);
cbm_discover_status_t limited =
cbm_discover_count_bounded(base, &opts, 1, cbm_now_ms() + 2000, &limited_count);
int exact_count = -1;
cbm_discover_status_t exact = cbm_discover_count_bounded(
base, &opts, 2, cbm_now_ms() + 2000, &exact_count);
cbm_discover_status_t exact =
cbm_discover_count_bounded(base, &opts, 2, cbm_now_ms() + 2000, &exact_count);
th_cleanup(base);
ASSERT_EQ(limited, CBM_DISCOVER_LIMIT_EXCEEDED);
@@ -358,8 +387,7 @@ TEST(discover_bounded_count_fails_closed_after_deadline) {
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
int count = 99;
cbm_discover_status_t status =
cbm_discover_count_bounded(base, &opts, 100, 1, &count);
cbm_discover_status_t status = cbm_discover_count_bounded(base, &opts, 100, 1, &count);
th_cleanup(base);
ASSERT_EQ(status, CBM_DISCOVER_ERROR);
@@ -1130,8 +1158,10 @@ TEST(discover_git_info_exclude_stacks_with_gitignore) {
bool found_log = false;
bool found_scratch = false;
for (int i = 0; i < count; i++) {
if (strstr(files[i].rel_path, ".log")) found_log = true;
if (strstr(files[i].rel_path, "scratch")) found_scratch = true;
if (strstr(files[i].rel_path, ".log"))
found_log = true;
if (strstr(files[i].rel_path, "scratch"))
found_scratch = true;
}
ASSERT_FALSE(found_log);
ASSERT_FALSE(found_scratch);
@@ -1294,29 +1324,25 @@ TEST(discover_many_nested_gitignores_do_not_exhaust_matcher_ownership) {
for (int i = 0; i < PACKAGE_COUNT; i++) {
char ignore_path[1024];
char source_path[1024];
int ignore_written = snprintf(ignore_path, sizeof(ignore_path),
"%s/package-%02d/.gitignore", base, i);
int source_written = snprintf(source_path, sizeof(source_path),
"%s/package-%02d/source.go", base, i);
fixture_ready =
fixture_ready && ignore_written > 0 &&
(size_t)ignore_written < sizeof(ignore_path) &&
source_written > 0 &&
(size_t)source_written < sizeof(source_path) &&
th_write_file(ignore_path, "ignored.tmp\n") == 0 &&
th_write_file(source_path, "package fixture\n") == 0;
int ignore_written =
snprintf(ignore_path, sizeof(ignore_path), "%s/package-%02d/.gitignore", base, i);
int source_written =
snprintf(source_path, sizeof(source_path), "%s/package-%02d/source.go", base, i);
fixture_ready = fixture_ready && ignore_written > 0 &&
(size_t)ignore_written < sizeof(ignore_path) && source_written > 0 &&
(size_t)source_written < sizeof(source_path) &&
th_write_file(ignore_path, "ignored.tmp\n") == 0 &&
th_write_file(source_path, "package fixture\n") == 0;
}
cbm_discover_opts_t opts = {0};
cbm_file_info_t *files = NULL;
int count = 0;
int discover_rc =
fixture_ready ? cbm_discover(base, &opts, &files, &count) : -1;
int discover_rc = fixture_ready ? cbm_discover(base, &opts, &files, &count) : -1;
int bounded_count = -1;
cbm_discover_status_t bounded_status =
fixture_ready
? cbm_discover_count_bounded(base, &opts, PACKAGE_COUNT + 1, 0,
&bounded_count)
? cbm_discover_count_bounded(base, &opts, PACKAGE_COUNT + 1, 0, &bounded_count)
: CBM_DISCOVER_ERROR;
cbm_discover_free(files, count);
@@ -1403,6 +1429,7 @@ SUITE(discover) {
/* Integration tests (cross-platform) */
RUN_TEST(discover_simple);
RUN_TEST(discover_wide_sibling_fanout_exceeds_initial_walk_stack);
RUN_TEST(discover_bounded_count_is_allocation_free_and_limit_exact);
RUN_TEST(discover_bounded_count_fails_closed_after_deadline);
RUN_TEST(discover_skips_git_dir);
+10 -4
View File
@@ -1566,7 +1566,9 @@ typedef struct {
static void *th_http_stop_watchdog(void *opaque) {
th_http_stop_watchdog_t *watchdog = opaque;
for (int elapsed_ms = 0; elapsed_ms < 1500; elapsed_ms += 10) {
/* Hang detector, not a latency assertion: sized for the slowest
* sanitizer/loaded-runner tail (see AGENTS.md, CI determinism). */
for (int elapsed_ms = 0; elapsed_ms < 10000; elapsed_ms += 10) {
if (atomic_load(&watchdog->stop_finished) ||
(watchdog->operation_finished && atomic_load(watchdog->operation_finished)))
return NULL;
@@ -1608,6 +1610,11 @@ TEST(httpd_interrupt_unblocks_nonreading_large_response_within_one_second) {
cbm_httpd_t *listener = cbm_httpd_listen(0);
ASSERT_NOT_NULL(listener);
cbm_httpd_set_send_buffer_for_test(listener, 64 * 1024);
/* Deadline pinned far out of reach: the only remaining way the blocked
* 8 MB send can end is the interrupt, so the join itself proves
* interrupt causality — no wall-clock discrimination needed (the old
* elapsed<500ms check was a lottery under sanitizer slowdown). */
cbm_httpd_set_send_deadline_for_test(listener, 60 * 1000);
th_httpd_large_reply_t reply = {.listener = listener};
atomic_init(&reply.accepted, 0);
atomic_init(&reply.finished, 0);
@@ -1630,17 +1637,16 @@ TEST(httpd_interrupt_unblocks_nonreading_large_response_within_one_second) {
cbm_thread_t watchdog_thread;
ASSERT_EQ(cbm_thread_create(&watchdog_thread, 0, th_http_stop_watchdog, &watchdog), 0);
uint64_t started = cbm_now_ms();
cbm_httpd_interrupt(listener);
ASSERT_EQ(cbm_thread_join(&reply_thread), 0);
uint64_t elapsed = cbm_now_ms() - started;
atomic_store(&watchdog.stop_finished, 1);
ASSERT_EQ(cbm_thread_join(&watchdog_thread), 0);
(void)th_sock_shutdown(socket);
th_sock_close(socket);
ASSERT_TRUE(cbm_httpd_close(listener));
ASSERT_LT(elapsed, 500);
/* The 60 s deadline cannot have fired, so the join above is the proof
* of interrupt delivery; the watchdog is purely a hang detector. */
ASSERT_EQ(atomic_load(&watchdog.watchdog_fired), 0);
PASS();
}
+62
View File
@@ -70,6 +70,35 @@ static int setup_parallel_repo(void) {
fprintf(f, "package util\n\nfunc Help() {}\n");
fclose(f);
/* Java interface/implements/extends trio: the parallel resolve path must
* make the same Interface-label edge split (IMPLEMENTS vs INHERITS) as
* the sequential semantic pass, and both must emit OVERRIDE for methods
* redefined from an explicit base. Without these files the IMPLEMENTS/
* INHERITS parity tests compare 0 == 0 and guard nothing (the demotion
* bug shipped: elasticsearch had 11k implements-as-INHERITS edges). */
snprintf(path, sizeof(path), "%s/probe", g_par_tmpdir);
cbm_mkdir(path);
snprintf(path, sizeof(path), "%s/probe/Shape.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic interface Shape {\n double area();\n}\n");
fclose(f);
snprintf(path, sizeof(path), "%s/probe/Circle.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic class Circle implements Shape {\n"
" @Override\n public double area() { return 1.0; }\n}\n");
fclose(f);
snprintf(path, sizeof(path), "%s/probe/Base.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic class Base extends Circle {\n"
" @Override\n public double area() { return 0.0; }\n}\n");
fclose(f);
return 0;
}
@@ -85,6 +114,20 @@ static void teardown_parallel_repo(void) {
/* ── Run sequential pipeline on files, returning gbuf ─────────────── */
/* Free the return-type table pass_calls may have built for a harness-owned
* ctx — mirrors the production teardown in pipeline.c; the harness never
* runs it, which leaked once the fixture gained typed (Java) methods. The
* fixture has no ObjectScript, so no macro table can exist here. */
static void harness_ctx_free_tables(cbm_pipeline_ctx_t *ctx) {
if (ctx->return_type_table) {
for (int i = 0; i < ctx->return_type_table->count; i++) {
free((void *)ctx->return_type_table->entries[i].return_type);
}
free((void *)ctx->return_type_table);
ctx->return_type_table = NULL;
}
}
static cbm_gbuf_t *run_sequential(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
@@ -106,6 +149,7 @@ static cbm_gbuf_t *run_sequential(const char *project, const char *repo_path,
cbm_pipeline_pass_usages(&ctx, files, file_count);
cbm_pipeline_pass_semantic(&ctx, files, file_count);
harness_ctx_free_tables(&ctx);
cbm_registry_free(reg);
return gbuf;
}
@@ -178,6 +222,7 @@ static cbm_gbuf_t *run_parallel_with_extract_opts(const char *project, const cha
cbm_free_result(result_cache[i]);
free(result_cache);
harness_ctx_free_tables(&ctx);
cbm_registry_free(reg);
return gbuf;
}
@@ -316,6 +361,22 @@ TEST(parallel_implements_parity) {
PASS();
}
/* Absolute counts on the fixture — parity alone passes vacuously at 0==0,
* which is how the parallel-path IMPLEMENTS demotion shipped unnoticed. */
TEST(parallel_semantic_fixture_expected_counts) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
/* Circle implements Shape; Base extends Circle — in BOTH venues. */
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "IMPLEMENTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "IMPLEMENTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "INHERITS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "INHERITS"), 1);
/* Circle.area overrides Shape.area; Base.area overrides Circle.area. */
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "OVERRIDE"), 2);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "OVERRIDE"), 2);
PASS();
}
TEST(parallel_total_edges) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
@@ -1173,6 +1234,7 @@ SUITE(parallel) {
RUN_TEST(parallel_usage_parity);
RUN_TEST(parallel_inherits_parity);
RUN_TEST(parallel_implements_parity);
RUN_TEST(parallel_semantic_fixture_expected_counts);
RUN_TEST(parallel_total_edges);
RUN_TEST(parallel_empty_files);
RUN_TEST(parallel_args_json_no_overflow);
+1 -1
View File
@@ -258,7 +258,7 @@ TEST(pylsp_bench_resolution_ratio) {
/* Time budget. ASan+UBSan instrumentation slows the parse ~5-10×, so
* scale the budget when a sanitizer is active. Native: 150 ms for a
* ~200-line fixture; sanitized: 1500 ms. */
#ifdef __SANITIZE_ADDRESS__
#if defined(CBM_SANITIZED_BUILD) || defined(__SANITIZE_ADDRESS__)
ASSERT(ms < 1500.0);
#else
ASSERT(ms < 150.0);