mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-22 06:09:35 +08:00
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:
+12
-2
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 ───────────────────────────────── */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 ──────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user