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: 4 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ The test project mirrors the production areas closely.
End-to-end upgrade path: seeds a pre-column legacy DB, opens it through `TryMigrateForRead`, and exercises the read paths that touch nullable symbol ordinals (outline, symbol search, nearby, unused, analyze bundle) to lock in the real-world failure mode behind #58 / #49.
- `IndexCommandRunnerTests.cs`, `QueryCommandRunnerTests.cs`, `ProgramCliTests.cs`, `InstallScriptTests.cs`
CLI parsing, command execution, and installer behavior. `ProgramCliTests.cs` covers top-level entrypoint behavior that must be exercised through a subprocess, while `InstallScriptTests.cs` runs focused bash snippets against `install.sh` in library mode to lock in release-installer regressions without performing real network installs.
- `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`, `Run_CancelDuringDryRunScan_ReturnsInterruptedJson`, and `Run_CancelBeforeFreshScan_ReturnsInterruptedJson`
exercise the same in-process cancellation paths used after Ctrl-C/SIGINT wiring, including scan-time cancellation, so interrupted index runs keep returning the canonical JSON error contract.
- `SymbolExtractorTests.Extract_CSharp_InstallScriptFixture_CompletesWithinPracticalBudget`
is a coarse runaway guard for the real `InstallScriptTests.cs` C# extraction fixture. Its wall-clock budget is intentionally broader than a benchmark so slower or noisy CI hosts do not fail the suite for ordinary variance.
- `IndexCommandRunnerTests.RunBackfillFold_PublishedTrimmedBinary_SerializesSuccessAndErrorJson`
Expand Down Expand Up @@ -225,6 +227,8 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
エンドツーエンドのアップグレード経路: カラム追加前のレガシー DB を用意し、`TryMigrateForRead` 経由で開いてから NULL になりうるシンボル列を触る read path(outline、シンボル検索、近傍、unused、analyze バンドル)を一通り叩き、#58 / #49 の実機失敗モードを固定する。
- `IndexCommandRunnerTests.cs`、`QueryCommandRunnerTests.cs`、`ProgramCliTests.cs`、`InstallScriptTests.cs`
CLI の引数解析、コマンド実行、installer 挙動のテスト。`ProgramCliTests.cs` はグローバル引数の解釈や完全な CLI 起動フローのように subprocess 経由で確認すべき Program エントリポイント挙動を扱い、`InstallScriptTests.cs` は `install.sh` を library mode で source した bash snippet を実行して、実ネットワーク install を行わずに release installer の回帰を固定する。
- `IndexCommandRunnerTests.Run_CancelDuringFreshIndex_ReturnsInterruptedJson`、`Run_CancelDuringDryRunScan_ReturnsInterruptedJson`、`Run_CancelBeforeFreshScan_ReturnsInterruptedJson`
Ctrl-C/SIGINT 配線後に使われる in-process cancellation 経路を、scan 中のキャンセルも含めて検証し、interrupted index run が標準の JSON error contract を返し続けることを固定する。
- `SymbolExtractorTests.Extract_CSharp_InstallScriptFixture_CompletesWithinPracticalBudget`
は実ファイル `InstallScriptTests.cs` を C# 抽出に通す coarse な runaway guard です。wall-clock の予算は benchmark より意図的に広く取り、遅い / 混雑した CI host で通常の揺れだけにより suite が失敗しないようにしています。
- `IndexCommandRunnerTests.RunBackfillFold_PublishedTrimmedBinary_SerializesSuccessAndErrorJson`
Expand Down
19 changes: 19 additions & 0 deletions changelog.d/unreleased/1818.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: fixed
issues:
- 1818
affected:
- tests/CodeIndex.Tests/IndexCommandRunnerTests.cs
- tests/CodeIndex.Tests/FileIndexerTests.cs
- tests/CodeIndex.Tests/SymbolExtractorTests.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
- TESTING_GUIDE.md
---

## English

- **Added cancellation-path regression tests for indexing and extractors (#1818)** — the test suite now exercises in-process index cancellation and cancelled-token entry points for the scanner, symbol extractor, and reference extractor so signal/Ctrl-C wiring regressions are caught earlier.

## 日本語

- **index と抽出器のキャンセル経路の回帰テストを追加しました (#1818)** — テストスイートは in-process index cancellation と、scanner・symbol extractor・reference extractor の cancelled-token 入口を検証し、signal / Ctrl-C 配線の退行をより早く検出します。
19 changes: 19 additions & 0 deletions changelog.d/unreleased/1841.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: fixed
issues:
- 1841
affected:
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- src/CodeIndex/Cli/IndexCommandRunner.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
---

## English

- **Index cancellation now reaches scanner, file IO, and extractor work (#1841)** — `cdidx index` and MCP indexing now pass cancellation tokens into file scanning, raw file reads, symbol extraction, and reference extraction so Ctrl-C and request cancellation can stop deeper in-flight work sooner.

## 日本語

- **index のキャンセルが scanner・ファイル IO・抽出処理まで届くようになりました (#1841)** — `cdidx index` と MCP indexing は cancellation token をファイル走査、raw file 読み込み、symbol 抽出、reference 抽出に渡すため、Ctrl-C やリクエストキャンセル後に深い処理もより早く停止できます。
69 changes: 54 additions & 15 deletions src/CodeIndex/Cli/IndexCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,30 @@ void RecordDryRunScanErrors(IEnumerable<FileIndexer.ScanError> scanErrors)
}
}

int WriteDryRunInterrupted() => WriteCommandError(
options.Json,
jsonOptions,
"Interrupted before dry-run scan completed.",
CommandExitCodes.Interrupted,
"Rerun `cdidx index --dry-run` when you are ready to inspect the candidate files again.",
CommandErrorCodes.Interrupted);

if (options.UpdateFiles.Count > 0)
{
// --files: only the specified files / --files: 指定ファイルのみ
var relevantIgnoreFileChanged = ContainsRelevantIgnoreFileUpdate(options.ProjectPath, options.UpdateFiles);
var updatePaths = NormalizeUpdateFileTargets(options.ProjectPath, options.UpdateFiles, options.Json);
if (relevantIgnoreFileChanged || ContainsIgnoreFilePath(updatePaths))
{
var scanResult = dryIndexer.ScanFilesDetailed();
FileIndexer.ScanFilesResult scanResult;
try
{
scanResult = dryIndexer.ScanFilesDetailed(cancellationToken: indexCancellation.Token);
}
catch (OperationCanceledException) when (indexCancellation.IsCancellationRequested)
{
return WriteDryRunInterrupted();
}
dryCandidates = scanResult.Files;
RecordDryRunScanErrors(scanResult.Errors);
}
Expand Down Expand Up @@ -340,7 +356,15 @@ void RecordDryRunScanErrors(IEnumerable<FileIndexer.ScanError> scanErrors)

if (relevantIgnoreFileChanged || ContainsIgnoreFilePath(changedFiles))
{
var scanResult = dryIndexer.ScanFilesDetailed();
FileIndexer.ScanFilesResult scanResult;
try
{
scanResult = dryIndexer.ScanFilesDetailed(cancellationToken: indexCancellation.Token);
}
catch (OperationCanceledException) when (indexCancellation.IsCancellationRequested)
{
return WriteDryRunInterrupted();
}
dryCandidates = scanResult.Files;
RecordDryRunScanErrors(scanResult.Errors);
}
Expand All @@ -354,7 +378,15 @@ void RecordDryRunScanErrors(IEnumerable<FileIndexer.ScanError> scanErrors)
}
else
{
var scanResult = dryIndexer.ScanFilesDetailed();
FileIndexer.ScanFilesResult scanResult;
try
{
scanResult = dryIndexer.ScanFilesDetailed(cancellationToken: indexCancellation.Token);
}
catch (OperationCanceledException) when (indexCancellation.IsCancellationRequested)
{
return WriteDryRunInterrupted();
}
dryCandidates = scanResult.Files;
RecordDryRunScanErrors(scanResult.Errors);
}
Expand Down Expand Up @@ -1706,7 +1738,7 @@ void ThrowIfUpdateCancelled()
var expandHeartbeat = StartJsonPhaseHeartbeat("expanding C# update set for static interface contracts");
try
{
foreach (var filePath in indexer.ScanFilesDetailed().Files)
foreach (var filePath in indexer.ScanFilesDetailed(cancellationToken: cancellationToken).Files)
{
var detection = FileIndexer.TryDetectLanguage(filePath);
if (detection.Status == FileIndexer.FileProbeStatus.Supported
Expand Down Expand Up @@ -2018,7 +2050,7 @@ void ThrowIfUpdateCancelled()
continue;
}

var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(absPath);
var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(absPath, cancellationToken);

if (warning != null && !options.Json && !options.Quiet)
{
Expand Down Expand Up @@ -2080,7 +2112,7 @@ void ThrowIfUpdateCancelled()
var chunks = ChunkSplitter.Split(fileId, content);
writer.InsertChunks(chunks);
currentUpdatePath = FormatIndexPhasePath(relPath, "symbols");
var symbols = SymbolExtractor.Extract(fileId, record.Lang, content, absPath, Path.GetFullPath(options.ProjectPath!));
var symbols = SymbolExtractor.Extract(fileId, record.Lang, content, absPath, Path.GetFullPath(options.ProjectPath!), cancellationToken);
SymbolExtractor.ApplyFamilyScope(symbols, indexer.GetFamilyScopeKey(absPath, record.Lang));
var fileContext = new FileContext(projectRoot, record.Path, absPath, record.Lang);
postExtractionHooks.OnSymbolsExtracted(fileContext, symbols);
Expand All @@ -2094,7 +2126,8 @@ void ThrowIfUpdateCancelled()
content,
symbols,
record.Path,
record.Lang == "csharp" ? csharpWorkspace.Symbols : null);
record.Lang == "csharp" ? csharpWorkspace.Symbols : null,
cancellationToken);
postExtractionHooks.OnReferencesExtracted(fileContext, references);
writer.InsertReferences(references);
// Validate content for encoding issues / エンコーディング問題を検証
Expand Down Expand Up @@ -3179,9 +3212,13 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal)
try
{
ThrowIfFullScanCancelled(0, null);
scanResult = indexer.ScanFilesDetailed(checkpointedDirectories, continueOnError: true);
scanResult = indexer.ScanFilesDetailed(checkpointedDirectories, continueOnError: true, cancellationToken: cancellationToken);
ThrowIfFullScanCancelled(0, null);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw new IndexInterruptedException(0, null);
}
finally
{
StopJsonPhaseHeartbeat(scanHeartbeat);
Expand Down Expand Up @@ -3508,7 +3545,7 @@ void StopJsonHeartbeat()
{
var relativeFilePath = FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, filePath));
activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(relativeFilePath, "reading");
var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(filePath);
var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(filePath, cancellationToken);
IReadOnlyList<ChunkRecord>? chunks = null;
IReadOnlyList<SymbolRecord>? symbols = null;
IReadOnlyList<ReferenceRecord>? references = null;
Expand All @@ -3518,7 +3555,7 @@ void StopJsonHeartbeat()
activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(record.Path, "chunking");
chunks = ChunkSplitter.Split(0, content);
activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(record.Path, "symbols");
symbols = SymbolExtractor.Extract(0, record.Lang, content, filePath, Path.GetFullPath(options.ProjectPath!));
symbols = SymbolExtractor.Extract(0, record.Lang, content, filePath, Path.GetFullPath(options.ProjectPath!), cancellationToken);
SymbolExtractor.ApplyFamilyScope(symbols, indexer.GetFamilyScopeKey(filePath, record.Lang));
activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(record.Path, "references");
references = ReferenceExtractor.Extract(
Expand All @@ -3527,7 +3564,8 @@ void StopJsonHeartbeat()
content,
symbols,
record.Path,
record.Lang == "csharp" ? csharpWorkspace.Symbols : null);
record.Lang == "csharp" ? csharpWorkspace.Symbols : null,
cancellationToken);
activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(record.Path, "validating");
issues = FileIndexer.ValidateContent(record.Path, rawBytes, content);
}
Expand Down Expand Up @@ -3698,7 +3736,7 @@ void StopJsonHeartbeat()
writer.InsertChunks(chunks);
currentJsonIndexFile = FormatIndexPhasePath(record.Path, "symbols");
var symbols = item.Symbols == null
? SymbolExtractor.Extract(fileId, record.Lang, item.Content!, item.FilePath, Path.GetFullPath(options.ProjectPath!))
? SymbolExtractor.Extract(fileId, record.Lang, item.Content!, item.FilePath, Path.GetFullPath(options.ProjectPath!), cancellationToken)
: ReassignSymbolFileIds(item.Symbols, fileId);
if (item.Symbols == null)
SymbolExtractor.ApplyFamilyScope(symbols, indexer.GetFamilyScopeKey(item.FilePath, record.Lang));
Expand All @@ -3717,7 +3755,8 @@ void StopJsonHeartbeat()
item.Content!,
symbols,
record.Path,
record.Lang == "csharp" ? csharpWorkspace.Symbols : null)
record.Lang == "csharp" ? csharpWorkspace.Symbols : null,
cancellationToken)
: ReassignReferenceFileIds(item.References, fileId);
postExtractionHooks.OnReferencesExtracted(fileContext, AsMutableList(references));
writer.InsertReferences(references);
Expand Down Expand Up @@ -4289,14 +4328,14 @@ private static CSharpStaticInterfaceWorkspaceSymbols BuildCSharpStaticInterfaceW
try
{
reportCurrentFile?.Invoke(relativePath);
var (record, content, _, _) = indexer.BuildRecordWithRawBytes(absolutePath);
var (record, content, _, _) = indexer.BuildRecordWithRawBytes(absolutePath, cancellationToken);
if (record.Lang != "csharp")
continue;

if (!MayContainCSharpStaticInterfaceContract(content))
continue;

pendingSymbols.AddRange(SymbolExtractor.Extract(0, record.Lang, content, record.Path));
pendingSymbols.AddRange(SymbolExtractor.Extract(0, record.Lang, content, record.Path, cancellationToken: cancellationToken));
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public sealed record ReferenceExtractionContext(
IReadOnlyList<SymbolRecord> Symbols,
string? Path = null,
IReadOnlyList<SymbolRecord>? WorkspaceSymbols = null,
string? RequestedLanguage = null);
string? RequestedLanguage = null,
CancellationToken CancellationToken = default);
13 changes: 11 additions & 2 deletions src/CodeIndex/Indexer/References/ReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -908,8 +908,10 @@ public static List<ReferenceRecord> Extract(
string content,
IReadOnlyList<SymbolRecord> symbols,
string? path = null,
IReadOnlyList<SymbolRecord>? workspaceSymbols = null)
IReadOnlyList<SymbolRecord>? workspaceSymbols = null,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var requestedLanguage = lang;
var pluginLanguage = NormalizePluginLanguage(lang);
if (!TryGetExtractor(lang, out var extractor))
Expand All @@ -924,6 +926,7 @@ public static List<ReferenceRecord> Extract(
if (content.Contains('\r'))
content = content.Replace("\r\n", "\n").Replace("\r", "\n");
content = FileIndexer.StripLineLeadingInvisibles(content);
cancellationToken.ThrowIfCancellationRequested();

return pluginExtractor.Extract(
fileId,
Expand All @@ -941,11 +944,13 @@ public static List<ReferenceRecord> Extract(
symbols,
path,
workspaceSymbols,
requestedLanguage));
requestedLanguage,
cancellationToken));
}

internal static List<ReferenceRecord> ExtractCore(ReferenceExtractionContext request)
{
request.CancellationToken.ThrowIfCancellationRequested();
var fileId = request.FileId;
var language = request.Language;
var content = request.Content;
Expand All @@ -958,6 +963,7 @@ internal static List<ReferenceRecord> ExtractCore(ReferenceExtractionContext req

if (!TryPrepareReferenceLines(language, content, isRazorFile, out var preparedInput))
return [];
request.CancellationToken.ThrowIfCancellationRequested();

content = preparedInput.Content;
var lines = preparedInput.Lines;
Expand Down Expand Up @@ -1167,6 +1173,9 @@ bool HasActiveSameFileCSharpTypeCandidate(string typeExpression, int lineNumber)

for (int i = 0; i < lines.Length; i++)
{
if ((i & 0x3f) == 0)
request.CancellationToken.ThrowIfCancellationRequested();

var lineNumber = i + 1;
var originalLine = lines[i];
var preparedLine = luaPreparedLines?[i] ?? lispReferenceLines?[i] ?? preparedLines[i];
Expand Down
Loading
Loading