From c1a905004ac913e9af959e27df2c005ad2d129f0 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 18 Aug 2026 11:06:08 +0200 Subject: [PATCH] test: separate diagnostic apparatus from release gates --- docs/ISSUES.md | 2 +- .../2026-06-11-building-render-port-plan.md | 2 +- docs/release-gate.md | 10 + ...ing-render-acdream-vs-retail-comparison.md | 2 +- .../2026-07-06-176-177-handoff-A7-lighting.md | 4 +- ...026-07-06-176-seam-floor-zfight-handoff.md | 4 +- .../2026-08-18-r3-test-truth-ledger.md | 76 +++- .../Rendering/CornerFloodReplayTests.cs | 1 + .../Rendering/Issue131SetupProbeTests.cs | 1 + .../Issue176177FacilityHubFloodReplayTests.cs | 12 +- .../Issue177StairDescentCameraFloodTests.cs | 18 +- .../Rendering/Issue181VisFlapReplayTests.cs | 1 + .../Issue181WallPressEquilibriumTests.cs | 1 + .../UI/Layout/FaPanelSlotProbeTests.cs | 1 + .../UI/Layout/MapHousePanelSlotProbeTests.cs | 1 + .../Layout/OptionsPanelLiveMountProbeTests.cs | 3 + .../UI/Layout/PowerbarLayoutProbeTests.cs | 3 + .../UI/SpewBoxLayoutDumpDiagnostic.cs | 1 + .../EnvCellSoundEmitterInventoryTests.cs | 6 +- .../CottageDoorwayCharacterizationTests.cs | 2 + .../DungeonLandblockDatProbeTests.cs | 1 + .../HoltburgTorchFalloffProbeTests.cs | 1 + .../Issue113DoorVanishDiagnosticTests.cs | 6 +- .../Issue113PhantomStairsDumpTests.cs | 3 + .../ThresholdDivergenceDiagnosticTests.cs | 1 + .../Physics/CameraCornerSealReplayTests.cs | 1 + .../Physics/DoorSetupGfxObjInspectionTests.cs | 2 + .../Issue137CorridorSeamInspectionTests.cs | 1 + ...186ConnectorCellGeometryInspectionTests.cs | 1 + ...188FadingDoorMotionTableInspectionTests.cs | 1 + .../Issue176177DungeonSeamInspectionTests.cs | 7 +- ...NetworkFountainRoomLightInspectionTests.cs | 1 + tools/audit-test-inventory.ps1 | 374 ++++++++++++++++++ 33 files changed, 527 insertions(+), 24 deletions(-) create mode 100644 tools/audit-test-inventory.ps1 diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 9da6482f..b38196dd 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -10738,7 +10738,7 @@ user descended the spiral (`retail-spiral2.log`, 767 samples). **Retail's flood and IDENTICAL in character to ours** — from the spiral cells it swings `num` 3→27 with gaze and COLLAPSES to 3 cells at many poses (e.g. cam=015d eye(60.05,-28.51,-3.66) → just {015d 015e 015f}; cam=014b → {014b 014c 014a}). Our flood does the same (3→43, headless -`FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral +`Diagnostic_FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral where we drop it — the flood is EXONERATED as the cause. **The vanish must be DOWNSTREAM of the flood** (unexplored): the steps in these spiral cells are STATIC objects (GfxObj `0x010000DE` ×6/cell), drawn via the separate viewcone cull (`ViewconeCuller.SphereVisibleInCell` diff --git a/docs/plans/2026-06-11-building-render-port-plan.md b/docs/plans/2026-06-11-building-render-port-plan.md index f367e32a..a305d62c 100644 --- a/docs/plans/2026-06-11-building-render-port-plan.md +++ b/docs/plans/2026-06-11-building-render-port-plan.md @@ -104,7 +104,7 @@ inline when ported. ### BR-1 — The surface gate — ✅ RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1) **Premise falsified before implementation (the BR-1 pre-check, -`ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses +`Diagnostic_ReplicateProductionEmission_OnPortalFills`):** acdream **already suppresses every portal fill** — all four extraction paths skip `Stippling.NoPos` positive sides (`ObjectMeshManager.PrepareGfxObjMeshData:1046`, `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, `GfxObjMesh.Build:71`), diff --git a/docs/release-gate.md b/docs/release-gate.md index b7cc8f4b..35c3cbcd 100644 --- a/docs/release-gate.md +++ b/docs/release-gate.md @@ -99,6 +99,16 @@ expected until the linked defect is fixed. Diagnostic apparatus (`Purpose=Diagnostic`) likewise reports separately and does not inflate the contract-test pass count. +The current diagnostic apparatus lives in App and Core. It is retained for +investigation output, and several methods require installed DATs: + +```powershell +dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj -c Release ` + --filter 'Purpose=Diagnostic' +dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Release ` + --filter 'Purpose=Diagnostic' +``` + ## Updating dependencies Do not edit lock files by hand. To make an intentional dependency change: diff --git a/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md b/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md index 2e5011b1..cf7617a8 100644 --- a/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md +++ b/docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md @@ -371,7 +371,7 @@ area files. > (`ObjectMeshManager.PrepareGfxObjMeshData:1046`, > `PrepareCellStructMeshData:1394`, `CellMesh.Build:44`, > `GfxObjMesh.Build:71`), and the fills have no negative surface -> (`ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every +> (`Diagnostic_ReplicateProductionEmission_OnPortalFills`: pos=False/neg=False for every > fill). The equivalence pin (`StipplingSurfaceEquivalenceTests`, 2,607 > polys, 0 violations) proves our build-time skip ⇔ retail's draw-time > `skipNoTexture` on this content. Consequences: the ledger rows diff --git a/docs/research/2026-07-06-176-177-handoff-A7-lighting.md b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md index 54dceac5..47ab117f 100644 --- a/docs/research/2026-07-06-176-177-handoff-A7-lighting.md +++ b/docs/research/2026-07-06-176-177-handoff-A7-lighting.md @@ -83,7 +83,7 @@ draw the purple lightning over the floor"): 3. **Striped floor z-fight-like artifact.** User's 2nd screenshot: regular magenta bands across one floor region, "like something is fighting to draw the purple over the floor." **NOT attributed.** Ruled out: not coincident dat - geometry (the `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep + geometry (the `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` sweep found only the legit z=−12 under-hall floor quad-fan, nothing near the −6 corridor floor); not a striped texture (all corridor surfaces are plain `Base1Image` stone 0x08000375/6/7/8). Leading guess: two draws of the same @@ -116,7 +116,7 @@ draw the purple lightning over the floor"): stair geometry owner (`0x8A020182`'s ramp shell, vertical portals, ZERO statics), CellBSP containment (partitions exactly at portal planes), under-hall + corridor drawn-poly surface colors, DXT1 alpha histograms (0 - transparent texels), and `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` + transparent texels), and `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` (the stripe-geometry sweep — came back empty for the −6 floor). - **`tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs`** — production-matched portal-flood replays (approach/descent/gaze-sweep/walk + diff --git a/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md b/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md index d9d84533..9aa5faf0 100644 --- a/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md +++ b/docs/research/2026-07-06-176-seam-floor-zfight-handoff.md @@ -48,7 +48,7 @@ NOT throw more lighting fixes at it (two already did nothing). | Lighting **selection** | `SelectForCell` (all dynamic lights per cell, retail-exact) → **no visual change** | | Light-set **camera-cap churn** (the OLD "confirmed" #176 theory) | visible-cell scoping shipped (`c500912b`); probe-proven ~285 through-floor lights dropped/frame — symptom unchanged. **REFUTED.** | | **Membership / the "flap"** | `ACDREAM_PROBE_FLAP`: render cell = `0x8A020164` on **100%** of 188,732 frames across **526 distinct camera angles**, `res=None` always; `ACDREAM_PROBE_PVINPUT` flood is stable-per-angle (17/10/8…), never oscillates at a fixed view; 1 `[cell-transit]` (the spawn teleport) total | -| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) | +| **Dat-geometry z-fight** | `Issue176177DungeonSeamInspectionTests.Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seeded on the ACTUAL cell `0x8A020164` + neighbors → **zero** coplanar drawn pairs at the z=−6 corridor floor (only the benign same-cell z=−12 under-hall floor tiling in `0x011E`) | | A2C / alpha-hole see-through | corridor floor surface `0x08000377` is **fully opaque** (`alpha0Texels=0`, `transl=0.00`) | | Translucent under-surface blend | the one translucent colored surface `0x08000034` is `NoPos` (not drawn) | | Flat (per-face) normals | corridor floor uses **smooth per-vertex dat normals** (center `(0,0,1)`, corners tilted ~27° — retail-style edge smoothing), NOT flat (`CellVertexNormals_SmoothOrFaceted_Dump`) | @@ -129,7 +129,7 @@ RenderDoc (do NOT assume — capture and read): - `ACDREAM_PROBE_INDOOR_LIGHT=1` — `[indoor-light]` scoped-pool SET composition. - `tools/cdb/issue176-floor-light.cdb` — retail light-setup trace. - `Issue176177DungeonSeamInspectionTests` — dat truth (coplanar sweep, floor - surfaces, vertex normals); `CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` + surfaces, vertex normals); `Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs` seed = `{0164,0165,016E,017A}`+neighbors. ## Repro + launch protocol diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md index 348e785a..107704ee 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -26,6 +26,50 @@ The six-case reduction from the earlier 14,748-pass headline is understood: seven launcher editor-focus theory rows were consolidated into one fact that still executes all seven variants in a single Avalonia application session. +## Fresh reproducible syntax inventory + +`tools/audit-test-inventory.ps1` now regenerates the complete attributed-method +manifest from tracked C# test sources with Roslyn: + +```powershell +pwsh ./tools/audit-test-inventory.ps1 +``` + +The generated JSON lives under ignored `artifacts/test-audit/`; it is not a +second 6.8 MB checked-in source of truth. The script and this reviewed ledger +are durable, while paths and line numbers regenerate after every batch. The +inventory at commit `8f490240` reports: + +| Syntax signal | Current count | +|---|---:| +| Tracked/parsed C# test files | 1,256 / 1,256 | +| Attributed test methods (not expanded theory cases) | 11,455 | +| Methods containing at least one empty `return;` | 399 | +| Empty-return sites | 426 | +| Prerequisite-return candidates | 282 | +| Mechanical output-only candidates | 47 | +| Methods directly using `Thread.Sleep` / `Task.Delay` | 20 / 23 | +| Methods directly reading environment variables | 47 | +| Methods directly reading `.cs` source text | 63 | + +The 282 prerequisite-return count is intentionally a review candidate set, not +a claim that all 399 methods are broken. It comprises 109 App, 125 Core, 29 +Content, 12 Launcher.Core, three Core.Net, three Bake, and one Headless method. +The classifier includes external-asset, opt-in, and platform gates; behavioral +early exits inside callback tests remain outside that candidate set. Every +candidate still requires source inspection because `Directory.Exists`, for +example, can also be the behavior under test. + +The output-only classifier follows same-file helper calls, but it cannot decide +whether an assertion merely establishes probe setup. The reviewed diagnostic +set is therefore 51 methods: the 47 mechanical candidates plus +`OptionsPanelLiveMountProbeTests.ProbeLiveMountShapes`, +`DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection`, and +the two already classified tower diagnostics. Relative to T-011's historical +51-method table, delete the removed PVS scaffold and add +`TowerAscentReplayTests.Diagnostic_TowerAscent_PerStepTable`; all other entries +remain. Those 51 methods expand to 70 discovered cases (28 App, 42 Core). + ## Exact 77-skip reconciliation The latest clean-gate TRX files account for every reported skip: @@ -83,11 +127,11 @@ identifiable in the lane report. | T-004 warning mismatches | resolved in R2 | Clean rebuild is zero-warning with repository-wide warnings-as-errors. | | T-005 unreachable panel stack | requires production reachability proof | Do not delete 52 meaningful tests until the dead presentation surface and any supported plugin compatibility promise are verified together. | | T-006 misleading installed-DAT reason | resolved in batch B | Shared opt-in is now `ACDREAM_RUN_INSTALLED_DAT_TESTS=1` (legacy switch retained), the reason names the lane, and all nine owners carry `Lane=InstalledDat`. | -| T-007 271 silent passing gates | open | Re-run the syntax inventory against current source, then convert each to a hermetic fixture, explicit prerequisite skip, or non-default lane. | +| T-007 271 silent passing gates | active; current candidate set generated | Current syntax has 282 prerequisite-return candidates across seven projects. Inspect/classify from the generated manifest; the count changed with source growth and includes platform gates, so do not mechanically rewrite all 282. | | T-008 incapable-of-failing diagnostics | batch A plus diagnostic lane work | Delete the literal wiring smoke test; repair the GPU contract tautology; later re-home output-only apparatus. | | T-009 wall-clock double-click tests | open | Introduce a behavior-preserving injectable monotonic clock and deterministic fake time. | | T-010 two useless cases | high-confidence cleanup batch A | Delete `SmokeTest.TestProject_IsWired` and `ChaseCameraTests.ImplementsICamera`; compilation already proves both claims. | -| T-011 51 output-only methods | open | Validate the historical 51-method list against current source; add stable oracles or move them outside default discovery. | +| T-011 51 output-only methods | classified in batch C | The reviewed current set is 51 methods / 70 cases. All carry `Purpose=Diagnostic`, preserving the apparatus while removing it from release pass totals. Contract-shaped names remain explicitly flagged until a stable oracle exists. | | T-012 source-text freezes | requires semantic replacement map | Retain whole-tree dependency rules; remove exact-text freezes only when an equivalent semantic/behavioral guard is identified. | | T-013 controller self-comparison | high-confidence cleanup batch A | Capture the first controller next to the first body and compare every retry with that reference. | | T-014 seven load-sensitive tests | active; first mechanism reproduced | `PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray` reported 6,904 B only while three App test hosts ran concurrently, then passed in an isolated run. Preserve both observations and remove ambient process-allocation sensitivity rather than loosening the zero-allocation oracle. | @@ -179,3 +223,31 @@ at 6,904 bytes while the Installed-DAT and discovery hosts ran beside it. The same unmodified hermetic command then passed 5,505/5,505 in isolation. This is evidence for removing cross-host allocation sensitivity; it is not grounds to weaken the assertion or claim a clean concurrent result. + +## Batch C diagnostic classification + +Batch C assigns the reviewed 51-method diagnostic set to +`Purpose=Diagnostic`. No body, output, historical comment, or fixture is +removed. This resolves the misleading release accounting without pretending +that every investigation has a stable regression oracle. + +The 15 contract-shaped methods called out by T-011 now have a `Diagnostic_` +prefix: four facility-hub scenarios, six stair/camera sweeps, two ambient-slot +inventories, production-emission replication, and two drawn-polygon reports. +Their current output and historical method references are preserved, while the +new names no longer imply an asserted oracle. Promoting them back into the +hermetic lane requires an asserted expected result; R3 will not invent those +values. + +Batch C verification: + +- App Release build: 0 warnings, 0 errors; +- Core Release build, serialized after App: 0 warnings, 0 errors; +- hermetic App: 5,479 passed / 0 skipped / 0 failed; +- hermetic Core: 4,753 passed / 0 skipped / 0 failed; and +- diagnostic discovery: 28 App + 42 Core = 70 cases from 51 methods. + +An attempted parallel App/Core build collided on the shared +`AcDream.Plugin.Abstractions` intermediate DLL. The repository gate serializes +projects, so the authoritative verification is the two isolated clean builds +above; no code or assertion was changed in response to that tooling collision. diff --git a/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs b/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs index 351b356b..74da2f81 100644 --- a/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs +++ b/tests/AcDream.App.Tests/Rendering/CornerFloodReplayTests.cs @@ -541,6 +541,7 @@ public class CornerFloodReplayTests /// Scratch: the homogeneous reciprocal primitive in isolation, on the /// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses. [Fact] + [Trait("Purpose", "Diagnostic")] public void Scratch_ReciprocalPrimitive_SyntheticPair() { var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY); diff --git a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs index 0c60c71a..808c2ab1 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs @@ -14,6 +14,7 @@ namespace AcDream.App.Tests.Rendering; /// candidate setup's parts + bounds from the dat. The portal's setup is the /// translucent swirl; lamp posts / creatures / signs identify by part shape. /// +[Trait("Purpose", "Diagnostic")] public class Issue131SetupProbeTests { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs b/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs index 0bc52627..e95dc97b 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue176177FacilityHubFloodReplayTests.cs @@ -98,7 +98,8 @@ public class Issue176177FacilityHubFloodReplayTests } [Fact] - public void ScenarioB_StairDescent_RampCellRetention() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_ScenarioB_StairDescent_RampCellRetention() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -140,7 +141,8 @@ public class Issue176177FacilityHubFloodReplayTests } [Fact] - public void ScenarioC_CorridorSeamGazeSweep_Bistability() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_ScenarioC_CorridorSeamGazeSweep_Bistability() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -212,9 +214,10 @@ public class Issue176177FacilityHubFloodReplayTests /// #177(a)/(c) at the stair transit. /// [Theory] + [Trait("Purpose", "Diagnostic")] [InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85 [InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33 - public void ScenarioE_RootLagWindow_ForwardChainRetention( + public void Diagnostic_ScenarioE_RootLagWindow_ForwardChainRetention( uint rootLow, uint forwardLow, float planeX, float eyeZ) { var datDir = CornerFloodReplayTests.ResolveDatDir(); @@ -239,7 +242,8 @@ public class Issue176177FacilityHubFloodReplayTests } [Fact] - public void ScenarioD_CorridorWalk_PerStepChurn() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_ScenarioD_CorridorWalk_PerStepChurn() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } diff --git a/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs index 5cf4e472..f10f0bc7 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs @@ -105,7 +105,8 @@ public class Issue177StairDescentCameraFloodTests => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}")); [Fact] - public void StairCellComposition_ShellVsStatics() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_StairCellComposition_ShellVsStatics() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -166,7 +167,8 @@ public class Issue177StairDescentCameraFloodTests /// locate the knife-edge and which cell flips. /// [Fact] - public void RealStaircase_FineYawZoomSweep_FindKnifeEdge() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_RealStaircase_FineYawZoomSweep_FindKnifeEdge() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -228,7 +230,8 @@ public class Issue177StairDescentCameraFloodTests /// caps well below retail's 26, our flood truncates the spiral (the vanish). /// [Fact] - public void FloodDepthFrom015E_VsRetail26() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_FloodDepthFrom015E_VsRetail26() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -349,7 +352,8 @@ public class Issue177StairDescentCameraFloodTests /// contact leaving the eye on the surface vs free-space boom geometry. /// [Fact] - public void StaircaseSweep_EyeClearanceFromCeilingPortal() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_StaircaseSweep_EyeClearanceFromCeilingPortal() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -392,10 +396,11 @@ public class Issue177StairDescentCameraFloodTests } [Theory] + [Trait("Purpose", "Diagnostic")] // spot label, player x, player cell, description [InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")] [InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")] - public void ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label) + public void Diagnostic_ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label) { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -451,7 +456,8 @@ public class Issue177StairDescentCameraFloodTests } [Fact] - public void Descent_RealCameraSweep_StairCellRetention() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_Descent_RealCameraSweep_StairCellRetention() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } diff --git a/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs b/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs index d026eb4d..c1da7914 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs @@ -56,6 +56,7 @@ public class Issue181VisFlapReplayTests => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}")); [Fact] + [Trait("Purpose", "Diagnostic")] public void Diagnostic_FlappingCellViewRegion_SliverOrLarge() { var datDir = CornerFloodReplayTests.ResolveDatDir(); diff --git a/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs b/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs index 0234a96c..3930723d 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue181WallPressEquilibriumTests.cs @@ -32,6 +32,7 @@ namespace AcDream.App.Tests.Rendering; /// numbers into hard pins. /// [Collection(CameraDiagnosticsCollection.Name)] +[Trait("Purpose", "Diagnostic")] public class Issue181WallPressEquilibriumTests { private const uint FacilityHubLandblock = 0x8A020000u; diff --git a/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs index 4e6c9b58..afee951e 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FaPanelSlotProbeTests.cs @@ -14,6 +14,7 @@ namespace AcDream.App.Tests.UI.Layout; /// identify the two FA slots definitively: 0x1000026F (fellowship name field) /// and 0x10000263 (swear-allegiance button). Env-gated like the other probes. /// +[Trait("Purpose", "Diagnostic")] public sealed class FaPanelSlotProbeTests { [Fact] diff --git a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs index 79cfce5e..e9696d0f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/MapHousePanelSlotProbeTests.cs @@ -25,6 +25,7 @@ namespace AcDream.App.Tests.UI.Layout; /// throwaway. /// /// +[Trait("Purpose", "Diagnostic")] public sealed class MapHousePanelSlotProbeTests { private const uint HostLayoutId = 0x2100006Eu; diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs index d5c8e694..540ab27d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs @@ -19,6 +19,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class OptionsPanelLiveMountProbeTests { [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbeLiveMountShapes() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1") @@ -314,6 +315,7 @@ public sealed class OptionsPanelLiveMountProbeTests /// FIXED 6-row + scrollbar popup in retail, user-gated during the vendor /// campaign) as the contrast control. [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbeMenuPopupSizingAndTextStyle() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1") @@ -479,6 +481,7 @@ public sealed class OptionsPanelLiveMountProbeTests /// to resolve in table 0x23000003 — sweep the plausible tables and key /// spellings against the live DAT to find their real home. [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbeFilterLabelHome() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1") diff --git a/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs index 4eb14762..7a79ea84 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs @@ -18,6 +18,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class PowerbarLayoutProbeTests { [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbePowerbarAuthoredStrings() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1") @@ -75,6 +76,7 @@ public sealed class PowerbarLayoutProbeTests /// gmSecureTradeUI's element tree (lane A gave element ids 0x10000085-8B /// but not the layout id) and dump it. [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbeSecureTradeLayout() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1") @@ -107,6 +109,7 @@ public sealed class PowerbarLayoutProbeTests /// ID_SecureTrade_TotalItemsLabel's fragments + variable hashes so the /// count text can use ResolveTemplate if token-free (AD-95). [Fact] + [Trait("Purpose", "Diagnostic")] public void ProbeTotalItemsTemplate() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1") diff --git a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs index e7b5f2ef..6d47c7ec 100644 --- a/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs +++ b/tests/AcDream.App.Tests/UI/SpewBoxLayoutDumpDiagnostic.cs @@ -75,6 +75,7 @@ namespace AcDream.App.Tests.UI; /// States dictionary was not walked) remain open. /// /// +[Trait("Purpose", "Diagnostic")] public sealed class SpewBoxLayoutDumpDiagnostic { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs b/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs index 0f6de976..f9f1e24d 100644 --- a/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs +++ b/tests/AcDream.Core.Tests/Audio/EnvCellSoundEmitterInventoryTests.cs @@ -127,7 +127,8 @@ public sealed class EnvCellSoundEmitterInventoryTests } [Fact] - public void PortalDat_SetupsWithAmbientSlotSoundTables() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_PortalDat_SetupsWithAmbientSlotSoundTables() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) @@ -184,7 +185,8 @@ public sealed class EnvCellSoundEmitterInventoryTests } [Fact] - public void SoundTables_WithAmbientSlots_ExistForWireBinding() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_SoundTables_WithAmbientSlots_ExistForWireBinding() { string? datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) diff --git a/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs b/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs index f45d7654..43276346 100644 --- a/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/CottageDoorwayCharacterizationTests.cs @@ -21,6 +21,7 @@ public class CottageDoorwayCharacterizationTests public CottageDoorwayCharacterizationTests(ITestOutputHelper output) => _out = output; [Fact] + [Trait("Purpose", "Diagnostic")] public void Characterize_CottageNeighborhood_PrintStructure() { var datDir = ConformanceDats.ResolveDatDir(); @@ -65,6 +66,7 @@ public class CottageDoorwayCharacterizationTests public const uint OutdoorLandcell0031 = 0xA9B40031u; [Fact] + [Trait("Purpose", "Diagnostic")] public void Characterize_Doorway_FindInteriorPoints() { var datDir = ConformanceDats.ResolveDatDir(); diff --git a/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs index e1fe5a96..70d1307c 100644 --- a/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/DungeonLandblockDatProbeTests.cs @@ -16,6 +16,7 @@ namespace AcDream.Core.Tests.Conformance; /// landblock have a LandBlock (0xXXYYFFFF) terrain record at all, or only /// LandBlockInfo + EnvCells? Output-only — no assertions. /// +[Trait("Purpose", "Diagnostic")] public sealed class DungeonLandblockDatProbeTests { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs b/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs index 1d3a7a41..dfd8ec02 100644 --- a/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/HoltburgTorchFalloffProbeTests.cs @@ -20,6 +20,7 @@ namespace AcDream.Core.Tests.Conformance; /// these falloffs ARE what retail reads (modulo any load-time transform, settled /// separately in the decomp). Output-only — no assertions; read the log. /// +[Trait("Purpose", "Diagnostic")] public sealed class HoltburgTorchFalloffProbeTests { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs index 90f37b7f..010b13c5 100644 --- a/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/Issue113DoorVanishDiagnosticTests.cs @@ -70,6 +70,7 @@ public sealed class Issue113DoorVanishDiagnosticTests /// vertical door-sized quads. /// [Fact] + [Trait("Purpose", "Diagnostic")] public void DumpHoltburgBuildings_OrphanGeometry() { var datDir = ConformanceDats.ResolveDatDir(); @@ -191,7 +192,8 @@ public sealed class Issue113DoorVanishDiagnosticTests /// must move. /// [Fact] - public void ReplicateProductionEmission_OnPortalFills() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_ReplicateProductionEmission_OnPortalFills() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } @@ -237,6 +239,7 @@ public sealed class Issue113DoorVanishDiagnosticTests /// which is exactly why retail shows doors but not the ramp. /// [Fact] + [Trait("Purpose", "Diagnostic")] public void DumpPortalFillSurfaceTypes() { var datDir = ConformanceDats.ResolveDatDir(); @@ -282,6 +285,7 @@ public sealed class Issue113DoorVanishDiagnosticTests /// the phantom geometry, for type-model comparison against the door. /// [Fact] + [Trait("Purpose", "Diagnostic")] public void DumpControls_HallAndCottage() { var datDir = ConformanceDats.ResolveDatDir(); diff --git a/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs index c0aa6cff..91545d8c 100644 --- a/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/Issue113PhantomStairsDumpTests.cs @@ -287,6 +287,7 @@ public sealed class Issue113PhantomStairsDumpTests /// sphere STRADDLES this plane (|dist| < radius + 0.0002, pc:310090-310120). /// [Fact] + [Trait("Purpose", "Diagnostic")] public void Dump_Cell104_ExteriorPortalPlane_Vs_GapPoint() { var datDir = ConformanceDats.ResolveDatDir(); @@ -559,6 +560,7 @@ public sealed class Issue113PhantomStairsDumpTests /// printed top→bottom DESCENDING (north up). /// [Fact] + [Trait("Purpose", "Diagnostic")] public void DumpAAB3_Watchtower_TopDownMap() { var datDir = ConformanceDats.ResolveDatDir(); @@ -679,6 +681,7 @@ public sealed class Issue113PhantomStairsDumpTests /// retail skips and our building-mesh extraction mishandles? /// [Fact] + [Trait("Purpose", "Diagnostic")] public void DumpHallModel_PolyFlagHistogram() { var datDir = ConformanceDats.ResolveDatDir(); diff --git a/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs b/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs index 0f7717db..860b9946 100644 --- a/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs +++ b/tests/AcDream.Core.Tests/Conformance/ThresholdDivergenceDiagnosticTests.cs @@ -13,6 +13,7 @@ namespace AcDream.Core.Tests.Conformance; /// FindCellList result — so the P0→P1 divergence is precisely characterized /// (real pick-criterion divergence vs a test/sphere artifact). /// +[Trait("Purpose", "Diagnostic")] public class ThresholdDivergenceDiagnosticTests { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs b/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs index 7977211b..ff5cf518 100644 --- a/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs +++ b/tests/AcDream.Core.Tests/Physics/CameraCornerSealReplayTests.cs @@ -129,6 +129,7 @@ public class CameraCornerSealReplayTests /// shadow-query path is the half that failed). /// [Fact] + [Trait("Purpose", "Diagnostic")] public void Diagnostic_DispatchTrace_LeakPath_vs_Controls() { var datDir = ConformanceDats.ResolveDatDir(); diff --git a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs index 4bd1bac0..f3e72b9a 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs @@ -237,6 +237,7 @@ public class DoorSetupGfxObjInspectionTests // computes a different outdoor cell than 0xA9B40029 // Findings drive the fix direction. [Fact] + [Trait("Purpose", "Diagnostic")] public void HoltburgCottage_CellPortals_DatInspection() { var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") @@ -271,6 +272,7 @@ public class DoorSetupGfxObjInspectionTests /// building wall sub-piece) lives at that XY. /// [Fact] + [Trait("Purpose", "Diagnostic")] public void HoltburgLandblockStatics_DatInspection() { var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") diff --git a/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs index f901d4ca..6a63d73c 100644 --- a/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamInspectionTests.cs @@ -280,6 +280,7 @@ public class Issue137CorridorSeamInspectionTests /// hit point + normal. /// [Fact] + [Trait("Purpose", "Diagnostic")] public void CorridorSeam_FindPolygonMatchingLiveHit() { var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") diff --git a/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs index 57e86e70..dcf8d68f 100644 --- a/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs @@ -33,6 +33,7 @@ public class Issue186ConnectorCellGeometryInspectionTests } [Fact] + [Trait("Purpose", "Diagnostic")] public void Dump_ConnectorCells_ShellAndCollision() { var datDir = DatDir(); diff --git a/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs index 289c97c4..a78498d1 100644 --- a/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/Issue188FadingDoorMotionTableInspectionTests.cs @@ -25,6 +25,7 @@ namespace AcDream.Core.Tests.Physics; /// wired). Kept as a reusable decoder for any future "why doesn't this /// animate" question — swap the MotionTableId. /// +[Trait("Purpose", "Diagnostic")] public class Issue188FadingDoorMotionTableInspectionTests { private readonly ITestOutputHelper _out; diff --git a/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs b/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs index 5013fb1d..54064199 100644 --- a/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Issue176177DungeonSeamInspectionTests.cs @@ -244,7 +244,8 @@ public class Issue176177DungeonSeamInspectionTests /// light exposed it. /// [Fact] - public void CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs() + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_CorridorNeighborhood_CoplanarOverlappingDrawnPolyPairs() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -470,7 +471,8 @@ public class Issue176177DungeonSeamInspectionTests [InlineData(0x8A02016Eu)] // corridor cells — the striped-floor screenshot area [InlineData(0x8A02017Au)] [InlineData(0x8A020165u)] - public void UnderHall_DrawnPolys_SurfaceColors(uint cellId) + [Trait("Purpose", "Diagnostic")] + public void Diagnostic_UnderHall_DrawnPolys_SurfaceColors(uint cellId) { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } @@ -635,6 +637,7 @@ public class Issue176177DungeonSeamInspectionTests [Theory] [InlineData(0x8A02015Eu)] // #176 repro corridor [InlineData(0x8A02016Eu)] // corridor with floor-portals + [Trait("Purpose", "Diagnostic")] public void CellVertexNormals_SmoothOrFaceted_Dump(uint cellId) { var datDir = ResolveDatDir(); diff --git a/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs b/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs index 9b6a228b..cc820caf 100644 --- a/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Issue93TownNetworkFountainRoomLightInspectionTests.cs @@ -50,6 +50,7 @@ public class Issue93TownNetworkFountainRoomLightInspectionTests [InlineData(0x00070146u)] [InlineData(0x00070155u)] [InlineData(0x00070157u)] + [Trait("Purpose", "Diagnostic")] public void StaticObjects_SetupLightsCount_Dump(uint cellId) { var datDir = ResolveDatDir(); diff --git a/tools/audit-test-inventory.ps1 b/tools/audit-test-inventory.ps1 new file mode 100644 index 00000000..5eb93737 --- /dev/null +++ b/tools/audit-test-inventory.ps1 @@ -0,0 +1,374 @@ +<# +.SYNOPSIS + Produces a syntax-based inventory of every attributed C# test method. + +.DESCRIPTION + Parses tracked C# files under tests/ with the Roslyn assemblies bundled in + the active .NET SDK. The report is evidence for the R3 test-truth audit: it + records test attributes and traits, static skips, empty-return sites, + assertion/throw signals (including same-file helper calls), diagnostic + output signals, wall-clock waits, environment-variable dependencies, and + source-text reads. + + This is a candidate generator, not a semantic proof. In particular, an + output-only candidate can call a failure-capable helper declared in another + file or base class. Every removal or lane change still requires inspection. +#> +[CmdletBinding()] +param( + [string]$OutputPath = 'artifacts/test-audit/test-inventory.json' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = [IO.Path]::GetFullPath((Split-Path -Parent $PSScriptRoot)) +$resolvedOutput = if ([IO.Path]::IsPathRooted($OutputPath)) { + [IO.Path]::GetFullPath($OutputPath) +} else { + [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath)) +} + +$dotnetCommand = Get-Command dotnet -ErrorAction Stop +$dotnetRoot = Split-Path -Parent $dotnetCommand.Source +$sdkVersion = (& dotnet --version).Trim() +$roslynDirectory = Join-Path $dotnetRoot "sdk/$sdkVersion/Roslyn/bincore" +$loadedRoslyn = @([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { + $_.GetName().Name -in @('Microsoft.CodeAnalysis', 'Microsoft.CodeAnalysis.CSharp') + }) +if ($loadedRoslyn.Count -lt 2) { + foreach ($assemblyName in 'Microsoft.CodeAnalysis.dll', 'Microsoft.CodeAnalysis.CSharp.dll') { + $assemblyPath = Join-Path $roslynDirectory $assemblyName + if (-not (Test-Path -LiteralPath $assemblyPath -PathType Leaf)) { + throw "Roslyn assembly not found at '$assemblyPath'." + } + Add-Type -Path $assemblyPath + } +} +$roslynVersion = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree].Assembly.GetName().Version.ToString() + +function Get-ShortAttributeName { + param([Parameter(Mandatory)]$Attribute) + + $name = $Attribute.Name.ToString().Split('.')[-1] + if ($name.EndsWith('Attribute', [StringComparison]::Ordinal)) { + $name = $name.Substring(0, $name.Length - 'Attribute'.Length) + } + return $name +} + +function Get-NodeLine { + param( + [Parameter(Mandatory)]$Tree, + [Parameter(Mandatory)]$Node + ) + + return $Tree.GetLineSpan($Node.Span).StartLinePosition.Line + 1 +} + +function Get-ContainingClassName { + param([Parameter(Mandatory)]$Method) + + $typeNode = Get-ContainingTypeNode $Method + if ($null -ne $typeNode) { + return $typeNode.Identifier.Text + } + return '' +} + +function Get-ContainingTypeNode { + param([Parameter(Mandatory)]$Method) + + $cursor = $Method.Parent + while ($null -ne $cursor) { + if ($cursor.GetType().Name -in @( + 'ClassDeclarationSyntax', + 'RecordDeclarationSyntax', + 'StructDeclarationSyntax')) { + return $cursor + } + $cursor = $cursor.Parent + } + return $null +} + +function Get-ReturnGateKind { + param([Parameter(Mandatory)][string]$Condition) + + if ($Condition -match '(?i)(GetEnvironmentVariable|ACDREAM_)') { + return 'OptIn' + } + if ($Condition -match '(?i)(OperatingSystem\.)') { + return 'Platform' + } + if ($Condition -match '(?i)(datDir|datDirectory|Directory\.Exists|File\.Exists|TryOpen|TryBuildScenario|steps is null|csvPath|\.pak)') { + return 'ExternalAsset' + } + return $null +} + +function Get-InvocationName { + param([Parameter(Mandatory)]$Invocation) + + $expression = $Invocation.Expression + if ($expression.GetType().Name -eq 'MemberAccessExpressionSyntax') { + return $expression.Name.Identifier.Text + } + if ($expression.GetType().Name -eq 'IdentifierNameSyntax') { + return $expression.Identifier.Text + } + if ($expression.GetType().Name -eq 'GenericNameSyntax') { + return $expression.Identifier.Text + } + return $expression.ToString() +} + +function Test-DirectFailureSignal { + param([Parameter(Mandatory)]$Method) + + if (@($Method.DescendantNodes() | Where-Object { + $_.GetType().Name -in @('ThrowStatementSyntax', 'ThrowExpressionSyntax') + }).Count -gt 0) { + return $true + } + + foreach ($invocation in @($Method.DescendantNodes() | Where-Object { + $_.GetType().Name -eq 'InvocationExpressionSyntax' + })) { + $expression = $invocation.Expression.ToString() + if ($expression -match '(^|\.)Assert(\.|$)' -or + $expression -match '(^|\.)(Should|Shouldly)(\.|$)' -or + $expression -match '(^|\.)Verify($|\.)') { + return $true + } + } + return $false +} + +function Test-DirectOutputSignal { + param([Parameter(Mandatory)]$Method) + + foreach ($invocation in @($Method.DescendantNodes() | Where-Object { + $_.GetType().Name -eq 'InvocationExpressionSyntax' + })) { + $expression = $invocation.Expression.ToString() + if ($expression -match '(^|\.)(_?out|output|Console|Debug|Trace)\.(Write|WriteLine)$' -or + $expression -match '(^|\.)(WriteDiagnostic|Dump|DumpSteps|Print)(\.|$)') { + return $true + } + } + return $false +} + +function Test-RecursiveSignal { + param( + [Parameter(Mandatory)][string]$MethodKey, + [Parameter(Mandatory)][hashtable]$MethodMap, + [Parameter(Mandatory)][hashtable]$DirectSignalMap, + [Parameter(Mandatory)][hashtable]$CallMap, + [Parameter(Mandatory)][hashtable]$Visited + ) + + if ($Visited.ContainsKey($MethodKey)) { + return $false + } + $Visited[$MethodKey] = $true + if ($DirectSignalMap[$MethodKey]) { + return $true + } + foreach ($calledName in @($CallMap[$MethodKey])) { + foreach ($candidateKey in @($MethodMap.Keys | Where-Object { + $_.EndsWith("::$calledName", [StringComparison]::Ordinal) + })) { + if (Test-RecursiveSignal $candidateKey $MethodMap $DirectSignalMap $CallMap $Visited) { + return $true + } + } + } + return $false +} + +$trackedFiles = @(& git -C $repoRoot ls-files tests | Where-Object { + $_.EndsWith('.cs', [StringComparison]::OrdinalIgnoreCase) + }) +if ($LASTEXITCODE -ne 0) { + throw 'git ls-files failed while enumerating test sources.' +} + +$records = [Collections.Generic.List[object]]::new() +$parsedFileCount = 0 +foreach ($relativePath in $trackedFiles) { + $absolutePath = Join-Path $repoRoot $relativePath + if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) { + continue + } + + $source = [IO.File]::ReadAllText($absolutePath) + $tree = [Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($source) + $root = $tree.GetRoot() + $parsedFileCount++ + + $allMethods = @($root.DescendantNodes() | Where-Object { + $_.GetType().Name -eq 'MethodDeclarationSyntax' + }) + $methodMap = @{} + $failureMap = @{} + $outputMap = @{} + $callMap = @{} + + foreach ($method in $allMethods) { + $className = Get-ContainingClassName $method + $key = "$className::$($method.Identifier.Text)" + $methodMap[$key] = $method + $failureMap[$key] = Test-DirectFailureSignal $method + $outputMap[$key] = Test-DirectOutputSignal $method + $callMap[$key] = @($method.DescendantNodes() | + Where-Object { $_.GetType().Name -eq 'InvocationExpressionSyntax' } | + ForEach-Object { Get-InvocationName $_ } | + Sort-Object -Unique) + } + + foreach ($method in $allMethods) { + $methodAttributes = @($method.AttributeLists | ForEach-Object { $_.Attributes }) + $attributeNames = @($methodAttributes | ForEach-Object { + Get-ShortAttributeName $_ + }) + $testAttributes = @($attributeNames | Where-Object { + $_ -in @('Fact', 'Theory', 'AvaloniaFact', 'InstalledDatFact') + }) + if ($testAttributes.Count -eq 0) { + continue + } + + $className = Get-ContainingClassName $method + $key = "$className::$($method.Identifier.Text)" + $hasFailureSignal = Test-RecursiveSignal ` + $key $methodMap $failureMap $callMap @{} + $hasOutputSignal = Test-RecursiveSignal ` + $key $methodMap $outputMap $callMap @{} + + $emptyReturns = [Collections.Generic.List[object]]::new() + foreach ($returnNode in @($method.DescendantNodes() | Where-Object { + $_.GetType().Name -eq 'ReturnStatementSyntax' -and + $null -eq $_.Expression + })) { + $condition = '' + $cursor = $returnNode.Parent + while ($null -ne $cursor -and $cursor -ne $method) { + if ($cursor.GetType().Name -eq 'IfStatementSyntax') { + $condition = $cursor.Condition.ToString() + break + } + $cursor = $cursor.Parent + } + $emptyReturns.Add([ordered]@{ + Line = Get-NodeLine $tree $returnNode + Condition = $condition + GateKind = Get-ReturnGateKind $condition + }) + } + + $traits = [Collections.Generic.List[string]]::new() + $staticSkip = $null + $typeNode = Get-ContainingTypeNode $method + $traitAttributes = @($methodAttributes) + if ($null -ne $typeNode) { + $traitAttributes += @($typeNode.AttributeLists | ForEach-Object { + $_.Attributes + }) + } + foreach ($attribute in $traitAttributes) { + $attributeName = Get-ShortAttributeName $attribute + $arguments = if ($null -eq $attribute.ArgumentList) { + '' + } else { + $attribute.ArgumentList.ToString() + } + if ($attributeName -eq 'Trait') { + $traits.Add($arguments.Trim('(', ')').Replace('"', '')) + } + } + foreach ($attribute in $methodAttributes) { + $arguments = if ($null -eq $attribute.ArgumentList) { + '' + } else { + $attribute.ArgumentList.ToString() + } + if ($arguments -match '(^|[,\s])Skip\s*=') { + $staticSkip = $arguments + } + } + + $bodyText = $method.ToString() + $environmentVariables = @([regex]::Matches( + $bodyText, + 'GetEnvironmentVariable\s*\(\s*"([A-Za-z0-9_]+)"') | + ForEach-Object { $_.Groups[1].Value } | + Sort-Object -Unique) + + $records.Add([ordered]@{ + Path = $relativePath.Replace('\', '/') + Line = Get-NodeLine $tree $method + Class = $className + Method = $method.Identifier.Text + Attributes = $testAttributes + Traits = @($traits) + StaticSkip = $staticSkip + EmptyReturns = @($emptyReturns) + PrerequisiteReturnCandidate = @($emptyReturns | Where-Object { + $null -ne $_.GateKind + }).Count -gt 0 + HasFailureSignal = $hasFailureSignal + HasOutputSignal = $hasOutputSignal + OutputOnlyCandidate = $hasOutputSignal -and -not $hasFailureSignal + EnvironmentVariables = $environmentVariables + HasThreadSleep = $bodyText -match '\bThread\.Sleep\s*\(' + HasTaskDelay = $bodyText -match '\bTask\.Delay\s*\(' + ReadsSourceText = $bodyText -match '(ReadAllText|ReadAllLines)\s*\(' -and + $bodyText -match '\.cs' + }) + } +} + +$orderedRecords = @($records | Sort-Object Path, Line, Method) +$workingTreeStatus = @(& git -C $repoRoot status --short --untracked-files=no) +$summary = [ordered]@{ + GeneratedUtc = [DateTime]::UtcNow.ToString('O') + RepositoryCommit = (& git -C $repoRoot rev-parse HEAD).Trim() + WorkingTreeDirty = $workingTreeStatus.Count -gt 0 + DotnetSdkVersion = $sdkVersion + RoslynAssemblyVersion = $roslynVersion + TrackedCSharpFiles = $trackedFiles.Count + ParsedCSharpFiles = $parsedFileCount + AttributedTestMethods = $orderedRecords.Count + StaticSkipMethods = @($orderedRecords | Where-Object { $null -ne $_.StaticSkip }).Count + EmptyReturnMethods = @($orderedRecords | Where-Object { $_.EmptyReturns.Count -gt 0 }).Count + EmptyReturnSites = @($orderedRecords | ForEach-Object { $_.EmptyReturns }).Count + PrerequisiteReturnCandidates = @($orderedRecords | Where-Object { + $_.PrerequisiteReturnCandidate + }).Count + OutputOnlyCandidates = @($orderedRecords | Where-Object { $_.OutputOnlyCandidate }).Count + DiagnosticMethods = @($orderedRecords | Where-Object { + $_.Traits -contains 'Purpose, Diagnostic' + }).Count + KnownFailureMethods = @($orderedRecords | Where-Object { + $_.Traits -contains 'Status, KnownFailure' + }).Count + ThreadSleepMethods = @($orderedRecords | Where-Object { $_.HasThreadSleep }).Count + TaskDelayMethods = @($orderedRecords | Where-Object { $_.HasTaskDelay }).Count + DirectEnvironmentVariableMethods = @($orderedRecords | Where-Object { + $_.EnvironmentVariables.Count -gt 0 + }).Count + SourceTextReadMethods = @($orderedRecords | Where-Object { $_.ReadsSourceText }).Count +} + +$report = [ordered]@{ + Summary = $summary + Tests = $orderedRecords +} +$outputDirectory = Split-Path -Parent $resolvedOutput +[IO.Directory]::CreateDirectory($outputDirectory) | Out-Null +$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $resolvedOutput -Encoding utf8 + +$summary | Format-List +Write-Output "Inventory: $resolvedOutput"