diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e97aa7ff4..e0b172faa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,16 +83,24 @@ jobs: include: - os: ubuntu-24.04 rid: linux-x64 + cross_compile: false + run_tests: true - os: ubuntu-24.04 rid: linux-arm64 cross_compile: true + run_tests: false - os: windows-2022 rid: win-x64 + cross_compile: false + run_tests: false - os: windows-2022 rid: win-arm64 cross_compile: true + run_tests: false - os: macos-14 rid: osx-arm64 + cross_compile: false + run_tests: false runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -101,27 +109,22 @@ jobs: fetch-depth: 0 ref: ${{ needs.preflight.outputs.ref }} - - name: Configure Windows test host - if: runner.os == 'Windows' && !matrix.cross_compile - shell: pwsh - run: ./.github/scripts/configure-windows-test-host.ps1 -Workspace "${{ github.workspace }}" - - name: Set up .NET SDKs - if: ${{ !matrix.cross_compile }} + if: matrix.run_tests uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: | 8.0.413 9.0.301 - - name: Set up cross-compile .NET SDK - if: matrix.cross_compile + - name: Set up publish-only .NET SDK + if: ${{ !matrix.run_tests }} uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: 9.0.301 - - name: Cache native NuGet packages - if: ${{ !matrix.cross_compile }} + - name: Cache test-lane NuGet packages + if: matrix.run_tests uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | @@ -129,14 +132,14 @@ jobs: ~\AppData\Local\NuGet\packages key: ${{ runner.os }}-release-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.Tests/packages.lock.json', 'tools/CodeIndex.Changelog/packages.lock.json', 'tools/CodeIndex.PackageNormalize/packages.lock.json', 'tools/CodeIndex.TestTelemetry/packages.lock.json') }} - - name: Cache cross-compile NuGet packages - if: matrix.cross_compile + - name: Cache publish-only NuGet packages + if: ${{ !matrix.run_tests }} uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.nuget/packages ~\AppData\Local\NuGet\packages - key: ${{ runner.os }}-release-cross-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json') }} + key: ${{ runner.os }}-release-publish-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json') }} # --locked-mode requires every resolved package to match the committed # packages.lock.json so an unexpected transitive bump (including silent @@ -147,19 +150,19 @@ jobs: # bump が公開アーティファクトに紛れ込まず release restore で気付ける。 # 詳細は issue #1556 参照。 - name: Restore test dependencies - if: ${{ !matrix.cross_compile }} + if: matrix.run_tests run: dotnet restore tests/CodeIndex.Tests/CodeIndex.Tests.csproj -p:RestoreTargetFrameworks=net8.0 --locked-mode - name: Restore publish dependencies - if: matrix.cross_compile + if: ${{ !matrix.run_tests }} run: dotnet restore src/CodeIndex/CodeIndex.csproj --locked-mode - name: Build tests - if: ${{ !matrix.cross_compile }} + if: matrix.run_tests run: dotnet build tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-restore - name: Test net8 - if: ${{ !matrix.cross_compile }} + if: matrix.run_tests run: dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-build --no-restore --nologo # Generate a CycloneDX SBOM once per release on the linux-x64 lane. @@ -217,8 +220,8 @@ jobs: # RestoreLockedMode=true here: publish for a new RID and trimming # (Microsoft.NET.ILLink.Tasks) legitimately add lock entries that did # not exist at the preceding locked restore (net8 test-project restore on - # native lanes, production-project restore on cross-compile lanes). The - # native restore covers the production dependency graph through the test + # the test lane, production-project restore on publish-only lanes). The + # test restore covers the production dependency graph through the test # project's ProjectReference. The security guarantee against silent # SQLitePCLRaw-style transitive drift is already enforced by that # lane-appropriate restore. See issue #1556 / DEVELOPER_GUIDE.md. @@ -226,9 +229,9 @@ jobs: # Directory.Build.props の RestorePackagesWithLockFile=true により # lock ファイルで固定された版を解決対象とする。ここで # RestoreLockedMode=true を渡さないのは意図的で、publish 時に新規 RID と - # trimming(Microsoft.NET.ILLink.Tasks)が、直前の locked restore(native lane - # では net8 test project、cross-compile lane では production project)時には - # なかった lock エントリを正当に追加するため。native restore は test project + # trimming(Microsoft.NET.ILLink.Tasks)が、直前の locked restore(test lane + # では net8 test project、publish-only lane では production project)時には + # なかった lock エントリを正当に追加するため。test restore は test project # の ProjectReference 経由で production dependency graph も検証する。 # SQLitePCLRaw のような推移依存ドリフトに対する保証は、その lane に対応する # restore で確保済み。 @@ -307,6 +310,41 @@ jobs: Remove-Item -LiteralPath $pfxPath -Force -ErrorAction SilentlyContinue } + # Run the complete RID-independent source suite once on linux-x64, then + # exercise each natively runnable artifact itself. This preserves OS and + # bundled-SQLite coverage without repeating the same 8k-test suite on + # Windows and macOS release runners. + # RID 非依存の完全な source suite は linux-x64 で 1 回実行し、native 実行 + # 可能な各 artifact 自体を検証する。Windows/macOS release runner で同じ + # 約 8k test を繰り返さず、OS と bundled SQLite の coverage を維持する。 + - name: Smoke-test native release artifact (Linux/macOS) + if: runner.os != 'Windows' && !matrix.cross_compile + shell: bash + run: | + set -euo pipefail + smoke_root="$(mktemp -d)" + trap 'rm -rf "$smoke_root"' EXIT + printf 'namespace ReleaseSmoke; public sealed class Sample { public int Value => 1; }\n' > "$smoke_root/Sample.cs" + ./publish/cdidx "$smoke_root" --db "$smoke_root/.cdidx/codeindex.db" --json + ./publish/cdidx status --db "$smoke_root/.cdidx/codeindex.db" --json + + - name: Smoke-test native release artifact (Windows) + if: runner.os == 'Windows' && !matrix.cross_compile + shell: pwsh + run: | + $smokeRoot = Join-Path $env:RUNNER_TEMP ([IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Force -Path $smokeRoot | Out-Null + try { + 'namespace ReleaseSmoke; public sealed class Sample { public int Value => 1; }' | + Set-Content -Encoding utf8 (Join-Path $smokeRoot 'Sample.cs') + & .\publish\cdidx.exe $smokeRoot --db (Join-Path $smokeRoot '.cdidx\codeindex.db') --json + if ($LASTEXITCODE -ne 0) { throw "Published cdidx index smoke failed with exit code $LASTEXITCODE." } + & .\publish\cdidx.exe status --db (Join-Path $smokeRoot '.cdidx\codeindex.db') --json + if ($LASTEXITCODE -ne 0) { throw "Published cdidx status smoke failed with exit code $LASTEXITCODE." } + } finally { + Remove-Item -LiteralPath $smokeRoot -Recurse -Force -ErrorAction SilentlyContinue + } + - name: Add license and trademark notices to publish output (Linux/macOS) if: runner.os != 'Windows' run: cp LICENSE LICENSES/FSL-1.1-ALv2.txt LICENSES/Apache-2.0.txt COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md publish/ && cp -R LICENSES publish/ diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 498d5a986..548c23c1e 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -65,16 +65,16 @@ Use the full suite by default. Use targeted filters only while iterating locally - Coverage collection runs only on the initial attempt of each coverage-enabled shard; the one flaky-classification retry reuses the same test arguments without rerunning the coverage collector. - Matrix test invocations use both `--no-build` and `--no-restore` because each lane completes its scoped locked restore and Release build before entering the shared test helper. - Primary-lane publish also uses `--no-build --no-restore`, reusing the production project output and dependency graph built through the Release test project. -- Release cross-compile lanes skip the RID-agnostic solution build because they do not run tests and the self-contained RID publish necessarily performs the real build; native lanes retain the solution build before testing. -- Release setup also skips Windows test-host hardening on the non-testing win-arm64 cross-compile lane, caches the pinned CycloneDX tool independently on linux-x64, and gives the fresh `publish-nuget` job a package cache keyed only by the production and package-normalizer lock files. -- Release cross-compile lanes likewise use a locked production-project restore instead of restoring test and tool projects they never build; native test lanes retain the locked solution restore. -- Release cross-compile lanes install only the repository-selected 9.0 SDK because they publish self-contained binaries and never execute the net8 test host; native lanes retain both pinned SDK lines. -- Release workflow tests use `--no-build --no-restore` after the solution's locked restore and Release build so each runtime lane does not reevaluate dependencies. +- Release runs the RID-independent `net8.0` source suite once on `linux-x64`; every natively runnable release artifact (`linux-x64`, `win-x64`, and `osx-arm64`) must then index a temporary C# source and read its SQLite-backed status through the published self-contained binary. Cross-compiled artifacts remain archive-verified because they cannot execute on their host runner. +- Release setup installs both pinned SDKs, restores the full test dependency graph, and builds/tests only on the `linux-x64` test lane. Every publish-only lane installs only the repository-selected 9.0 SDK and performs a locked production-project restore, avoiding unused test/tool downloads and repeated source-suite execution. Windows test-host hardening is omitted when no release matrix entry runs tests on Windows. +- Release setup caches the pinned CycloneDX tool independently on linux-x64 and gives the fresh `publish-nuget` job a package cache keyed only by the production and package-normalizer lock files. +- Release workflow tests use `--no-build --no-restore` after the test lane's locked restore and Release build so dependency evaluation is not repeated. - Curated release-note generation caches the changelog tool lock file, performs one conditional locked restore, and runs the tool with `--no-restore`. - Keep package audit, primary-lane build/lint, publish, and build artifact upload keyed to the matrix `primary_lane` value. Key coverage collection and coverage artifact upload to `collect_coverage` so both complementary Ubuntu `net8.0` shards contribute coverage without duplicating primary validation; define both values once in explicit matrix entries instead of recomputing or excluding combinations in later steps. +- CI matrix contract tests build each expected lane through one shared lane assertion. Keep OS/framework/SDK/coverage/shard/filter values explicit at the call sites instead of copying the full YAML block for every lane. - Workflow path filters do not repeat individual Markdown files already covered by `**.md`; keep equivalent push and pull-request filters aligned. Build/Test and CodeQL also ignore license text paths owned by the focused license-policy workflow. - Test workflows group pull-request runs by workflow and pull request, use a unique run ID otherwise, and cancel only superseded pull-request runs so push, schedule, and manual runs remain independent. -- TRX telemetry summarization reuses the Release telemetry tool already built through the test project's direct reference and must not restore or build it again after the test step. +- TRX telemetry summarization reuses the Release telemetry tool already built through the test project's direct reference and must not restore or build it again after the test step. Its streaming reader accepts up to 64 MiB per TRX so the current full suite remains observable while retaining a bounded XML-document guard; bounded slow/failure lists use ordered insertion instead of re-sorting on every result. - The changelog-fragments workflow caches packages from the changelog tool lock file, performs one locked restore, and validates with `dotnet run --no-restore`. - The focused license-policy workflow caches NuGet packages, performs a locked `net8.0`-only restore, and runs its filtered tests with `--no-restore` so dependency resolution is not repeated. - The C# CodeQL lane uses setup-dotnet's lock-file-keyed NuGet cache; the Actions-only lane skips both SDK setup and package caching. @@ -818,6 +818,24 @@ Use the inventory below before adding or moving a test class: - `Console.Out` or `Console.Error` replacement: prefer `ConsoleCapture`, which owns the shared gate and checks the restored writer identity. If a specialized fixture must swap a writer directly, lock `TestConsoleLock.Gate` around the whole capture/swap window, restore in `finally`, and restore before disposing the captured writer. - Console-only test classes belong in the dedicated non-parallel console-sensitive collection once every capture/swap window, including writer-disposal checks, is protected by `ConsoleCapture` or `TestConsoleLock.Gate`; they do not also need the SQLite-sensitive collection. - Pure i18n resolution, self-locking JSON-envelope capture, and isolated LSP request/budget fixtures should remain outside the SQLite-sensitive collection; owning a temporary DB is not itself process-global state when the context is disposed before helper cleanup. +- Independent C# query regressions for static lambdas, declaration continuations, and named-argument labels use standalone test classes plus `QueryCommandTestSupport`; do not fold them back into the SQLite-pool-sensitive `QueryCommandRunnerTests` partial class. +- JSON compatibility-alias and versioned-error query regressions also use standalone classes and the same self-locking support so their independent temporary databases can run outside the pool-sensitive collection. +- Search fixture classification and count-mode guard-filter regressions are standalone for the same reason; their per-test databases and self-locking console captures do not require SQLite pool serialization. +- Status hotspot-readiness regressions are standalone and reuse the shared partial-type database fixture from `QueryCommandTestSupport`; keep the fixture centralized even though the status class runs outside the pool-sensitive collection. +- Pure query-bound parser coverage for visibility, status scopes, paths, and map sections stays in the standalone `QueryCommandRunnerBoundsTests` class so validation-only cases are not serialized behind database tests. +- Find line-scan and symmetric-context limit regressions use standalone classes and the shared console-input capture, keeping their independent databases and parser failures outside the pool-sensitive query megaclass; the nested batch-dispatch variant stays console-sensitive because concurrent request logging can outlive another capture boundary. +- Find terminal scan-state, partial-result, row-format, envelope, and human-summary regressions use a standalone class with shared JSON and console helpers so their isolated database fixture is not serialized behind pool-reset coverage. +- Search literal-highlight, raw-FTS conflict, punctuation-hint, and rank-only regressions use a standalone class with shared JSON and console helpers; keep their isolated read/query databases outside the pool-sensitive query megaclass. +- Search-to-find recovery guidance, shell-safe alternatives, option mapping, blocker, validation, and response-budget regressions are parser/console-only coverage in a standalone class and must not be serialized behind database tests. +- Validate limit/severity parser errors and content-classification checks use a standalone contract class, while indexed validation views and pool-release coverage remain in their pool-sensitive fixtures. +- Batch structured-command shape and configurable budget validation use a console-sensitive standalone class because nested dispatch can receive asynchronous request logs; tests that mutate batch scheduling hooks remain in the pool-sensitive query class and restore every hook in `finally`. +- Report argument parsing for output aliases, inclusion flags, bounds, unknown options, and positional errors uses a standalone parser class; bundle creation and process-global diagnostic state remain pool-sensitive. +- Metrics flag/language parsing and deterministic retry-delay calculation use a standalone argument class; active sink sessions, rotation, queues, diagnostics, and file writes remain in the pool-sensitive metrics class. +- Database maintenance parsing for integrity, schema projection, prune/checkpoint/restore, size controls, and invalid combinations uses a standalone parser class; direct SQLite execution and pool-release helpers remain pool-sensitive. +- Program project-path classification across common, POSIX literal-backslash, and Windows path forms uses a standalone cross-platform class; environment, transport, process, and database behaviors remain pool-sensitive. +- MCP audit-log integration stays in the console-sensitive collection: request-correlated server diagnostics write directly to stderr and can otherwise contaminate concurrently captured command output. +- Audit SARIF UTF-8 byte-budget, whole-result truncation, replay, help, and ad-hoc rejection regressions use one standalone class so their isolated recipe databases can run independently of pool-reset coverage. +- Files/symbols top-level and positional path-filter regressions use a standalone class with shared JSON parsing so their isolated databases can run in parallel with the pool-sensitive query class. - LSP telemetry fixtures scope captured activities to an ambient parent trace so unrelated parallel requests cannot enter their assertions. The external-WAL snapshot fixture keeps its writer open through the before/after artifact comparison, then disposes its readers and writers before resilient project cleanup; it must not reintroduce process-wide pool resets. - Reader issue fixtures with per-instance DB ownership and external-process fixtures with per-instance directories are likewise parallel-safe when their `Dispose` paths release those resources through the shared helpers. - Schema-constraint fixtures dispose every `DbContext`, connection, command, and reader before directory cleanup; do not add unconditional pool resets that serialize these independent schema checks. @@ -1100,16 +1118,16 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" - coverage collection は coverage が有効な各 shard の初回 test attempt だけで実行し、flaky classification の1回だけの retry では同じ test 引数を再利用しつつ coverage collector を再実行しないでください。 - matrix test invocation は shared test helper の前に各 lane の scoped locked restore と Release build が完了しているため、`--no-build` と `--no-restore` の両方を使ってください。 - primary-lane publish も `--no-build --no-restore` を使い、Release test project 経由で build 済みの production project output と dependency graph を再利用してください。 -- release の cross-compile lane は test を実行せず、self-contained RID publish が実 build を必ず行うため、RID 非依存の solution build を省略する。native lane は test 前の solution build を維持する。 -- release setupでは、testを実行しないwin-arm64 cross-compile laneのWindows test-host hardeningも省略し、linux-x64では固定CycloneDX toolを独立cacheし、freshな`publish-nuget` jobにはproduction / package-normalizer lock fileだけをkeyにしたpackage cacheを持たせてください。 -- release の cross-compile lane は build しない test / tool project を復元せず、production project だけを locked restore する。native test lane は locked solution restore を維持する。 -- release の cross-compile lane は self-contained binary を publish し、net8 test host を実行しないため、repository が選択する9.0 SDK だけを install する。native lane はpinされた両 SDK lineを維持する。 -- release workflow の test も solution の locked restore と Release build 後に `--no-build --no-restore` を使い、runtime lane ごとの dependency 再評価を避けてください。 +- release は RID 非依存の `net8.0` source suite を `linux-x64` で 1 回だけ実行します。その後、native 実行可能な各 release artifact(`linux-x64`、`win-x64`、`osx-arm64`)は、publish 済み self-contained binary を使って一時 C# source の index と SQLite-backed status read を通してください。cross-compile artifact は host runner 上で実行できないため archive 検証を維持します。 +- release setup は `linux-x64` test lane だけで pin 済み両 SDK を導入し、完全な test dependency graph の restore と build/test を行います。publish-only lane は repository が選択する 9.0 SDK と production project の locked restore だけを使い、未使用 test/tool download と source suite の反復を避けます。release matrix の Windows entry は test を実行しないため Windows test-host hardening も省略します。 +- release setup では linux-x64 の固定 CycloneDX tool を独立 cache し、fresh な `publish-nuget` job には production / package-normalizer lock file だけを key にした package cache を持たせてください。 +- release workflow の test は test lane の locked restore と Release build 後に `--no-build --no-restore` を使い、dependency の再評価を避けてください。 - curated release-note生成はchangelog toolのlock fileをcacheし、conditional locked restoreを1回行ってからtoolを`--no-restore`で実行してください。 - package audit、primary-lane build/lint、publish、build artifact upload は matrix の `primary_lane` 値に揃えてください。coverage の収集と coverage artifact upload は `collect_coverage` に揃え、補完的な Ubuntu `net8.0` shard の両方で coverage を取りつつ primary 検証は重複させません。両方の値は明示的な matrix entry で一度だけ定義し、後続 step で再計算したり exclude したりしません。 +- CI matrix の contract test は、期待する各 lane を1つの共有 lane assertion で組み立てます。lane ごとに YAML block 全体を複製せず、OS/framework/SDK/coverage/shard/filter の値は call site に明示してください。 - workflow path filter では `**.md` がすでに対象とする個別 Markdown file を重複して列挙せず、同等の push / pull-request filter を同期させます。Build/Test と CodeQL は focused license-policy workflow が所有する license text path も無視します。 - test workflow は pull-request run を workflow と pull request ごとに group 化し、それ以外は一意な run ID を使ってください。古い pull-request run だけを cancel し、push、schedule、manual run は独立させます。 -- TRX telemetry summary は test project の direct reference 経由で build 済みの Release telemetry tool を再利用し、test step 後に restore/build を繰り返さないでください。 +- TRX telemetry summary は test project の direct reference 経由で build 済みの Release telemetry tool を再利用し、test step 後に restore/build を繰り返さないでください。streaming reader は TRX ごとに 64 MiB まで受け入れ、現行 full suite を観測可能にしつつ XML document guard を有界に保ちます。上限付き slow/failure list は result ごとの再 sort ではなく ordered insertion を使ってください。 - changelog-fragments workflow は changelog tool の lock file を使って package を cache し、locked restore を1回行ってから `dotnet run --no-restore` で検証してください。 - focused license-policy workflow は NuGet package を cache し、`net8.0` だけを locked restore した後、dependency resolution を繰り返さないよう filtered test を `--no-restore` で実行します。 - C# CodeQL lane は setup-dotnet の lock-file-keyed NuGet cache を使い、Actions だけの lane は SDK setup と package cache の両方を skip します。 @@ -1855,6 +1873,24 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" - `Console.Out` / `Console.Error` の差し替え: shared gate の所有と復元writerの同一性確認を行う `ConsoleCapture` を優先する。特殊なfixtureで直接差し替える必要がある場合は、capture / swap 期間全体を `TestConsoleLock.Gate` で lockし、`finally`で復元し、capture writerをdisposeする前に元writerへ戻す。 - console だけを扱う test class は、writer disposal check を含むすべての capture / swap 期間を `ConsoleCapture` または `TestConsoleLock.Gate` で保護したうえで、専用の non-parallel な console-sensitive collection に入れる。SQLite-sensitive collection にも入れる必要はない。 - pure i18n resolution、内部で lock する JSON-envelope capture、独立した LSP request / budget fixture は SQLite-sensitive collection の外に保つ。一時 DB を所有するだけなら、context を helper cleanup 前に dispose している限り process-global state ではない。 +- static lambda、宣言 continuation、named-argument label の独立した C# query regression は standalone test class と `QueryCommandTestSupport` を使います。SQLite-pool-sensitive な `QueryCommandRunnerTests` partial class に戻さないでください。 +- JSON compatibility-alias と versioned-error の query regression も standalone class と同じ self-locking support を使い、独立した一時 database を pool-sensitive collection の外で実行してください。 +- search fixture classification と count-mode guard-filter の regression も同じ理由で standalone にします。test ごとの database と self-locking console capture は SQLite pool の直列化を必要としません。 +- status hotspot-readiness regression は standalone とし、共有 partial-type database fixture を `QueryCommandTestSupport` から再利用します。status class を pool-sensitive collection の外で実行しても fixture は一元化してください。 +- visibility、status scope、path、map section の pure query-bound parser coverage は standalone `QueryCommandRunnerBoundsTests` class に置き、validation-only case を database test の後ろで直列化しないでください。 +- find の line-scan と symmetric-context limit regression は standalone class と共有 console-input capture を使い、独立 database と parser failure を pool-sensitive な query megaclass の外で実行してください。ただし nested batch-dispatch variant は並行 request log が別の capture 境界を越えて届くため console-sensitive に維持してください。 +- find の terminal scan-state、partial-result、row-format、envelope、human-summary regression は共有 JSON / console helper を使う standalone class に置き、独立 database fixture を pool-reset coverage の後ろで直列化しないでください。 +- search の literal-highlight、raw-FTS conflict、punctuation-hint、rank-only regression は共有 JSON / console helper を使う standalone class に置き、独立した read/query database を pool-sensitive query megaclass の外で実行してください。 +- search から find への recovery guidance、shell-safe alternative、option mapping、blocker、validation、response-budget regression は parser / console のみを扱う standalone class に置き、database test の後ろで直列化しないでください。 +- validate の limit / severity parser error と content classification check は standalone contract class に置き、indexed validation view と pool-release coverage は pool-sensitive fixture に残してください。 +- batch の structured-command shape と configurable budget validation は nested dispatch に asynchronous request log が届くため console-sensitive standalone class に置き、batch scheduling hook を変更するテストは pool-sensitive query class に残して、すべての hook を `finally` で復元してください。 +- report の output alias、inclusion flag、bound、unknown option、positional error に関する引数解析は standalone parser class に置き、bundle 作成と process-global diagnostic state は pool-sensitive のままにしてください。 +- metrics flag / language parsing と決定的な retry-delay calculation は standalone argument class に置き、active sink session、rotation、queue、diagnostics、file write は pool-sensitive metrics class に残してください。 +- database maintenance の integrity、schema projection、prune / checkpoint / restore、size control、invalid combination の解析は standalone parser class に置き、直接の SQLite 実行と pool-release helper は pool-sensitive のままにしてください。 +- program の project-path classification は common、POSIX literal-backslash、Windows path form をまとめた standalone cross-platform class に置き、environment、transport、process、database behavior は pool-sensitive のままにしてください。 +- MCP audit-log integration は console-sensitive collection に維持してください。request correlation 付き server diagnostics は stderr へ直接書き込むため、並列実行すると別の command output capture を汚染します。 +- audit SARIF の UTF-8 byte-budget、whole-result truncation、replay、help、ad-hoc rejection regression は一つの standalone class に置き、独立した recipe database を pool-reset coverage と並列実行できるようにしてください。 +- files/symbols の top-level および positional path-filter regression は共有 JSON parsing を使う standalone class に置き、独立 database を pool-sensitive query class と並列実行できるようにしてください。 - LSP telemetry fixture は capture した activity を ambient parent trace に限定し、無関係な parallel request が assertion に入らないようにする。external-WAL snapshot fixture は before / after の artifact 比較が終わるまで writer を開いたままにし、その後 resilient な project cleanup 前に reader / writer を dispose する。process-wide pool reset を再導入しないこと。 - instance ごとに DB を所有する reader issue fixture と、instance ごとに directory を所有する external-process fixture も、`Dispose` で共有 helper を通して resource を解放する限り parallel-safe である。 - schema-constraint fixture は directory cleanup 前にすべての `DbContext`、connection、command、reader を dispose する。独立した schema check を直列化する無条件 pool reset を追加しないこと。 diff --git a/changelog.d/unreleased/+scale-trx-telemetry.internal.md b/changelog.d/unreleased/+scale-trx-telemetry.internal.md new file mode 100644 index 000000000..258c0edf7 --- /dev/null +++ b/changelog.d/unreleased/+scale-trx-telemetry.internal.md @@ -0,0 +1,14 @@ +--- +category: internal +affected: + - tools/CodeIndex.TestTelemetry/TrxTelemetry.cs + - TESTING_GUIDE.md +--- + +## English + +- **TRX telemetry now handles the full repository suite efficiently** — The guarded per-file limit now accommodates current full-suite result files, while bounded slow/failure rankings use ordered insertion instead of sorting their full retained list for every test result. + +## 日本語 + +- **TRX telemetry が repository の full suite を効率的に扱えるようになりました** — file ごとの保護上限を現行 full-suite result に対応させ、上限付き slow/failure ranking は test result ごとの全 retained list 再 sort を ordered insertion に置き換えました。 diff --git a/changelog.d/unreleased/+streamline-release-validation.internal.md b/changelog.d/unreleased/+streamline-release-validation.internal.md new file mode 100644 index 000000000..d612d56c7 --- /dev/null +++ b/changelog.d/unreleased/+streamline-release-validation.internal.md @@ -0,0 +1,17 @@ +--- +category: internal +affected: + - .github/workflows/release.yml + - tests/CodeIndex.Tests/ReleaseWorkflowTests.cs + - tests/CodeIndex.Tests/PackagesLockTests.cs + - tests/CodeIndex.Tests/CiWorkflowTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Release validation now avoids repeating the full suite across native RIDs** — The release workflow runs the RID-independent net8 suite once on linux-x64, scopes publish-only lanes to the production restore graph, and exercises each natively runnable self-contained artifact with an index/status SQLite smoke test. + +## 日本語 + +- **release 検証で native RID ごとの full suite 反復を廃止しました** — release workflow は RID 非依存の net8 suite を linux-x64 で 1 回実行し、publish-only lane の restore を production graph に限定したうえで、native 実行可能な各 self-contained artifact に index/status の SQLite smoke test を通します。 diff --git a/tests/CodeIndex.Tests/CiWorkflowTests.cs b/tests/CodeIndex.Tests/CiWorkflowTests.cs index a60417354..9c54dfdf8 100644 --- a/tests/CodeIndex.Tests/CiWorkflowTests.cs +++ b/tests/CodeIndex.Tests/CiWorkflowTests.cs @@ -22,77 +22,57 @@ public void DotnetWorkflow_RunsTestsWithRunsettingsBlameRetryAndArtifacts() "--results-directory\", $resultsDirectory"); AssertContainsAll( workflow, - "include:\n" + - " - os: ubuntu-24.04\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: true\n" + - " collect_coverage: true\n" + - " test-shard: index-command\n" + - " test-filter: FullyQualifiedName~CodeIndex.Tests.IndexCommandRunnerTests\n" + - " - os: ubuntu-24.04\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: true\n" + - " test-shard: remaining\n" + - " test-filter: FullyQualifiedName!~CodeIndex.Tests.IndexCommandRunnerTests\n" + - " - os: ubuntu-24.04\n" + - " test-framework: net9.0\n" + - " sdk-versions: 9.0.301\n" + - " sdk-label: 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: false\n" + - " test-shard: full\n" + - " test-filter: ''\n" + - " - os: windows-2022\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: false\n" + - " test-shard: index-command\n" + - " test-filter: FullyQualifiedName~CodeIndex.Tests.IndexCommandRunnerTests\n" + - " - os: windows-2022\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: false\n" + - " test-shard: remaining\n" + - " test-filter: FullyQualifiedName!~CodeIndex.Tests.IndexCommandRunnerTests\n" + - " - os: macos-14\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: false\n" + - " test-shard: index-command\n" + - " test-filter: FullyQualifiedName~CodeIndex.Tests.IndexCommandRunnerTests\n" + - " - os: macos-14\n" + - " test-framework: net8.0\n" + - " sdk-versions: |\n" + - " 8.0.413\n" + - " 9.0.301\n" + - " sdk-label: 8.0.413 9.0.301\n" + - " primary_lane: false\n" + - " collect_coverage: false\n" + - " test-shard: remaining\n" + - " test-filter: FullyQualifiedName!~CodeIndex.Tests.IndexCommandRunnerTests", + "include:", "- name: Set up .NET SDK\n id: setup-dotnet\n continue-on-error: true\n uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0\n with:\n dotnet-version: ${{ matrix.sdk-versions }}", "- name: Retry .NET SDK setup\n if: steps.setup-dotnet.outcome == 'failure'\n uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0\n with:\n dotnet-version: ${{ matrix.sdk-versions }}"); + AssertWorkflowLane( + workflow, + "ubuntu-24.04", + "net8.0", + ["8.0.413", "9.0.301"], + primaryLane: true, + collectCoverage: true, + testShard: "index-command", + testFilter: "FullyQualifiedName~CodeIndex.Tests.IndexCommandRunnerTests"); + AssertWorkflowLane( + workflow, + "ubuntu-24.04", + "net8.0", + ["8.0.413", "9.0.301"], + primaryLane: false, + collectCoverage: true, + testShard: "remaining", + testFilter: "FullyQualifiedName!~CodeIndex.Tests.IndexCommandRunnerTests"); + AssertWorkflowLane( + workflow, + "ubuntu-24.04", + "net9.0", + ["9.0.301"], + primaryLane: false, + collectCoverage: false, + testShard: "full", + testFilter: "''"); + foreach (var os in new[] { "windows-2022", "macos-14" }) + { + AssertWorkflowLane( + workflow, + os, + "net8.0", + ["8.0.413", "9.0.301"], + primaryLane: false, + collectCoverage: false, + testShard: "index-command", + testFilter: "FullyQualifiedName~CodeIndex.Tests.IndexCommandRunnerTests"); + AssertWorkflowLane( + workflow, + os, + "net8.0", + ["8.0.413", "9.0.301"], + primaryLane: false, + collectCoverage: false, + testShard: "remaining", + testFilter: "FullyQualifiedName!~CodeIndex.Tests.IndexCommandRunnerTests"); + } AssertDoesNotContainAny( workflow, "function Invoke-TestRun"); @@ -267,14 +247,9 @@ public void WindowsTestHostSetup_SplitsTempAndSkipsExclusionsOnlyWhenDefenderIsU " if: runner.os == 'Windows'\n" + " shell: pwsh\n" + " run: ./.github/scripts/configure-windows-test-host.ps1 -Workspace \"${{ github.workspace }}\""; - const string expectedReleaseStep = - "- name: Configure Windows test host\n" + - " if: runner.os == 'Windows' && !matrix.cross_compile\n" + - " shell: pwsh\n" + - " run: ./.github/scripts/configure-windows-test-host.ps1 -Workspace \"${{ github.workspace }}\""; AssertContainsAll(dotnetWorkflow, expectedDotnetStep); - AssertContainsAll(releaseWorkflow, expectedReleaseStep); + AssertDoesNotContainAny(releaseWorkflow, "Configure Windows test host"); AssertDoesNotContainAny(dotnetWorkflow, "Add-MpPreference", "Get-MpPreference"); AssertDoesNotContainAny(releaseWorkflow, "Add-MpPreference", "Get-MpPreference"); AssertContainsAll( @@ -556,6 +531,40 @@ private static void AssertContainsAll(string text, params string[] expectedValue Assert.Contains(expected, text); } + private static void AssertWorkflowLane( + string workflow, + string os, + string framework, + string[] sdkVersions, + bool primaryLane, + bool collectCoverage, + string testShard, + string testFilter) + { + var lines = new List + { + $" - os: {os}", + $" test-framework: {framework}", + }; + if (sdkVersions.Length == 1) + { + lines.Add($" sdk-versions: {sdkVersions[0]}"); + } + else + { + lines.Add(" sdk-versions: |"); + lines.AddRange(sdkVersions.Select(static version => $" {version}")); + } + + lines.Add($" sdk-label: {string.Join(' ', sdkVersions)}"); + lines.Add($" primary_lane: {primaryLane.ToString().ToLowerInvariant()}"); + lines.Add($" collect_coverage: {collectCoverage.ToString().ToLowerInvariant()}"); + lines.Add($" test-shard: {testShard}"); + lines.Add($" test-filter: {testFilter}"); + + Assert.Contains(string.Join('\n', lines), workflow); + } + private static void AssertContainsAll(string text, StringComparison comparisonType, params string[] expectedValues) { foreach (var expected in expectedValues) diff --git a/tests/CodeIndex.Tests/ConsoleCaptureTests.cs b/tests/CodeIndex.Tests/ConsoleCaptureTests.cs index d4591c3f5..6acf7e64a 100644 --- a/tests/CodeIndex.Tests/ConsoleCaptureTests.cs +++ b/tests/CodeIndex.Tests/ConsoleCaptureTests.cs @@ -16,6 +16,7 @@ public void ConsoleSensitiveCollection_AssignsGlobalCaptureClassesAndDisablesPar typeof(DiffCommandHelpersTests), typeof(ExportImportCommandRunnerCancellationTests), typeof(LicensePolicyTests), + typeof(McpAuditLogTests), typeof(ProgramCliTests), typeof(SymbolExtractorTests), typeof(TestTelemetryTests), diff --git a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs index 80ff03e21..6284da8ec 100644 --- a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs @@ -9,72 +9,8 @@ namespace CodeIndex.Tests; /// Tests for `cdidx db` maintenance commands. /// `cdidx db` 保守コマンドのテスト。 /// -[Collection("SQLite pool sensitive")] -public class DbCommandRunnerTests +public class DbCommandRunnerParseTests { - private readonly JsonSerializerOptions _jsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - }; - - public static IEnumerable DirectSqliteModeArgs() - { - yield return new object[] { new[] { "--integrity-check" } }; - yield return new object[] { new[] { "integrity" } }; - yield return new object[] { new[] { "schema" } }; - yield return new object[] { new[] { "prune", "--dry-run" } }; - } - - private static void InitializeEmptyDb(string dbPath) - { - using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) - db.InitializeSchema(); - ReleaseSqlitePools(); - } - - private static void InitializeDbWithOrphans(string dbPath) - { - using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) - db.InitializeSchema(); - SeedOrphans(dbPath); - ReleaseSqlitePools(); - } - - private static void ReleaseSqlitePools() - => SqliteConnection.ClearAllPools(); - - private static void DeleteDbFile(string dbPath) - { - ReleaseSqlitePools(); - TestProjectHelper.DeleteFile(dbPath); - } - - private static void DeleteWorkDirectory(string root) - { - ReleaseSqlitePools(); - TestProjectHelper.DeleteDirectory(root); - } - - private static void CreateUnixFifo(string path) - { - var startInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = "mkfifo", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - startInfo.ArgumentList.Add(path); - - using var process = System.Diagnostics.Process.Start(startInfo) - ?? throw new InvalidOperationException("Failed to start mkfifo / mkfifo の起動に失敗"); - var stderr = process.StandardError.ReadToEnd(); - process.WaitForExit(); - if (process.ExitCode != 0) - throw new InvalidOperationException($"mkfifo failed: {stderr.Trim()}"); - } - [Fact] public void ParseArgs_IntegrityCheckFlagSetsFlag() { @@ -256,6 +192,73 @@ public void ParseArgs_RestoreBackupsPruneSetsKeep_Issue3833() Assert.Equal(3, options.RestoreBackupsKeep); Assert.Null(options.ParseError); } +} + +[Collection("SQLite pool sensitive")] +public class DbCommandRunnerTests +{ + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + public static IEnumerable DirectSqliteModeArgs() + { + yield return new object[] { new[] { "--integrity-check" } }; + yield return new object[] { new[] { "integrity" } }; + yield return new object[] { new[] { "schema" } }; + yield return new object[] { new[] { "prune", "--dry-run" } }; + } + + private static void InitializeEmptyDb(string dbPath) + { + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + db.InitializeSchema(); + ReleaseSqlitePools(); + } + + private static void InitializeDbWithOrphans(string dbPath) + { + using (var db = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + db.InitializeSchema(); + SeedOrphans(dbPath); + ReleaseSqlitePools(); + } + + private static void ReleaseSqlitePools() + => SqliteConnection.ClearAllPools(); + + private static void DeleteDbFile(string dbPath) + { + ReleaseSqlitePools(); + TestProjectHelper.DeleteFile(dbPath); + } + + private static void DeleteWorkDirectory(string root) + { + ReleaseSqlitePools(); + TestProjectHelper.DeleteDirectory(root); + } + + private static void CreateUnixFifo(string path) + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "mkfifo", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + startInfo.ArgumentList.Add(path); + + using var process = System.Diagnostics.Process.Start(startInfo) + ?? throw new InvalidOperationException("Failed to start mkfifo / mkfifo の起動に失敗"); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + throw new InvalidOperationException($"mkfifo failed: {stderr.Trim()}"); + } [Fact] public void Run_WithoutModeFlag_ReturnsUsageError() diff --git a/tests/CodeIndex.Tests/McpAuditLogTests.cs b/tests/CodeIndex.Tests/McpAuditLogTests.cs index 0b35356a7..96946f3d9 100644 --- a/tests/CodeIndex.Tests/McpAuditLogTests.cs +++ b/tests/CodeIndex.Tests/McpAuditLogTests.cs @@ -13,6 +13,7 @@ namespace CodeIndex.Tests; /// MCP audit ログ配線の統合テスト (#1562)。実際の AuditLogSink を組み込んだ McpServer を駆動し、 /// ディスク上の JSONL レコードがワイヤーレスポンスと一致することを確認する。 /// +[Collection("Console sensitive")] public class McpAuditLogTests : IDisposable { private readonly string _projectRoot; diff --git a/tests/CodeIndex.Tests/MetricsSinkTests.cs b/tests/CodeIndex.Tests/MetricsSinkTests.cs index a20e20b93..5fada4d5b 100644 --- a/tests/CodeIndex.Tests/MetricsSinkTests.cs +++ b/tests/CodeIndex.Tests/MetricsSinkTests.cs @@ -5,8 +5,7 @@ namespace CodeIndex.Tests; -[Collection("SQLite pool sensitive")] -public class MetricsSinkTests +public class MetricsSinkArgumentTests { [Fact] public void TryConsumeMetricsFlag_StripsSeparatedFormAndReturnsPath() @@ -72,6 +71,27 @@ public void TryParseLanguageFromArgs_ReturnsValueWhenPresent() Assert.Null(ProgramRunner.TryParseLanguageFromArgs(["search", "--", "--lang=csharp"])); } + [Theory] + [InlineData(1, 100)] + [InlineData(2, 200)] + [InlineData(3, 400)] + [InlineData(8, 12_800)] + [InlineData(9, 25_600)] + [InlineData(10, 30_000)] + [InlineData(21, 30_000)] + public void CalculateRetryDelay_GrowsExponentiallyAndCapsAtThirtySeconds_Issue4552( + int consecutiveFailureCount, + int expectedMilliseconds) + { + Assert.Equal( + TimeSpan.FromMilliseconds(expectedMilliseconds), + MetricsSink.Session.CalculateRetryDelay(consecutiveFailureCount)); + } +} + +[Collection("SQLite pool sensitive")] +public class MetricsSinkTests +{ [Fact] public void Run_WithMetricsFlag_AppendsJsonlRecordForEachInvocation() { @@ -285,23 +305,6 @@ public void Record_RuntimeFailuresBackOffThenRecoverAndWarnOnce_Issue4552() } } - [Theory] - [InlineData(1, 100)] - [InlineData(2, 200)] - [InlineData(3, 400)] - [InlineData(8, 12_800)] - [InlineData(9, 25_600)] - [InlineData(10, 30_000)] - [InlineData(21, 30_000)] - public void CalculateRetryDelay_GrowsExponentiallyAndCapsAtThirtySeconds_Issue4552( - int consecutiveFailureCount, - int expectedMilliseconds) - { - Assert.Equal( - TimeSpan.FromMilliseconds(expectedMilliseconds), - MetricsSink.Session.CalculateRetryDelay(consecutiveFailureCount)); - } - [Fact] public void Record_FullQueueDropsWithoutBlockingAndBatchesQueuedEvents_Issue4552() { diff --git a/tests/CodeIndex.Tests/PackagesLockTests.cs b/tests/CodeIndex.Tests/PackagesLockTests.cs index 3b2ac8911..a03ddf0be 100644 --- a/tests/CodeIndex.Tests/PackagesLockTests.cs +++ b/tests/CodeIndex.Tests/PackagesLockTests.cs @@ -137,8 +137,8 @@ public void RestoreSurfaces_UseLockedModeExactCacheKeysAndDockerRidRestore() StringComparison.Ordinal); Assert.DoesNotContain("Cache Stryker tool and NuGet packages", mutationWorkflow, StringComparison.Ordinal); Assert.Contains( - "- name: Cache native NuGet packages\n" + - " if: ${{ !matrix.cross_compile }}", + "- name: Cache test-lane NuGet packages\n" + + " if: matrix.run_tests", releaseWorkflow, StringComparison.Ordinal); Assert.Contains( @@ -146,12 +146,12 @@ public void RestoreSurfaces_UseLockedModeExactCacheKeysAndDockerRidRestore() releaseWorkflow, StringComparison.Ordinal); Assert.Contains( - "- name: Cache cross-compile NuGet packages\n" + - " if: matrix.cross_compile", + "- name: Cache publish-only NuGet packages\n" + + " if: ${{ !matrix.run_tests }}", releaseWorkflow, StringComparison.Ordinal); Assert.Contains( - "key: ${{ runner.os }}-release-cross-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json') }}", + "key: ${{ runner.os }}-release-publish-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json') }}", releaseWorkflow, StringComparison.Ordinal); Assert.Contains( diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index d00bd194f..5bacf3884 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -16,8 +16,7 @@ namespace CodeIndex.Tests; -[Collection("SQLite pool sensitive")] -public class ProgramRunnerTests +public class ProgramRunnerProjectPathTests { [Theory] [InlineData("foo.cs", false)] @@ -48,7 +47,11 @@ public void IsProjectPathArg_WindowsPathForms_ReturnTrueOnWindows(string arg) Assert.True(ProgramRunner.IsProjectPathArg(arg)); } +} +[Collection("SQLite pool sensitive")] +public class ProgramRunnerTests +{ [Fact] public void ResolveMcpHttpBearerTokenFromEnvironment_HttpTokenWinsThenFallsBackToGeneric() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerAuditSarifIssue4903Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerAuditSarifIssue4903Tests.cs index 28c658297..833c4c399 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerAuditSarifIssue4903Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerAuditSarifIssue4903Tests.cs @@ -1,10 +1,11 @@ using System.Text; using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public class QueryCommandRunnerAuditSarifIssue4903Tests { [Fact] public void RunSearch_RecipeSarifMaxJsonBytesUsesExactUtf8BudgetAndWholeResults_Issue4903() @@ -42,7 +43,7 @@ public void Run(Exception ex) "--limit", "20", ]; var (unboundedExitCode, unboundedStdout, unboundedStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(args, _jsonOptions)); + () => QueryCommandRunner.RunSearch(args, JsonOptions)); var exactBudget = Encoding.UTF8.GetByteCount(unboundedStdout); Assert.Equal(CommandExitCodes.Success, unboundedExitCode); @@ -51,7 +52,7 @@ public void Run(Exception ex) var exactArgs = args.Concat(["--max-json-bytes", exactBudget.ToString()]).ToArray(); var (exactExitCode, exactStdout, exactStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(exactArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(exactArgs, JsonOptions)); Assert.Equal(CommandExitCodes.Success, exactExitCode); Assert.Equal(string.Empty, exactStderr); @@ -61,7 +62,7 @@ public void Run(Exception ex) var boundedBudget = exactBudget - 1; var boundedArgs = args.Concat(["--max-json-bytes", boundedBudget.ToString()]).ToArray(); var (boundedExitCode, boundedStdout, boundedStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(boundedArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(boundedArgs, JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, boundedExitCode); Assert.Equal(string.Empty, boundedStderr); @@ -114,7 +115,7 @@ public void Run(Exception ex) var allowPartialArgs = boundedArgs.Concat(["--allow-partial"]).ToArray(); var (allowedExitCode, allowedStdout, allowedStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(allowPartialArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(allowPartialArgs, JsonOptions)); Assert.Equal(CommandExitCodes.Success, allowedExitCode); Assert.Equal(string.Empty, allowedStderr); @@ -157,7 +158,7 @@ public void RunSearch_RecipeSarifMaxJsonBytesOmitsOversizedResultAndPreflightsMi ]; var (exitCode, stdout, stderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(args, _jsonOptions)); + () => QueryCommandRunner.RunSearch(args, JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, exitCode); Assert.Equal(string.Empty, stderr); @@ -196,7 +197,7 @@ public void RunSearch_RecipeSarifMaxJsonBytesOmitsOversizedResultAndPreflightsMi .Append("1") .ToArray(); var (tooSmallExitCode, tooSmallStdout, tooSmallStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(tooSmallArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(tooSmallArgs, JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, tooSmallExitCode); Assert.Equal(string.Empty, tooSmallStdout); @@ -259,7 +260,7 @@ public sealed class Diagnostic{{index:D2}} "--limit", "2", ]; var (firstPageExitCode, firstPageStdout, firstPageStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(firstPageArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(firstPageArgs, JsonOptions)); Assert.Equal(CommandExitCodes.Success, firstPageExitCode); Assert.Equal(string.Empty, firstPageStderr); @@ -273,7 +274,7 @@ public sealed class Diagnostic{{index:D2}} var secondPageArgs = firstPageArgs.Concat(["--cursor", activeCursor!]).ToArray(); var (secondPageExitCode, secondPageStdout, secondPageStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(secondPageArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(secondPageArgs, JsonOptions)); Assert.Equal(CommandExitCodes.Success, secondPageExitCode); Assert.Equal(string.Empty, secondPageStderr); var boundedBudget = Encoding.UTF8.GetByteCount(secondPageStdout) - 1; @@ -282,7 +283,7 @@ public sealed class Diagnostic{{index:D2}} .Concat(["--max-json-bytes", boundedBudget.ToString()]) .ToArray(); var (boundedExitCode, boundedStdout, boundedStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(boundedArgs, _jsonOptions)); + () => QueryCommandRunner.RunSearch(boundedArgs, JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, boundedExitCode); Assert.Equal(string.Empty, boundedStderr); @@ -325,7 +326,7 @@ public void RunSearch_EmptyRecipeSarifRequiresExactMinimumWithoutPartialJson_Iss "--limit", "10", ]; var (unboundedExitCode, unboundedStdout, unboundedStderr) = CaptureConsole( - () => QueryCommandRunner.RunSearch(args, _jsonOptions)); + () => QueryCommandRunner.RunSearch(args, JsonOptions)); var exactBudget = Encoding.UTF8.GetByteCount(unboundedStdout); Assert.Equal(CommandExitCodes.Success, unboundedExitCode); @@ -335,7 +336,7 @@ public void RunSearch_EmptyRecipeSarifRequiresExactMinimumWithoutPartialJson_Iss var (exactExitCode, exactStdout, exactStderr) = CaptureConsole( () => QueryCommandRunner.RunSearch( args.Concat(["--max-json-bytes", exactBudget.ToString()]).ToArray(), - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exactExitCode); Assert.Equal(string.Empty, exactStderr); Assert.Equal(unboundedStdout, exactStdout); @@ -343,7 +344,7 @@ public void RunSearch_EmptyRecipeSarifRequiresExactMinimumWithoutPartialJson_Iss var (underExitCode, underStdout, underStderr) = CaptureConsole( () => QueryCommandRunner.RunSearch( args.Concat(["--max-json-bytes", (exactBudget - 1).ToString()]).ToArray(), - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, underExitCode); Assert.Equal(string.Empty, underStdout); Assert.Contains( @@ -383,7 +384,7 @@ public void RunSearch_AdHocSarifMaxJsonBytesRemainsRejected_Issue4903() var (exitCode, stdout, stderr) = CaptureConsole( () => QueryCommandRunner.RunSearch( ["Needle", "--db", dbPath, "--format", "sarif", "--max-json-bytes", "4000"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stdout); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerBatchIssue4723Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerBatchIssue4723Tests.cs index 35ce01a9e..f69314b98 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerBatchIssue4723Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerBatchIssue4723Tests.cs @@ -1,8 +1,10 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +[Collection("Console sensitive")] +public class QueryCommandRunnerBatchParsingTests { [Fact] public void RunBatch_AcceptsStructuredCommandsAndValidatesTheirShape_Issue4723() @@ -19,7 +21,7 @@ public void RunBatch_AcceptsStructuredCommandsAndValidatesTheirShape_Issue4723() var (exitCode, stdout, stderr) = CaptureConsoleWithInput( input, - () => QueryCommandRunner.RunBatch(["--db", dbPath, "--json-summary"], _jsonOptions)); + () => QueryCommandRunner.RunBatch(["--db", dbPath, "--json-summary"], JsonOptions)); var lines = ParseJsonLines(stdout); try { @@ -76,7 +78,7 @@ public void RunBatch_ConfigurableBudgetsUseEffectiveValuesAndRejectUnsafeValues_ "--max-input-lines", "2", "--max-output-chars=8192", ], - _jsonOptions)); + JsonOptions)); var lines = ParseJsonLines(stdout); try { @@ -95,22 +97,22 @@ public void RunBatch_ConfigurableBudgetsUseEffectiveValuesAndRejectUnsafeValues_ var (invalidExitCode, _, invalidStderr) = CaptureConsole(() => QueryCommandRunner.RunBatch( ["--db", dbPath, "--json-summary", "--parallel", (QueryCommandRunner.BatchMaxParallelism + 1).ToString()], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, invalidExitCode); Assert.Contains($"from 1 to {QueryCommandRunner.BatchMaxParallelism}", invalidStderr); var (parallelWithoutSummaryExitCode, _, parallelWithoutSummaryStderr) = CaptureConsole( - () => QueryCommandRunner.RunBatch(["--db", dbPath, "--parallel", "2"], _jsonOptions)); + () => QueryCommandRunner.RunBatch(["--db", dbPath, "--parallel", "2"], JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, parallelWithoutSummaryExitCode); Assert.Contains("--parallel requires --json-summary", parallelWithoutSummaryStderr); var (defaultParallelWithoutSummaryExitCode, _, defaultParallelWithoutSummaryStderr) = CaptureConsole( - () => QueryCommandRunner.RunBatch(["--db", dbPath, "--parallel", "1"], _jsonOptions)); + () => QueryCommandRunner.RunBatch(["--db", dbPath, "--parallel", "1"], JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, defaultParallelWithoutSummaryExitCode); Assert.Contains("--parallel requires --json-summary", defaultParallelWithoutSummaryStderr); var (outputWithoutSummaryExitCode, _, outputWithoutSummaryStderr) = CaptureConsole( - () => QueryCommandRunner.RunBatch(["--db", dbPath, "--max-output-chars", "8192"], _jsonOptions)); + () => QueryCommandRunner.RunBatch(["--db", dbPath, "--max-output-chars", "8192"], JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, outputWithoutSummaryExitCode); Assert.Contains("--max-output-chars requires --json-summary", outputWithoutSummaryStderr); @@ -120,7 +122,7 @@ public void RunBatch_ConfigurableBudgetsUseEffectiveValuesAndRejectUnsafeValues_ "--db", dbPath, "--max-output-chars", QueryCommandRunner.BatchDefaultTotalOutputChars.ToString(), ], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, defaultOutputWithoutSummaryExitCode); Assert.Contains("--max-output-chars requires --json-summary", defaultOutputWithoutSummaryStderr); @@ -131,7 +133,7 @@ public void RunBatch_ConfigurableBudgetsUseEffectiveValuesAndRejectUnsafeValues_ "--json-summary", "--max-input-lines", (QueryCommandRunner.BatchMaxInputLines + 1).ToString(), ], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, inputAboveMaximumExitCode); Assert.Contains($"from 1 to {QueryCommandRunner.BatchMaxInputLines}", inputAboveMaximumStderr); @@ -142,13 +144,16 @@ public void RunBatch_ConfigurableBudgetsUseEffectiveValuesAndRejectUnsafeValues_ "--json-summary", "--max-output-chars", (QueryCommandRunner.BatchMaxTotalOutputChars + 1).ToString(), ], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, outputAboveMaximumExitCode); Assert.Contains( $"from {QueryCommandRunner.BatchMinTotalOutputChars} to {QueryCommandRunner.BatchMaxTotalOutputChars}", outputAboveMaximumStderr); } +} +public partial class QueryCommandRunnerTests +{ [Fact] public void RunBatch_ParallelReadsOverlapButEmitInInputOrderAndIsolateFailures_Issue4723() { @@ -178,7 +183,7 @@ public void RunBatch_ParallelReadsOverlapButEmitInInputOrderAndIsolateFailures_I input, () => QueryCommandRunner.RunBatch( ["--db", dbPath, "--json-summary", "--parallel", "3"], - _jsonOptions)); + JsonOptions)); var lines = ParseJsonLines(batchStdout); try { @@ -234,7 +239,7 @@ public async Task RunBatch_ParallelStreamsFirstResultBeforeMoreInputOrEof_Issue4 using var capture = ConsoleCapture.Start(stdout, stderr, input); return QueryCommandRunner.RunBatch( ["--db", dbPath, "--json-summary", "--parallel", "2"], - _jsonOptions, + JsonOptions, cancellationToken: cancellation.Token); }); input.WriteLine("""{"command":"languages","args":["--format","count"]}"""); @@ -296,7 +301,7 @@ public void RunBatch_ParallelFailureExitCodeFollowsInputOrder_Issue4723() input, () => QueryCommandRunner.RunBatch( ["--db", dbPath, "--json-summary", "--parallel", "2"], - _jsonOptions)); + JsonOptions)); var lines = ParseJsonLines(stdout); try { @@ -331,7 +336,7 @@ public void RunBatch_ParallelReadsSerializeCancellationAndRestoreConsole_Issues4 """{"command":"recipes","args":["--json"]}""" + "\n", () => QueryCommandRunner.RunBatch( ["--db", dbPath, "--json-summary", "--parallel", "2"], - _jsonOptions, + JsonOptions, cancellationToken: cancellation.Token)); Assert.True(cancellation.IsCancellationRequested); Assert.Equal(CommandExitCodes.CancelledBySignal, exitCode); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs index c166cd8fb..e7eafb5c5 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerBoundsTests.cs @@ -2,7 +2,7 @@ namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerBoundsTests { [Theory] [InlineData("--visibility")] diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs index b5c42600a..7c90d8ba4 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFilesTests.cs @@ -7,6 +7,7 @@ using CodeIndex.Indexer.Hooks; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4350Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4350Tests.cs index c42ee45c0..9af0fad48 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4350Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4350Tests.cs @@ -1,8 +1,9 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerFindIssue4350Tests { [Fact] public void RunFind_AllScopeLineScanLimitControlsCountJson_Issue4350() @@ -15,7 +16,7 @@ public void RunFind_AllScopeLineScanLimitControlsCountJson_Issue4350() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--count", "--line-scan-limit", "1"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); var json = document.RootElement; @@ -47,7 +48,7 @@ public void RunFind_LineScanLimitRejectsInvalidBounds_Issue4350(string value) { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["needle", "--all", "--line-scan-limit", value], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("--line-scan-limit", stderr); @@ -58,7 +59,7 @@ public void RunFind_LineScanLimitRequiresAll_Issue4350() { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["needle", "--path", "src/app.txt", "--line-scan-limit", "1000"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("--line-scan-limit is only supported with find --all", stderr); @@ -72,7 +73,7 @@ public void RunFind_CompactRejectsContextFlags_Issue4350(string flag, string val { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["needle", "--path", "src/app.txt", "--format", "compact", flag, value], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("find --format compact does not include snippets", stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4578Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4578Tests.cs index 017e75886..55cb7b443 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4578Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4578Tests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public class QueryCommandRunnerFindIssue4578Tests { [Fact] public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_Issue4578() @@ -16,7 +17,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (rowExitCode, rowStdout, rowStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--line-scan-limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, rowExitCode); Assert.Equal(string.Empty, rowStderr); @@ -29,7 +30,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (allowedRowExitCode, allowedRowStdout, allowedRowStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--line-scan-limit", "1", "--allow-partial"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, allowedRowExitCode); Assert.Equal(string.Empty, allowedRowStderr); @@ -38,7 +39,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (countExitCode, countStdout, countStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--count", "--line-scan-limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, countExitCode); Assert.Equal(string.Empty, countStderr); @@ -47,7 +48,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (allowedCountExitCode, allowedCountStdout, allowedCountStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--count", "--line-scan-limit", "1", "--allow-partial"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, allowedCountExitCode); Assert.Equal(string.Empty, allowedCountStderr); @@ -56,7 +57,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (completeExitCode, completeStdout, completeStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, completeExitCode); Assert.Equal(string.Empty, completeStderr); @@ -72,7 +73,7 @@ public void RunFind_AllScopeRowsAndCountsExposeTerminalScanStateAndPartialOptIn_ var (limitedExitCode, limitedStdout, limitedStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--json", "--limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, limitedExitCode); Assert.Equal(string.Empty, limitedStderr); @@ -119,7 +120,7 @@ public void RunFind_AllScopeRejectsRowFormatsThatCannotCarryScanMetadata_Issue45 var args = new List { "alpha", "--db", dbPath, "--all" }; args.AddRange(formatArgs); var (exitCode, stdout, stderr) = CaptureConsole(() => - QueryCommandRunner.RunFind([.. args], _jsonOptions)); + QueryCommandRunner.RunFind([.. args], JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stdout); @@ -129,7 +130,7 @@ public void RunFind_AllScopeRejectsRowFormatsThatCannotCarryScanMetadata_Issue45 var (normalizedExitCode, normalizedStdout, normalizedStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--format", "text", "--json"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, normalizedExitCode); Assert.Equal(string.Empty, normalizedStderr); @@ -154,7 +155,7 @@ public void RunFind_AllScopeJsonEnvelopeKeepsTerminalOutOfRows_Issue4578() var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( ["find", "alpha", "--db", dbPath, "--all", "--json-envelope", "--line-scan-limit", "1"], - _jsonOptions, + JsonOptions, "test")); Assert.Equal(CommandExitCodes.PartialResult, exitCode); @@ -172,7 +173,7 @@ public void RunFind_AllScopeJsonEnvelopeKeepsTerminalOutOfRows_Issue4578() var (countExitCode, countStdout, countStderr) = CaptureConsole(() => ProgramRunner.Run( ["find", "alpha", "--db", dbPath, "--all", "--count", "--json-envelope", "--line-scan-limit", "1"], - _jsonOptions, + JsonOptions, "test")); Assert.Equal(CommandExitCodes.PartialResult, countExitCode); @@ -201,7 +202,7 @@ public void RunFind_AllScopeTextSummaryCarriesAuthorityCapsAndRecovery_Issue4578 var (rowExitCode, _, rowStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--line-scan-limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, rowExitCode); Assert.Contains("candidate_file_limit=", rowStderr, StringComparison.Ordinal); @@ -213,7 +214,7 @@ public void RunFind_AllScopeTextSummaryCarriesAuthorityCapsAndRecovery_Issue4578 var (countExitCode, _, countStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--count", "--line-scan-limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.PartialResult, countExitCode); Assert.Contains("authoritative_count=false", countStderr, StringComparison.Ordinal); @@ -222,7 +223,7 @@ public void RunFind_AllScopeTextSummaryCarriesAuthorityCapsAndRecovery_Issue4578 var (completeExitCode, _, completeStderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["alpha", "--db", dbPath, "--all", "--limit", "10"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, completeExitCode); Assert.Contains("scan_complete=true", completeStderr, StringComparison.Ordinal); @@ -269,4 +270,11 @@ private static void AssertPartialFindTerminal(JsonElement json, bool countMode) Assert.Equal(20, json.GetProperty("applied_limit").GetInt32()); } } + + private static JsonDocument ParseLastNdjsonRecord(string stdout) + { + var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); + Assert.NotEmpty(lines); + return JsonDocument.Parse(lines[^1]); + } } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4621Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4621Tests.cs index 9a1ce916e..867ef35c7 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4621Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFindIssue4621Tests.cs @@ -1,10 +1,11 @@ using System.Text.Json; using CodeIndex.Cli; using Xunit; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerFindIssue4621Tests { [Theory] [InlineData(new[] { "--context", "2" }, 2, 2)] @@ -30,7 +31,7 @@ public void RunFind_ContextIsSymmetricAndExplicitSidesWinRegardlessOfOrder_Issue { "Issue4621Needle", "--db", dbPath, "--path", "src/context.cs", "--json", }.Concat(contextArgs).ToArray(); - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind(args, _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind(args, JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); @@ -39,29 +40,6 @@ public void RunFind_ContextIsSymmetricAndExplicitSidesWinRegardlessOfOrder_Issue Assert.Equal(5 + expectedAfter, document.RootElement.GetProperty("end_line").GetInt32()); } - [Fact] - public void RunBatch_FindAcceptsContextAndPreservesSymmetricWindow_Issue4621() - { - using var project = TestProjectHelper.CreateTempProjectScope("cdidx_batch_find_context_4621"); - var dbPath = TestProjectHelper.CreateProjectDb(project.Root); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/context.cs", - "csharp", - "line1\nline2\nIssue4621BatchNeedle\nline4\nline5\n"); - var input = "[\"find\",\"Issue4621BatchNeedle\",\"--path\",\"src/context.cs\",\"--context\",\"1\",\"--json\"]\n"; - - var (exitCode, stdout, stderr) = CaptureConsoleWithInput( - input, - () => QueryCommandRunner.RunBatch(["--db", dbPath], _jsonOptions)); - - Assert.Equal(CommandExitCodes.Success, exitCode); - Assert.Equal(string.Empty, stderr); - using var document = JsonDocument.Parse(stdout.Trim().Split('\n')[0]); - Assert.Equal(2, document.RootElement.GetProperty("start_line").GetInt32()); - Assert.Equal(4, document.RootElement.GetProperty("end_line").GetInt32()); - } - [Fact] public void RunFind_OptionShapedLiteralQueryDoesNotEnableContext_Issue4621() { @@ -75,7 +53,7 @@ public void RunFind_OptionShapedLiteralQueryDoesNotEnableContext_Issue4621() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["--db", dbPath, "--path", "src/context.cs", "--json", "--", "--context=2"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); @@ -91,7 +69,7 @@ public void RunFind_ContextRejectsValuesAboveDocumentedLimit_Issue4621(string va { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["needle", "--path", "src/**", "--context", value], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("--context", stderr, StringComparison.Ordinal); @@ -104,7 +82,7 @@ public void RunFind_CompactContextErrorNamesSymmetricFlag_Issue4621() { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunFind( ["needle", "--path", "src/**", "--format", "compact", "--context", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("--context", stderr, StringComparison.Ordinal); @@ -126,3 +104,30 @@ public void FindContextAppearsInSharedHelpAndEveryCompletion_Issue4621() Assert.Contains("--context", ConsoleCompletionRenderer.GetCompletionScript("powershell"), StringComparison.Ordinal); } } + +[Collection("Console sensitive")] +public sealed class QueryCommandRunnerFindBatchIssue4621Tests +{ + [Fact] + public void RunBatch_FindAcceptsContextAndPreservesSymmetricWindow_Issue4621() + { + using var project = TestProjectHelper.CreateTempProjectScope("cdidx_batch_find_context_4621"); + var dbPath = TestProjectHelper.CreateProjectDb(project.Root); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/context.cs", + "csharp", + "line1\nline2\nIssue4621BatchNeedle\nline4\nline5\n"); + var input = "[\"find\",\"Issue4621BatchNeedle\",\"--path\",\"src/context.cs\",\"--context\",\"1\",\"--json\"]\n"; + + var (exitCode, stdout, stderr) = CaptureConsoleWithInput( + input, + () => QueryCommandRunner.RunBatch(["--db", dbPath], JsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = JsonDocument.Parse(stdout.Trim().Split('\n')[0]); + Assert.Equal(2, document.RootElement.GetProperty("start_line").GetInt32()); + Assert.Equal(4, document.RootElement.GetProperty("end_line").GetInt32()); + } +} diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerFixtureTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerFixtureTests.cs index b07fc1273..d6c854841 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerFixtureTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerFixtureTests.cs @@ -1,8 +1,9 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerFixtureTests { [Fact] public void RunSearch_ExcludeFixturesKeepsProductionFilesWithTestSubstringNames_Issue3450() @@ -16,7 +17,7 @@ public void RunSearch_ExcludeFixturesKeepsProductionFilesWithTestSubstringNames_ var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["ProdFixtureNeedle", "--db", dbPath, "--exact-substring", "--json=array", "--exclude-fixtures"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4830Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4830Tests.cs index 09216152e..74ea49614 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4830Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4830Tests.cs @@ -1,8 +1,9 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerIssue4830Tests { [Fact] public void RunSymbols_CSharpStaticLambdaCorpusHasNoPhantomDeclarations_Issue4830() @@ -35,7 +36,7 @@ public static string Fold(string value) var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( ["--db", dbPath, "--json", "--lang", "csharp"], - _jsonOptions)); + JsonOptions)); var rows = ParseJsonLines(stdout); var symbols = rows .Select(row => ( diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4831Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4831Tests.cs index 40a32b69a..a4c0cd98d 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4831Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4831Tests.cs @@ -1,8 +1,9 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerIssue4831Tests { [Fact] public void RunSymbols_CSharpExactNameRejectsDeclarationContinuationPhantoms_Issue4831() @@ -19,7 +20,7 @@ public void RunSymbols_CSharpExactNameRejectsDeclarationContinuationPhantoms_Iss var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( [projectRoot, "--json", "--quiet"], - _jsonOptions)); + JsonOptions)); var (definitionExitCode, definitionStdout, definitionStderr) = CaptureConsole( () => QueryCommandRunner.RunSymbols( [ @@ -29,15 +30,15 @@ public void RunSymbols_CSharpExactNameRejectsDeclarationContinuationPhantoms_Iss "--exact-name", "--lang", "csharp", ], - _jsonOptions)); + JsonOptions)); var (outExitCode, outStdout, outStderr) = CaptureConsole( () => QueryCommandRunner.RunSymbols( ["out", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], - _jsonOptions)); + JsonOptions)); var (paramsExitCode, paramsStdout, paramsStderr) = CaptureConsole( () => QueryCommandRunner.RunSymbols( ["params", "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"], - _jsonOptions)); + JsonOptions)); var definitionRows = ParseJsonLines(definitionStdout); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4833Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4833Tests.cs index 29c94e04c..b43dfe694 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerIssue4833Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerIssue4833Tests.cs @@ -1,10 +1,11 @@ using CodeIndex.Cli; using CodeIndex.Indexer; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerIssue4833Tests { [Fact] public void CSharpNamedArgumentLabels_DoNotCreateExactReferencesOrDependencies_Issue4833() @@ -91,7 +92,7 @@ private static void SinkQuery(object query, object other) var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( [projectRoot, "--json", "--quiet"], - _jsonOptions)); + JsonOptions)); var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); Assert.Equal(CommandExitCodes.Success, indexExitCode); @@ -107,7 +108,7 @@ private static void SinkQuery(object query, object other) "--kind", "type_reference", "--lang", "csharp", ], - _jsonOptions)); + JsonOptions)); using var labelDocument = ParseJsonOutput(labelStdout); Assert.Equal(CommandExitCodes.Success, labelExitCode); @@ -125,7 +126,7 @@ private static void SinkQuery(object query, object other) "--kind", "type_reference", "--lang", "csharp", ], - _jsonOptions)); + JsonOptions)); using var payloadDocument = ParseJsonOutput(payloadStdout); var payloadReference = payloadDocument.RootElement; @@ -185,7 +186,7 @@ AND reference.symbol_name IN ('payload', 'selector', 'query', 'other', 'Value') var (depsExitCode, depsStdout, depsStderr) = CaptureConsole( () => QueryCommandRunner.RunDeps( ["--db", dbPath, "--json", "--lang", "csharp", "--limit", "100"], - _jsonOptions)); + JsonOptions)); using var depsDocument = ParseJsonOutput(depsStdout); var dependencyEdges = depsDocument.RootElement.GetProperty("edges").EnumerateArray().ToArray(); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerJsonCompatibilityAliasTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerJsonCompatibilityAliasTests.cs index 27a9f45ec..719862426 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerJsonCompatibilityAliasTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerJsonCompatibilityAliasTests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerJsonCompatibilityAliasTests { [Fact] public void QueryFindCountJsonResult_DeprecatedAliasHasLifecycleRegistryAndStillSerializes_Issue4182() @@ -77,7 +78,7 @@ internal static class JsonCompatibilityAliasLifecycles var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["JsonCompatibilityAliasLifecycle", "--db", dbPath, "--source-only", "--origin", "code", "--json=array", "--limit", "10"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerJsonErrorIssue4564Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerJsonErrorIssue4564Tests.cs index 3ca0eb1c5..80b1203f1 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerJsonErrorIssue4564Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerJsonErrorIssue4564Tests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerJsonErrorIssue4564Tests { [Fact] public void JsonValidationAndLookupFailures_EmitVersionedErrorEnvelope_Issue4564() @@ -24,37 +25,37 @@ public void JsonValidationAndLookupFailures_EmitVersionedErrorEnvelope_Issue4564 "search validation", CommandExitCodes.UsageError, CommandErrorCodes.UsageError, - () => QueryCommandRunner.RunSearch(["--json"], _jsonOptions)), + () => QueryCommandRunner.RunSearch(["--json"], JsonOptions)), ( "find validation", CommandExitCodes.UsageError, CommandErrorCodes.UsageError, - () => QueryCommandRunner.RunFind(["--json"], _jsonOptions)), + () => QueryCommandRunner.RunFind(["--json"], JsonOptions)), ( "status mode validation", CommandExitCodes.UsageError, CommandErrorCodes.UsageError, - () => QueryCommandRunner.RunStatus(["--config", "--check", "--json"], _jsonOptions)), + () => QueryCommandRunner.RunStatus(["--config", "--check", "--json"], JsonOptions)), ( "goto not found", CommandExitCodes.NotFound, CommandErrorCodes.QueryNotFound, - () => QueryCommandRunner.RunGoto(["__NOT_FOUND_9fb0__", "--db", dbPath, "--json"], _jsonOptions)), + () => QueryCommandRunner.RunGoto(["__NOT_FOUND_9fb0__", "--db", dbPath, "--json"], JsonOptions)), ( "excerpt file not found", CommandExitCodes.NotFound, CommandErrorCodes.FileNotFound, - () => QueryCommandRunner.RunExcerpt(["NOPE.md", "--start", "1", "--db", dbPath, "--json"], _jsonOptions)), + () => QueryCommandRunner.RunExcerpt(["NOPE.md", "--start", "1", "--db", dbPath, "--json"], JsonOptions)), ( "excerpt line out of range", CommandExitCodes.InvalidArgument, CommandErrorCodes.LineOutOfRange, - () => QueryCommandRunner.RunExcerpt(["src/Sample.cs", "--start", "99", "--db", dbPath, "--json"], _jsonOptions)), + () => QueryCommandRunner.RunExcerpt(["src/Sample.cs", "--start", "99", "--db", dbPath, "--json"], JsonOptions)), ( "excerpt non-positive line", CommandExitCodes.InvalidArgument, CommandErrorCodes.LineOutOfRange, - () => QueryCommandRunner.RunExcerpt(["src/Sample.cs", "--start", "0", "--db", dbPath, "--json"], _jsonOptions)), + () => QueryCommandRunner.RunExcerpt(["src/Sample.cs", "--start", "0", "--db", dbPath, "--json"], JsonOptions)), }; foreach (var testCase in cases) diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerPathFilterTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerPathFilterTests.cs index 554b9d1ed..888f57690 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerPathFilterTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerPathFilterTests.cs @@ -1,8 +1,9 @@ using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerPathFilterTests { [Fact] public void RunFiles_PositionalGlobUsesPathFilterSemantics_Issue4565() @@ -27,7 +28,7 @@ public void RunFiles_PositionalGlobUsesPathFilterSemantics_Issue4565() { var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( ["--db", dbPath, "--json", testCase.Pattern, "--limit", "20"], - _jsonOptions)); + JsonOptions)); var paths = ParseJsonLines(stdout) .Select(document => document.RootElement.TryGetProperty("path", out var path) ? path.GetString() : null) @@ -79,7 +80,7 @@ public void RunFiles_PathFilterAnchorsTopLevelFile_Issue4163(string pathFilter) var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunFiles( ["--db", dbPath, "--json", "--path", pathFilter, "--limit", "20"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); Assert.Equal(CommandExitCodes.Success, exitCode); @@ -112,7 +113,7 @@ public void RunSymbols_PathFilterAnchorsTopLevelDirectory_Issue4163() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( ["--db", dbPath, "--json", "--path", "tools", "--limit", "20"], - _jsonOptions)); + JsonOptions)); var rows = ParseJsonLines(stdout).Select(document => document.RootElement).ToList(); var paths = rows.Select(row => row.GetProperty("path").GetString()).ToHashSet(StringComparer.Ordinal); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardIssue4349Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardIssue4349Tests.cs index 31d5fde35..1e40384bd 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardIssue4349Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardIssue4349Tests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerSearchGuardIssue4349Tests { [Fact] public void RunSearch_CountJsonGuardFiltersUseSearchResultUnitsAndExposeContext_Issue4349() @@ -108,7 +109,7 @@ public void Run() private JsonDocument RunSearchCountJson(params string[] args) { - var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch(args, _jsonOptions)); + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch(args, JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs index efa7e2507..601490880 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public class QueryCommandRunnerSearchHintTests { [Fact] public void RunSearch_ExactSubstringJsonOutputsLiteralHighlightMetadata() @@ -20,7 +21,7 @@ public void RunSearch_ExactSubstringJsonOutputsLiteralHighlightMetadata() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["CommandText = $", "--db", dbPath, "--json", "--exact-substring", "--snippet-lines", "2"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); var root = document.RootElement; @@ -67,7 +68,7 @@ public void RunSearch_RejectsRawFtsWithLiteralModesBeforeDatabaseDispatch_Issue4 { var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( testCase.Args, - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); if (testCase.Json) @@ -113,7 +114,7 @@ public void RunSearch_RawFtsJsonReportsLiteralHighlightGapMetadata_Issue3558() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["CommandText", "--db", dbPath, "--json", "--fts", "--snippet-lines", "3", "--max-line-width", "80"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); var root = document.RootElement; @@ -148,7 +149,7 @@ public void RunSearch_PunctuationHeavyTextSuggestsExactSubstring() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["CommandText = $", "--db", dbPath, "--limit", "1"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Contains("src/sql.cs", stdout); @@ -177,7 +178,7 @@ public void RunSearch_PunctuationHeavyJsonAddsExactSubstringHint() var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["CommandText = $", "--db", dbPath, "--json", "--limit", "1"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); var hint = document.RootElement.GetProperty("exact_substring_hint"); @@ -217,7 +218,7 @@ public void RunSearch_PunctuationHeavyJsonArrayAddsHintOnlyToFirstResult_Issue39 var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["ToArray()", "--db", dbPath, "--json=array", "--limit", "2"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); var rows = document.RootElement.EnumerateArray().ToArray(); @@ -255,7 +256,7 @@ public void RunSearch_PunctuationHeavyJsonArraySuppressesRankOnlyRows_Issue2821( var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( ["throw;", "--db", dbPath, "--json=array"], - _jsonOptions)); + JsonOptions)); using var document = ParseJsonOutput(stdout); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchIssue4906Tests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchIssue4906Tests.cs index acfc8fdcc..e6dade07d 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchIssue4906Tests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchIssue4906Tests.cs @@ -1,9 +1,10 @@ using System.Text.Json; using CodeIndex.Cli; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public class QueryCommandRunnerSearchIssue4906Tests { [Theory] [InlineData("Widget.*", "--regex")] @@ -15,7 +16,7 @@ public void RunSearch_ScanOnlyFlagsPointHumanUsersToFindWithoutExecuting_Issue49 var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( ["search", query, scanFlag], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -52,7 +53,7 @@ public void RunSearch_RegexJsonReturnsTypedShellSafeFindAlternative_Issue4906() "--max-line-width", "120", "--json", ], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -104,7 +105,7 @@ public void RunSearch_StructuredFormatDoesNotInventJsonFlag_Issue4906() var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( ["search", "TODO", "--regex", "--path", "src/**", "--format", "csv"], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -119,7 +120,7 @@ public void RunSearch_FormatCountPreservesStructuredFindOutput_Issue4906() var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run( ["search", "TODO", "--regex", "--format", "count"], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -155,7 +156,7 @@ public void RunSearch_OptionShapedQueryIsNotReinterpretedInFindAlternative_Issue foreach (var testCase in cases) { var (exitCode, stdout, stderr) = CaptureConsole(() => - ProgramRunner.Run(testCase.Args, _jsonOptions, "1.0.0-test")); + ProgramRunner.Run(testCase.Args, JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stderr); @@ -179,7 +180,7 @@ public void RunSearch_OptionShapedQueryIsNotReinterpretedInFindAlternative_Issue foreach (var args in consumedOptionCases) { var (exitCode, stdout, stderr) = CaptureConsole(() => - ProgramRunner.Run(args, _jsonOptions, "1.0.0-test")); + ProgramRunner.Run(args, JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stderr); @@ -227,7 +228,7 @@ public void RunSearch_UnmappableOrUnsafeScanRequestsExplainWhyWithoutCommand_Iss foreach (var testCase in cases) { var (exitCode, stdout, stderr) = CaptureConsole(() => - ProgramRunner.Run(testCase.Args, _jsonOptions, "1.0.0-test")); + ProgramRunner.Run(testCase.Args, JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stderr); @@ -259,7 +260,7 @@ public void RunSearch_FindAlternativeRejectsIncompatibleCompactSnippetOutput_Iss "search", "TODO", "--regex", "--path", "src/**", "--format", "compact", "--snippet-lines", "3", ], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -284,7 +285,7 @@ public void RunSearch_FindAlternativeHonorsJsonByteBudget_Issue4906() "search", "TODO", "--regex", "--path", "src/**", "--json", "--max-json-bytes", "200", ], - _jsonOptions, + JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); @@ -322,7 +323,7 @@ public void RunSearch_FindAlternativeRejectsFindValidationFailures_Issue4906() foreach (var testCase in cases) { var (exitCode, stdout, stderr) = CaptureConsole(() => - ProgramRunner.Run(testCase.Args, _jsonOptions, "1.0.0-test")); + ProgramRunner.Run(testCase.Args, JsonOptions, "1.0.0-test")); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs index 2ccfc7460..6488f03c8 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerStatusReadinessTests.cs @@ -3,10 +3,11 @@ using CodeIndex.Database; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; -public partial class QueryCommandRunnerTests +public sealed class QueryCommandRunnerStatusReadinessTests { [Fact] public void RunStatus_Json_ReportsHotspotFamilyReadinessDegradationRebuild_2959() @@ -17,7 +18,7 @@ public void RunStatus_Json_ReportsHotspotFamilyReadinessDegradationRebuild_2959( var dbPath = CreateHotspotFamilyFixtureDb(projectRoot, markHotspotFamilyReady: false); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( ["--db", dbPath, "--json"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); @@ -40,7 +41,7 @@ public void RunStatus_Explain_HotspotFamilyReadyRecommendsRebuild() { var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus( ["--explain", "hotspot_family_ready"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs index 1030c5615..eb5a0e82f 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs @@ -5,6 +5,7 @@ using CodeIndex.Diagnostics; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 5398c173e..34185f52c 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -7,6 +7,7 @@ using CodeIndex.Indexer.Extensibility; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; @@ -9158,52 +9159,6 @@ void InsertCaller(string path, string containerName, int count) } } - private static string CreateHotspotFamilyFixtureDb(string projectRoot, bool markHotspotFamilyReady) - { - var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Api.Part1.cs", - "csharp", - """ - public partial class Api - { - public void Run() { } - } - """); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Api.Part2.cs", - "csharp", - """ - public partial class Api - { - public void Run(int value) { } - } - """); - TestProjectHelper.InsertIndexedFile( - dbPath, - "src/Caller.cs", - "csharp", - """ - public class Caller - { - public void Call(Api api) - { - api.Run(); - api.Run(1); - } - } - """); - - using var db = new DbContext(DbOpenIntent.WriteIndex, dbPath); - var writer = new DbWriter(db.Connection); - writer.MarkGraphReady(); - if (markHotspotFamilyReady) - writer.MarkHotspotFamilyReady("csharp", "fixture-fingerprint"); - return dbPath; - } - private static string CreateLegacyDbWithoutIndexedAt(string projectRoot) { var dbPath = Path.Combine(projectRoot, "legacy.db"); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs index 9b4082bb2..479891093 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerValidateTests.cs @@ -4,6 +4,7 @@ using CodeIndex.Indexer; using CodeIndex.Models; using Microsoft.Data.Sqlite; +using static CodeIndex.Tests.QueryCommandTestSupport; namespace CodeIndex.Tests; @@ -28,12 +29,12 @@ public void RunValidate_IndexedIssueViewsShareSupersetFixture_Issues1582_2992_30 var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run( [projectRoot, "--db", dbPath, "--json", "--quiet"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.Success, indexExitCode); Assert.Equal(string.Empty, indexStderr); (int ExitCode, string Stdout, string Stderr) RunValidate(params string[] args) - => CaptureConsole(() => QueryCommandRunner.RunValidate(["--db", dbPath, .. args], _jsonOptions)); + => CaptureConsole(() => QueryCommandRunner.RunValidate(["--db", dbPath, .. args], JsonOptions)); // Both pagination aliases cap returned rows without changing the command contract (#2992). var (limitExitCode, limitStdout, limitStderr) = RunValidate("--json", "--limit", "1"); @@ -125,6 +126,12 @@ public void RunValidate_IndexedIssueViewsShareSupersetFixture_Issues1582_2992_30 Assert.Equal("bom", excludeIssues[0].GetProperty("kind").GetString()); } + private static void WriteUtf8BomFile(string projectRoot, string relativePath, string content) + => TestProjectHelper.WriteBinaryFile(projectRoot, relativePath, [0xEF, 0xBB, 0xBF, .. System.Text.Encoding.UTF8.GetBytes(content)]); +} + +public class QueryCommandRunnerValidationContractTests +{ [Theory] [InlineData("--limit")] [InlineData("--top")] @@ -132,7 +139,7 @@ public void RunValidate_InvalidLimitOrTopReturnsUsageError_Issue2992(string flag { var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( [flag, "nope"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Contains("requires an integer between 1 and 10000", stderr); @@ -147,7 +154,7 @@ public void RunValidate_InvalidSeverityJsonReturnsStructuredError_Issue3896() { var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunValidate( ["--severity", "invalid", "--json"], - _jsonOptions)); + JsonOptions)); Assert.Equal(CommandExitCodes.UsageError, exitCode); Assert.Equal(string.Empty, stderr); @@ -176,6 +183,4 @@ public void ValidateContent_SuppressesSolutionUtf8BomNoise_Issue3897() Assert.Contains(csharpIssues, issue => issue.Kind == "bom"); } - private static void WriteUtf8BomFile(string projectRoot, string relativePath, string content) - => TestProjectHelper.WriteBinaryFile(projectRoot, relativePath, [0xEF, 0xBB, 0xBF, .. System.Text.Encoding.UTF8.GetBytes(content)]); } diff --git a/tests/CodeIndex.Tests/QueryCommandTestSupport.cs b/tests/CodeIndex.Tests/QueryCommandTestSupport.cs new file mode 100644 index 000000000..d02be6570 --- /dev/null +++ b/tests/CodeIndex.Tests/QueryCommandTestSupport.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using CodeIndex.Database; + +namespace CodeIndex.Tests; + +internal static class QueryCommandTestSupport +{ + internal static JsonSerializerOptions JsonOptions { get; } = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + + internal static (int Result, string Stdout, string Stderr) CaptureConsole(Func action) + => ConsoleCapture.Capture(action); + + internal static (int Result, string Stdout, string Stderr) CaptureConsoleWithInput( + string input, + Func action) + { + using var reader = new StringReader(input); + using var capture = ConsoleCapture.StartWithInput(reader, captureOut: true, captureError: true); + return (action(), capture.Out!.ToString()!, capture.Error!.ToString()!); + } + + internal static JsonDocument ParseJsonOutput(string stdout) + { + var jsonLine = stdout + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Last(line => + { + using var document = JsonDocument.Parse(line); + return !IsJsonStreamDoneSentinel(document.RootElement); + }); + return JsonDocument.Parse(jsonLine); + } + + internal static List ParseJsonLines(string stdout) + => stdout + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(static line => JsonDocument.Parse(line)) + .Where(document => !IsJsonStreamDoneSentinel(document.RootElement)) + .ToList(); + + internal static string CreateHotspotFamilyFixtureDb(string projectRoot, bool markHotspotFamilyReady) + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Api.Part1.cs", + "csharp", + """ + public partial class Api + { + public void Run() { } + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Api.Part2.cs", + "csharp", + """ + public partial class Api + { + public void Run(int value) { } + } + """); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/Caller.cs", + "csharp", + """ + public class Caller + { + public void Call(Api api) + { + api.Run(); + api.Run(1); + } + } + """); + + using var db = new DbContext(DbOpenIntent.WriteIndex, dbPath); + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + if (markHotspotFamilyReady) + writer.MarkHotspotFamilyReady("csharp", "fixture-fingerprint"); + return dbPath; + } + + private static bool IsJsonStreamDoneSentinel(JsonElement element) + => element.ValueKind == JsonValueKind.Object + && element.TryGetProperty("done", out var done) + && done.ValueKind is JsonValueKind.True + && element.TryGetProperty("interrupted", out _) + && element.TryGetProperty("count", out _); +} diff --git a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs index 74f05337d..55e41c086 100644 --- a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs +++ b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs @@ -9,17 +9,28 @@ namespace CodeIndex.Tests; public partial class ReleaseWorkflowTests { [Fact] - public void ReleaseWorkflow_ScopesNativeValidationToNet8TestProject() + public void ReleaseWorkflow_RunsFullSuiteOnceAndSmokesEachNativeArtifact() { var workflow = ReadReleaseWorkflow(); AssertContainsAll( workflow, - "- name: Set up .NET SDKs\n if: ${{ !matrix.cross_compile }}", - "- name: Set up cross-compile .NET SDK\n if: matrix.cross_compile", + "rid: linux-x64\n cross_compile: false\n run_tests: true", + "rid: linux-arm64\n cross_compile: true\n run_tests: false", + "rid: win-x64\n cross_compile: false\n run_tests: false", + "rid: win-arm64\n cross_compile: true\n run_tests: false", + "rid: osx-arm64\n cross_compile: false\n run_tests: false", + "- name: Set up .NET SDKs\n if: matrix.run_tests", + "- name: Set up publish-only .NET SDK\n if: ${{ !matrix.run_tests }}", "dotnet-version: 9.0.301", - "- name: Build tests\n if: ${{ !matrix.cross_compile }}\n run: dotnet build tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-restore", - "- name: Test net8\n if: ${{ !matrix.cross_compile }}\n run: dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-build --no-restore --nologo"); + "- name: Build tests\n if: matrix.run_tests\n run: dotnet build tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-restore", + "- name: Test net8\n if: matrix.run_tests\n run: dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework net8.0 --no-build --no-restore --nologo", + "- name: Smoke-test native release artifact (Linux/macOS)\n if: runner.os != 'Windows' && !matrix.cross_compile", + "./publish/cdidx \"$smoke_root\" --db \"$smoke_root/.cdidx/codeindex.db\" --json", + "./publish/cdidx status --db \"$smoke_root/.cdidx/codeindex.db\" --json", + "- name: Smoke-test native release artifact (Windows)\n if: runner.os == 'Windows' && !matrix.cross_compile", + "& .\\publish\\cdidx.exe $smokeRoot --db (Join-Path $smokeRoot '.cdidx\\codeindex.db') --json", + "& .\\publish\\cdidx.exe status --db (Join-Path $smokeRoot '.cdidx\\codeindex.db') --json"); AssertDoesNotContainAny( workflow, "dotnet build CodeIndex.sln --configuration Release --no-restore", diff --git a/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs b/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs index c4d36c8a7..9c21edccc 100644 --- a/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs @@ -19,7 +19,7 @@ namespace CodeIndex.Tests; /// 生の引数が混入しないことを担保する。 /// [Collection("SQLite pool sensitive")] -public class ReportCommandRunnerTests +public partial class ReportCommandRunnerTests { private const UnixFileMode PermissionBits = UnixFileMode.UserRead | @@ -36,7 +36,10 @@ public class ReportCommandRunnerTests { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, }; +} +public class ReportCommandRunnerParseTests +{ [Fact] public void ParseArgs_OutputFlagCapturesValue() { @@ -135,7 +138,10 @@ public void ParseArgs_PositionalArgRecordsParseError() Assert.NotNull(options.ParseError); Assert.Contains("positional", options.ParseError); } +} +public partial class ReportCommandRunnerTests +{ [Fact] public void Run_MissingOutputFlag_ReturnsUsageError() { diff --git a/tools/CodeIndex.TestTelemetry/TrxTelemetry.cs b/tools/CodeIndex.TestTelemetry/TrxTelemetry.cs index 7db4bb74a..a94fd5e8a 100644 --- a/tools/CodeIndex.TestTelemetry/TrxTelemetry.cs +++ b/tools/CodeIndex.TestTelemetry/TrxTelemetry.cs @@ -9,7 +9,7 @@ public static class TrxTelemetry public const int MaxTrxFiles = 256; public const int MaxTraversalDirectories = 256; public const int MaxTraversalEntries = 4096; - public const long MaxTrxFileBytes = 16 * 1024 * 1024; + public const long MaxTrxFileBytes = 64L * 1024 * 1024; public static TrxTelemetrySummary Load(string resultsDirectory, int top) { @@ -286,9 +286,21 @@ private static IEnumerable ReadResults(string path) private static void AddTopResult(List results, TrxTestResult result, int limit) { - results.Add(result); - results.Sort(CompareByDurationDescendingThenName); + var low = 0; + var high = results.Count; + while (low < high) + { + var middle = low + ((high - low) / 2); + if (CompareByDurationDescendingThenName(result, results[middle]) < 0) + high = middle; + else + low = middle + 1; + } + + if (low >= limit) + return; + results.Insert(low, result); if (results.Count > limit) results.RemoveAt(limit); }