diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 36bcb9bd5..d99a3d4bb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -51,6 +51,7 @@ jobs: cache-dependency-path: | src/CodeIndex/packages.lock.json tests/CodeIndex.HookIsolationFixture/packages.lock.json + tests/CodeIndex.PostExtractionHookFixture/packages.lock.json tests/CodeIndex.PluginIsolationFixture/packages.lock.json tests/CodeIndex.Tests/packages.lock.json tools/CodeIndex.Changelog/packages.lock.json diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2306806f6..32d04c279 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -130,7 +130,7 @@ jobs: ~\AppData\Local\NuGet\packages # Locked restore keeps package inputs honest; avoid evicting the cache for project-only test edits. # locked restore がパッケージ入力を検証するため、テスト用 project だけの変更ではキャッシュを失効させない。 - key: ${{ runner.os }}-dotnet-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }} + key: ${{ runner.os }}-dotnet-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PostExtractionHookFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }} # --locked-mode requires every resolved package to match the committed # packages.lock.json so an unexpected transitive bump (including silent diff --git a/.github/workflows/license-policy.yml b/.github/workflows/license-policy.yml index f4766a316..5df02adb7 100644 --- a/.github/workflows/license-policy.yml +++ b/.github/workflows/license-policy.yml @@ -116,6 +116,7 @@ jobs: cache-dependency-path: | src/CodeIndex/packages.lock.json tests/CodeIndex.HookIsolationFixture/packages.lock.json + tests/CodeIndex.PostExtractionHookFixture/packages.lock.json tests/CodeIndex.PluginIsolationFixture/packages.lock.json tests/CodeIndex.Tests/packages.lock.json tools/CodeIndex.Changelog/packages.lock.json diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml index 31e0a9dd3..12d2575be 100644 --- a/.github/workflows/mutation-testing.yml +++ b/.github/workflows/mutation-testing.yml @@ -29,7 +29,7 @@ jobs: uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.nuget/packages - key: ${{ runner.os }}-mutation-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }} + key: ${{ runner.os }}-mutation-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PostExtractionHookFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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 Stryker tool id: stryker-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d01e14af..26359760d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: path: | ~/.nuget/packages ~\AppData\Local\NuGet\packages - key: ${{ runner.os }}-release-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }} + key: ${{ runner.os }}-release-nuget-${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PostExtractionHookFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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 publish-only NuGet packages if: ${{ !matrix.run_tests }} diff --git a/CodeIndex.sln b/CodeIndex.sln index 215ba3aa3..6fa06f191 100644 --- a/CodeIndex.sln +++ b/CodeIndex.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.Tests", "tests\Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.HookIsolationFixture", "tests\CodeIndex.HookIsolationFixture\CodeIndex.HookIsolationFixture.csproj", "{D3F4D3D1-0D94-4DCE-BD78-468900000001}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.PostExtractionHookFixture", "tests\CodeIndex.PostExtractionHookFixture\CodeIndex.PostExtractionHookFixture.csproj", "{D3F4D3D1-0D94-4DCE-BD78-468900000003}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.PluginIsolationFixture", "tests\CodeIndex.PluginIsolationFixture\CodeIndex.PluginIsolationFixture.csproj", "{D3F4D3D1-0D94-4DCE-BD78-468900000002}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.Changelog", "tools\CodeIndex.Changelog\CodeIndex.Changelog.csproj", "{6A1D4E0B-24F8-4E8E-9E7A-28CC55D3F7AC}" @@ -67,6 +69,18 @@ Global {D3F4D3D1-0D94-4DCE-BD78-468900000001}.Release|x64.Build.0 = Release|Any CPU {D3F4D3D1-0D94-4DCE-BD78-468900000001}.Release|x86.ActiveCfg = Release|Any CPU {D3F4D3D1-0D94-4DCE-BD78-468900000001}.Release|x86.Build.0 = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|x64.Build.0 = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Debug|x86.Build.0 = Debug|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|Any CPU.Build.0 = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|x64.ActiveCfg = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|x64.Build.0 = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|x86.ActiveCfg = Release|Any CPU + {D3F4D3D1-0D94-4DCE-BD78-468900000003}.Release|x86.Build.0 = Release|Any CPU {D3F4D3D1-0D94-4DCE-BD78-468900000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3F4D3D1-0D94-4DCE-BD78-468900000002}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3F4D3D1-0D94-4DCE-BD78-468900000002}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -123,6 +137,7 @@ Global {22DF2626-2208-47EA-8EBE-C2503D92E445} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {A33D0351-ACAC-485B-9517-9ABD6A878ECD} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {D3F4D3D1-0D94-4DCE-BD78-468900000001} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {D3F4D3D1-0D94-4DCE-BD78-468900000003} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {D3F4D3D1-0D94-4DCE-BD78-468900000002} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {6A1D4E0B-24F8-4E8E-9E7A-28CC55D3F7AC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {4EA94432-F249-46FC-8EB4-99F1EB9933A0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 9a24fa213..4bf8dda24 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -721,13 +721,13 @@ Candidate-ordered parallel-index recovery tests must prove that the fatal result JSON summary coverage keeps `output_path` as the artifact basename with and without `--redact-paths`, while asserting that diagnostic paths and parent directories remain redacted. Keep no-overwrite refusal and explicit replacement in one shared-fixture test. Inject both a staging failure and a post-replace parent-directory flush failure to prove an existing bundle survives every reported failure path. Provenance coverage should reuse one saved-event fixture for current, stale, cross-workspace, cross-database, and missing/unsafe-provenance variants; also pin default report DB precedence, option-before-project index parsing, and the query `--` literal sentinel. Persisted-failure coverage must prove that a stack frame that cannot be represented canonically falls back to a valid bounded exception chain rather than producing an event that report rejects as invalid. Assert both archive membership and manifest disposition/reason on `net8.0` and `net9.0`. - `PostExtractionHookTests.cs` - Post-extraction hook discovery, mutation, diagnostics, callback budgets, and collectible hook assembly cleanup. Heavy hook worker and collectible assembly-load integration tests use `ProductionRuntimeFactAttribute` and run only on the `net8.0` production target, while direct worker protocol and metadata tests remain cross-target. Timed-out and canceled callback tests use a hook delay shorter than their leak-observation window, not a full one-second absence check, so worker-kill regressions still write the completion marker before the assertion exits. Duplicate-hook isolation coverage copies the dedicated `CodeIndex.HookIsolationFixture` assembly twice, so only the two hooks under test launch callback workers; the healthy worker retains a bounded one-second startup-and-callback budget while the selected slow hook blocks for 30 seconds. These tests mutate hook-related environment variables and test-only callback budget state, so the class belongs to the `SQLite pool sensitive` non-parallel collection. + Post-extraction hook discovery, mutation, diagnostics, callback budgets, and collectible hook assembly cleanup. Heavy hook worker and collectible assembly-load integration tests use `ProductionRuntimeFactAttribute` and run only on the `net8.0` production target, while direct worker protocol and metadata tests remain cross-target. Production-runtime discovery coverage copies the bounded `CodeIndex.PostExtractionHookFixture` assembly instead of `CodeIndex.Tests.dll`; keep that fixture below the default production type-inspection cap, and keep the lowered test-only cap case as the independent production-cap regression. Timed-out and canceled callback tests use a hook delay shorter than their leak-observation window, not a full one-second absence check, so worker-kill regressions still write the completion marker before the assertion exits. Duplicate-hook isolation coverage separately copies `CodeIndex.HookIsolationFixture` twice, so only the isolation hooks under test launch callback workers; the healthy worker retains a bounded one-second startup-and-callback budget while the selected slow hook blocks for 30 seconds. These tests mutate hook-related environment variables and test-only callback budget state, so the class belongs to the `SQLite pool sensitive` non-parallel collection. Keep the timed-out hook delay well beyond the callback budget while still below its bounded leak-observation window, so a loaded runner cannot let the hook finish at the timeout boundary and worker-kill regressions remain observable. - `GitHubIssueReporterTests.cs` GitHub token resolution logic (CDIDX_GITHUB_TOKEN only; generic GITHUB_TOKEN is ignored), outbound code scrubbing, idempotency checks, and rate-limit diagnostics. - `PackagesLockTests.cs` NuGet lock-file guard coverage for direct package references that must remain synchronized across all target frameworks, including the net9.0 compatibility references that keep locked CI restore green. - Workflow restore-contract coverage fingerprints only lock files reachable from each restore. Build/Test, CodeQL, license-policy, mutation, and native release lanes share the explicit six-lock solution/test graph, while cross-compile release lanes use only `src/CodeIndex/packages.lock.json`; curated release notes and NuGet normalization retain their one-lock and two-lock graphs. Keep `global.json` and the unrelated `examples/hooks` lock outside NuGet cache identities, use exact-hit caches without `restore-keys`, and keep the version-pinned Stryker tool cache independent from the mutation job's NuGet cache. + Workflow restore-contract coverage fingerprints only lock files reachable from each restore. Build/Test, CodeQL, license-policy, mutation, and native release lanes share the explicit eight-lock solution/test graph, while cross-compile release lanes use only `src/CodeIndex/packages.lock.json`; curated release notes and NuGet normalization retain their one-lock and two-lock graphs. Keep `global.json` and the unrelated `examples/hooks` lock outside NuGet cache identities, use exact-hit caches without `restore-keys`, and keep the version-pinned Stryker tool cache independent from the mutation job's NuGet cache. - `ConcurrencyTests.cs` Concurrent read and read-during-write scenarios (WAL mode validation), including the issue #180 bug-catching snapshot-isolation regressions for all three multi-statement reader entry points: (1) `GetStatus` seeds `refs == files * refsPerFile` and asserts every concurrent observation preserves that invariant; (2) `AnalyzeSymbol` seeds one symbol `S` plus matching reference/caller pairs, toggles a second file symmetrically, and asserts `references.Count == callers.Count` across every `inspect`/`analyze_symbol` bundle; (3) `GetRepoMap` seeds a baseline modified timestamp and toggles a newer file, asserting `latest_modified == workspace_latest_modified` across every map call. Each test fails without the DEFERRED-transaction wrap on the matching reader and passes with it. - `PerformanceTests.cs` @@ -1817,7 +1817,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" JSON summary の coverage では、`--redact-paths` の有無にかかわらず `output_path` が artifact の basename を保持し、診断用 path と親 directory は伏字化されたままであることを確認します。 no-overwrite の拒否と明示的な置換は shared fixture を使う1つの test にまとめます。staging failure と置換後の parent-directory flush failure の両方を注入し、報告されるすべての failure path で既存 bundle が残ることを証明してください。provenance coverage は current、stale、cross-workspace、cross-database、missing / unsafe provenance で1つの保存 event fixture を再利用し、report の既定 DB precedence、option-before-project の index parsing、query の `--` literal sentinel も固定します。persisted-failure coverage では canonical に表現できない stack frame が、report で invalid として拒否される event ではなく、有効な上限付き exception chain へ fallback することを証明します。`net8.0` / `net9.0` の両方で archive membership と manifest の disposition / reason を検証します。 - `PostExtractionHookTests.cs` - post-extraction hook の discovery、mutation、diagnostics、callback budget、collectible hook assembly cleanup のテスト。重い hook worker と collectible assembly-load の integration test は `ProductionRuntimeFactAttribute` を使って `net8.0` production target でのみ実行し、direct worker protocol と metadata test は cross-target のままにします。timeout / cancel された callback のテストは、hook delay を leak-observation window より短くし、1 秒丸ごとの absence check には戻しません。worker kill の回帰がある場合は assertion が終わる前に completion marker が書かれるようにします。duplicate-hook isolation coverage では専用の `CodeIndex.HookIsolationFixture` assembly を 2 つの名前で copy し、対象の 2 hook だけが callback worker を起動するようにします。正常な worker には bounded な 1 秒の startup-and-callback budget を残し、選択した slow hook は 30 秒 block させます。hook 関連の環境変数と test-only callback budget 状態を変更するため、このクラスは non-parallel な `SQLite pool sensitive` collection に入れます。 + post-extraction hook の discovery、mutation、diagnostics、callback budget、collectible hook assembly cleanup のテスト。重い hook worker と collectible assembly-load の integration test は `ProductionRuntimeFactAttribute` を使って `net8.0` production target でのみ実行し、direct worker protocol と metadata test は cross-target のままにします。production-runtime の discovery coverage は `CodeIndex.Tests.dll` ではなく、上限を設けた `CodeIndex.PostExtractionHookFixture` assembly を copy します。この fixture は production の既定 type-inspection cap 未満に保ち、production cap 自体の回帰は test-only cap を引き下げる独立した case で維持します。timeout / cancel された callback のテストは、hook delay を leak-observation window より短くし、1 秒丸ごとの absence check には戻しません。worker kill の回帰がある場合は assertion が終わる前に completion marker が書かれるようにします。duplicate-hook isolation coverage では別の `CodeIndex.HookIsolationFixture` を 2 つの名前で copy し、対象の isolation hook だけが callback worker を起動するようにします。正常な worker には bounded な 1 秒の startup-and-callback budget を残し、選択した slow hook は 30 秒 block させます。hook 関連の環境変数と test-only callback budget 状態を変更するため、このクラスは non-parallel な `SQLite pool sensitive` collection に入れます。 timeout 対象 hook の delay は callback budget より十分長く、かつ bounded な leak-observation window より短く保ってください。高負荷 runner で hook が timeout 境界上に完了する競合を避けながら、worker kill の回帰を観測可能にします。 - `GitHubIssueReporterTests.cs` GitHubトークン解決ロジック(CDIDX_GITHUB_TOKENのみ。汎用GITHUB_TOKENは無視)、送信前のコード scrubbing、冪等性チェック、rate-limit diagnostics を扱います。 @@ -1825,7 +1825,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" dependency-lock graph の fixture は明示的な親 package → 子 package reference と、合成された top-level package reference が存在しないことを検証します。NuGet と npm の coverage を揃え、同じ resolved package 集合から `deps` と caller traversal のいずれにも lock-file 間の類似 edge が再生成されず、`callers` が要求元 package を container として維持することを確認してください。NuGet の query regression では無関係な2つの lock file と、複数 target / RID に反復する package を使い、file 内に限定された定義、owner scope の callers/callees、各 JSON node の実際の行を検証します。upgrade regression では、以前の dependency-lock extractor / reference-identity contract stamp により unchanged row と stale candidate が無効化されることも検証します。 - `PackagesLockTests.cs` すべての target framework で同期が必要な direct package reference の NuGet lock-file guard。CI の locked restore を通すための net9.0 compatibility reference も対象です。 - workflow restore contract の coverage は各 restore から到達可能な lock file だけを fingerprint にします。Build/Test、CodeQL、license-policy、mutation、native release lane は明示的な6 lock fileのsolution/test graphを共有し、cross-compile release laneは `src/CodeIndex/packages.lock.json` だけを使います。curated release notesとNuGet normalizationは1 lock / 2 lock graphを維持します。`global.json` と無関係な `examples/hooks` lockをNuGet cache identityから除外し、`restore-keys` のないexact-hit cacheを使い、version固定Stryker tool cacheをmutation jobのNuGet cacheから独立させてください。 + workflow restore contract の coverage は各 restore から到達可能な lock file だけを fingerprint にします。Build/Test、CodeQL、license-policy、mutation、native release lane は明示的な8 lock fileのsolution/test graphを共有し、cross-compile release laneは `src/CodeIndex/packages.lock.json` だけを使います。curated release notesとNuGet normalizationは1 lock / 2 lock graphを維持します。`global.json` と無関係な `examples/hooks` lockをNuGet cache identityから除外し、`restore-keys` のないexact-hit cacheを使い、version固定Stryker tool cacheをmutation jobのNuGet cacheから独立させてください。 - `ConcurrencyTests.cs` 並行読み取りと書き込み中読み取りシナリオ(WALモード検証)。issue #180 の bug-catching な snapshot 隔離回帰テストを 3 つの multi-statement reader 経路について含む。(1) `GetStatus` は `refs == files * refsPerFile` の seed 不変条件を立て、並行観測が常にこの条件を維持することを要求する。(2) `AnalyzeSymbol` はシンボル `S` に対して reference/caller を対称に 1 対 1 で seed し、もう 1 ファイルを対称に toggle することで `inspect` / `analyze_symbol` bundle の `references.Count == callers.Count` を常に保証する。(3) `GetRepoMap` はベースラインの modified と新しい toggle 対象ファイルを用意し、`latest_modified == workspace_latest_modified` が常に一致することを要求する。各テストは対応する reader の DEFERRED transaction を外すと落ち、戻すと通ることを確認済み。 - `PerformanceTests.cs` diff --git a/changelog.d/unreleased/5135.internal.md b/changelog.d/unreleased/5135.internal.md new file mode 100644 index 000000000..5e9eb0a8c --- /dev/null +++ b/changelog.d/unreleased/5135.internal.md @@ -0,0 +1,19 @@ +--- +category: internal +issues: + - 5135 +affected: + - .github/workflows/ + - tests/CodeIndex.PostExtractionHookFixture/ + - tests/CodeIndex.Tests/PostExtractionHookTests.cs + - tests/CodeIndex.Tests/PackagesLockTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Post-extraction hook integration tests now use a bounded fixture assembly (#5135)** — production-runtime discovery tests no longer depend on the growing `CodeIndex.Tests.dll` type count, while a focused test still enforces the production discovery cap. + +## 日本語 + +- **post-extraction hook の integration test が上限管理された fixture assembly を使うようになりました (#5135)** — production-runtime discovery test は増え続ける `CodeIndex.Tests.dll` の型数に依存せず、production の discovery cap は focused test で引き続き検証します。 diff --git a/tests/CodeIndex.PostExtractionHookFixture/CodeIndex.PostExtractionHookFixture.csproj b/tests/CodeIndex.PostExtractionHookFixture/CodeIndex.PostExtractionHookFixture.csproj new file mode 100644 index 000000000..9875bb823 --- /dev/null +++ b/tests/CodeIndex.PostExtractionHookFixture/CodeIndex.PostExtractionHookFixture.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + false + + + + + + + diff --git a/tests/CodeIndex.PostExtractionHookFixture/PostExtractionHookFixture.cs b/tests/CodeIndex.PostExtractionHookFixture/PostExtractionHookFixture.cs new file mode 100644 index 000000000..49d1cf1ef --- /dev/null +++ b/tests/CodeIndex.PostExtractionHookFixture/PostExtractionHookFixture.cs @@ -0,0 +1,341 @@ +using System.Runtime.Loader; +using CodeIndex.Indexer.Hooks; +using CodeIndex.Models; + +namespace CodeIndex.PostExtractionHookFixture; + +public static class PostExtractionHookFixtureEnvironment +{ + public const string SlowHookDelayMilliseconds = "CDIDX_TEST_SLOW_POST_EXTRACTION_HOOK_MS"; + public const string SlowHookCompletionPath = "CDIDX_TEST_SLOW_POST_EXTRACTION_HOOK_DONE_PATH"; + public const string CancellableHookDelayMilliseconds = "CDIDX_TEST_CANCELLABLE_POST_EXTRACTION_HOOK_MS"; + public const string CancellableHookCompletionPath = "CDIDX_TEST_CANCELLABLE_POST_EXTRACTION_HOOK_DONE_PATH"; + public const string SlowConstructorHookDelayMilliseconds = "CDIDX_TEST_SLOW_CTOR_POST_EXTRACTION_HOOK_MS"; + public const string StatefulHook = "CDIDX_TEST_STATEFUL_POST_EXTRACTION_HOOK"; + public const string ThrowingConstructorHook = "CDIDX_TEST_THROWING_CTOR_POST_EXTRACTION_HOOK"; + public const string ExpandingHook = "CDIDX_TEST_EXPANDING_POST_EXTRACTION_HOOK"; + public const string CSharpDeclarationMutation = "CDIDX_TEST_CSHARP_DECLARATION_MUTATION_HOOK"; +} + +public sealed class AWaitingPostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + DelayAndSignalWhenRequested(); + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + DelayAndSignalWhenRequested(); + } + + private static void DelayAndSignalWhenRequested() + { + var raw = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.CancellableHookDelayMilliseconds); + if (!int.TryParse(raw, out var milliseconds) || milliseconds <= 0) + return; + + Thread.Sleep(milliseconds); + var completionPath = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.CancellableHookCompletionPath); + if (!string.IsNullOrWhiteSpace(completionPath)) + File.WriteAllText(completionPath, "done"); + } +} + +public sealed class SamplePostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + var csharpMutation = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.CSharpDeclarationMutation); + if (csharpMutation == "split-and-move") + { + var method = symbols.FirstOrDefault(symbol => symbol.Name == "M"); + if (method != null) + { + method.Name = "N"; + method.Kind = "test.method"; + method.SubKind = "hook-reclassified"; + method.Signature = "void N();"; + } + + var movedContainer = symbols.FirstOrDefault(symbol => symbol.Name == "Inner"); + if (movedContainer != null) + { + movedContainer.ContainerKind = "class"; + movedContainer.ContainerName = "New"; + movedContainer.ContainerQualifiedName = "New"; + } + + var fileType = symbols.FirstOrDefault(symbol => symbol.Name == "SplitType"); + if (fileType != null) + { + fileType.Name = "SplitTypeRenamed"; + fileType.Signature = "class SplitTypeRenamed { }"; + } + return; + } + + if (csharpMutation == "1") + { + var container = symbols.FirstOrDefault(symbol => symbol.Name == "HookContainer"); + if (container != null) + { + container.Name = "HookContainerRenamed"; + container.Signature = "file partial class HookContainerRenamed"; + } + var existing = symbols.FirstOrDefault(symbol => symbol.Name == "HookPartial"); + if (existing != null) + { + existing.Name = "HookOrdinary"; + existing.Signature = "void HookOrdinary();"; + existing.ContainerName = "HookContainerRenamed"; + existing.ContainerQualifiedName = "HookContainerRenamed"; + } + symbols.Add(new SymbolRecord + { + FileId = existing?.FileId ?? 0, + Kind = "function", + Name = "HookAddedPartial", + Signature = "[Obsolete] partial void HookAddedPartial();", + ContainerKind = "class", + ContainerName = "HookContainerRenamed", + ContainerQualifiedName = "HookContainerRenamed", + Line = 3, + StartLine = 3, + EndLine = 3, + }); + symbols.Add(new SymbolRecord + { + FileId = existing?.FileId ?? 0, + Kind = "class", + Name = "HookFileType", + Signature = "file partial class HookFileType { }", + Line = 4, + StartLine = 4, + EndLine = 4, + }); + return; + } + + symbols.Add(new SymbolRecord + { + FileId = symbols.FirstOrDefault()?.FileId ?? 0, + Kind = "domain_tag", + Name = "AppDomainTag", + Line = 1, + StartLine = 1, + EndLine = 1, + Signature = $"domain tag for {context.Path}", + }); + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + references.Add(new ReferenceRecord + { + FileId = 10, + SymbolName = "AppDomainTag", + ReferenceKind = "domain_reference", + Line = 1, + Column = 1, + Context = context.Path, + }); + } +} + +public sealed class ThrowingPostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + => throw new InvalidOperationException("boom"); + + public void OnReferencesExtracted(FileContext context, IList references) + => throw new InvalidOperationException("boom"); +} + +public sealed class ThrowingConstructorPostExtractionHook : IPostExtractionHook +{ + public ThrowingConstructorPostExtractionHook() + { + if (Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.ThrowingConstructorHook) == "1") + { + throw new InvalidOperationException("ctor boom"); + } + } + + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + } +} + +public sealed class SlowConstructorPostExtractionHook : IPostExtractionHook +{ + public SlowConstructorPostExtractionHook() + { + var raw = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.SlowConstructorHookDelayMilliseconds); + if (int.TryParse(raw, out var milliseconds) && milliseconds > 0) + Thread.Sleep(milliseconds); + } + + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + } +} + +public sealed class StatefulPostExtractionHook : IPostExtractionHook +{ + private bool sawSymbols; + + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + if (Environment.GetEnvironmentVariable(PostExtractionHookFixtureEnvironment.StatefulHook) == "1") + sawSymbols = true; + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + if (!sawSymbols + || Environment.GetEnvironmentVariable(PostExtractionHookFixtureEnvironment.StatefulHook) != "1") + { + return; + } + + references.Add(new ReferenceRecord + { + SymbolName = "StatefulHookSawSymbols", + ReferenceKind = "domain_reference", + Line = 1, + Column = 1, + Context = context.Path, + }); + } +} + +public sealed class SlowPostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + if (!DelayWhenRequested()) + return; + + symbols.Add(new SymbolRecord + { + Kind = "domain_tag", + Name = "SlowHookTag", + Line = 1, + StartLine = 1, + EndLine = 1, + }); + SignalCompletionWhenRequested(); + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + if (!DelayWhenRequested()) + return; + + references.Add(new ReferenceRecord + { + SymbolName = "SlowHookTag", + ReferenceKind = "domain_reference", + Line = 1, + Column = 1, + Context = context.Path, + }); + SignalCompletionWhenRequested(); + } + + private static bool DelayWhenRequested() + { + var raw = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.SlowHookDelayMilliseconds); + if (!int.TryParse(raw, out var milliseconds) || milliseconds <= 0) + return false; + + Thread.Sleep(milliseconds); + return true; + } + + private static void SignalCompletionWhenRequested() + { + var completionPath = Environment.GetEnvironmentVariable( + PostExtractionHookFixtureEnvironment.SlowHookCompletionPath); + if (!string.IsNullOrWhiteSpace(completionPath)) + File.WriteAllText(completionPath, "done"); + } +} + +public sealed class LoadContextReportingPostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + var loadContext = AssemblyLoadContext.GetLoadContext(GetType().Assembly); + if (loadContext is { IsCollectible: true } + && !ReferenceEquals(loadContext, AssemblyLoadContext.Default)) + { + symbols.Add(new SymbolRecord + { + Kind = "domain_tag", + Name = "CollectibleHookLoadContext", + Line = 1, + StartLine = 1, + EndLine = 1, + }); + } + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + } +} + +public sealed class ExpandingPostExtractionHook : IPostExtractionHook +{ + public void OnSymbolsExtracted(FileContext context, IList symbols) + { + if (Environment.GetEnvironmentVariable(PostExtractionHookFixtureEnvironment.ExpandingHook) != "1") + return; + + for (var index = 0; index < 5; index++) + { + symbols.Add(new SymbolRecord + { + Kind = "domain_tag", + Name = $"ExpandedHookSymbol{index}", + Line = index + 1, + StartLine = index + 1, + EndLine = index + 1, + }); + } + } + + public void OnReferencesExtracted(FileContext context, IList references) + { + if (Environment.GetEnvironmentVariable(PostExtractionHookFixtureEnvironment.ExpandingHook) != "1") + return; + + for (var index = 0; index < 5; index++) + { + references.Add(new ReferenceRecord + { + SymbolName = $"ExpandedHookSymbol{index}", + ReferenceKind = "domain_reference", + Line = index + 1, + Column = 1, + Context = context.Path, + }); + } + } +} diff --git a/tests/CodeIndex.PostExtractionHookFixture/packages.lock.json b/tests/CodeIndex.PostExtractionHookFixture/packages.lock.json new file mode 100644 index 000000000..2390afa29 --- /dev/null +++ b/tests/CodeIndex.PostExtractionHookFixture/packages.lock.json @@ -0,0 +1,67 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Microsoft.Data.Sqlite": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "/eBwiZPcNisn0qZX+Zk4YCftlK/vnoWqv7hHnmSk8MjPxFdYYkmPObpogT0MfCCWN6oAIZnMCo0SoOtZlbbmgQ==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "iZrONyMKPjxfVZnUktqO30QjzNwAGH+AxM61s8lKQnVhgbQ3bn0hiXI129ZmVicEbIcwljyy2OVsIYUR51ZHKQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "SourceGear.sqlite3": { + "type": "Transitive", + "resolved": "3.50.4.5", + "contentHash": "UtnipXhJYZKQOQIfpws/msLK7IRhMplE1CZCaZLIQXRnGD474QVpO/J9nMlQQY8NZueGz1aidjoxDRnrC1NT3Q==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "3.0.3", + "contentHash": "Zt8jmSL5zcDWGk8rmzhWBJ6IRyLWh1yWS04Pg72+GIvo3Ba4E/rG4Y/4l7AWlSEogEbzyKRTCXUAs1v/O7Pkkg==", + "dependencies": { + "SQLitePCLRaw.config.e_sqlite3": "3.0.3", + "SourceGear.sqlite3": "3.50.4.5" + } + }, + "SQLitePCLRaw.config.e_sqlite3": { + "type": "Transitive", + "resolved": "3.0.3", + "contentHash": "caP/ap0X2fyVmstCXu5ueOmcr2XWAxA2XyKghV7H4bOAFmq3nWcsGl9q44iY1HYG+i8Qr4G9XEqdfti0rV6/ZQ==", + "dependencies": { + "SQLitePCLRaw.provider.e_sqlite3": "3.0.3" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "3.0.3", + "contentHash": "bjm6FY4lZyP+t7GmiuvSM0QXpFihAvyE0Y9O2yibm3g95AAWJPNnHOKVNJGyPTGIKuK7Pr4Wh8Rd8/aOtAclQw==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "3.0.3", + "contentHash": "wd+fGvZTrr3BJNe48opSczmC176Okd61ZgoZNQcdvZwkek6to978ccdpcFmNo5GHxCnk29KwT+f+lAZYgfLVZg==", + "dependencies": { + "SQLitePCLRaw.core": "3.0.3" + } + }, + "cdidx": { + "type": "Project", + "dependencies": { + "Microsoft.Data.Sqlite": "[10.0.9, )", + "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.3, )" + } + } + } + } +} \ No newline at end of file diff --git a/tests/CodeIndex.Tests/CodeIndex.Tests.csproj b/tests/CodeIndex.Tests/CodeIndex.Tests.csproj index 96ae6d17b..200194ed3 100644 --- a/tests/CodeIndex.Tests/CodeIndex.Tests.csproj +++ b/tests/CodeIndex.Tests/CodeIndex.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/tests/CodeIndex.Tests/PackagesLockTests.cs b/tests/CodeIndex.Tests/PackagesLockTests.cs index 6fbe095a1..a9f83e5c5 100644 --- a/tests/CodeIndex.Tests/PackagesLockTests.cs +++ b/tests/CodeIndex.Tests/PackagesLockTests.cs @@ -20,6 +20,7 @@ public class PackagesLockTests [ "src/CodeIndex/packages.lock.json", "tests/CodeIndex.HookIsolationFixture/packages.lock.json", + "tests/CodeIndex.PostExtractionHookFixture/packages.lock.json", "tests/CodeIndex.PluginIsolationFixture/packages.lock.json", "tests/CodeIndex.Tests/packages.lock.json", "tools/CodeIndex.Changelog/packages.lock.json", @@ -28,7 +29,7 @@ public class PackagesLockTests ]; private const string SolutionRestoreLockHashExpression = - "${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }}"; + "${{ hashFiles('src/CodeIndex/packages.lock.json', 'tests/CodeIndex.HookIsolationFixture/packages.lock.json', 'tests/CodeIndex.PostExtractionHookFixture/packages.lock.json', 'tests/CodeIndex.PluginIsolationFixture/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') }}"; private static readonly string SolutionRestoreCacheDependencyPath = "cache-dependency-path: |\n" + diff --git a/tests/CodeIndex.Tests/PostExtractionHookTests.cs b/tests/CodeIndex.Tests/PostExtractionHookTests.cs index e80ed9a82..35c5fce47 100644 --- a/tests/CodeIndex.Tests/PostExtractionHookTests.cs +++ b/tests/CodeIndex.Tests/PostExtractionHookTests.cs @@ -1,26 +1,25 @@ using System.Diagnostics; -using System.Reflection; -using System.Runtime.Loader; using System.Text.Json; using CodeIndex.HookIsolationFixture; using CodeIndex.Indexer; using CodeIndex.Indexer.Hooks; using CodeIndex.Models; +using CodeIndex.PostExtractionHookFixture; namespace CodeIndex.Tests; [Collection("SQLite pool sensitive")] public class PostExtractionHookTests { - internal const string SlowHookDelayEnvironmentVariable = "CDIDX_TEST_SLOW_POST_EXTRACTION_HOOK_MS"; - internal const string SlowHookCompletionPathEnvironmentVariable = "CDIDX_TEST_SLOW_POST_EXTRACTION_HOOK_DONE_PATH"; - internal const string CancellableHookDelayEnvironmentVariable = "CDIDX_TEST_CANCELLABLE_POST_EXTRACTION_HOOK_MS"; - internal const string CancellableHookCompletionPathEnvironmentVariable = "CDIDX_TEST_CANCELLABLE_POST_EXTRACTION_HOOK_DONE_PATH"; - internal const string SlowConstructorHookDelayEnvironmentVariable = "CDIDX_TEST_SLOW_CTOR_POST_EXTRACTION_HOOK_MS"; - internal const string StatefulHookEnvironmentVariable = "CDIDX_TEST_STATEFUL_POST_EXTRACTION_HOOK"; - internal const string ThrowingConstructorHookEnvironmentVariable = "CDIDX_TEST_THROWING_CTOR_POST_EXTRACTION_HOOK"; - internal const string ExpandingHookEnvironmentVariable = "CDIDX_TEST_EXPANDING_POST_EXTRACTION_HOOK"; - internal const string CSharpDeclarationMutationEnvironmentVariable = "CDIDX_TEST_CSHARP_DECLARATION_MUTATION_HOOK"; + internal const string SlowHookDelayEnvironmentVariable = PostExtractionHookFixtureEnvironment.SlowHookDelayMilliseconds; + internal const string SlowHookCompletionPathEnvironmentVariable = PostExtractionHookFixtureEnvironment.SlowHookCompletionPath; + internal const string CancellableHookDelayEnvironmentVariable = PostExtractionHookFixtureEnvironment.CancellableHookDelayMilliseconds; + internal const string CancellableHookCompletionPathEnvironmentVariable = PostExtractionHookFixtureEnvironment.CancellableHookCompletionPath; + internal const string SlowConstructorHookDelayEnvironmentVariable = PostExtractionHookFixtureEnvironment.SlowConstructorHookDelayMilliseconds; + internal const string StatefulHookEnvironmentVariable = PostExtractionHookFixtureEnvironment.StatefulHook; + internal const string ThrowingConstructorHookEnvironmentVariable = PostExtractionHookFixtureEnvironment.ThrowingConstructorHook; + internal const string ExpandingHookEnvironmentVariable = PostExtractionHookFixtureEnvironment.ExpandingHook; + internal const string CSharpDeclarationMutationEnvironmentVariable = PostExtractionHookFixtureEnvironment.CSharpDeclarationMutation; internal const string ModuleInitializerDelayEnvironmentVariable = HookIsolationFixtureEnvironment.ModuleInitializerDelayMilliseconds; internal const string PersistentDiscoveryWorkerPidPathEnvironmentVariable = HookIsolationFixtureEnvironment.PersistentDiscoveryWorkerPidPath; internal const string PersistentDiscoveryDescendantPidPathEnvironmentVariable = HookIsolationFixtureEnvironment.PersistentDiscoveryDescendantPidPath; @@ -66,6 +65,17 @@ public void OnSymbolsExtractedAfterSourceObservation_PreservesEvidenceWithoutSec Assert.Equal(0, nonCSharpSymbols.EnumerationCount); } + [Fact] + public void HookFixture_RemainsBelowProductionTypeInspectionLimit_Issue5135() + { + var loadableTypeCount = typeof(PostExtractionHookFixtureEnvironment).Assembly.GetTypes().Length; + + Assert.True( + loadableTypeCount <= PostExtractionHookRunner.DefaultTypeInspectionLimit, + $"The post-extraction hook fixture contains {loadableTypeCount} loadable types; " + + $"the production limit is {PostExtractionHookRunner.DefaultTypeInspectionLimit}."); + } + [ProductionRuntimeFact] public void Discover_LoadsHooksAndAllowsSymbolAndReferenceMutation() { @@ -74,7 +84,7 @@ public void Discover_LoadsHooksAndAllowsSymbolAndReferenceMutation() { var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -88,7 +98,9 @@ public void Discover_LoadsHooksAndAllowsSymbolAndReferenceMutation() runner.OnSymbolsExtracted(context, symbols); runner.OnReferencesExtracted(context, references); - Assert.Contains(runner.Hooks, hook => hook.TypeName == typeof(SamplePostExtractionHook).FullName); + Assert.Contains( + runner.Hooks, + hook => hook.TypeName == typeof(CodeIndex.PostExtractionHookFixture.SamplePostExtractionHook).FullName); var synthetic = Assert.Single(symbols, symbol => symbol.Name == "AppDomainTag"); Assert.Equal(10, synthetic.FileId); var reference = Assert.Single(references, item => item.SymbolName == "AppDomainTag"); @@ -113,7 +125,7 @@ public void Discover_RecomputesCSharpDeclarationMetadataAfterHookMutation_Issue4 { var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); mutation.Set(CSharpDeclarationMutationEnvironmentVariable, "1"); using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -209,7 +221,7 @@ public void Discover_PreservesSplitModifiersAndRefreshesMovedContainerDescendant { var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); mutation.Set(CSharpDeclarationMutationEnvironmentVariable, "split-and-move"); using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -663,8 +675,9 @@ public void CallbackWorker_LoadsHookAssemblyInCollectibleContext_3413() var handled = PostExtractionHookCallbackWorker.TryRunCommand( [ PostExtractionHookCallbackWorker.CommandName, - Assembly.GetExecutingAssembly().Location, - typeof(LoadContextReportingPostExtractionHook).FullName!, + typeof(CodeIndex.PostExtractionHookFixture.LoadContextReportingPostExtractionHook) + .Assembly.Location, + typeof(CodeIndex.PostExtractionHookFixture.LoadContextReportingPostExtractionHook).FullName!, ], input, output, @@ -691,7 +704,7 @@ public void CallbackExceptions_AreDiagnosticsAndDoNotBlockOtherHooks() { var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -703,7 +716,8 @@ public void CallbackExceptions_AreDiagnosticsAndDoNotBlockOtherHooks() Assert.Contains(symbols, symbol => symbol.Name == "AppDomainTag"); var diagnostic = Assert.Single( runner.Diagnostics, - diagnostic => diagnostic.TypeName == typeof(ThrowingPostExtractionHook).FullName); + diagnostic => diagnostic.TypeName + == typeof(CodeIndex.PostExtractionHookFixture.ThrowingPostExtractionHook).FullName); Assert.Equal("hook_callback_failed", diagnostic.Category); Assert.DoesNotContain("boom", diagnostic.Message, StringComparison.Ordinal); } @@ -727,7 +741,7 @@ public void WorkerConstructionFailure_DisablesHookForCurrentRun() env.Set(ThrowingConstructorHookEnvironmentVariable, "1"); var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -740,7 +754,9 @@ public void WorkerConstructionFailure_DisablesHookForCurrentRun() var diagnostic = Assert.Single( runner.Diagnostics, - diagnostic => diagnostic.TypeName == typeof(ThrowingConstructorPostExtractionHook).FullName); + diagnostic => diagnostic.TypeName + == typeof(CodeIndex.PostExtractionHookFixture.ThrowingConstructorPostExtractionHook) + .FullName); Assert.Equal("constructor_failed", diagnostic.Category); Assert.Contains("isolated worker", diagnostic.Message, StringComparison.Ordinal); Assert.DoesNotContain("ctor boom", diagnostic.Message, StringComparison.Ordinal); @@ -766,7 +782,7 @@ public void Callbacks_ReuseIsolatedWorkerHookInstance() env.Set(StatefulHookEnvironmentVariable, "1"); var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -806,7 +822,7 @@ public void CallbackBudgetExceeded_KillsWorkerAndSkipsTimedOutMutation() var completionPath = Path.Combine(projectRoot, "slow-hook.done"); env.Set(SlowHookCompletionPathEnvironmentVariable, completionPath); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -819,7 +835,8 @@ public void CallbackBudgetExceeded_KillsWorkerAndSkipsTimedOutMutation() Assert.DoesNotContain(symbols, symbol => symbol.Name == "SlowHookTag"); var diagnostic = Assert.Single( runner.Diagnostics, - item => item.TypeName == typeof(SlowPostExtractionHook).FullName + item => item.TypeName + == typeof(CodeIndex.PostExtractionHookFixture.SlowPostExtractionHook).FullName && item.Callback == nameof(IPostExtractionHook.OnSymbolsExtracted)); Assert.Equal("callback_timeout", diagnostic.Category); Assert.True( @@ -855,7 +872,7 @@ public void CallbackBudgetExceeded_KillsSlowConstructorAfterLargeRequestIsSent() PostExtractionHookRunner.CallbackBudgetForTesting = () => TimeSpan.FromMilliseconds(50); var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -878,7 +895,8 @@ public void CallbackBudgetExceeded_KillsSlowConstructorAfterLargeRequestIsSent() var diagnostic = Assert.Single( runner.Diagnostics, - item => item.TypeName == typeof(SlowConstructorPostExtractionHook).FullName + item => item.TypeName + == typeof(CodeIndex.PostExtractionHookFixture.SlowConstructorPostExtractionHook).FullName && item.Callback == nameof(IPostExtractionHook.OnSymbolsExtracted)); Assert.Equal("callback_timeout", diagnostic.Category); Assert.Contains("exceeded", diagnostic.Message, StringComparison.Ordinal); @@ -912,7 +930,7 @@ public void OnSymbolsExtracted_CancellationWhileWaitingForCallback_KillsWorker_I var completionPath = Path.Combine(projectRoot, "cancellable-hook.done"); env.Set(CancellableHookCompletionPathEnvironmentVariable, completionPath); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); using (var runner = PostExtractionHookRunner.Discover(hooksDir)) using (var cancellation = new CancellationTokenSource()) @@ -1285,7 +1303,7 @@ public void Callbacks_TruncateHookMaterializationAndReportDiagnostics_Issue3744( env.Set(ExpandingHookEnvironmentVariable, "1"); var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); { using var runner = PostExtractionHookRunner.Discover( @@ -1331,7 +1349,7 @@ public void Discover_SkipsAssembliesAboveTypeInspectionLimit_Issue3790() PostExtractionHookRunner.TypeInspectionLimitForTesting = () => 1; var hooksDir = Path.Combine(projectRoot, "hooks"); Directory.CreateDirectory(hooksDir); - File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(hooksDir, "CodeIndex.Tests.dll")); + CopyHookFixtureAssembly(hooksDir); using var runner = PostExtractionHookRunner.Discover(hooksDir); @@ -1383,6 +1401,14 @@ private static void CollectUnloadedHookAssemblies() GC.Collect(); } + private static void CopyHookFixtureAssembly(string hooksDirectory) + { + var fixtureAssemblyPath = typeof(PostExtractionHookFixtureEnvironment).Assembly.Location; + File.Copy( + fixtureAssemblyPath, + Path.Combine(hooksDirectory, Path.GetFileName(fixtureAssemblyPath))); + } + private static void AssertFileDoesNotAppear(string path, TimeSpan duration) => TestDeterminism.AssertConditionRemainsTrue( () => !File.Exists(path), @@ -1452,314 +1478,3 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } } - -public sealed class AWaitingPostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - DelayAndSignalWhenRequested(); - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - DelayAndSignalWhenRequested(); - } - - private static void DelayAndSignalWhenRequested() - { - var raw = Environment.GetEnvironmentVariable(PostExtractionHookTests.CancellableHookDelayEnvironmentVariable); - if (!int.TryParse(raw, out var milliseconds) || milliseconds <= 0) - return; - - Thread.Sleep(milliseconds); - var completionPath = Environment.GetEnvironmentVariable(PostExtractionHookTests.CancellableHookCompletionPathEnvironmentVariable); - if (!string.IsNullOrWhiteSpace(completionPath)) - File.WriteAllText(completionPath, "done"); - } -} - -public sealed class SamplePostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - var csharpMutation = Environment.GetEnvironmentVariable( - PostExtractionHookTests.CSharpDeclarationMutationEnvironmentVariable); - if (csharpMutation == "split-and-move") - { - var method = symbols.FirstOrDefault(symbol => symbol.Name == "M"); - if (method != null) - { - method.Name = "N"; - method.Kind = "test.method"; - method.SubKind = "hook-reclassified"; - method.Signature = "void N();"; - } - - var movedContainer = symbols.FirstOrDefault(symbol => symbol.Name == "Inner"); - if (movedContainer != null) - { - movedContainer.ContainerKind = "class"; - movedContainer.ContainerName = "New"; - movedContainer.ContainerQualifiedName = "New"; - } - - var fileType = symbols.FirstOrDefault(symbol => symbol.Name == "SplitType"); - if (fileType != null) - { - fileType.Name = "SplitTypeRenamed"; - fileType.Signature = "class SplitTypeRenamed { }"; - } - return; - } - - if (csharpMutation == "1") - { - var container = symbols.FirstOrDefault(symbol => symbol.Name == "HookContainer"); - if (container != null) - { - container.Name = "HookContainerRenamed"; - container.Signature = "file partial class HookContainerRenamed"; - } - var existing = symbols.FirstOrDefault(symbol => symbol.Name == "HookPartial"); - if (existing != null) - { - existing.Name = "HookOrdinary"; - existing.Signature = "void HookOrdinary();"; - existing.ContainerName = "HookContainerRenamed"; - existing.ContainerQualifiedName = "HookContainerRenamed"; - } - symbols.Add(new SymbolRecord - { - FileId = existing?.FileId ?? 0, - Kind = "function", - Name = "HookAddedPartial", - Signature = "[Obsolete] partial void HookAddedPartial();", - ContainerKind = "class", - ContainerName = "HookContainerRenamed", - ContainerQualifiedName = "HookContainerRenamed", - Line = 3, - StartLine = 3, - EndLine = 3, - }); - symbols.Add(new SymbolRecord - { - FileId = existing?.FileId ?? 0, - Kind = "class", - Name = "HookFileType", - Signature = "file partial class HookFileType { }", - Line = 4, - StartLine = 4, - EndLine = 4, - }); - return; - } - - symbols.Add(new SymbolRecord - { - FileId = symbols.FirstOrDefault()?.FileId ?? 0, - Kind = "domain_tag", - Name = "AppDomainTag", - Line = 1, - StartLine = 1, - EndLine = 1, - Signature = $"domain tag for {context.Path}", - }); - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - references.Add(new ReferenceRecord - { - FileId = 10, - SymbolName = "AppDomainTag", - ReferenceKind = "domain_reference", - Line = 1, - Column = 1, - Context = context.Path, - }); - } -} - -public sealed class ThrowingPostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - => throw new InvalidOperationException("boom"); - - public void OnReferencesExtracted(FileContext context, IList references) - => throw new InvalidOperationException("boom"); -} - -public sealed class ThrowingConstructorPostExtractionHook : IPostExtractionHook -{ - public ThrowingConstructorPostExtractionHook() - { - if (Environment.GetEnvironmentVariable(PostExtractionHookTests.ThrowingConstructorHookEnvironmentVariable) == "1") - throw new InvalidOperationException("ctor boom"); - } - - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - } -} - -public sealed class SlowConstructorPostExtractionHook : IPostExtractionHook -{ - public SlowConstructorPostExtractionHook() - { - var raw = Environment.GetEnvironmentVariable(PostExtractionHookTests.SlowConstructorHookDelayEnvironmentVariable); - if (int.TryParse(raw, out var milliseconds) && milliseconds > 0) - Thread.Sleep(milliseconds); - } - - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - } -} - -public sealed class StatefulPostExtractionHook : IPostExtractionHook -{ - private bool sawSymbols; - - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - if (Environment.GetEnvironmentVariable(PostExtractionHookTests.StatefulHookEnvironmentVariable) == "1") - sawSymbols = true; - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - if (!sawSymbols || Environment.GetEnvironmentVariable(PostExtractionHookTests.StatefulHookEnvironmentVariable) != "1") - return; - - references.Add(new ReferenceRecord - { - SymbolName = "StatefulHookSawSymbols", - ReferenceKind = "domain_reference", - Line = 1, - Column = 1, - Context = context.Path, - }); - } -} - -public sealed class SlowPostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - if (!DelayWhenRequested()) - return; - - symbols.Add(new SymbolRecord - { - Kind = "domain_tag", - Name = "SlowHookTag", - Line = 1, - StartLine = 1, - EndLine = 1, - }); - SignalCompletionWhenRequested(); - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - if (!DelayWhenRequested()) - return; - - references.Add(new ReferenceRecord - { - SymbolName = "SlowHookTag", - ReferenceKind = "domain_reference", - Line = 1, - Column = 1, - Context = context.Path, - }); - SignalCompletionWhenRequested(); - } - - private static bool DelayWhenRequested() - { - var raw = Environment.GetEnvironmentVariable(PostExtractionHookTests.SlowHookDelayEnvironmentVariable); - if (!int.TryParse(raw, out var milliseconds) || milliseconds <= 0) - return false; - - Thread.Sleep(milliseconds); - return true; - } - - private static void SignalCompletionWhenRequested() - { - var completionPath = Environment.GetEnvironmentVariable(PostExtractionHookTests.SlowHookCompletionPathEnvironmentVariable); - if (!string.IsNullOrWhiteSpace(completionPath)) - File.WriteAllText(completionPath, "done"); - } -} - -public sealed class LoadContextReportingPostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - var loadContext = AssemblyLoadContext.GetLoadContext(GetType().Assembly); - if (loadContext is { IsCollectible: true } && !ReferenceEquals(loadContext, AssemblyLoadContext.Default)) - { - symbols.Add(new SymbolRecord - { - Kind = "domain_tag", - Name = "CollectibleHookLoadContext", - Line = 1, - StartLine = 1, - EndLine = 1, - }); - } - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - } -} - -public sealed class ExpandingPostExtractionHook : IPostExtractionHook -{ - public void OnSymbolsExtracted(FileContext context, IList symbols) - { - if (Environment.GetEnvironmentVariable(PostExtractionHookTests.ExpandingHookEnvironmentVariable) != "1") - return; - - for (var index = 0; index < 5; index++) - { - symbols.Add(new SymbolRecord - { - Kind = "domain_tag", - Name = $"ExpandedHookSymbol{index}", - Line = index + 1, - StartLine = index + 1, - EndLine = index + 1, - }); - } - } - - public void OnReferencesExtracted(FileContext context, IList references) - { - if (Environment.GetEnvironmentVariable(PostExtractionHookTests.ExpandingHookEnvironmentVariable) != "1") - return; - - for (var index = 0; index < 5; index++) - { - references.Add(new ReferenceRecord - { - SymbolName = $"ExpandedHookSymbol{index}", - ReferenceKind = "domain_reference", - Line = index + 1, - Column = 1, - Context = context.Path, - }); - } - } -} diff --git a/tests/CodeIndex.Tests/packages.lock.json b/tests/CodeIndex.Tests/packages.lock.json index cd7c42d22..b1cc7fea7 100644 --- a/tests/CodeIndex.Tests/packages.lock.json +++ b/tests/CodeIndex.Tests/packages.lock.json @@ -1224,6 +1224,12 @@ "cdidx": "[1.42.0, )" } }, + "codeindex.postextractionhookfixture": { + "type": "Project", + "dependencies": { + "cdidx": "[1.42.0, )" + } + }, "codeindex.testtelemetry": { "type": "Project" } @@ -2451,6 +2457,12 @@ "cdidx": "[1.42.0, )" } }, + "codeindex.postextractionhookfixture": { + "type": "Project", + "dependencies": { + "cdidx": "[1.42.0, )" + } + }, "codeindex.testtelemetry": { "type": "Project" }