test: separate diagnostic apparatus from release gates
This commit is contained in:
parent
8f490240d4
commit
c1a905004a
33 changed files with 527 additions and 24 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 +
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -541,6 +541,7 @@ public class CornerFloodReplayTests
|
|||
/// <summary>Scratch: the homogeneous reciprocal primitive in isolation, on the
|
||||
/// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses.</summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void Scratch_ReciprocalPrimitive_SyntheticPair()
|
||||
{
|
||||
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public class Issue131SetupProbeTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[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).
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace AcDream.App.Tests.Rendering;
|
|||
/// numbers into hard pins.
|
||||
/// </summary>
|
||||
[Collection(CameraDiagnosticsCollection.Name)]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public class Issue181WallPressEquilibriumTests
|
||||
{
|
||||
private const uint FacilityHubLandblock = 0x8A020000u;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public sealed class FaPanelSlotProbeTests
|
||||
{
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
/// throwaway.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public sealed class MapHousePanelSlotProbeTests
|
||||
{
|
||||
private const uint HostLayoutId = 0x2100006Eu;
|
||||
|
|
|
|||
|
|
@ -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.</summary>
|
||||
[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.</summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void ProbeFilterLabelHome()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
|
||||
|
|
|
|||
|
|
@ -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.</summary>
|
||||
[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).</summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void ProbeTotalItemsTemplate()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ namespace AcDream.App.Tests.UI;
|
|||
/// <c>States</c> dictionary was not walked) remain open.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public sealed class SpewBoxLayoutDumpDiagnostic
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public sealed class DungeonLandblockDatProbeTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public sealed class HoltburgTorchFalloffProbeTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ public sealed class Issue113DoorVanishDiagnosticTests
|
|||
/// vertical door-sized quads.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void DumpHoltburgBuildings_OrphanGeometry()
|
||||
{
|
||||
var datDir = ConformanceDats.ResolveDatDir();
|
||||
|
|
@ -191,7 +192,8 @@ public sealed class Issue113DoorVanishDiagnosticTests
|
|||
/// must move.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void DumpControls_HallAndCottage()
|
||||
{
|
||||
var datDir = ConformanceDats.ResolveDatDir();
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ public sealed class Issue113PhantomStairsDumpTests
|
|||
/// sphere STRADDLES this plane (|dist| < radius + 0.0002, pc:310090-310120).
|
||||
/// </summary>
|
||||
[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).
|
||||
/// </summary>
|
||||
[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?
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void DumpHallModel_PolyFlagHistogram()
|
||||
{
|
||||
var datDir = ConformanceDats.ResolveDatDir();
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public class ThresholdDivergenceDiagnosticTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ public class CameraCornerSealReplayTests
|
|||
/// shadow-query path is the half that failed).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void Diagnostic_DispatchTrace_LeakPath_vs_Controls()
|
||||
{
|
||||
var datDir = ConformanceDats.ResolveDatDir();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void HoltburgLandblockStatics_DatInspection()
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ public class Issue137CorridorSeamInspectionTests
|
|||
/// hit point + normal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void CorridorSeam_FindPolygonMatchingLiveHit()
|
||||
{
|
||||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public class Issue186ConnectorCellGeometryInspectionTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public void Dump_ConnectorCells_ShellAndCollision()
|
||||
{
|
||||
var datDir = DatDir();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Trait("Purpose", "Diagnostic")]
|
||||
public class Issue188FadingDoorMotionTableInspectionTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
|
|
|
|||
|
|
@ -244,7 +244,8 @@ public class Issue176177DungeonSeamInspectionTests
|
|||
/// light exposed it.
|
||||
/// </summary>
|
||||
[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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
374
tools/audit-test-inventory.ps1
Normal file
374
tools/audit-test-inventory.ps1
Normal file
|
|
@ -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 '<global>'
|
||||
}
|
||||
|
||||
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 = '<unconditional>'
|
||||
$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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue