mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(tests): ignore an inherited git repository environment (#2003)
Git exports repository-local variables (GIT_DIR, GIT_INDEX_FILE,
GIT_WORK_TREE, ...) into hooks, and they take precedence over
`git -C <dir>`. Every fixture git command in the test runner therefore
acted on the caller's real repository whenever the suite ran with those
variables set: with GIT_DIR alone, fixture assertions failed and commits,
branches and linked worktrees were written into the caller's clone; with
the full hook set, fixture commits failed and git-dependent tests degraded
to "git not available" SKIPs. The pre-commit hook already clears the
variables (7883507d), but any other caller (a shell with GIT_DIR set, an
editor hook, `GIT_DIR=... scripts/test.sh`) still hit it.
The test runner and the repro runner now clear git's local-env list
(`git rev-parse --local-env-vars`) once at startup, before any fixture or
re-exec'd child spawns git, so every fixture scopes to its own repository
no matter who invokes the suite.
tests/test_git_env_isolation_contract.sh (Step 3a of scripts/test.sh)
drives the real runner's git_context suite with GIT_DIR, and with the full
hook set, aimed at a throwaway decoy repository. It requires the same
pass/skip summary as a clean run and a decoy left untouched (no refs, no
linked worktrees).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
0f52d30c29
commit
cf72f1a657
5 files changed
+169
No files matched your search
@@ -357,6 +357,12 @@ else
|
|||||||
make -f Makefile.cbm test-par ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
make -f Makefile.cbm test-par ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Step 3a: the runner must ignore an inherited git repository environment
|
||||||
|
# (#2003: the pre-commit hook exports GIT_DIR, which overrides `git -C` and
|
||||||
|
# pointed fixture git commands at the committer's real repository).
|
||||||
|
echo "=== Step 3a: inherited git environment isolation (#2003) ==="
|
||||||
|
bash "$ROOT/tests/test_git_env_isolation_contract.sh" "$ROOT/$BUILD_DIR/test-runner"
|
||||||
|
|
||||||
# Step 4: C++ large-TU index-hang regression guard (#410). Runs the PROD binary
|
# Step 4: C++ large-TU index-hang regression guard (#410). Runs the PROD binary
|
||||||
# in a subprocess with a wall-clock timeout — a hang must fail, not block the run.
|
# in a subprocess with a wall-clock timeout — a hang must fail, not block the run.
|
||||||
# Opt-in via CBM_RUN_HANG_TEST=1 (it needs the prod binary, which the ASan unit
|
# Opt-in via CBM_RUN_HANG_TEST=1 (it needs the prod binary, which the ASan unit
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ int tf_fail_count = 0;
|
|||||||
int tf_skip_count = 0;
|
int tf_skip_count = 0;
|
||||||
|
|
||||||
#include "test_framework.h"
|
#include "test_framework.h"
|
||||||
|
#include "test_helpers.h"
|
||||||
#include "repro_runner.h"
|
#include "repro_runner.h"
|
||||||
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
|
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
|
||||||
#include "daemon/bootstrap.h" /* refuse the developer's default daemon rendezvous */
|
#include "daemon/bootstrap.h" /* refuse the developer's default daemon rendezvous */
|
||||||
@@ -139,6 +140,8 @@ int main(void) {
|
|||||||
* loudly instead of reaching it. */
|
* loudly instead of reaching it. */
|
||||||
cbm_daemon_bootstrap_forbid_default_runtime_for_test(true);
|
cbm_daemon_bootstrap_forbid_default_runtime_for_test(true);
|
||||||
#endif
|
#endif
|
||||||
|
/* #2003: fixture git commands must never reach the caller's repository. */
|
||||||
|
th_clear_git_repo_env();
|
||||||
|
|
||||||
/* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a
|
/* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a
|
||||||
* leak) before stdio flushes — keep every printed line so the summary and the
|
* leak) before stdio flushes — keep every printed line so the summary and the
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Contract (#2003): the test runner must be immune to an inherited git
|
||||||
|
# repository environment.
|
||||||
|
#
|
||||||
|
# Git exports GIT_DIR, GIT_INDEX_FILE, ... into hooks, and those variables take
|
||||||
|
# precedence over `git -C <dir>`. The pre-commit hook runs the suite, so every
|
||||||
|
# fixture git command in the runner used to act on the committer's REAL
|
||||||
|
# repository: fixture assertions failed, and branches, commits and linked
|
||||||
|
# worktrees were written into the contributor's clone. The runner now clears
|
||||||
|
# git's local-env list at startup (th_clear_git_repo_env in tests/test_helpers.h).
|
||||||
|
#
|
||||||
|
# This drives the real runner with those variables aimed at a throwaway decoy
|
||||||
|
# repository and asserts both halves: the git fixture suite passes, and the
|
||||||
|
# decoy is left exactly as it was (no refs, no linked worktrees).
|
||||||
|
#
|
||||||
|
# Usage: tests/test_git_env_isolation_contract.sh [path/to/test-runner]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
RUNNER="${1:-$ROOT/build/c/test-runner}"
|
||||||
|
SUITE="git_context"
|
||||||
|
|
||||||
|
if [ ! -x "$RUNNER" ]; then
|
||||||
|
echo "FAIL: test runner not found at '$RUNNER'; build it first (make -f Makefile.cbm build/c/test-runner)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "SKIP: git is not on PATH, so there is no git environment to leak."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This script's own git calls must address the decoy, not whatever repository
|
||||||
|
# the caller (e.g. the pre-commit hook) exported.
|
||||||
|
while IFS= read -r variable; do
|
||||||
|
unset "$variable"
|
||||||
|
done < <(git rev-parse --local-env-vars)
|
||||||
|
|
||||||
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cbm-git-env-XXXXXX")"
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
decoy_state() {
|
||||||
|
printf 'refs:\n'
|
||||||
|
git -C "$1" for-each-ref
|
||||||
|
printf 'worktrees:\n'
|
||||||
|
git -C "$1" worktree list --porcelain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Baseline: the same suite in a clean environment. Comparing summaries (not just
|
||||||
|
# exit codes) matters: with GIT_WORK_TREE inherited, fixture commits fail and
|
||||||
|
# the suite SKIPs as "git not available" instead of failing -- a silent green.
|
||||||
|
BASE_LOG="$WORK/baseline.log"
|
||||||
|
"$RUNNER" "$SUITE" >"$BASE_LOG" 2>&1 || {
|
||||||
|
echo "FAIL: '$SUITE' is red even in a clean environment; fix that first." >&2
|
||||||
|
grep -E 'FAIL|passed' "$BASE_LOG" >&2 || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
summary() { grep -E '^ *[0-9]+ passed' "$1" | tail -1; }
|
||||||
|
BASE_SUMMARY="$(summary "$BASE_LOG")"
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
# Two inherited shapes: GIT_DIR alone (the reported `GIT_DIR=... scripts/test.sh`
|
||||||
|
# case: fixture git commands write into the decoy) and the full set a hook
|
||||||
|
# exports (fixture commits fail, so the suite degrades to SKIPs).
|
||||||
|
check_variant() {
|
||||||
|
local name="$1"
|
||||||
|
local decoy="$WORK/decoy-$name"
|
||||||
|
shift
|
||||||
|
git init -q "$decoy"
|
||||||
|
local before
|
||||||
|
before="$(decoy_state "$decoy")"
|
||||||
|
local -a assignments=()
|
||||||
|
local var
|
||||||
|
for var in "$@"; do
|
||||||
|
assignments+=("$var=$decoy$(git_env_suffix "$var")")
|
||||||
|
done
|
||||||
|
local log="$WORK/runner-$name.log"
|
||||||
|
local rc
|
||||||
|
set +e
|
||||||
|
env "${assignments[@]}" "$RUNNER" "$SUITE" >"$log" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
local env_summary after
|
||||||
|
env_summary="$(summary "$log")"
|
||||||
|
after="$(decoy_state "$decoy")"
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo "FAIL[$name]: '$SUITE' exited $rc with an inherited git environment." >&2
|
||||||
|
grep -E 'FAIL|passed' "$log" >&2 || true
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
if [ -z "$BASE_SUMMARY" ] || [ "$BASE_SUMMARY" != "$env_summary" ]; then
|
||||||
|
echo "FAIL[$name]: '$SUITE' behaves differently with an inherited git environment." >&2
|
||||||
|
echo " clean env: ${BASE_SUMMARY:-<no summary>}" >&2
|
||||||
|
echo " inherited env: ${env_summary:-<no summary>}" >&2
|
||||||
|
grep -E 'FAIL|SKIP' "$log" >&2 || true
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
if [ "$before" != "$after" ]; then
|
||||||
|
echo "FAIL[$name]: the test runner modified the repository named by the inherited GIT_DIR." >&2
|
||||||
|
echo "--- before ---" >&2
|
||||||
|
echo "$before" >&2
|
||||||
|
echo "--- after ---" >&2
|
||||||
|
echo "$after" >&2
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where each exported variable points inside a repository, as a hook sees it.
|
||||||
|
git_env_suffix() {
|
||||||
|
case "$1" in
|
||||||
|
GIT_WORK_TREE) printf '' ;;
|
||||||
|
GIT_INDEX_FILE) printf '/.git/index' ;;
|
||||||
|
*) printf '/.git' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
check_variant git-dir GIT_DIR
|
||||||
|
check_variant hook-env GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR
|
||||||
|
|
||||||
|
if [ "$FAILED" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PASS: test runner ignores an inherited git repository environment (#2003)."
|
||||||
@@ -226,4 +226,37 @@ static inline void th_cleanup(const char *path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Inherited git repository environment (#2003) ─────────────── */
|
||||||
|
|
||||||
|
/* Git exports repository-local variables into hooks (GIT_DIR, GIT_INDEX_FILE,
|
||||||
|
* ...), and they take precedence over `git -C <dir>`. A test runner started
|
||||||
|
* from a pre-commit hook (or any shell with GIT_DIR set) would otherwise point
|
||||||
|
* every fixture's git command at the caller's real repository: branches and
|
||||||
|
* worktrees get created there and fixture assertions fail. This is the list
|
||||||
|
* `git rev-parse --local-env-vars` prints; runners clear it once at startup so
|
||||||
|
* every spawned git (and every re-exec'd child) scopes to its fixture. */
|
||||||
|
static const char *const th_git_repo_env_vars[] = {
|
||||||
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||||
|
"GIT_CONFIG",
|
||||||
|
"GIT_CONFIG_PARAMETERS",
|
||||||
|
"GIT_CONFIG_COUNT",
|
||||||
|
"GIT_OBJECT_DIRECTORY",
|
||||||
|
"GIT_DIR",
|
||||||
|
"GIT_WORK_TREE",
|
||||||
|
"GIT_IMPLICIT_WORK_TREE",
|
||||||
|
"GIT_GRAFT_FILE",
|
||||||
|
"GIT_INDEX_FILE",
|
||||||
|
"GIT_NO_REPLACE_OBJECTS",
|
||||||
|
"GIT_REPLACE_REF_BASE",
|
||||||
|
"GIT_PREFIX",
|
||||||
|
"GIT_SHALLOW_FILE",
|
||||||
|
"GIT_COMMON_DIR",
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline void th_clear_git_repo_env(void) {
|
||||||
|
for (size_t i = 0U; i < sizeof(th_git_repo_env_vars) / sizeof(th_git_repo_env_vars[0]); i++) {
|
||||||
|
(void)cbm_unsetenv(th_git_repo_env_vars[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endif /* TEST_HELPERS_H */
|
#endif /* TEST_HELPERS_H */
|
||||||
@@ -1028,6 +1028,9 @@ extern void suite_dump_verify_io(void);
|
|||||||
extern void cbm_kind_in_set_free_cache(void);
|
extern void cbm_kind_in_set_free_cache(void);
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
|
/* #2003: never let a caller's GIT_DIR/GIT_INDEX_FILE/... redirect fixture
|
||||||
|
* git commands at the caller's real repository. */
|
||||||
|
th_clear_git_repo_env();
|
||||||
int memory_limit_probe_rc = tf_maybe_run_windows_memory_limit_probe(argc, argv);
|
int memory_limit_probe_rc = tf_maybe_run_windows_memory_limit_probe(argc, argv);
|
||||||
if (memory_limit_probe_rc >= 0) {
|
if (memory_limit_probe_rc >= 0) {
|
||||||
return memory_limit_probe_rc;
|
return memory_limit_probe_rc;
|
||||||
|
|||||||
Reference in new issue
Block a user