Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
- HEAD freshness coverage uses one two-commit fixture to prove a `--files` refresh remains stale before a `--commits HEAD` refresh marks the current head matched.
- Hook timeout identity coverage uses a one-second callback budget so the healthy duplicate has cold worker-startup margin while the selected 30-second hook still times out well below the production-sized five-second budget.
- SSE oversized-frame coverage uses the minimum practical keep-alive interval because the frame-size rejection, not elapsed idle time, is the contract; retain bounded polling for stream removal instead of a production-scale interval.
- Low-level Git subprocess timeout tests use a 100 ms injected command budget. Commit-diff timeout coverage uses 500 ms because it must first complete ref validation before the fake executable hangs on `diff-tree`; keep both well below the production timeout without timing out the setup command.
- Low-level Git subprocess timeout tests use a 100 ms injected command budget. Commit-diff timeout coverage uses 500 ms because it must first complete ref validation before the fake executable hangs on `diff-tree`; its fake command records its PID, sleeps for 15 seconds, and must be reaped before the call returns in under 10 seconds. This keeps the setup command below the production timeout while leaving scheduler and cleanup margin without allowing natural fake-git completion to satisfy the assertion.
- Signal-gated transaction dispose/rollback and HTTP serialization/header tests use the shared 100 ms blocked-observation window after explicit entry signals. Keep the synchronization points and later positive completion assertions; transaction coverage must not return to 200 ms sleeps.
- Keep hang collection on both CI attempts and crash collection on the initial attempt so transient host crashes retain evidence; the retry skips duplicate crash collection while preserving hang diagnostics.
- When a test locks a long table of equivalent key/value expectations, keep the table as data and route the repeated lookup/assertion shape through one helper so duplicate rows are visible.
Expand Down Expand Up @@ -1681,7 +1681,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- HEAD freshness coverage は 1 つの two-commit fixture で、`--files` refresh 後は stale のまま、`--commits HEAD` refresh 後は current head が matched になることを検証してください。
- hook timeoutのidentity coverageは1秒のcallback budgetを使い、正常なduplicate workerのcold startupに余裕を残しつつ、選択した30秒hookを本番相当の5秒budgetより十分早くtimeoutさせます。
- SSE oversized-frame coverage は、経過idle時間ではなくframe-size rejectionが契約なので、実用上最小のkeep-alive intervalを使います。stream除去は本番相当intervalではなく境界付きpollingで検証してください。
- 低レベルGit subprocess timeout testは注入した100 msのcommand budgetを使います。commit-diff timeout coverageはfake executableが`diff-tree`でhangする前にref validationを完了する必要があるため500 msを使い、setup commandを誤ってtimeoutさせず本番timeoutより十分短く保ちます
- 低レベルGit subprocess timeout testは注入した100 msのcommand budgetを使います。commit-diff timeout coverageはfake executableが`diff-tree`でhangする前にref validationを完了する必要があるため500 msを使います。fake commandはPIDを記録して15秒sleepし、呼び出しが10秒未満で返る前に回収済みであることを検証します。これによりsetup commandを誤ってtimeoutさせず、schedulerとcleanupの余裕を残しながら、fake gitの自然終了ではassertionを満たせないようにします
- signal-gatedなtransaction dispose/rollbackとHTTP serialization/header testは、明示的entry signalの後に共有100 ms blocked-observation windowを使います。同期点と後続のpositive completion assertionを維持し、transaction coverageを200 ms sleepへ戻さないでください。
- CIのhang収集は両attemptで維持し、crash収集は一過性host crashのevidenceを残すため初回attemptで行います。retryでは重複するcrash収集を省き、hang診断は維持します。
- 同種の key/value 期待値を長い表で固定するテストでは、期待値をデータとして残し、繰り返しの lookup/assertion 形は helper に通してください。重複行を見つけやすくするためです。
Expand Down
16 changes: 16 additions & 0 deletions changelog.d/unreleased/4982.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: internal
issues:
- 4982
affected:
- tests/CodeIndex.Tests/GitHelperTests.cs
- TESTING_GUIDE.md
---

## English

- **Stabilized commit-diff Git timeout regression coverage (#4982)** — the fake `diff-tree` command now stays blocked beyond the assertion window, records its process ID, and verifies that the 500 ms timeout reaps the process and returns within 10 seconds under full-suite load.

## 日本語

- **commit-diff Git timeout の回帰テストを安定化しました (#4982)** — fake `diff-tree` command が assertion window を超えて停止し、process ID を記録するようにして、全スイート負荷下でも 500 ms の timeout が process を回収して 10 秒以内に返ることを検証します。
31 changes: 29 additions & 2 deletions tests/CodeIndex.Tests/GitHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,17 +733,30 @@ public void GetChangedFilesFromCommit_FailsWhenGitCommandTimesOut()
var fakeGitDir = Path.Combine(_tempDir, "fake-git-timeout");
Directory.CreateDirectory(fakeGitDir);
WriteFakeGitThatHangsOnDiffTree(fakeGitDir);
var fakeGitPidPath = Path.Combine(fakeGitDir, "diff-tree.pid");

var oldGitExecutablePath = GitHelper.GitExecutablePathOverride;
var oldTimeout = GitHelper.GitCommandTimeout;
GitHelper.GitExecutablePathOverride = Path.Combine(fakeGitDir, "git");
GitHelper.GitCommandTimeout = TimeSpan.FromMilliseconds(500);
try
{
var stopwatch = Stopwatch.StartNew();
var ex = Assert.Throws<InvalidOperationException>(
() => GitHelper.GetChangedFilesFromCommit(repoDir, commitId));
stopwatch.Stop();

Assert.True(File.Exists(fakeGitPidPath), "Fake git did not reach diff-tree.");
var fakeGitPid = int.Parse(
File.ReadAllText(fakeGitPidPath),
System.Globalization.CultureInfo.InvariantCulture);
Assert.False(
IsProcessRunning(fakeGitPid),
$"Timed-out fake git process {fakeGitPid} was not reaped.");
Assert.Contains("timed out", ex.Message);
Assert.True(
stopwatch.Elapsed < GitCancellationWallClockLimit,
$"Commit-diff timeout took {stopwatch.Elapsed}, expected less than {GitCancellationWallClockLimit} before the {FakeGitHangSeconds}-second fake git sleep completed.");
}
finally
{
Expand Down Expand Up @@ -1971,7 +1984,7 @@ exit 0
private static void WriteFakeGitThatHangsOnDiffTree(string directory)
{
var script = Path.Combine(directory, "git");
File.WriteAllText(script, """
File.WriteAllText(script, $$"""
#!/bin/sh
if [ "$1" = "rev-parse" ]; then
if [ "$2" = "--symbolic-full-name" ]; then
Expand All @@ -1983,7 +1996,8 @@ exit 0
fi
fi
if [ "$1" = "diff-tree" ]; then
sleep 5
printf '%s\n' "$$" > "$(dirname "$0")/diff-tree.pid"
sleep {{FakeGitHangSeconds}}
exit 0
fi
exit 1
Expand All @@ -1992,6 +2006,19 @@ exit 1
File.SetUnixFileMode(script, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}

private static bool IsProcessRunning(int processId)
{
try
{
using var process = Process.GetProcessById(processId);
return !process.HasExited;
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
{
return false;
}
}

private static void WriteFakeGitThatFailsWithLongSensitiveStderr(string directory)
{
var script = Path.Combine(directory, "git");
Expand Down
Loading