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"
}