From fdab502b42393504df837a79fe3a07e94d11126d Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 3 Sep 2026 14:30:11 -0400 Subject: [PATCH] Revert "Add new SOS test harness (#5979)" This reverts commit bf833ee69df37bbe99e017213eff91a595516680. --- build.sln | 107 +--- src/tests/Debuggees.proj | 35 - src/tests/SOS.TestHarness.Capturer/Program.cs | 8 - .../SOS.TestHarness.Capturer.csproj | 17 - .../SOS.TestHarness.EngineHost/Program.cs | 129 ---- .../SOS.TestHarness.EngineHost.csproj | 17 - .../MirrorGenerator.cs | 174 ----- .../SOS.TestHarness.SourceGen.csproj | 17 - src/tests/SOS.TestHarness/CaptureCli.cs | 46 -- .../SOS.TestHarness/ChildEngineClient.cs | 220 ------- src/tests/SOS.TestHarness/CoreVersion.cs | 52 -- src/tests/SOS.TestHarness/CoreVersions.cs | 176 ----- src/tests/SOS.TestHarness/Dac.cs | 28 - src/tests/SOS.TestHarness/DacPolicy.cs | 27 - src/tests/SOS.TestHarness/DbgEngCapturer.cs | 127 ---- src/tests/SOS.TestHarness/DbgEngDumpHost.cs | 32 - src/tests/SOS.TestHarness/DbgEngHostBase.cs | 232 ------- src/tests/SOS.TestHarness/DbgEngLiveHost.cs | 175 ----- src/tests/SOS.TestHarness/DeadTarget.cs | 49 -- src/tests/SOS.TestHarness/DotNetDumpHost.cs | 252 -------- .../DumpGenerationRequirements.cs | 101 --- src/tests/SOS.TestHarness/DumpKind.cs | 31 - src/tests/SOS.TestHarness/DumpSession.cs | 152 ----- src/tests/SOS.TestHarness/EngineProtocol.cs | 27 - src/tests/SOS.TestHarness/Flavor.cs | 26 - src/tests/SOS.TestHarness/GcType.cs | 23 - .../SOS.TestHarness/HarnessCancellation.cs | 27 - src/tests/SOS.TestHarness/HarnessSkip.cs | 27 - src/tests/SOS.TestHarness/HostDiagnostics.cs | 172 ----- src/tests/SOS.TestHarness/HostFactory.cs | 37 -- src/tests/SOS.TestHarness/HostSlot.cs | 123 ---- src/tests/SOS.TestHarness/Hosts.cs | 23 - src/tests/SOS.TestHarness/IDebuggerHost.cs | 36 -- src/tests/SOS.TestHarness/IDiagnosticHost.cs | 16 - .../SOS.TestHarness/ILiveDebuggerHost.cs | 31 - src/tests/SOS.TestHarness/LiveTarget.cs | 164 ----- src/tests/SOS.TestHarness/Liveness.cs | 12 - src/tests/SOS.TestHarness/LldbCliHost.cs | 72 --- src/tests/SOS.TestHarness/LldbHostBase.cs | 398 ------------ src/tests/SOS.TestHarness/LldbLiveHost.cs | 257 -------- src/tests/SOS.TestHarness/ReplayContext.cs | 129 ---- src/tests/SOS.TestHarness/RepoLayout.cs | 136 ---- .../SOS.TestHarness/SOS.TestHarness.csproj | 48 -- src/tests/SOS.TestHarness/SnapshotStore.cs | 534 ---------------- .../SOS.TestHarness/SosAssertException.cs | 33 - src/tests/SOS.TestHarness/SosCell.cs | 83 --- src/tests/SOS.TestHarness/SosColumn.cs | 56 -- src/tests/SOS.TestHarness/SosConvert.cs | 84 --- src/tests/SOS.TestHarness/SosDataExtractor.cs | 15 - src/tests/SOS.TestHarness/SosDataRow.cs | 54 -- .../SOS.TestHarness/SosDataRowExtensions.cs | 46 -- src/tests/SOS.TestHarness/SosField.cs | 133 ---- src/tests/SOS.TestHarness/SosOutput.cs | 424 ------------ src/tests/SOS.TestHarness/SosRow.cs | 94 --- src/tests/SOS.TestHarness/SosTable.cs | 140 ---- src/tests/SOS.TestHarness/SosToken.cs | 111 ---- src/tests/SOS.TestHarness/Target.cs | 106 --- src/tests/SOS.TestHarness/TargetCatalog.cs | 183 ------ src/tests/SOS.TestHarness/Targets.cs | 105 --- src/tests/SOS.TestHarness/TestConfig.cs | 353 ---------- src/tests/SOS.TestHarness/ToolPaths.cs | 435 ------------- src/tests/SOS.TestHarness/lldbhelper.py | 41 -- src/tests/SOS.Tests/COVERAGE.md | 126 ---- .../SOS.Tests/ClrStackAllThreadsTests.cs | 58 -- .../SOS.Tests/ClrStackArgsLocalsTests.cs | 120 ---- .../SOS.Tests/ClrStackFrameCountTests.cs | 59 -- src/tests/SOS.Tests/ClrStackFullTests.cs | 78 --- src/tests/SOS.Tests/ClrStackICorDebugTests.cs | 87 --- src/tests/SOS.Tests/ClrStackLinesTests.cs | 116 ---- src/tests/SOS.Tests/ClrStackTests.cs | 198 ------ src/tests/SOS.Tests/ClrThreadsParsing.cs | 109 ---- src/tests/SOS.Tests/ClrThreadsTests.cs | 62 -- src/tests/SOS.Tests/ClrUTests.cs | 100 --- src/tests/SOS.Tests/CodeCommandParsing.cs | 291 --------- src/tests/SOS.Tests/CodeInfoTests.cs | 79 --- src/tests/SOS.Tests/CollectionQueueTests.cs | 43 -- src/tests/SOS.Tests/DiagnosticCommandTests.cs | 90 --- src/tests/SOS.Tests/DumpArrayTests.cs | 79 --- src/tests/SOS.Tests/DumpDelegateTests.cs | 60 -- src/tests/SOS.Tests/DumpDomainTests.cs | 102 --- .../SOS.Tests/DumpHeapGenerationsTests.cs | 100 --- src/tests/SOS.Tests/DumpHeapLiveDeadTests.cs | 46 -- src/tests/SOS.Tests/DumpHeapObjectsTests.cs | 93 --- src/tests/SOS.Tests/DumpHeapParsing.cs | 292 --------- src/tests/SOS.Tests/DumpHeapStringsTests.cs | 44 -- src/tests/SOS.Tests/DumpHeapThinLockTests.cs | 45 -- src/tests/SOS.Tests/DumpIlTests.cs | 57 -- src/tests/SOS.Tests/DumpModuleTests.cs | 80 --- src/tests/SOS.Tests/DumpSigTests.cs | 52 -- src/tests/SOS.Tests/EeHeapParsing.cs | 412 ------------ src/tests/SOS.Tests/EeHeapTests.cs | 95 --- src/tests/SOS.Tests/GcInspectionTests.cs | 153 ----- src/tests/SOS.Tests/GcWhereTests.cs | 68 -- src/tests/SOS.Tests/HeapAnalysisTests.cs | 61 -- src/tests/SOS.Tests/LiveBpmdTests.cs | 38 -- src/tests/SOS.Tests/MemoryAndDecodeTests.cs | 85 --- src/tests/SOS.Tests/MiscCommandTests.cs | 61 -- src/tests/SOS.Tests/ModuleCommandParsing.cs | 485 -------------- src/tests/SOS.Tests/ModuleResolveTests.cs | 69 -- .../SOS.Tests/NativeAddressSpaceTests.cs | 32 - src/tests/SOS.Tests/ObjectCommandParsing.cs | 428 ------------- src/tests/SOS.Tests/ObjectFieldsTests.cs | 69 -- src/tests/SOS.Tests/ObjectGcHelperTests.cs | 119 ---- src/tests/SOS.Tests/ObjectInspectionTests.cs | 139 ---- src/tests/SOS.Tests/PrintExceptionTests.cs | 68 -- src/tests/SOS.Tests/README.md | 221 ------- src/tests/SOS.Tests/RuntimeInfoTests.cs | 87 --- src/tests/SOS.Tests/SOS.Tests.csproj | 36 -- src/tests/SOS.Tests/SosReplayAttribute.cs | 248 ------- src/tests/SOS.Tests/SosTheoryAttribute.cs | 26 - .../SOS.Tests/SpecializedInspectionTests.cs | 106 --- src/tests/SOS.Tests/StackInspectionTests.cs | 80 --- src/tests/SOS.Tests/TargetExtensions.cs | 605 ------------------ src/tests/SOS.Tests/TestMatrices.cs | 139 ---- src/tests/SOS.Tests/TestObjectHelpers.cs | 27 - src/tests/SOS.Tests/WindowsTheoryAttribute.cs | 44 -- .../Debuggees/Directory.Build.props | 9 - .../SosHarnessScenarios.cs | 392 ------------ .../SosHarnessScenarios.csproj | 12 - .../SosHarnessScenarios/TestHarness.cs | 82 --- 120 files changed, 1 insertion(+), 14179 deletions(-) delete mode 100644 src/tests/SOS.TestHarness.Capturer/Program.cs delete mode 100644 src/tests/SOS.TestHarness.Capturer/SOS.TestHarness.Capturer.csproj delete mode 100644 src/tests/SOS.TestHarness.EngineHost/Program.cs delete mode 100644 src/tests/SOS.TestHarness.EngineHost/SOS.TestHarness.EngineHost.csproj delete mode 100644 src/tests/SOS.TestHarness.SourceGen/MirrorGenerator.cs delete mode 100644 src/tests/SOS.TestHarness.SourceGen/SOS.TestHarness.SourceGen.csproj delete mode 100644 src/tests/SOS.TestHarness/CaptureCli.cs delete mode 100644 src/tests/SOS.TestHarness/ChildEngineClient.cs delete mode 100644 src/tests/SOS.TestHarness/CoreVersion.cs delete mode 100644 src/tests/SOS.TestHarness/CoreVersions.cs delete mode 100644 src/tests/SOS.TestHarness/Dac.cs delete mode 100644 src/tests/SOS.TestHarness/DacPolicy.cs delete mode 100644 src/tests/SOS.TestHarness/DbgEngCapturer.cs delete mode 100644 src/tests/SOS.TestHarness/DbgEngDumpHost.cs delete mode 100644 src/tests/SOS.TestHarness/DbgEngHostBase.cs delete mode 100644 src/tests/SOS.TestHarness/DbgEngLiveHost.cs delete mode 100644 src/tests/SOS.TestHarness/DeadTarget.cs delete mode 100644 src/tests/SOS.TestHarness/DotNetDumpHost.cs delete mode 100644 src/tests/SOS.TestHarness/DumpGenerationRequirements.cs delete mode 100644 src/tests/SOS.TestHarness/DumpKind.cs delete mode 100644 src/tests/SOS.TestHarness/DumpSession.cs delete mode 100644 src/tests/SOS.TestHarness/EngineProtocol.cs delete mode 100644 src/tests/SOS.TestHarness/Flavor.cs delete mode 100644 src/tests/SOS.TestHarness/GcType.cs delete mode 100644 src/tests/SOS.TestHarness/HarnessCancellation.cs delete mode 100644 src/tests/SOS.TestHarness/HarnessSkip.cs delete mode 100644 src/tests/SOS.TestHarness/HostDiagnostics.cs delete mode 100644 src/tests/SOS.TestHarness/HostFactory.cs delete mode 100644 src/tests/SOS.TestHarness/HostSlot.cs delete mode 100644 src/tests/SOS.TestHarness/Hosts.cs delete mode 100644 src/tests/SOS.TestHarness/IDebuggerHost.cs delete mode 100644 src/tests/SOS.TestHarness/IDiagnosticHost.cs delete mode 100644 src/tests/SOS.TestHarness/ILiveDebuggerHost.cs delete mode 100644 src/tests/SOS.TestHarness/LiveTarget.cs delete mode 100644 src/tests/SOS.TestHarness/Liveness.cs delete mode 100644 src/tests/SOS.TestHarness/LldbCliHost.cs delete mode 100644 src/tests/SOS.TestHarness/LldbHostBase.cs delete mode 100644 src/tests/SOS.TestHarness/LldbLiveHost.cs delete mode 100644 src/tests/SOS.TestHarness/ReplayContext.cs delete mode 100644 src/tests/SOS.TestHarness/RepoLayout.cs delete mode 100644 src/tests/SOS.TestHarness/SOS.TestHarness.csproj delete mode 100644 src/tests/SOS.TestHarness/SnapshotStore.cs delete mode 100644 src/tests/SOS.TestHarness/SosAssertException.cs delete mode 100644 src/tests/SOS.TestHarness/SosCell.cs delete mode 100644 src/tests/SOS.TestHarness/SosColumn.cs delete mode 100644 src/tests/SOS.TestHarness/SosConvert.cs delete mode 100644 src/tests/SOS.TestHarness/SosDataExtractor.cs delete mode 100644 src/tests/SOS.TestHarness/SosDataRow.cs delete mode 100644 src/tests/SOS.TestHarness/SosDataRowExtensions.cs delete mode 100644 src/tests/SOS.TestHarness/SosField.cs delete mode 100644 src/tests/SOS.TestHarness/SosOutput.cs delete mode 100644 src/tests/SOS.TestHarness/SosRow.cs delete mode 100644 src/tests/SOS.TestHarness/SosTable.cs delete mode 100644 src/tests/SOS.TestHarness/SosToken.cs delete mode 100644 src/tests/SOS.TestHarness/Target.cs delete mode 100644 src/tests/SOS.TestHarness/TargetCatalog.cs delete mode 100644 src/tests/SOS.TestHarness/Targets.cs delete mode 100644 src/tests/SOS.TestHarness/TestConfig.cs delete mode 100644 src/tests/SOS.TestHarness/ToolPaths.cs delete mode 100644 src/tests/SOS.TestHarness/lldbhelper.py delete mode 100644 src/tests/SOS.Tests/COVERAGE.md delete mode 100644 src/tests/SOS.Tests/ClrStackAllThreadsTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackArgsLocalsTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackFrameCountTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackFullTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackICorDebugTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackLinesTests.cs delete mode 100644 src/tests/SOS.Tests/ClrStackTests.cs delete mode 100644 src/tests/SOS.Tests/ClrThreadsParsing.cs delete mode 100644 src/tests/SOS.Tests/ClrThreadsTests.cs delete mode 100644 src/tests/SOS.Tests/ClrUTests.cs delete mode 100644 src/tests/SOS.Tests/CodeCommandParsing.cs delete mode 100644 src/tests/SOS.Tests/CodeInfoTests.cs delete mode 100644 src/tests/SOS.Tests/CollectionQueueTests.cs delete mode 100644 src/tests/SOS.Tests/DiagnosticCommandTests.cs delete mode 100644 src/tests/SOS.Tests/DumpArrayTests.cs delete mode 100644 src/tests/SOS.Tests/DumpDelegateTests.cs delete mode 100644 src/tests/SOS.Tests/DumpDomainTests.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapGenerationsTests.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapLiveDeadTests.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapObjectsTests.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapParsing.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapStringsTests.cs delete mode 100644 src/tests/SOS.Tests/DumpHeapThinLockTests.cs delete mode 100644 src/tests/SOS.Tests/DumpIlTests.cs delete mode 100644 src/tests/SOS.Tests/DumpModuleTests.cs delete mode 100644 src/tests/SOS.Tests/DumpSigTests.cs delete mode 100644 src/tests/SOS.Tests/EeHeapParsing.cs delete mode 100644 src/tests/SOS.Tests/EeHeapTests.cs delete mode 100644 src/tests/SOS.Tests/GcInspectionTests.cs delete mode 100644 src/tests/SOS.Tests/GcWhereTests.cs delete mode 100644 src/tests/SOS.Tests/HeapAnalysisTests.cs delete mode 100644 src/tests/SOS.Tests/LiveBpmdTests.cs delete mode 100644 src/tests/SOS.Tests/MemoryAndDecodeTests.cs delete mode 100644 src/tests/SOS.Tests/MiscCommandTests.cs delete mode 100644 src/tests/SOS.Tests/ModuleCommandParsing.cs delete mode 100644 src/tests/SOS.Tests/ModuleResolveTests.cs delete mode 100644 src/tests/SOS.Tests/NativeAddressSpaceTests.cs delete mode 100644 src/tests/SOS.Tests/ObjectCommandParsing.cs delete mode 100644 src/tests/SOS.Tests/ObjectFieldsTests.cs delete mode 100644 src/tests/SOS.Tests/ObjectGcHelperTests.cs delete mode 100644 src/tests/SOS.Tests/ObjectInspectionTests.cs delete mode 100644 src/tests/SOS.Tests/PrintExceptionTests.cs delete mode 100644 src/tests/SOS.Tests/README.md delete mode 100644 src/tests/SOS.Tests/RuntimeInfoTests.cs delete mode 100644 src/tests/SOS.Tests/SOS.Tests.csproj delete mode 100644 src/tests/SOS.Tests/SosReplayAttribute.cs delete mode 100644 src/tests/SOS.Tests/SosTheoryAttribute.cs delete mode 100644 src/tests/SOS.Tests/SpecializedInspectionTests.cs delete mode 100644 src/tests/SOS.Tests/StackInspectionTests.cs delete mode 100644 src/tests/SOS.Tests/TargetExtensions.cs delete mode 100644 src/tests/SOS.Tests/TestMatrices.cs delete mode 100644 src/tests/SOS.Tests/TestObjectHelpers.cs delete mode 100644 src/tests/SOS.Tests/WindowsTheoryAttribute.cs delete mode 100644 src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.cs delete mode 100644 src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.csproj delete mode 100644 src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/TestHarness.cs diff --git a/build.sln b/build.sln index 7e47e54da9..2b073da928 100644 --- a/build.sln +++ b/build.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build.proj", "{8AEC426C-BB15-4924-9E39-9C2A4C11D602}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dbgshim", "src\dbgshim\dbgshim.vcxproj", "{BD779298-8631-3F5D-AA59-82897E5454A7}" @@ -123,16 +123,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp3", "src\tests\SOS.Un EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.UnitTests", "src\tests\SOS.UnitTests\SOS.UnitTests.csproj", "{3EE8CB83-EC2F-415D-A681-997AC6FB0B9A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.Tests", "src\tests\SOS.Tests\SOS.Tests.csproj", "{78F49599-5A2B-4D40-8D0D-08A539F80E95}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.TestHarness", "src\tests\SOS.TestHarness\SOS.TestHarness.csproj", "{6F9C3623-C03B-4139-B37C-DCC38218E661}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.TestHarness.EngineHost", "src\tests\SOS.TestHarness.EngineHost\SOS.TestHarness.EngineHost.csproj", "{93CA6105-938C-4074-B8FF-7123346D0AE4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.TestHarness.Capturer", "src\tests\SOS.TestHarness.Capturer\SOS.TestHarness.Capturer.csproj", "{529DB77C-1E13-41BF-BBDC-51D04EE27D03}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.TestHarness.SourceGen", "src\tests\SOS.TestHarness.SourceGen\SOS.TestHarness.SourceGen.csproj", "{AD8E5B2E-813D-40FB-8749-513008C0F756}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackTracee", "src\tests\StackTracee\StackTracee.csproj", "{614E442A-A8FD-4E14-9C3E-1F312864D9E0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestExtension", "src\tests\TestExtension\TestExtension.csproj", "{B5378BE2-284A-4614-9415-7FB6A7B9D452}" @@ -1325,96 +1315,6 @@ Global {3EE8CB83-EC2F-415D-A681-997AC6FB0B9A}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU {3EE8CB83-EC2F-415D-A681-997AC6FB0B9A}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU {3EE8CB83-EC2F-415D-A681-997AC6FB0B9A}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|Any CPU.Build.0 = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|x64.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|x64.Build.0 = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|x86.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Debug|x86.Build.0 = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|Any CPU.ActiveCfg = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|Any CPU.Build.0 = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|x64.ActiveCfg = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|x64.Build.0 = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|x86.ActiveCfg = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Release|x86.Build.0 = Release|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Checked|x64.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.Checked|x86.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU - {78F49599-5A2B-4D40-8D0D-08A539F80E95}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|x64.Build.0 = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|x86.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Debug|x86.Build.0 = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|Any CPU.Build.0 = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|x64.ActiveCfg = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|x64.Build.0 = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|x86.ActiveCfg = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Release|x86.Build.0 = Release|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Checked|x64.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.Checked|x86.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU - {6F9C3623-C03B-4139-B37C-DCC38218E661}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|x64.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|x64.Build.0 = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|x86.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Debug|x86.Build.0 = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|Any CPU.Build.0 = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|x64.ActiveCfg = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|x64.Build.0 = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|x86.ActiveCfg = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Release|x86.Build.0 = Release|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Checked|x64.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.Checked|x86.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU - {93CA6105-938C-4074-B8FF-7123346D0AE4}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|Any CPU.Build.0 = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|x64.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|x64.Build.0 = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|x86.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Debug|x86.Build.0 = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|Any CPU.ActiveCfg = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|Any CPU.Build.0 = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|x64.ActiveCfg = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|x64.Build.0 = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|x86.ActiveCfg = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Release|x86.Build.0 = Release|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Checked|x64.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.Checked|x86.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU - {529DB77C-1E13-41BF-BBDC-51D04EE27D03}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|x64.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|x64.Build.0 = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|x86.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Debug|x86.Build.0 = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|Any CPU.Build.0 = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|x64.ActiveCfg = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|x64.Build.0 = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|x86.ActiveCfg = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Release|x86.Build.0 = Release|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Checked|x64.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.Checked|x86.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU - {AD8E5B2E-813D-40FB-8749-513008C0F756}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU {614E442A-A8FD-4E14-9C3E-1F312864D9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {614E442A-A8FD-4E14-9C3E-1F312864D9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {614E442A-A8FD-4E14-9C3E-1F312864D9E0}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -1679,11 +1579,6 @@ Global {AEDAD92F-C123-4880-A4EB-569074A31DE7} = {C02DE277-7341-4C63-B105-BA695F40277F} {C02DE277-7341-4C63-B105-BA695F40277F} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} {3EE8CB83-EC2F-415D-A681-997AC6FB0B9A} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} - {78F49599-5A2B-4D40-8D0D-08A539F80E95} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} - {6F9C3623-C03B-4139-B37C-DCC38218E661} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} - {93CA6105-938C-4074-B8FF-7123346D0AE4} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} - {529DB77C-1E13-41BF-BBDC-51D04EE27D03} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} - {AD8E5B2E-813D-40FB-8749-513008C0F756} = {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} {2E5032C3-87A7-43F7-B729-6B1ECDC84B32} = {59E97F80-3F6D-4E83-9962-236C98411375} {DE21994B-542A-437E-8405-F62A5A3F41D4} = {59E97F80-3F6D-4E83-9962-236C98411375} {CEDB87DB-22B9-4466-B79F-AA2A40BE6AA9} = {59E97F80-3F6D-4E83-9962-236C98411375} diff --git a/src/tests/Debuggees.proj b/src/tests/Debuggees.proj index ed5a0abc50..3f112eba95 100644 --- a/src/tests/Debuggees.proj +++ b/src/tests/Debuggees.proj @@ -7,39 +7,4 @@ - - - - - - - - - - - - - - - - <_SOSSingleFileRuntime Include="@(RuntimeTestVersions)" - Condition="'%(RuntimeTestVersions.TargetFramework)' != ''" /> - - - - - - - - diff --git a/src/tests/SOS.TestHarness.Capturer/Program.cs b/src/tests/SOS.TestHarness.Capturer/Program.cs deleted file mode 100644 index 106b7ad89d..0000000000 --- a/src/tests/SOS.TestHarness.Capturer/Program.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; - -// Out-of-process desktop-dump capturer. Runs the risky in-process dbgeng live-debugging work in -// a short-lived child process so it can never crash the test host. See CaptureCli for usage. -return CaptureCli.Run(args); diff --git a/src/tests/SOS.TestHarness.Capturer/SOS.TestHarness.Capturer.csproj b/src/tests/SOS.TestHarness.Capturer/SOS.TestHarness.Capturer.csproj deleted file mode 100644 index e47f1be839..0000000000 --- a/src/tests/SOS.TestHarness.Capturer/SOS.TestHarness.Capturer.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - Exe - $(NetCoreAppTestTargetFramework) - enable - enable - latest - SOS.TestHarness.Capturer - $(TargetRid) - - - - - - - diff --git a/src/tests/SOS.TestHarness.EngineHost/Program.cs b/src/tests/SOS.TestHarness.EngineHost/Program.cs deleted file mode 100644 index 5fe6fc1871..0000000000 --- a/src/tests/SOS.TestHarness.EngineHost/Program.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; - -// The EngineHost child process: hosts dbgeng IN THIS PROCESS (reusing the in-process host -// classes) and serves commands over a stdin/stdout REPL. The test host spawns one of these per -// target and talks to it via EngineProtocol, so the test host itself never loads dbgeng/SOS/DAC — -// which is what was crashing it (ExecutionEngineException). If the engine corrupts THIS process, -// only this child dies and the test sees a clean failure. -// -// Usage: -// EngineHost dump -// EngineHost live - -return EngineHostMain.Run(args); - -internal static class EngineHostMain -{ - public static int Run(string[] args) - { - if (args.Length < 1) - { - Console.Error.WriteLine("usage: EngineHost | live >"); - return 2; - } - - IDebuggerHost host; - DbgEngLiveHost? liveHost = null; - try - { - switch (args[0]) - { - case "dump" when args.Length == 2: - host = new DbgEngDumpHost(args[1]); - host.LoadSos(); - break; - - case "live" when args.Length == 2: - liveHost = new DbgEngLiveHost(args[1]); // launches, breaks, loads SOS - host = liveHost; - break; - - default: - Console.Error.WriteLine("invalid arguments"); - return 2; - } - } - catch (Exception ex) - { - Console.Error.WriteLine("engine open failed: " + ex); - return 1; - } - - // Signal that the target is open and SOS is ready. - Console.Out.WriteLine(EngineProtocol.Ready); - Console.Out.Flush(); - - try - { - string? line; - while ((line = Console.In.ReadLine()) is not null) - { - if (line.Length == 0) - { - continue; - } - - try - { - SosOutput result; - if (line.StartsWith(EngineProtocol.RunToBpmdPrefix, StringComparison.Ordinal)) - { - if (liveHost is null) - { - throw new InvalidOperationException("runtobpmd is only valid for a live engine."); - } - - string[] parts = line[EngineProtocol.RunToBpmdPrefix.Length..].Split(' ', 2); - result = liveHost.RunToBpmd(parts[0], parts[1]); - } - else if (line == EngineProtocol.RunToCrash) - { - if (liveHost is null) - { - throw new InvalidOperationException("runtocrash is only valid for a live engine."); - } - - result = liveHost.RunToCrash(); - } - else if (line == EngineProtocol.RunToBreak) - { - if (liveHost is null) - { - throw new InvalidOperationException("runtobreak is only valid for a live engine."); - } - - result = liveHost.RunToBreakpoint(); - } - else - { - result = host.Execute(line); - } - - Console.Out.Write(result.Text); - if (!result.Text.EndsWith('\n')) - { - Console.Out.Write('\n'); - } - - Console.Out.WriteLine(EngineProtocol.End); - Console.Out.Flush(); - } - catch (Exception ex) - { - Console.Out.WriteLine(ex.Message); - Console.Out.WriteLine(EngineProtocol.Error); - Console.Out.Flush(); - } - } - } - finally - { - host.Dispose(); - } - - return 0; - } -} diff --git a/src/tests/SOS.TestHarness.EngineHost/SOS.TestHarness.EngineHost.csproj b/src/tests/SOS.TestHarness.EngineHost/SOS.TestHarness.EngineHost.csproj deleted file mode 100644 index 2cce79d93c..0000000000 --- a/src/tests/SOS.TestHarness.EngineHost/SOS.TestHarness.EngineHost.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - Exe - $(NetCoreAppTestTargetFramework) - enable - enable - latest - SOS.TestHarness.EngineHost - $(TargetRid) - - - - - - - diff --git a/src/tests/SOS.TestHarness.SourceGen/MirrorGenerator.cs b/src/tests/SOS.TestHarness.SourceGen/MirrorGenerator.cs deleted file mode 100644 index 81297f9ec0..0000000000 --- a/src/tests/SOS.TestHarness.SourceGen/MirrorGenerator.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -namespace TestTargetsGen; - -/// -/// Source generator that mirrors the public const and public static readonly -/// (literal-initialized) fields of the test targets into the test project, so tests can assert -/// against the target's own declared values with full intellisense: -/// pe.Field("Message").Equals(TestTargets.NestedExceptions.OuterMessage); -/// -/// The test-target source files are supplied as AdditionalFiles. Only literal-initialized -/// fields are mirrored (the value is known at compile time), which is exactly the const / -/// static-readonly-constant case — no runtime/ClrMD read is needed. Mirrors are emitted under a -/// single fixed namespace () regardless of where the source type -/// lives, since the test targets declare their types in the global namespace. -/// -[Generator] -public sealed class TestTargetsMirrorGenerator : IIncrementalGenerator -{ - private const string MirrorNamespace = "TestTargets"; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - IncrementalValueProvider> sources = context.AdditionalTextsProvider - .Where(static a => a.Path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) - .Select(static (a, ct) => a.GetText(ct)?.ToString() ?? string.Empty) - .Collect(); - - context.RegisterSourceOutput(sources, static (spc, files) => - { - Dictionary types = new(StringComparer.Ordinal); - foreach (string file in files) - { - foreach (MirrorType type in ParseFile(file)) - { - types[type.Name] = type; // last writer wins; names are unique across targets - } - } - - if (types.Count == 0) - { - return; - } - - spc.AddSource("TestTargetsMirror.g.cs", SourceText.From(Emit(types.Values), Encoding.UTF8)); - }); - } - - private static string Emit(IEnumerable types) - { - StringBuilder sb = new(); - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - sb.AppendLine(); - sb.Append("namespace ").AppendLine(MirrorNamespace); - sb.AppendLine("{"); - - foreach (MirrorType type in types.OrderBy(t => t.Name, StringComparer.Ordinal)) - { - sb.AppendLine($" /// Compile-time mirror of const / static-readonly fields of test target type '{type.Name}'."); - sb.AppendLine($" public static class {type.Name}"); - sb.AppendLine(" {"); - foreach (MirrorField field in type.Fields) - { - string kind = field.IsConst ? "const" : "static readonly"; - sb.AppendLine($" public {kind} {field.Type} {field.Name} = {field.Initializer};"); - } - - sb.AppendLine(" }"); - } - - sb.AppendLine("}"); - return sb.ToString(); - } - - private static IEnumerable ParseFile(string source) - { - SyntaxNode root = CSharpSyntaxTree.ParseText(source).GetRoot(); - - foreach (TypeDeclarationSyntax type in root.DescendantNodes().OfType()) - { - if (!type.Modifiers.Any(SyntaxKind.PublicKeyword)) - { - continue; - } - - List fields = new(); - foreach (FieldDeclarationSyntax fd in type.Members.OfType()) - { - SyntaxTokenList mods = fd.Modifiers; - if (!mods.Any(SyntaxKind.PublicKeyword)) - { - continue; - } - - bool isConst = mods.Any(SyntaxKind.ConstKeyword); - bool isStaticReadonly = mods.Any(SyntaxKind.StaticKeyword) && mods.Any(SyntaxKind.ReadOnlyKeyword); - if (!isConst && !isStaticReadonly) - { - continue; - } - - string typeText = fd.Declaration.Type.ToString(); - foreach (VariableDeclaratorSyntax v in fd.Declaration.Variables) - { - if (v.Initializer is null || !IsLiteralLike(v.Initializer.Value)) - { - continue; - } - - fields.Add(new MirrorField(isConst, typeText, v.Identifier.Text, v.Initializer.Value.ToString())); - } - } - - if (fields.Count > 0) - { - yield return new MirrorType(type.Identifier.Text, fields); - } - } - } - - /// True for expressions whose value is known at compile time (literals and operators over them). - private static bool IsLiteralLike(ExpressionSyntax expr) => expr switch - { - LiteralExpressionSyntax => true, - PrefixUnaryExpressionSyntax u => IsLiteralLike(u.Operand), - ParenthesizedExpressionSyntax p => IsLiteralLike(p.Expression), - BinaryExpressionSyntax b => IsLiteralLike(b.Left) && IsLiteralLike(b.Right), - _ => false, - }; - - private sealed class MirrorType - { - public MirrorType(string name, List fields) - { - Name = name; - Fields = fields; - } - - public string Name { get; } - - public List Fields { get; } - } - - private sealed class MirrorField - { - public MirrorField(bool isConst, string type, string name, string initializer) - { - IsConst = isConst; - Type = type; - Name = name; - Initializer = initializer; - } - - public bool IsConst { get; } - - public string Type { get; } - - public string Name { get; } - - public string Initializer { get; } - } -} diff --git a/src/tests/SOS.TestHarness.SourceGen/SOS.TestHarness.SourceGen.csproj b/src/tests/SOS.TestHarness.SourceGen/SOS.TestHarness.SourceGen.csproj deleted file mode 100644 index 5a6a6b3d89..0000000000 --- a/src/tests/SOS.TestHarness.SourceGen/SOS.TestHarness.SourceGen.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - netstandard2.0 - enable - true - true - false - false - - - - - - - - diff --git a/src/tests/SOS.TestHarness/CaptureCli.cs b/src/tests/SOS.TestHarness/CaptureCli.cs deleted file mode 100644 index d6145bdb0d..0000000000 --- a/src/tests/SOS.TestHarness/CaptureCli.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Entry point for the out-of-process capture helper (the Capturer exe). Captures the -/// desktop .NET Framework dumps for one (target) using in-process dbgeng — but in a -/// short-lived child process, so the heavy/risky live-debugging dbgeng work never runs -/// inside the test host (where it can corrupt the host CLR; see the design note's subprocess -/// recommendation). On success the dumps land in the given directory and the process exits 0. -/// -public static class CaptureCli -{ - /// Args: <exePath> <targetName> <dumpDir> <dumpKind>. - public static int Run(string[] args) - { - if (args.Length != 4) - { - Console.Error.WriteLine("usage: Capturer "); - return 2; - } - - string exePath = args[0]; - string targetName = args[1]; - string dumpDir = args[2]; - if (!Enum.TryParse(args[3], ignoreCase: true, out DumpKind dumpKind)) - { - Console.Error.WriteLine($"Unknown dump kind '{args[3]}'."); - return 2; - } - - try - { - TargetDefinition target = TargetCatalog.Get(targetName); - Directory.CreateDirectory(dumpDir); - DbgEngCapturer.Capture(exePath, target, dumpDir, dumpKind); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine(ex); - return 1; - } - } -} diff --git a/src/tests/SOS.TestHarness/ChildEngineClient.cs b/src/tests/SOS.TestHarness/ChildEngineClient.cs deleted file mode 100644 index 3d1fa037d8..0000000000 --- a/src/tests/SOS.TestHarness/ChildEngineClient.cs +++ /dev/null @@ -1,220 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Text; - -namespace SOS.TestHarness; - -/// -/// The test-host side of the subprocess dbgeng backend: spawns an EngineHost child that -/// hosts dbgeng in-process and drives it over . From the test host's -/// perspective this is just another child-process REPL (like ), so the -/// test host never loads dbgeng/SOS/DAC and can't be crashed by them. Because each target is its -/// own child process, many can be alive at once — lifting the single-instance limit that -/// in-process dbgeng imposed. -/// -/// The child blocks on stdin between commands (no busy-wait), so idle clients are cheap. -/// -public sealed class ChildEngineClient : ILiveDebuggerHost -{ - private readonly Process _process; - private readonly StreamWriter _stdin; - private readonly BlockingCollection _lines = new(); - private readonly Thread _reader; - - public string Name { get; } - - private ChildEngineClient(string name, string mode, IReadOnlyList modeArgs, string? dacDir, Dac dac, Flavor flavor) - { - Name = name; - - ProcessStartInfo psi = new() - { - FileName = RepoLayout.DotNetExe, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - psi.ArgumentList.Add(SnapshotStore.EngineHostDll); - psi.ArgumentList.Add(mode); - foreach (string a in modeArgs) - { - psi.ArgumentList.Add(a); - } - - // _NT_SYMBOL_PATH is ALWAYS fully replaced with a harness-constructed value - never inherited - // (the dev's ambient _NT_SYMBOL_PATH may point at the Azure-authed symweb, which crashes SOS host - // init and makes tests network-dependent). - Directory.CreateDirectory(RepoLayout.SymbolCache); - string symbolPath = RepoLayout.SymbolCache; - - // For self-contained single-file the runtime (coreclr.dll) and the DAC (mscordaccore.dll) are - // bundled in the exe, so dbgeng can't find them on disk. Point it at the runtime pack that has - // both: on the symbol/image path so dbgeng can index coreclr, and via SOSHARNESS_DAC_DIR so the - // EngineHost runs `.cordll -lp ` (DAC load path) before `.load sos`. - if (!string.IsNullOrEmpty(dacDir)) - { - symbolPath += ";" + dacDir; - psi.Environment["_NT_EXECUTABLE_IMAGE_PATH"] = dacDir; - psi.Environment["SOSHARNESS_DAC_DIR"] = dacDir; - } - - psi.Environment["_NT_SYMBOL_PATH"] = symbolPath; - - if (mode == "live") - { - // bpmd patches JIT code when binding managed breakpoints. Disable W^X for live - // debuggees so those writes do not trigger access violations and wedge the target. - psi.Environment["DOTNET_EnableWriteXorExecute"] = "0"; - } - - // The live debuggee is launched by the EngineHost (via CreateProcessAndAttach) and inherits its - // environment. For a framework-dependent (Core) live target, point the apphost at the multi-version - // test runtime install so it binds the runtime matching its target framework (net8 -> 8.0.x, - // net9 -> 9.0.x, net11 -> the installed preview). Without this the debuggee inherits the ambient - // DOTNET_ROOT (the product .dotnet, which carries only the repo's own runtime, e.g. net10), so a - // net9/net11 apphost fails framework resolution ("Framework 'Microsoft.NETCore.App' version 'x' not - // found") and exits at launch — before bpmd can bind — surfacing as "Process exited without hitting - // a breakpoint". Dump mode launches nothing, and SingleFile/Framework don't use a shared runtime, so - // this only applies to a live Core target. Mirrors LldbLiveHost and the dump-capture path - // (SnapshotStore.ApplyRuntimeRoot). MULTILEVEL_LOOKUP=0 keeps resolution strictly within the test - // install (no machine-wide fallback), so the debuggee's coreclr is the deterministic on-disk one. - if (mode == "live" && flavor == Flavor.Core) - { - psi.Environment["DOTNET_ROOT"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_ROOT(x86)"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_MULTILEVEL_LOOKUP"] = "0"; - } - - _process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start EngineHost"); - _stdin = _process.StandardInput; - - _reader = new Thread(ReadLoop) { IsBackground = true, Name = $"enginehost-reader-{name}" }; - _reader.Start(); - - WaitForReady(TimeSpan.FromSeconds(120)); - - // Select the DAC for this config's Dac axis (Legacy => false, CDac on .NET 11+ => true). SOS is - // already loaded by the time the child reports ready, so issue it now. The same dump is reused - // across both DAC values — only this debug-time toggle differs. - Send("!runtimes --usecdac " + DacPolicy.UseCDac(dac)); - } - - /// A child engine over a crash/snapshot dump. - public static ChildEngineClient ForDump(string hostName, string dumpPath, string? dacDir = null, Dac dac = Dac.Legacy) => - new(hostName, "dump", new[] { dumpPath }, dacDir, dac, Flavor.Core); - - /// A live child engine that launches the target (parked at the loader break, SOS loaded). - public static ChildEngineClient ForLive(string hostName, string exePath, string? dacDir = null, Dac dac = Dac.Legacy, Flavor flavor = Flavor.Core) => - new(hostName, "live", new[] { exePath }, dacDir, dac, flavor); - - public void LoadSos() - { - // The child already loads SOS when it opens the target; nothing to do. - } - - public SosOutput Execute(string command) => new(Name, command, Send(command)); - - public SosOutput Sos(string command) => new(Name, command, Send("!" + command)); - - /// Live only: set a managed breakpoint and run to it (handled inside the child). - public SosOutput RunToBpmd(string module, string method) => - new(Name, $"bpmd {module} {method}", Send(EngineProtocol.RunToBpmdPrefix + module + " " + method)); - - /// Live only: run the process to its second-chance crash (handled inside the child). - public SosOutput RunToCrash() => - new(Name, "run-to-crash", Send(EngineProtocol.RunToCrash)); - - /// Live only: resume to the next breakpoint (handled inside the child). - public SosOutput RunToBreakpoint() => - new(Name, "run-to-breakpoint", Send(EngineProtocol.RunToBreak)); - - private string Send(string command) - { - _stdin.WriteLine(command); - _stdin.Flush(); - return DrainToEnd(TimeSpan.FromSeconds(120), command); - } - - private void WaitForReady(TimeSpan timeout) - { - while (true) - { - if (!_lines.TryTake(out string? line, (int)timeout.TotalMilliseconds, HarnessCancellation.Token)) - { - throw new TimeoutException("EngineHost did not become ready in time."); - } - - if (line == EngineProtocol.Ready) - { - return; - } - } - } - - private string DrainToEnd(TimeSpan timeout, string command) - { - StringBuilder sb = new(); - while (true) - { - if (!_lines.TryTake(out string? line, (int)timeout.TotalMilliseconds, HarnessCancellation.Token)) - { - throw new TimeoutException($"EngineHost did not return output for '{command}' within {timeout}."); - } - - if (line == EngineProtocol.End) - { - break; - } - - if (line == EngineProtocol.Error) - { - // The child threw while processing this command (e.g. RunToBreakpoint hit a crash or - // the process exited). Surface it as an exception rather than returning silently with - // a dead session that later commands would fail against. - throw new InvalidOperationException( - $"EngineHost command '{command}' failed: {sb.ToString().TrimEnd()}"); - } - - sb.AppendLine(line); - } - - return sb.ToString(); - } - - private void ReadLoop() - { - string? line; - while ((line = _process.StandardOutput.ReadLine()) is not null) - { - _lines.Add(line); - } - } - - public void Dispose() - { - try - { - if (!_process.HasExited) - { - _stdin.Close(); // EOF -> child's ReadLine returns null -> clean exit - if (!_process.WaitForExit(5000)) - { - _process.Kill(entireProcessTree: true); - } - } - } - catch - { - // best effort - } - finally - { - _process.Dispose(); - } - } -} diff --git a/src/tests/SOS.TestHarness/CoreVersion.cs b/src/tests/SOS.TestHarness/CoreVersion.cs deleted file mode 100644 index 87194ff1cb..0000000000 --- a/src/tests/SOS.TestHarness/CoreVersion.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The .NET Core runtime version a target is built and dumped against — a test-matrix axis. The bit for a -/// version is its major number (so is bit 8, bit 10, ...), which -/// keeps the values self-describing and leaves room up to .NET 31. -/// -/// Semantics: this is a DISABLE mechanism, not a positive declaration of support. A test -/// defaults to (every bit set) and runs against every version the harness actually -/// builds and installs (see ); bits for versions that aren't built are -/// silently ignored. To stop a test running on a version that hits an unfixable runtime/DAC bug, mask the -/// bit off — e.g. CoreVersion.All & ~CoreVersion.Net8 ("everything except .NET 8"). Never use it -/// to positively enumerate "the versions this works on"; the built set is the source of truth for what -/// runs, and the mask only ever removes from it. -/// -[Flags] -public enum CoreVersion : uint -{ - /// - /// No .NET Core version. Used for the desktop flavor, whose runtime is - /// desktop .NET Framework (clr.dll), not a .NET Core version — the axis is inert there, so every Framework - /// config collapses to this single value instead of fanning out one meaningless row per Core version. - /// - None = 0, - - /// .NET 8. - Net8 = 1u << 8, - - /// .NET 9. - Net9 = 1u << 9, - - /// .NET 10. - Net10 = 1u << 10, - - /// .NET 11. - Net11 = 1u << 11, - - /// .NET 12 (not yet built; reserved so a test can pre-emptively opt in/out). - Net12 = 1u << 12, - - /// - /// Every version. Intersected with at matrix-expansion time, so a - /// test left at the default runs against exactly the versions the harness builds and installs — minus the - /// out-of-support set (), which is excluded from the default - /// matrix unless opted into via or named explicitly in - /// SOSHARNESS_ONLY_COREVERSIONS. - /// - All = uint.MaxValue, -} diff --git a/src/tests/SOS.TestHarness/CoreVersions.cs b/src/tests/SOS.TestHarness/CoreVersions.cs deleted file mode 100644 index 243d4bc03d..0000000000 --- a/src/tests/SOS.TestHarness/CoreVersions.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Maps the matrix axis onto the concrete runtimes the repo build installed: -/// the set actually present (), each version's target framework moniker -/// (), and — for the self-contained single-file DAC lookup — its exact runtime patch -/// version (). -/// -/// The source of truth is artifacts/dotnet-test/Debugger.Tests.Versions.txt, the manifest -/// eng/InstallRuntimes.proj writes when it acquires the RuntimeTestVersions (8/9/10/11). If -/// the manifest isn't present (runtimes not yet installed), falls back to the -/// versions in Directory.Build.props's SupportedSubProcessTargetFrameworks. -/// -public static class CoreVersions -{ - // tfm major -> exact runtime version, parsed once from the install manifest. - private static readonly IReadOnlyDictionary s_runtimeVersions = ReadManifest(); - - /// - /// The versions the harness actually builds debuggees for and has runtimes installed for. The matrix - /// only ever runs versions in this set; a requested bit outside it is - /// silently dropped (the axis is a disable mechanism — see ). - /// - public static CoreVersion Available { get; } = ComputeAvailable(); - - /// - /// The .NET Core versions that are out of support. They are excluded from the default test matrix - /// (see ) and only run when opted in via - /// or explicitly named in - /// SOSHARNESS_ONLY_COREVERSIONS. - /// - public static CoreVersion OutOfSupport => CoreVersion.Net9; - - /// - /// Whether out-of-support versions are opted into the default matrix, via - /// SOSHARNESS_TEST_OUT_OF_SUPPORT_CORE=1. - /// - public static bool TestOutOfSupportCore => - Environment.GetEnvironmentVariable("SOSHARNESS_TEST_OUT_OF_SUPPORT_CORE") == "1"; - - /// - /// The net*.0 target framework moniker for a single bit, or - /// netfx for (the desktop .NET Framework flavor, which has no Core - /// version — used as a stable dump/output folder segment). - /// - public static string Tfm(CoreVersion version) => version == CoreVersion.None ? "netfx" : $"net{Major(version)}.0"; - - /// The major version number (8, 9, 10, ...) for a single bit. - public static int Major(CoreVersion version) - { - uint v = (uint)version; - if (v == 0 || (v & (v - 1)) != 0) - { - throw new ArgumentException($"Expected a single CoreVersion flag, got '{version}'.", nameof(version)); - } - - return System.Numerics.BitOperations.Log2(v); - } - - /// - /// The exact runtime patch version (e.g. 8.0.25, 11.0.0-preview.6.26318.108) for a - /// version, from the install manifest. Returns null if that version wasn't installed. - /// - public static string? RuntimeVersion(CoreVersion version) => - s_runtimeVersions.TryGetValue(Major(version), out string? v) ? v : null; - - private static CoreVersion ComputeAvailable() - { - // Prefer what's actually installed (the manifest); fall back to the props-declared set. - CoreVersion fromManifest = 0; - foreach (int major in s_runtimeVersions.Keys) - { - fromManifest |= (CoreVersion)(1u << major); - } - - return fromManifest != 0 ? fromManifest : ReadSupportedFrameworksFromProps(); - } - - private static IReadOnlyDictionary ReadManifest() - { - Dictionary map = new(); - string manifest = Path.Combine(RepoLayout.Root, "artifacts", "dotnet-test", "Debugger.Tests.Versions.txt"); - if (!File.Exists(manifest)) - { - return map; - } - - // The manifest pairs net{N}.0 with {version} for - // each slot (Latest, Servicing1, ...). Collect both, then join on slot. - Dictionary tfms = new(StringComparer.OrdinalIgnoreCase); // slot -> netN.0 - Dictionary versions = new(StringComparer.OrdinalIgnoreCase); // slot -> version - foreach (string line in File.ReadLines(manifest)) - { - CollectTagged(line, "TargetFramework", tfms); - CollectTagged(line, "RuntimeVersion", versions); - } - - foreach ((string slot, string tfm) in tfms) - { - if (versions.TryGetValue(slot, out string? version) && - tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase) && - tfm.EndsWith(".0", StringComparison.Ordinal) && - int.TryParse(tfm.AsSpan(3, tfm.Length - 5), out int major)) - { - map[major] = version; - } - } - - return map; - } - - /// If is <{prefix}{slot}>value</...>, record slot->value. - private static void CollectTagged(string line, string prefix, Dictionary into) - { - string open = $"<{prefix}"; - int start = line.IndexOf(open, StringComparison.Ordinal); - if (start < 0) - { - return; - } - - int slotStart = start + open.Length; - int slotEnd = line.IndexOf('>', slotStart); - if (slotEnd < 0) - { - return; - } - - string slot = line.Substring(slotStart, slotEnd - slotStart); - int valEnd = line.IndexOf(""; - int open = line.IndexOf(tag, StringComparison.Ordinal); - if (open < 0) - { - continue; - } - - int close = line.IndexOf(" -/// Which DAC (the data-access component SOS reads runtime state through) to debug a target with — a -/// test-matrix axis that applies only to .NET 11+. The same dump is reused across both -/// values; only the DAC SOS loads at debug time differs (driven by runtimes --usecdac), so this -/// axis never multiplies dump capture. -/// -/// Default is . prunes for any -/// version earlier than .NET 11 (the cDAC doesn't exist there), so a default config collapses to -/// -only on ≤net10 and runs both on net11+. Mask a value off (e.g. -/// Dac.All & ~Dac.CDac) to disable a DAC that hits an unfixable bug for a given test. -/// -[Flags] -public enum Dac -{ - /// The classic native DAC (libmscordaccore), selected by the dump's coreclr build. - Legacy = 1, - - /// The cDAC (managed contract reader). Only valid on .NET 11+. - CDac = 2, - - /// Both DAC kinds (pruned to what's valid for the version by ). - All = Legacy | CDac, -} diff --git a/src/tests/SOS.TestHarness/DacPolicy.cs b/src/tests/SOS.TestHarness/DacPolicy.cs deleted file mode 100644 index f1e616f9a4..0000000000 --- a/src/tests/SOS.TestHarness/DacPolicy.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Resolves the runtimes --usecdac argument a host issues for a config's axis: -/// true, false. -/// -/// SOSHARNESS_USECDAC is a global clamp (off by default; never set in CI). When set it -/// overrides the per-config axis so the whole run forces one DAC — the escape hatch for a dev box whose -/// installed runtimes are skewed such that the cDAC can't load. Setting it collapses both DAC matrix rows -/// onto the same DAC, so the cDAC axis is only meaningful with it unset. -/// -internal static class DacPolicy -{ - public static string UseCDac(Dac dac) - { - string? clamp = Environment.GetEnvironmentVariable("SOSHARNESS_USECDAC"); - if (!string.IsNullOrEmpty(clamp)) - { - return clamp; - } - - return dac == Dac.CDac ? "true" : "false"; - } -} diff --git a/src/tests/SOS.TestHarness/DbgEngCapturer.cs b/src/tests/SOS.TestHarness/DbgEngCapturer.cs deleted file mode 100644 index b526e17fd6..0000000000 --- a/src/tests/SOS.TestHarness/DbgEngCapturer.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; -using Microsoft.Diagnostics.Runtime.Utilities.DbgEng; - -namespace SOS.TestHarness; - -/// -/// Captures dumps with in-process dbgeng for flavors that can't self-collect: desktop -/// (no diagnostics IPC) and self-contained -/// crash dumps (the single-file bundle doesn't ship/launch -/// createdump). Launches the debuggee under dbgeng once and drives it through every stop point: -/// stops are reached via a managed bpmd breakpoint on the -/// marker method and dumped with dbgeng's .dump command; the stop is -/// reached by running to the second-chance exception. -/// -/// Holds the dbgeng exclusive lease for the duration so it never collides with a shared cdb host. -/// -public static class DbgEngCapturer -{ - public static void Capture(string exePath, TargetDefinition target, string dumpDir, DumpKind dumpKind) - { - using IDisposable lease = HostSlot.DbgEng.AcquireExclusive(); - - using IDisposable clientDisposable = IDebugClient.Create(ToolPaths.DbgEngDirectory); - IDebugClient client = (IDebugClient)clientDisposable; - IDebugControl control = (IDebugControl)clientDisposable; - - StringBuilder buffer = new(); - using DbgEngOutputHolder output = new(client); - output.OutputReceived += (text, _) => buffer.Append(text); - - string Run(string command) - { - buffer.Clear(); - control.Execute(DEBUG_OUTCTL.THIS_CLIENT, command, DEBUG_EXECUTE.DEFAULT); - return buffer.ToString(); - } - - try - { - control.AddEngineOptions(DEBUG_ENGOPT.INITIAL_BREAK); - - DEBUG_CREATE_PROCESS_OPTIONS options = new() { CreateFlags = DEBUG_CREATE_PROCESS.DEBUG_ONLY_THIS_PROCESS }; - int hr = client.CreateProcessAndAttach($"\"{exePath}\"", Path.GetDirectoryName(exePath), DEBUG_ATTACH.DEFAULT, in options); - if (hr < 0) - { - throw new InvalidOperationException($"CreateProcessAndAttach('{exePath}') failed: 0x{hr:x8}"); - } - - control.WaitForEvent(TimeSpan.FromSeconds(60)); // initial loader break - Run($".load {ToolPaths.SosPath}"); - - // For desktop, the managed module is the EXE itself (e.g. GcPromotion.exe), not the .dll. - string bpmdModule = Path.GetFileName(exePath); - - foreach (StopPoint stop in target.StopPoints) - { - string dumpPath = Path.Combine(dumpDir, stop.Name + ".dmp"); - - if (stop.Kind == StopKind.Snapshot) - { - if (stop.Method is null) - { - throw new InvalidOperationException($"Snapshot stop '{stop.Name}' has no bpmd method."); - } - - Run($"!bpmd {bpmdModule} {stop.Method}"); - RunToBreak(control, $"bpmd {stop.Method}"); - } - else // Crash - { - RunToBreak(control, "second-chance crash"); - } - - Run($".dump /o {DbgEngDumpOption(dumpKind)} \"{dumpPath}\""); - if (!File.Exists(dumpPath)) - { - throw new InvalidOperationException($"DbgEng capture failed to write '{dumpPath}'."); - } - } - } - finally - { - try - { - client.EndSession(DEBUG_END.ACTIVE_TERMINATE); - } - catch - { - // best effort - } - } - } - - private static string DbgEngDumpOption(DumpKind dumpKind) => dumpKind switch - { - DumpKind.Full => "/ma", - DumpKind.Heap => "/mw", - DumpKind.Mini => "/m", - _ => throw new ArgumentOutOfRangeException(nameof(dumpKind), dumpKind, "Unsupported dump kind"), - }; - - private static void RunToBreak(IDebugControl control, string what) - { - const int MaxResumes = 40; - for (int i = 0; i < MaxResumes; i++) - { - control.Execute(DEBUG_OUTCTL.THIS_CLIENT, "g", DEBUG_EXECUTE.DEFAULT); - control.WaitForEvent(TimeSpan.FromSeconds(60)); - control.GetExecutionStatus(out DEBUG_STATUS status); - - if (status == DEBUG_STATUS.BREAK) - { - return; - } - - if (status == DEBUG_STATUS.NO_DEBUGGEE) - { - throw new InvalidOperationException($"Debuggee exited before reaching {what}."); - } - } - - throw new InvalidOperationException($"Did not reach {what} after {MaxResumes} resumes."); - } -} diff --git a/src/tests/SOS.TestHarness/DbgEngDumpHost.cs b/src/tests/SOS.TestHarness/DbgEngDumpHost.cs deleted file mode 100644 index e7890a97ab..0000000000 --- a/src/tests/SOS.TestHarness/DbgEngDumpHost.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The "cdb" host over a snapshot/crash dump: hosts dbgeng in-process and opens the dump. -/// SOS output is captured through dbgeng's output callbacks rather than scraping stdout. -/// -public sealed class DbgEngDumpHost : DbgEngHostBase -{ - private readonly string _dumpPath; - - public override string Name => "cdb"; - - public DbgEngDumpHost(string dumpPath) - { - _dumpPath = dumpPath; - Initialize(); - } - - protected override void OnOpen() - { - int hr = Client.OpenDumpFile(_dumpPath); - if (hr < 0) - { - throw new InvalidOperationException($"OpenDumpFile('{_dumpPath}') failed: 0x{hr:x8}"); - } - - Control.WaitForEvent(TimeSpan.FromSeconds(60)); - } -} diff --git a/src/tests/SOS.TestHarness/DbgEngHostBase.cs b/src/tests/SOS.TestHarness/DbgEngHostBase.cs deleted file mode 100644 index e95ac59cfe..0000000000 --- a/src/tests/SOS.TestHarness/DbgEngHostBase.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Text; -using Microsoft.Diagnostics.Runtime.Utilities.DbgEng; - -namespace SOS.TestHarness; - -/// -/// Shared machinery for the dbgeng-backed hosts (dump and live). Hosts dbgeng in-process, -/// captures command output via (no stdout sentinel scraping), -/// and pins every engine call to one dedicated worker thread — honoring dbgeng's -/// single-threaded, one-target-at-a-time nature. Derived classes only implement how the -/// target is opened (open a dump vs. launch-and-run-to-a-breakpoint). -/// -/// This type does NOT enforce the "one in-process dbgeng per process" limit itself; that is -/// the job of , which decides when hosts may exist. -/// -public abstract class DbgEngHostBase : IDebuggerHost -{ - private readonly BlockingCollection _work = new(); - private readonly Thread _worker; - private readonly StringBuilder _buffer = new(); - - private IDisposable _clientDisposable = null!; - private DbgEngOutputHolder _output = null!; - private bool _sosLoaded; - - /// The dbgeng client. Only touch from the worker thread. - protected IDebugClient Client { get; private set; } = null!; - - /// The dbgeng control. Only touch from the worker thread. - protected IDebugControl Control { get; private set; } = null!; - - public abstract string Name { get; } - - protected DbgEngHostBase() - { - _worker = new Thread(WorkerLoop) { IsBackground = true, Name = "dbgeng-host" }; - _worker.Start(); - } - - /// Derived ctors call this after their fields are set; runs on the worker. - protected void Initialize() - { - try - { - Invoke(() => { - _clientDisposable = IDebugClient.Create(ToolPaths.DbgEngDirectory); - Client = (IDebugClient)_clientDisposable; - Control = (IDebugControl)_clientDisposable; - - _output = new DbgEngOutputHolder(Client); - _output.OutputReceived += (text, _) => _buffer.Append(text); - - // Raw dbgeng leaves SYMOPT_LOAD_LINES off by default, so SOS suppresses the - // [file @ line] annotation on managed frames (clrstack, !pe, etc). cdb-based hosts - // (and the legacy SOSRunner) turn line loading on; do the same so source/line - // resolution matches the dotnet-dump host. Without this, !clrstack never shows - // source lines under cdb even when the PDB is found. - RunCore(".lines -e"); - - OnOpen(); - }); - } - catch - { - // Release the dbgeng gate and tear down so a failed open doesn't wedge the - // other dbgeng-backed collection forever. - Dispose(); - throw; - } - } - - /// Open the target (dump or live). Runs on the worker thread; engine is ready. - protected abstract void OnOpen(); - - public void LoadSos() => Invoke(LoadSosCore); - - /// Worker-thread SOS load. Safe to call from . - protected void LoadSosCore() - { - if (_sosLoaded) - { - return; - } - - // cdb auto-loads an SOS when it opens a managed dump - notably the classic desktop - // .NET Framework SOS from C:\Windows\Microsoft.NET\... for desktop dumps, which only - // exposes the old command names (e.g. `threads`, not `clrthreads`). Unload any such - // pre-loaded SOS first so the SOS under test is unambiguously OUR build, then load ours - // and verify via .chain that ours is the one in the chain. - EnsureNoSosLoaded(); - - // For self-contained single-file dumps the runtime (and DAC) is bundled in the exe, so dbgeng - // can't find mscordaccore.dll on disk. When the harness tells us where the matching DAC lives, - // set its load path before loading SOS so the DAC is available when SOS initializes. (Mirrors - // `.cordll -ve -u -lp ` from the use-local-sos workflow.) - string? dacDir = Environment.GetEnvironmentVariable("SOSHARNESS_DAC_DIR"); - if (!string.IsNullOrEmpty(dacDir)) - { - RunCore($".cordll -ve -u -lp {dacDir}"); - - // .cordll can cause dbgeng to auto-load an SOS after the initial cleanup. Re-assert a clean - // extension chain immediately before loading the build under test. - EnsureNoSosLoaded(); - } - - RunCore($".load {ToolPaths.SosPath}"); - VerifyOurSosLoaded(); - - _sosLoaded = true; - } - - /// - /// Remove any SOS cdb auto-loaded, looping over .chain until none remain. Throws if an SOS is - /// still present after the unload attempts - we must not .load ours on top of a stale SOS we - /// can't remove, because commands could route to the wrong SOS. - /// - private void EnsureNoSosLoaded() - { - const int MaxUnloads = 5; - for (int i = 0; i <= MaxUnloads; i++) - { - string chain = RunCore(".chain"); - if (!ChainContainsSos(chain)) - { - return; // chain is clean - safe to load ours - } - - if (i == MaxUnloads) - { - throw new InvalidOperationException( - $"Could not unload pre-loaded SOS before loading ours; .chain still shows an SOS after {MaxUnloads} attempts:\n{chain}"); - } - - // .unload sos removes the extension named "sos" (cdb's auto-loaded one). Harmless if - // none is loaded - cdb just reports it and we ignore that. - RunCore(".unload sos"); - } - } - - /// Confirm our SOS (and only ours) is in the extension chain after loading it. - private void VerifyOurSosLoaded() - { - string chain = RunCore(".chain"); - if (chain.IndexOf(ToolPaths.SosPath, StringComparison.OrdinalIgnoreCase) < 0) - { - throw new InvalidOperationException( - $"Expected our SOS '{ToolPaths.SosPath}' to be loaded, but .chain was:\n{chain}"); - } - } - - private static bool ChainContainsSos(string chain) => - chain.Contains("sos.dll", StringComparison.OrdinalIgnoreCase) - || chain.Contains("\\sos:", StringComparison.OrdinalIgnoreCase); - - public SosOutput Execute(string command) => new(Name, command, Invoke(() => RunCore(command))); - - public SosOutput Sos(string command) => new(Name, command, Invoke(() => RunCore("!" + command))); - - /// Worker-thread command execution returning captured output. - protected string RunCore(string command) - { - _buffer.Clear(); - Control.Execute(DEBUG_OUTCTL.THIS_CLIENT, command, DEBUG_EXECUTE.DEFAULT); - return _buffer.ToString(); - } - - public void Dispose() - { - try - { - Invoke(() => { - _output?.Dispose(); - Client?.EndSession(DEBUG_END.ACTIVE_TERMINATE); - _clientDisposable?.Dispose(); - }); - } - catch - { - // best effort teardown - } - finally - { - _work.CompleteAdding(); - } - } - - // ---- worker marshaling --------------------------------------------------------------- - - private void WorkerLoop() - { - foreach (Action action in _work.GetConsumingEnumerable()) - { - action(); - } - } - - private protected void Invoke(Action action) - { - using ManualResetEventSlim done = new(); - Exception? error = null; - _work.Add(() => { - try - { - action(); - } - catch (Exception ex) - { - error = ex; - } - finally - { - done.Set(); - } - }); - done.Wait(); - if (error is not null) - { - throw new InvalidOperationException($"{Name} host operation failed: {error.Message}", error); - } - } - - private protected T Invoke(Func func) - { - T result = default!; - Invoke(() => { result = func(); }); - return result; - } -} diff --git a/src/tests/SOS.TestHarness/DbgEngLiveHost.cs b/src/tests/SOS.TestHarness/DbgEngLiveHost.cs deleted file mode 100644 index a6012fc009..0000000000 --- a/src/tests/SOS.TestHarness/DbgEngLiveHost.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Diagnostics.Runtime.Utilities.DbgEng; - -namespace SOS.TestHarness; - -/// -/// The live "cdb" host: launches the debuggee under in-process dbgeng and stops at the loader -/// breakpoint with SOS loaded, ready to set managed breakpoints. Unlike the dump hosts this is -/// a stateful, advancing target — it is owned exclusively by one test, never shared. -/// -/// sets a managed breakpoint on a method and runs to it, which is how -/// both the stop-point-driven navigation () and raw bpmd -/// tests reach a precise point. -/// -public sealed class DbgEngLiveHost : DbgEngHostBase -{ - private readonly string _commandLine; - private readonly string _workingDirectory; - - public override string Name => "cdb-live"; - - public DbgEngLiveHost(string exePath) - { - _commandLine = $"\"{exePath}\""; - _workingDirectory = Path.GetDirectoryName(exePath)!; - Initialize(); - } - - protected override void OnOpen() - { - // Stop at the loader breakpoint so we can load SOS and arm bpmd before the app runs. - Control.AddEngineOptions(DEBUG_ENGOPT.INITIAL_BREAK); - - DEBUG_CREATE_PROCESS_OPTIONS options = new() { CreateFlags = DEBUG_CREATE_PROCESS.DEBUG_ONLY_THIS_PROCESS }; - int hr = Client.CreateProcessAndAttach(_commandLine, _workingDirectory, DEBUG_ATTACH.DEFAULT, in options); - if (hr < 0) - { - throw new InvalidOperationException($"CreateProcessAndAttach('{_commandLine}') failed: 0x{hr:x8}"); - } - - Control.WaitForEvent(TimeSpan.FromSeconds(60)); - RequireStatus(DEBUG_STATUS.BREAK, "initial break"); - - LoadSosCore(); - } - - /// - /// Set a managed breakpoint on ! and run - /// until it is hit. Throws if the process exits first or the breakpoint is never reached. - /// - public SosOutput RunToBpmd(string module, string method) - { - string bpmdOutput = string.Empty; - - Invoke(() => { - // Clear any breakpoints left from a previous stop point. Otherwise a stale bpmd (e.g. the - // still-pending AtGen0 breakpoint) gets resolved and re-hit the moment we resume toward - // the next one, stranding us at the old location instead of advancing. - RunCore("!bpmd -clearall"); - RunCore("bc *"); - - bpmdOutput = RunCore($"!bpmd {module} {method}"); - - // bpmd reaches the method in two breaks (a JIT/prestub notification, then the entry), so - // resume until clrstack confirms we are actually stopped at the requested method. - const int MaxResumes = 50; - for (int i = 0; i < MaxResumes; i++) - { - RunCore("g"); - Control.WaitForEvent(TimeSpan.FromSeconds(60)); - Control.GetExecutionStatus(out DEBUG_STATUS status); - - if (status == DEBUG_STATUS.NO_DEBUGGEE) - { - throw new InvalidOperationException($"Debuggee exited before hitting bpmd {module}!{method}."); - } - - if (status == DEBUG_STATUS.BREAK && StoppedAtMethod(method)) - { - return; - } - } - - throw new InvalidOperationException($"Did not reach bpmd {module}!{method} after {MaxResumes} resumes."); - }); - - return new SosOutput(Name, $"bpmd {module} {method}", bpmdOutput); - } - - /// Worker-thread check: is the managed call stack currently topped by ? - private bool StoppedAtMethod(string method) - { - string stack = RunCore("!clrstack"); - return stack.Contains(method, StringComparison.Ordinal); - } - - /// - /// Run the process until it crashes (a second-chance exception break). Throws if it exits - /// cleanly without crashing. Used by the live GoToCrash path. - /// - public SosOutput RunToCrash() - { - Invoke(() => { - // Drop any bpmd breakpoints from earlier stop points so they don't break before the crash. - RunCore("!bpmd -clearall"); - RunCore("bc *"); - - const int MaxResumes = 25; - for (int i = 0; i < MaxResumes; i++) - { - RunCore("g"); - Control.WaitForEvent(TimeSpan.FromSeconds(60)); - Control.GetExecutionStatus(out DEBUG_STATUS status); - - if (status == DEBUG_STATUS.BREAK) - { - return; // second-chance crash break - } - - if (status == DEBUG_STATUS.NO_DEBUGGEE) - { - throw new InvalidOperationException("Process exited without crashing."); - } - } - - throw new InvalidOperationException($"Process did not crash after {MaxResumes} resumes."); - }); - - return new SosOutput(Name, "run-to-crash", string.Empty); - } - - /// - /// Resume the process to the next breakpoint. Throws if the process exits without hitting one, - /// or if it stops on a second-chance exception (a crash) rather than a breakpoint. The caller is - /// responsible for arming the breakpoint (e.g. Sos("bpmd …")); this sets/clears nothing. - /// - public SosOutput RunToBreakpoint() - { - const uint StatusBreakpoint = 0x80000003; // int3 — a real breakpoint, not a crash - - Invoke(() => { - RunCore("g"); - Control.WaitForEvent(TimeSpan.FromSeconds(60)); - Control.GetExecutionStatus(out DEBUG_STATUS status); - - if (status == DEBUG_STATUS.NO_DEBUGGEE) - { - throw new InvalidOperationException("Process exited without hitting a breakpoint."); - } - - // GetLastEvent is true only when the stop was an exception. A second-chance exception - // whose code isn't a break instruction is a crash, not the breakpoint we ran to. - if (Control.GetLastEvent(out DEBUG_LAST_EVENT_INFO_EXCEPTION ex, out _, out string? description) - && ex.FirstChance == 0 - && ex.ExceptionRecord.ExceptionCode != StatusBreakpoint) - { - throw new InvalidOperationException( - $"Hit a second-chance exception (0x{ex.ExceptionRecord.ExceptionCode:x8}), not a breakpoint: {description}"); - } - }); - - return new SosOutput(Name, "run-to-breakpoint", string.Empty); - } - - private void RequireStatus(DEBUG_STATUS expected, string phase) - { - Control.GetExecutionStatus(out DEBUG_STATUS status); - if (status != expected) - { - throw new InvalidOperationException($"Expected status {expected} at {phase}, but was {status}."); - } - } -} diff --git a/src/tests/SOS.TestHarness/DeadTarget.cs b/src/tests/SOS.TestHarness/DeadTarget.cs deleted file mode 100644 index b35e7271a7..0000000000 --- a/src/tests/SOS.TestHarness/DeadTarget.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A dump-backed target. It is a lightweight cursor: each / -/// resolves the (process-wide memoized, read-only) -/// for that point and makes it current, so many tests navigating to the same point share one loaded -/// dump host. Because nothing advances, points can be visited in any order and revisited. -/// throws until the first navigation, since there is no dump loaded yet. -/// -public sealed class DeadTarget : Target -{ - private readonly TestConfig _config; - private DumpSession? _current; - - internal DeadTarget(TestConfig config) - : base(config.Host, config.Target, config.Flavor) - { - _config = config; - } - - public override string DumpPath => Current.DumpPath; - - protected override void GoToStopPointCore(string stopName) - { - StopPoint stop = TargetCatalog.Get(TargetName).Stop(stopName); - _current = Targets.ResolveSession(_config, stop.Name); - } - - protected override void GoToCrashCore() - { - StopPoint crash = TargetCatalog.Get(TargetName).StopPoints.Single(s => s.Kind == StopKind.Crash); - _current = Targets.ResolveSession(_config, crash.Name); - } - - protected override SosOutput SosCore(string command) => Current.Sos(command); - - protected override SosOutput ExecuteCore(string command) => Current.Execute(command); - - internal override HostDiagnostics? CurrentDiagnostics => _current?.Diagnostics; - - private DumpSession Current => - _current ?? throw new InvalidOperationException( - "Target is not at a point yet; call GoToStopPoint(...) or GoToCrash() before Sos."); - - // Sessions are owned and disposed by the Targets registry; the cursor owns nothing. -} diff --git a/src/tests/SOS.TestHarness/DotNetDumpHost.cs b/src/tests/SOS.TestHarness/DotNetDumpHost.cs deleted file mode 100644 index f8d32d4622..0000000000 --- a/src/tests/SOS.TestHarness/DotNetDumpHost.cs +++ /dev/null @@ -1,252 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Text; - -namespace SOS.TestHarness; - -/// -/// The "dotnet-dump" host: drives dotnet-dump analyze <dump> as a child process and -/// talks to its REPL over stdin/stdout. Per-command output is delimited by the -/// <END_COMMAND_OUTPUT> marker that the dotnet-dump REPL emits natively after the -/// banner and after every command — the same marker the legacy SOS harness keys on. -/// -/// SOS commands are bare here (no ! prefix), so passes the command -/// through unchanged while the dbgeng host adds the !. -/// -public sealed class DotNetDumpHost : IDebuggerHost, IDiagnosticHost -{ - private const string EndMarker = ""; - private const string ErrorMarker = ""; - - private readonly Process _process; - private readonly StreamWriter _stdin; - private readonly BlockingCollection _lines = new(); - private readonly Thread _reader; - private readonly Thread? _stderrReader; - private readonly HostDiagnostics? _diagnostics; - private readonly Flavor _flavor; - private readonly Dac _dac; - private readonly CoreVersion _coreVersion; - - public string Name => "dotnet-dump"; - - /// Captured stdout/stderr and crash dumps for this host (see ). - public HostDiagnostics? Diagnostics => _diagnostics; - - public DotNetDumpHost(string dumpPath, Flavor flavor, Dac dac = Dac.Legacy, CoreVersion coreVersion = CoreVersion.Net10, HostDiagnostics? diagnostics = null) - { - _flavor = flavor; - _dac = dac; - _coreVersion = coreVersion; - _diagnostics = diagnostics; - ProcessStartInfo psi = new() - { - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - // Drive the repo-built dotnet-dump as `dotnet analyze ` so the harness always - // validates the freshly-built tool, not a machine-wide install. - psi.FileName = RepoLayout.DotNetExe; - psi.ArgumentList.Add(ToolPaths.DotNetDumpDll); - psi.ArgumentList.Add("analyze"); - psi.ArgumentList.Add(dumpPath); - - // Hermetic, local-only symbols (the dev's _NT_SYMBOL_PATH may point at the Azure-authed symweb). - // NOTE: unlike the cdb/lldb hosts, `dotnet-dump analyze` does NOT honor _NT_SYMBOL_PATH — its - // Analyzer unconditionally adds the public msdl symbol server on startup (see dotnet-dump - // Analyzer.cs). We still scrub the env for good measure, but the real seal is the - // `setsymbolserver -disable` issued in LoadSos below; otherwise a command that resolves symbols - // (e.g. `!clrstack -gc`) would synchronously download PDBs from msdl and intermittently stall - // past the harness command timeout, breaking hermeticity and flaking the suite. - Directory.CreateDirectory(RepoLayout.SymbolCache); - psi.Environment["_NT_SYMBOL_PATH"] = RepoLayout.SymbolCache; - - // dotnet-dump is itself a .NET process, so the standard crash-dump environment makes a fatal fault - // in it (or in the SOS code it hosts) write a full dump we can surface as an artifact. - _diagnostics?.ConfigureCrashDumps(psi); - - _process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start dotnet-dump"); - _stdin = _process.StandardInput; - _diagnostics?.RecordProcess(_process); - - _reader = new Thread(ReadLoop) { IsBackground = true, Name = "dotnet-dump-reader" }; - _reader.Start(); - - // Drain stderr on its own thread (previously redirected but never read) so a crash's diagnostics - // are retained for the replay instead of discarded. - if (_diagnostics is not null) - { - _stderrReader = new Thread(StderrLoop) { IsBackground = true, Name = "dotnet-dump-stderr" }; - _stderrReader.Start(); - } - - // Drain the startup banner up to the first marker so the host is ready for commands. - DrainToMarker(TimeSpan.FromSeconds(120)); - } - - public void LoadSos() - { - // SOS is built into dotnet-dump's analyze host; nothing to load. - - // Seal the host against the network: `dotnet-dump analyze` auto-adds the public msdl symbol - // server on startup, so clear it before any command runs. This keeps the suite hermetic and - // prevents the intermittent multi-minute hang where a symbol-resolving command (e.g. - // `!clrstack -gc`) blocks on a PDB download from msdl. - Run("setsymbolserver -disable"); - - // Self-contained single-file bundles carry coreclr inside the exe, so there is no runtime - // directory on disk next to which SOS can find the matching DAC — `analyze` was relying on the - // (now-disabled) msdl server to download it. Point SOS's symbol store at the runtime pack's - // native directory (a *local directory*, no network) so it resolves the DAC for the dump's - // coreclr build locally and the session stays hermetic. Mirrors LldbCliHost. Other flavors find - // their DAC next to the on-disk runtime and need no override. - string? dacDir = _dac == Dac.CDac ? ToolPaths.CDacOverrideDirectory : null; - dacDir ??= _flavor == Flavor.SingleFile ? ToolPaths.SingleFileDacDirectory(_coreVersion) : null; - if (dacDir is { Length: > 0 }) - { - Run($"setsymbolserver -directory \"{dacDir}\""); - } - - // Select the DAC for this config's Dac axis: Legacy => `--usecdac false`, CDac (.NET 11+ only) => - // `--usecdac true`. The same dump is reused across both, so only this debug-time toggle differs. - // SOSHARNESS_USECDAC (off by default; never set in CI) is a global clamp that overrides the axis on - // a dev box whose installed runtimes are skewed such that the cDAC can't load. (Mirrors LldbCliHost.) - // - // Also disable DAC authenticode signature verification. dotnet-dump enables it by default on - // Windows (Analyzer.cs), so the legacy-DAC load path runs WinVerifyTrust on mscordaccore.dll. The - // test runtimes come from the locally-acquired artifacts/dotnet-test install, and a preview/CI - // runtime's DAC may be unsigned — WinVerifyTrust then fails with 0x800B0100 (TRUST_E_NOSIGNATURE), - // which surfaces as "Failed to load data access module, 0x80004002" and breaks EVERY legacy-DAC - // command on that runtime (e.g. net11 preview). Verification is a production safeguard against - // loading an untrusted DAC downloaded from a symbol server; it's inappropriate for a trusted local - // test DAC, which is exactly why the cDAC path and non-Windows already skip it. Turn it off here so - // Windows matches that behavior and the legacy DAC loads for unsigned preview runtimes. - Run($"runtimes --usecdac {DacPolicy.UseCDac(_dac)} --DacSignatureVerification false"); - } - - public SosOutput Execute(string command) => new(Name, command, Run(command)); - - public SosOutput Sos(string command) => new(Name, command, Run(command)); - - private string Run(string command) - { - _stdin.WriteLine(command); - _stdin.Flush(); - return DrainToMarker(TimeSpan.FromSeconds(120), command); - } - - /// - /// Collect output lines until the end marker. Strips the echoed prompt line - /// ("> command") that dotnet-dump prints when stdin is redirected. - /// - private string DrainToMarker(TimeSpan timeout, string? command = null) - { - StringBuilder sb = new(); - while (true) - { - if (!_lines.TryTake(out string? line, (int)timeout.TotalMilliseconds, HarnessCancellation.Token)) - { - throw new TimeoutException($"dotnet-dump did not return output for '{command ?? ""}' within {timeout}."); - } - - string trimmed = line.TrimEnd(); - if (trimmed.EndsWith(EndMarker, StringComparison.Ordinal) || trimmed.EndsWith(ErrorMarker, StringComparison.Ordinal)) - { - break; - } - - // Skip the echoed prompt+command line. - if (command is not null && IsPromptEcho(line, command)) - { - continue; - } - - sb.AppendLine(line); - } - - return sb.ToString(); - } - - private static bool IsPromptEcho(string line, string command) - { - string trimmed = line.TrimStart('>', ' '); - return trimmed == command; - } - - private void ReadLoop() - { - try - { - string? line; - while ((line = _process.StandardOutput.ReadLine()) is not null) - { - _diagnostics?.AppendStdout(line); - _lines.Add(line); - } - } - catch (Exception ex) when (ex is IOException or ObjectDisposedException or InvalidOperationException) - { - // Best effort: Dispose can race with the reader thread during host teardown. - } - finally - { - _lines.CompleteAdding(); - } - } - - private void StderrLoop() - { - try - { - string? line; - while ((line = _process.StandardError.ReadLine()) is not null) - { - _diagnostics?.AppendStderr(line); - } - } - catch - { - // Best effort: the process may die mid-read. Whatever we captured is still available. - } - } - - public void Dispose() - { - try - { - if (!_process.HasExited) - { - _stdin.WriteLine("exit"); - _stdin.Flush(); - if (!_process.WaitForExit(5000)) - { - _process.Kill(entireProcessTree: true); - } - } - } - catch - { - // best effort - } - finally - { - try - { - _process.WaitForExit(10000); - } - catch - { - // best effort - } - - _process.Dispose(); - } - } -} diff --git a/src/tests/SOS.TestHarness/DumpGenerationRequirements.cs b/src/tests/SOS.TestHarness/DumpGenerationRequirements.cs deleted file mode 100644 index f80ce54ddd..0000000000 --- a/src/tests/SOS.TestHarness/DumpGenerationRequirements.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Security; -using Microsoft.Win32; - -namespace SOS.TestHarness; - -/// -/// Handles the Windows machine prerequisite for capturing a reduced (Heap/Mini) .NET Core dump of an -/// unsigned test runtime. -/// -/// Why the prerequisite exists. On Windows a reduced dump is written by createdump via -/// MiniDumpWriteDump, which only captures MEM_PRIVATE read/write pages directly. The CLR's -/// loader-allocator heaps — which hold the MethodTable/Module structures SOS and ClrMD need -/// to enumerate modules, types and the GC heap — are MEM_MAPPED (the double-mapped executable -/// allocator), so they are captured only through dbghelp's auxiliary DAC provider (dbghelp loads -/// mscordaccore.dll and calls ICLRDataEnumMemoryRegions::EnumMemoryRegions). dbghelp refuses -/// to load a DAC that isn't Authenticode-signed unless -/// HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\MiniDumpSettings\DisableAuxProviderSignatureCheck -/// is set to 1. The locally-built and preview (net11) test runtimes ship an unsigned DAC, so without -/// that value their loader heaps are silently omitted from a reduced dump and every module/type/heap -/// command then fails with "Unable to create a ClrHeap …". (Released runtimes such as net8–net10 have a -/// signed DAC; desktop Framework and single-file/Full captures do not use this path.) -/// -/// Why we only check, never set. That value lives under HKLM, so writing it requires -/// elevation and changes machine-wide state. When it is absent, Heap requests are captured as Full dumps, -/// which preserve the required data without changing the machine. Mini rows are skipped because substituting -/// a Full dump would not test Mini-dump behavior. -/// -internal static class DumpGenerationRequirements -{ - private static readonly string s_root = RuntimeInformation.ProcessArchitecture == Architecture.X86 ? @"SOFTWARE\WOW6432Node\" : @"SOFTWARE\"; - private static readonly string s_settingsNode = s_root + @"Microsoft\Windows NT\CurrentVersion\MiniDumpSettings"; - private const string DisableCheckValue = "DisableAuxProviderSignatureCheck"; - - // Read the registry value at most once per process (cheap, read-only; reading HKLM needs no elevation). - private static readonly Lazy s_signatureCheckDisabled = new(ReadSignatureCheckDisabled); - - /// - /// Resolves the dump kind that can be captured on this machine. Only a reduced (Heap/Mini) - /// Core dump goes - /// through that path: Full dumps capture all memory directly, single-file snapshots are always collected - /// Full, and desktop Framework is captured via dbgeng using the signed in-box DAC — none of those need - /// the bypass, so their requested kind is returned unchanged (as it is on non-Windows). - /// - internal static DumpKind ResolveCaptureKind(Flavor flavor, DumpKind dumpKind) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return dumpKind; - } - - if (dumpKind == DumpKind.Full || flavor == Flavor.Framework || flavor == Flavor.SingleFile) - { - return dumpKind; - } - - if (s_signatureCheckDisabled.Value) - { - return dumpKind; - } - - if (dumpKind == DumpKind.Mini) - { - HarnessSkipException.Now( - $@"Mini dump capture requires HKLM\{s_settingsNode}\{DisableCheckValue}=1 so dbghelp can " + - "load the unsigned test DAC"); - } - - return DumpKind.Full; - } - - private static bool ReadSignatureCheckDisabled() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return true; - } - - return ReadSignatureCheckDisabledWindows(); - } - - [SupportedOSPlatform("windows")] - private static bool ReadSignatureCheckDisabledWindows() - { - try - { - using RegistryKey? key = Registry.LocalMachine.OpenSubKey(s_settingsNode); - return key?.GetValue(DisableCheckValue) is int value && value == 1; - } - catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException or IOException) - { - return false; - } - } -} diff --git a/src/tests/SOS.TestHarness/DumpKind.cs b/src/tests/SOS.TestHarness/DumpKind.cs deleted file mode 100644 index b2e207a44a..0000000000 --- a/src/tests/SOS.TestHarness/DumpKind.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The kind of dump captured for a target — a matrix axis (flags, like ). Selects how -/// much of the process the dump contains, which lets tests validate SOS against reduced dumps. -/// -/// — a full dump (createdump type 4 / --type Full) used by tests that need -/// data omitted from reduced dumps. -/// — the default dump type (createdump type 2 / --type Heap). -/// — a heap-less minidump (createdump type 1 / --type Mini) used only by -/// commands that can validate meaningful data from the reduced dump. -/// -/// -[Flags] -public enum DumpKind -{ - /// Heap dump (createdump type 2 / --type Heap) — the default capture type. - Heap = 1, - - /// Minidump (createdump type 1 / --type Mini) without managed heap memory. - Mini = 2, - - /// Full dump (createdump type 4 / --type Full) with all memory. - Full = 4, - - /// The reduced dump kinds used by default expanded dump-kind coverage. - All = Heap | Mini, -} diff --git a/src/tests/SOS.TestHarness/DumpSession.cs b/src/tests/SOS.TestHarness/DumpSession.cs deleted file mode 100644 index 3f0c842650..0000000000 --- a/src/tests/SOS.TestHarness/DumpSession.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; - -namespace SOS.TestHarness; - -/// -/// An internal, read-only debug session over a specific dump (one (target, stopPoint, flavor)) -/// loaded into a host, with SOS ready. Because the dump is immutable, a session is safe to reuse -/// across many assertions, and is memoized by and reused by every -/// cursor that navigates to the same point. -/// -/// Host lifetime differs by backend: -/// -/// cdb runs dbgeng in its own child process. -/// Each child is independent and blocks on stdin when idle, so many can be alive at once — the -/// single-instance limit that in-process dbgeng imposed is gone. The host is created once and -/// kept. Because one session may be reused by several tests at once, concurrent commands -/// on its single child are serialized on a per-session gate. -/// dotnet-dump children busy-wait on stdin at ~100% CPU, so keeping many alive would -/// saturate the machine. They route through a capacity-1 (most-recently-used -/// stays open, reopened on demand). -/// -/// -internal sealed class DumpSession : IPooledHost, IDisposable -{ - private readonly Host _hostKind; - private readonly bool _pooled; // dotnet-dump: route through the single slot - private readonly HostSlot? _slot; - private readonly object _gate = new(); // serializes concurrent commands on this shared child - private IDebuggerHost? _host; // kept-alive host for non-pooled (cdb child) targets - - // One diagnostics collector for the life of this session (survives the pooled dotnet-dump host being - // closed and reopened), for the child-process hosts that support capture. Null for the cdb child host. - private readonly HostDiagnostics? _diagnostics; - - public Host Host { get; } - public string TargetName { get; } - public string StopName { get; } - public Flavor Flavor { get; } - public string DumpPath { get; } - public CoreVersion CoreVersion { get; } - public Dac Dac { get; } - - /// Captured stdout/stderr and crash dumps for this session's host, or null for the cdb child - /// host (which does not support capture). Surfaced in a failing test's replay. - public HostDiagnostics? Diagnostics => _diagnostics; - - internal DumpSession(Host hostKind, string targetName, string stopName, Flavor flavor, string dumpPath, - CoreVersion coreVersion = CoreVersion.Net10, Dac dac = Dac.Legacy) - { - _hostKind = hostKind; - Host = hostKind; - TargetName = targetName; - StopName = stopName; - Flavor = flavor; - DumpPath = dumpPath; - CoreVersion = coreVersion; - Dac = dac; - - // dotnet-dump children spin on stdin -> bound to one via the slot. cdb children block - // when idle -> keep alive concurrently (no slot), which is the subprocess-backend payoff. - _pooled = hostKind == Host.DotnetDump; - _slot = _pooled ? HostSlot.DotNetDump : null; - - // The child-process hosts (lldb, dotnet-dump) capture their stdout/stderr and crash dumps; the cdb - // child host runs dbgeng out-of-process and is not wired for capture. - _diagnostics = hostKind is Host.Lldb or Host.DotnetDump ? new HostDiagnostics(hostKind.ToString().ToLowerInvariant()) : null; - - if (!_pooled) - { - _host = HostFactory.CreateDumpHost(hostKind, flavor, dumpPath, dac, coreVersion, - SnapshotStore.TargetExe(flavor, targetName, coreVersion), _diagnostics); - _host.LoadSos(); - } - } - - /// - /// Run a SOS command against this target (host prefixing handled by the host). A shared target - /// may be handed to several tests at once (it is memoized by host/target/stop/flavor), and the - /// cdb backend is a single child process whose stdin/stdout pipe is not safe for concurrent - /// callers — so non-pooled commands are serialized on a per-target gate. The dotnet-dump path - /// serializes itself on the slot lock. - /// - public SosOutput Sos(string command) => - RunCommand("SOS", command, h => h.Sos(command)); - - /// Run a raw debugger command against this target. - public SosOutput Execute(string command) => - RunCommand("Execute", command, h => h.Execute(command)); - - private SosOutput RunCommand(string kind, string command, Func action) - { - try - { - return _pooled ? _slot!.Run(this, action) : RunGuarded(action); - } - catch (Exception ex) when (ex is IOException or ObjectDisposedException or InvalidOperationException or TimeoutException) - { - throw new InvalidOperationException( - $"Dump session command failed:{Environment.NewLine}" + - $"Host={Host}{Environment.NewLine}" + - $"Target={TargetName}{Environment.NewLine}" + - $"Stop={StopName}{Environment.NewLine}" + - $"Flavor={Flavor}{Environment.NewLine}" + - $"CoreVersion={CoreVersion}{Environment.NewLine}" + - $"Dac={Dac}{Environment.NewLine}" + - $"DumpPath={DumpPath}{Environment.NewLine}" + - $"CommandKind={kind}{Environment.NewLine}" + - $"Command={command}", - ex); - } - } - - private SosOutput RunGuarded(Func action) - { - lock (_gate) - { - return action(_host!); - } - } - - // IPooledHost — used only for the pooled (dotnet-dump) path. - - IDebuggerHost IPooledHost.Host => _host!; - - void IPooledHost.OpenHost() - { - _host = HostFactory.CreateDumpHost(_hostKind, Flavor, DumpPath, Dac, CoreVersion, - SnapshotStore.TargetExe(Flavor, TargetName, CoreVersion), _diagnostics); - _host.LoadSos(); - } - - void IPooledHost.CloseHost() - { - _host?.Dispose(); - _host = null; - } - - public void Dispose() - { - if (_pooled) - { - // The slot owns the pooled host's lifetime; closed at teardown via the slot. - return; - } - - _host?.Dispose(); - _host = null; - } -} diff --git a/src/tests/SOS.TestHarness/EngineProtocol.cs b/src/tests/SOS.TestHarness/EngineProtocol.cs deleted file mode 100644 index a396d501fe..0000000000 --- a/src/tests/SOS.TestHarness/EngineProtocol.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The tiny line protocol between the test host and the EngineHost child process that hosts -/// dbgeng. The child prints once the target is open, then for each command line -/// it receives it prints the captured output followed by on its own line. The -/// special request asks the child to set a managed breakpoint and run -/// to it. Markers are deliberately unlikely to appear in SOS output. -/// -internal static class EngineProtocol -{ - public const string Ready = "<<>>"; - public const string End = "<<>>"; - public const string Error = "<<>>"; - - /// Request: @runtobpmd <module> <method>. - public const string RunToBpmdPrefix = "@runtobpmd "; - - /// Request: @runtocrash — run the live process to its second-chance crash. - public const string RunToCrash = "@runtocrash"; - - /// Request: @runtobreak — resume the live process to the next breakpoint. - public const string RunToBreak = "@runtobreak"; -} diff --git a/src/tests/SOS.TestHarness/Flavor.cs b/src/tests/SOS.TestHarness/Flavor.cs deleted file mode 100644 index ec5ef83760..0000000000 --- a/src/tests/SOS.TestHarness/Flavor.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The runtime flavor a target is built and dumped as — the third test axis (after host and -/// stop point). Mirrors what ClrMD's test suite covers. -/// -[Flags] -public enum Flavor -{ - /// Framework-dependent .NET (net10.0). Self-snapshots via dotnet-dump collect. - Core = 1, - - /// Self-contained single-file publish. Self-snapshots via dotnet-dump collect (no bundled createdump). - SingleFile = 2, - - /// Desktop .NET Framework (net48, Windows-only). No diagnostics IPC — dumps captured externally via DbgEng. - Framework = 4, - - /// - /// All flavors valid for the platform. - /// - AllValid = Core | SingleFile | Framework, -} diff --git a/src/tests/SOS.TestHarness/GcType.cs b/src/tests/SOS.TestHarness/GcType.cs deleted file mode 100644 index ae965152fe..0000000000 --- a/src/tests/SOS.TestHarness/GcType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The GC flavor the debuggee runs under when captured/launched — a matrix axis (flags, like -/// ). Server GC produces a multi-heap GC (a fixed heap count with DATAS off, so it -/// can't collapse back to a single heap), which the eeheap parser and the generation/region tests -/// exercise; Workstation is the single-heap default. -/// -[Flags] -public enum GcType -{ - /// Single-heap workstation GC (the runtime default). - Workstation = 1, - - /// Multi-heap server GC (forced to a deterministic heap count via env vars at capture/launch). - Server = 2, - - /// Both GC modes. - AllValid = Workstation | Server, -} diff --git a/src/tests/SOS.TestHarness/HarnessCancellation.cs b/src/tests/SOS.TestHarness/HarnessCancellation.cs deleted file mode 100644 index 62be55f6c6..0000000000 --- a/src/tests/SOS.TestHarness/HarnessCancellation.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace SOS.TestHarness; - -/// -/// Bridges the ambient xunit/MTP test cancellation into the harness's blocking debugger-host waits. -/// -/// The debugger-host REPLs (cdb/dotnet-dump/lldb) drive a child process and block on its output with -/// a generous per-command timeout (up to ~2 minutes, since a live process continue can legitimately -/// run that long under a saturated matrix). Those waits used to ignore cancellation, so when the user hit -/// Ctrl+C the runner printed "Canceling the test session..." and then appeared to hang until the in-flight -/// command's timeout elapsed (or it happened to complete). Observing this token in the blocking waits lets a -/// canceled run unwind promptly — the wait throws , the test's -/// using target/host disposes (which tears the child process down), and the session ends. -/// -internal static class HarnessCancellation -{ - /// - /// The ambient test cancellation token, signaled by the runner on Ctrl+C / session cancellation / - /// global timeout. Returns when there is no active test context - /// (e.g. one-time fixture teardown), so callers can use it unconditionally. - /// - public static CancellationToken Token => TestContext.Current?.CancellationToken ?? CancellationToken.None; -} diff --git a/src/tests/SOS.TestHarness/HarnessSkip.cs b/src/tests/SOS.TestHarness/HarnessSkip.cs deleted file mode 100644 index 012d4a8feb..0000000000 --- a/src/tests/SOS.TestHarness/HarnessSkip.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit.v3; - -namespace SOS.TestHarness; - -/// -/// Thrown by the harness to turn a structural host/flavor limitation into an xUnit dynamic skip -/// (rather than a failure) from inside harness code, where the per-test Assert.Skip APIs are not -/// available. xUnit v3 recognizes a skip by the prefix on the -/// exception message, so any test that lets this exception propagate is reported as skipped with -/// . -/// -/// Use this only for limitations that are intrinsic to a (host, flavor, liveness) combination and apply -/// uniformly to every test exercising it — i.e. there is no per-test variation to express. -/// -public sealed class HarnessSkipException : Exception -{ - public HarnessSkipException(string reason) - : base(DynamicSkipToken.Value + reason) - { - } - - /// Throw a to skip the current test with . - public static void Now(string reason) => throw new HarnessSkipException(reason); -} diff --git a/src/tests/SOS.TestHarness/HostDiagnostics.cs b/src/tests/SOS.TestHarness/HostDiagnostics.cs deleted file mode 100644 index 12fe81156c..0000000000 --- a/src/tests/SOS.TestHarness/HostDiagnostics.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Text; - -namespace SOS.TestHarness; - -/// -/// Captures everything a launched debugger host process (lldb, dotnet-dump) emitted — its command line -/// and the raw stdout/stderr transcript — plus any crash dump it dropped, so a failing test's replay can -/// show what the underlying process actually did before it wedged or died. The two symptoms this exists -/// to diagnose are a broken stdin pipe (the host process crashed out from under us) and a command timeout -/// (the host stopped answering); in both cases the useful evidence is the tail of stderr and a crash dump, -/// neither of which the harness previously retained. -/// -/// The host process is run with the standard .NET crash-dump environment (see -/// ): because SOS's managed extension runs on a real .NET runtime inside -/// lldb (and dotnet-dump is itself a .NET process), a fatal fault in that runtime writes a full dump we can -/// point at with an artifact: line. Dumps for every host land in one shared directory and are -/// attributed back to the right host by the crashing process id (). -/// -/// One instance is owned per (so it survives the pooled dotnet-dump host being -/// closed and reopened) or per live lldb host. It is thread-safe: the stdout and stderr reader threads -/// append concurrently while the test thread reads snapshots. -/// -public sealed class HostDiagnostics -{ - // Keep only the tail of each stream: a crash's useful context is at the end, and an unbounded buffer - // on a long-lived shared host would grow without limit across the many tests that reuse it. - private const int MaxStreamChars = 128 * 1024; - - private static readonly string s_crashRoot = - Path.Combine(RepoLayout.Root, "artifacts", "replays", "crashdumps"); - - private readonly object _gate = new(); - private readonly StringBuilder _stdout = new(); - private readonly StringBuilder _stderr = new(); - private readonly HashSet _pids = new(); - private bool _stdoutTruncated; - private bool _stderrTruncated; - private string _commandLine = string.Empty; - - public HostDiagnostics(string name) => Name = name; - - /// Short host label, e.g. "lldb" or "dotnet-dump". - public string Name { get; } - - /// The shared directory crash dumps are written to (created on demand). - public static string CrashDumpDirectory => s_crashRoot; - - /// The launched command line (exe + args), captured for the replay. - public string CommandLine - { - get { lock (_gate) { return _commandLine; } } - } - - /// - /// Set the standard .NET crash-dump environment on a host process so a fatal fault in its managed - /// runtime (the SOS extension hosted inside lldb, or dotnet-dump itself) writes a full dump plus a - /// crash report and a createdump log into the shared crash directory. Dumps are named with the crashing - /// process id/time so can attribute them to this host by the ids it records. - /// - public void ConfigureCrashDumps(ProcessStartInfo psi) - { - Directory.CreateDirectory(s_crashRoot); - psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; - psi.Environment["DOTNET_DbgMiniDumpType"] = "4"; // Full — required for ClrMD/SOS analysis - psi.Environment["DOTNET_DbgMiniDumpName"] = Path.Combine(s_crashRoot, "%e.%p.%t.dmp"); - psi.Environment["DOTNET_EnableCrashReport"] = "1"; - psi.Environment["DOTNET_CreateDumpDiagnostics"] = "1"; - // Send createdump's own diagnostics to a file rather than the host's stderr, so it neither floods - // the transcript nor gets tangled with the SOS output we scrape for command framing. - psi.Environment["DOTNET_CreateDumpLogToFile"] = Path.Combine(s_crashRoot, "createdump.%p.log"); - psi.Environment["DOTNET_DbgCreateDumpToolPath"] = ToolPaths.CreateDumpPath; - } - - /// Record the launched process (its command line and pid) so a dump it writes can be found. - public void RecordProcess(Process process) - { - lock (_gate) - { - _commandLine = $"{process.StartInfo.FileName} {string.Join(' ', process.StartInfo.ArgumentList)}".Trim(); - try - { - _pids.Add(process.Id); - } - catch - { - // Id can throw if the process already exited; the command line is still useful. - } - } - } - - public void AppendStdout(string line) => Append(_stdout, line, ref _stdoutTruncated); - - public void AppendStderr(string line) => Append(_stderr, line, ref _stderrTruncated); - - private void Append(StringBuilder sb, string line, ref bool truncated) - { - lock (_gate) - { - sb.Append(line).Append('\n'); - if (sb.Length > MaxStreamChars) - { - sb.Remove(0, sb.Length - MaxStreamChars); - truncated = true; - } - } - } - - /// The captured stdout tail (empty if nothing was captured). - public string StdoutTail() => Snapshot(_stdout, _stdoutTruncated); - - /// The captured stderr tail (empty if nothing was captured). - public string StderrTail() => Snapshot(_stderr, _stderrTruncated); - - private string Snapshot(StringBuilder sb, bool truncated) - { - lock (_gate) - { - if (sb.Length == 0) - { - return string.Empty; - } - - return (truncated ? "... (truncated, showing tail)\n" : string.Empty) + sb; - } - } - - /// - /// Crash dumps, crash reports, and createdump logs this host produced — matched out of the shared - /// crash directory by the process ids it recorded. Returns full paths for artifact: lines. - /// - public IReadOnlyList Artifacts() - { - int[] pids; - lock (_gate) - { - if (_pids.Count == 0) - { - return Array.Empty(); - } - - pids = _pids.ToArray(); - } - - if (!Directory.Exists(s_crashRoot)) - { - return Array.Empty(); - } - - List hits = new(); - foreach (string file in Directory.EnumerateFiles(s_crashRoot)) - { - string name = Path.GetFileName(file); - // Dump/report names embed the crashing pid as ".." (%e.%p.%t.dmp); the createdump log is - // "createdump..log". Match either form against the ids we launched. - foreach (int pid in pids) - { - if (name.Contains($".{pid}.", StringComparison.Ordinal)) - { - hits.Add(file); - break; - } - } - } - - hits.Sort(StringComparer.Ordinal); - return hits; - } -} diff --git a/src/tests/SOS.TestHarness/HostFactory.cs b/src/tests/SOS.TestHarness/HostFactory.cs deleted file mode 100644 index 6079bea3cb..0000000000 --- a/src/tests/SOS.TestHarness/HostFactory.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// Creates the concrete host for a given host kind. -internal static class HostFactory -{ - /// Create a dump-backed host. - public static IDebuggerHost CreateDumpHost(Host host, Flavor flavor, string dumpPath, Dac dac = Dac.Legacy, CoreVersion coreVersion = CoreVersion.Net10, string? targetExe = null, HostDiagnostics? diagnostics = null) => host switch - { - // cdb runs dbgeng in a CHILD process (EngineHost), so the test host never loads dbgeng. - Host.Cdb => ChildEngineClient.ForDump(host.ToString().ToLowerInvariant(), dumpPath, DacDirFor(flavor, coreVersion), dac), - Host.DotnetDump => new DotNetDumpHost(dumpPath, flavor, dac, coreVersion, diagnostics), - Host.Lldb => new LldbCliHost(dumpPath, flavor, dac, coreVersion, targetExe, diagnostics), - _ => throw new ArgumentException($"Unknown host '{host}'."), - }; - - /// A live host (exclusive, advancing). On Windows this is the in-process dbgeng engine driven - /// through a child EngineHost process; on Linux/macOS it drives the lldb CLI directly. - public static ILiveDebuggerHost CreateLiveHost(Host host, Flavor flavor, string exePath, CoreVersion coreVersion = CoreVersion.Net10, Dac dac = Dac.Legacy) => host switch - { - Host.Cdb => ChildEngineClient.ForLive(host.ToString().ToLowerInvariant(), exePath, DacDirFor(flavor, coreVersion), dac, flavor), - Host.Lldb => new LldbLiveHost(exePath, flavor, coreVersion, dac), - Host.DotnetDump => throw new ArgumentException("dotnet-dump is post-mortem only; it has no live host."), - _ => throw new ArgumentException($"Unknown live host '{host}'."), - }; - - /// - /// The DAC directory to make dbgeng load explicitly for a flavor. Self-contained single-file bundles - /// the runtime, so cdb can't find mscordaccore.dll on disk — point it at the runtime pack's - /// DAC for the published version. Other flavors find their DAC next to the runtime, so they need no - /// override. - /// - private static string? DacDirFor(Flavor flavor, CoreVersion coreVersion) => - flavor == Flavor.SingleFile ? ToolPaths.SingleFileDacDirectory(coreVersion) : null; -} diff --git a/src/tests/SOS.TestHarness/HostSlot.cs b/src/tests/SOS.TestHarness/HostSlot.cs deleted file mode 100644 index 8e3215fbfc..0000000000 --- a/src/tests/SOS.TestHarness/HostSlot.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A host whose lifetime a may open and close on demand. -/// -internal interface IPooledHost -{ - IDebuggerHost Host { get; } - - void OpenHost(); - - void CloseHost(); -} - -/// -/// Governs how many live host instances of one kind may exist at once — here, exactly one. -/// -/// Two kinds need this for different reasons: -/// -/// cdb (in-process dbgeng) is genuinely one-instance-per-process (a second client -/// throws). -/// dotnet-dump children each busy-wait on stdin at ~100% CPU; keeping many alive -/// saturates the machine, so we keep at most one. -/// -/// The most-recently-used host stays open and is evicted (disposed) only when a different target -/// of the same kind is needed — so a run of assertions against one dump reuses the open host, and -/// switching dumps reopens (cheap relative to the work). Live targets take an exclusive lease for -/// their lifetime. This single-slot constraint is exactly what a subprocess-per-target backend -/// would lift, without changing the test-facing API. -/// -internal sealed class HostSlot -{ - /// The in-process dbgeng slot (cdb dump hosts and live hosts). - public static readonly HostSlot DbgEng = new(); - - /// The dotnet-dump slot (one analyze child alive at a time). - public static readonly HostSlot DotNetDump = new(); - - private readonly object _lock = new(); - private IPooledHost? _open; - private bool _exclusiveHeld; - - /// - /// Ensure 's host is the one open host for this slot, then run - /// against it. Serializes all work on this slot. - /// - public SosOutput Run(IPooledHost owner, Func action) - { - lock (_lock) - { - while (_exclusiveHeld) - { - System.Threading.Monitor.Wait(_lock); - } - - if (!ReferenceEquals(_open, owner)) - { - _open?.CloseHost(); - owner.OpenHost(); - _open = owner; - } - - return action(owner.Host); - } - } - - /// - /// Acquire exclusive use of this slot for a live target's lifetime. Evicts any open host and - /// blocks other use until the returned lease is disposed. - /// - public IDisposable AcquireExclusive() - { - lock (_lock) - { - while (_exclusiveHeld) - { - System.Threading.Monitor.Wait(_lock); - } - - _open?.CloseHost(); - _open = null; - _exclusiveHeld = true; - } - - return new Lease(this); - } - - /// Close the currently-open host, if any (teardown). - public void CloseCurrent() - { - lock (_lock) - { - _open?.CloseHost(); - _open = null; - } - } - - private sealed class Lease : IDisposable - { - private readonly HostSlot _slot; - private bool _disposed; - - public Lease(HostSlot slot) => _slot = slot; - - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - lock (_slot._lock) - { - _slot._exclusiveHeld = false; - System.Threading.Monitor.PulseAll(_slot._lock); - } - } - } -} diff --git a/src/tests/SOS.TestHarness/Hosts.cs b/src/tests/SOS.TestHarness/Hosts.cs deleted file mode 100644 index 8a50e949bd..0000000000 --- a/src/tests/SOS.TestHarness/Hosts.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// The known debugger hosts. Tests parameterize over these (xunit serializes enums). -[Flags] -public enum Host -{ - /// In-process dbgeng, run in a child EngineHost (Windows). - Cdb = 1, - - /// LLDB debugger host (Linux/macOS). - Lldb = 2, - - /// Managed dotnet-dump analyze host (all OSes). - DotnetDump = 4, - - /// - /// All hosts that can be used for the given liveness on this platform. - /// - AllValid = Cdb | Lldb | DotnetDump, -} diff --git a/src/tests/SOS.TestHarness/IDebuggerHost.cs b/src/tests/SOS.TestHarness/IDebuggerHost.cs deleted file mode 100644 index 522c2cd55f..0000000000 --- a/src/tests/SOS.TestHarness/IDebuggerHost.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// One debugger host that SOS can run inside. A test author programs against this -/// surface without depending on whether the backend is dbgeng, LLDB, or dotnet-dump. -/// -/// A host owns exactly one loaded target and is single-threaded with respect to -/// command execution — mirroring the real-world constraint that a dbgeng instance -/// is single-threaded and holds one dump at a time. Parallelism therefore comes -/// from running different hosts/targets concurrently, never from issuing -/// concurrent commands to one host. -/// -public interface IDebuggerHost : IDisposable -{ - /// Short host name, e.g. "cdb" or "dotnet-dump". Drives host-conditional assertions. - string Name { get; } - - /// Load the SOS extension into the host. Idempotent. - void LoadSos(); - - /// - /// Run a raw debugger command exactly as typed (no SOS prefixing). Use for engine - /// commands like .load, ~*k, etc. - /// - SosOutput Execute(string command); - - /// - /// Run a SOS command. The host applies whatever prefixing it needs (dbgeng wants a - /// leading !; dotnet-dump takes the bare command), so the test author writes - /// Sos("clrstack") once and it works everywhere. - /// - SosOutput Sos(string command); -} diff --git a/src/tests/SOS.TestHarness/IDiagnosticHost.cs b/src/tests/SOS.TestHarness/IDiagnosticHost.cs deleted file mode 100644 index 123fcb47e6..0000000000 --- a/src/tests/SOS.TestHarness/IDiagnosticHost.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Optional capability on a host that captures its underlying process's stdout/stderr and any crash dump -/// (see ). Only the child-process hosts on Linux/macOS (lldb, dotnet-dump) -/// implement it; the in-process dbgeng hosts do not, so callers probe for it with as rather than -/// requiring it on every . -/// -public interface IDiagnosticHost -{ - /// The captured diagnostics for this host, or null if none are being collected. - HostDiagnostics? Diagnostics { get; } -} diff --git a/src/tests/SOS.TestHarness/ILiveDebuggerHost.cs b/src/tests/SOS.TestHarness/ILiveDebuggerHost.cs deleted file mode 100644 index 0ad377e56c..0000000000 --- a/src/tests/SOS.TestHarness/ILiveDebuggerHost.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A live, advancing debugger host: an that additionally drives a running -/// debuggee forward. Where a dump host opens a frozen target, a live host launches the debuggee, parks it -/// at the loader break with SOS loaded, and then advances it on demand — to a managed method -/// (), to its crash (), or to the next armed breakpoint -/// (). -/// -/// This is the seam that lets be backend-agnostic: the Windows backend is the -/// in-process dbgeng engine (driven out-of-process through ), and the -/// Linux/macOS backend drives the lldb CLI. A test author programs against -/// and never sees which one is underneath. -/// -public interface ILiveDebuggerHost : IDebuggerHost -{ - /// Set a managed breakpoint on ! and run to - /// it. Throws if the process exits or crashes before reaching the method. - SosOutput RunToBpmd(string module, string method); - - /// Run the process until it crashes (a second-chance/fatal fault). Throws if it exits - /// cleanly without crashing. - SosOutput RunToCrash(); - - /// Resume to the next breakpoint the caller has already armed (e.g. via Sos("bpmd …")). - /// Sets/clears nothing itself. Throws if the process exits without hitting one. - SosOutput RunToBreakpoint(); -} diff --git a/src/tests/SOS.TestHarness/LiveTarget.cs b/src/tests/SOS.TestHarness/LiveTarget.cs deleted file mode 100644 index 9fe0d4463c..0000000000 --- a/src/tests/SOS.TestHarness/LiveTarget.cs +++ /dev/null @@ -1,164 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A live, advancing target owned exclusively by one test. The debuggee is launched under a child -/// (its own EngineHost process) and parked at the debugger's initial -/// breakpoint - before CoreCLR loads - with SOS loaded. already works there (e.g. -/// bpmd, which sets a pending managed breakpoint), so it does not throw before the -/// first navigation, unlike a . -/// -/// Navigation only moves forward and tracks where we are stopped: re-asking for the current point is -/// a no-op; asking for a later one runs there (skipping intervening stop points); if the process -/// crashes or exits before reaching it, an is thrown. -/// is the raw form for breaking on an arbitrary method. Dispose (use -/// using) to shut the child down. -/// -public sealed class LiveTarget : Target -{ - // Sentinel for "stopped at the crash" - not a real stop-point name (can't collide). - private const string CrashMarker = "\0crash"; - - // Live debugging does not parallelize like dump replay: every live session spawns its own debugger - // (lldb) ptrace-attached to a running debuggee. Letting xunit launch one per core (≈20 here) - // overwhelms the box - lldb sessions begin to busy-spin and never return, stacking command timeouts - // into an apparent hang. Bound the number of concurrent live sessions so a few slow ones can't - // saturate every core. Override with SOSHARNESS_MAX_LIVE; default leaves generous headroom below the - // core count. - private static readonly SemaphoreSlim s_liveGate = new(ComputeMaxConcurrentLive()); - - private readonly TargetDefinition _definition; - private ILiveDebuggerHost? _host; - private bool _gateHeld; - private string? _at; // current stop name, CrashMarker, or null (still at the initial break) - private bool _disposed; - - internal LiveTarget(Host hostKind, TargetDefinition definition, Flavor flavor, string exePath, - CoreVersion coreVersion = CoreVersion.Net10, Dac dac = Dac.Legacy) - : base(hostKind, definition.Name, flavor) - { - _definition = definition; - - // Hold the live-session slot for the entire lifetime of this target (launch -> commands -> - // dispose), not just creation, so the cap actually bounds concurrent live debuggers. Observe the - // ambient test cancellation so a Ctrl+C while queued behind the gate unwinds promptly instead of - // blocking until a slot frees. - s_liveGate.Wait(HarnessCancellation.Token); - _gateHeld = true; - try - { - _host = HostFactory.CreateLiveHost(hostKind, flavor, exePath, coreVersion, dac); - } - catch - { - s_liveGate.Release(); - _gateHeld = false; - throw; - } - } - - private static int ComputeMaxConcurrentLive() - { - if (int.TryParse(Environment.GetEnvironmentVariable("SOSHARNESS_MAX_LIVE"), out int configured) && configured > 0) - { - return configured; - } - - return Math.Clamp(Environment.ProcessorCount / 4, 2, 6); - } - - protected override void GoToStopPointCore(string stopName) - { - StopPoint stop = _definition.Stop(stopName); - if (stop.Method is null) - { - throw new InvalidOperationException( - $"Stop point '{stopName}' has no method to break on (kind {stop.Kind}); use GoToCrash()."); - } - - if (_at == stop.Name) - { - return; // already here - } - - SkipIfLiveBpmdUnsupported(); - - // Runs forward to the marker; throws if the process exits/crashes before reaching it. The - // managed module for bpmd is flavor-specific (desktop's is the EXE, .NET Core's the DLL). - Engine.RunToBpmd(_definition.ModuleFor(Flavor), stop.Method); - _at = stop.Name; - } - - protected override void GoToCrashCore() - { - if (_at == CrashMarker) - { - return; // already at the crash; repeatable no-op - } - - Engine.RunToCrash(); // throws if the process exits without crashing - _at = CrashMarker; - } - - /// - /// Resume the live process until it next hits a breakpoint. Unlike - /// this sets and clears nothing — the caller arms the breakpoint (e.g. - /// Sos("bpmd Module Method")) and this just runs to it. Throws if the process exits first. - /// - public void RunToBreakpoint() - { - SkipIfLiveBpmdUnsupported(); - - Engine.RunToBreakpoint(); - _at = null; // arbitrary, caller-managed location — not a named point - ReplayContext.Current?.Add(ReplayStepKind.Navigate, "RunToBreakpoint()", null); - } - - /// - /// Live bpmd cannot bind in a self-contained single-file image under the lldb host: bpmd arms a - /// JIT/prestub notification breakpoint on a CoreCLR routine, but in a self-contained single-file - /// publish the runtime is statically linked into the (symbol-stripped) app image, so lldb has no - /// symbol to place that breakpoint on and the debuggee simply runs past every managed stop point. The - /// .NET Core flavor keeps CoreCLR as a distinct libcoreclr.so module, so the same notification - /// breakpoint resolves there. This applies uniformly to every live, single-file, lldb test that - /// navigates via a managed stop point, so it is enforced here rather than per test. - /// - private void SkipIfLiveBpmdUnsupported() - { - if (Host == Host.Lldb && Flavor == Flavor.SingleFile) - { - HarnessSkipException.Now( - "live bpmd cannot bind in a single-file image under lldb (CoreCLR is statically linked and " + - "symbol-stripped)"); - } - } - - protected override SosOutput SosCore(string command) => Engine.Sos(command); - - protected override SosOutput ExecuteCore(string command) => Engine.Execute(command); - - internal override HostDiagnostics? CurrentDiagnostics => (_host as IDiagnosticHost)?.Diagnostics; - - private ILiveDebuggerHost Engine => - _host ?? throw new ObjectDisposedException(nameof(LiveTarget)); - - public override void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _host?.Dispose(); - _host = null; - - if (_gateHeld) - { - _gateHeld = false; - s_liveGate.Release(); - } - } -} diff --git a/src/tests/SOS.TestHarness/Liveness.cs b/src/tests/SOS.TestHarness/Liveness.cs deleted file mode 100644 index d98a0af335..0000000000 --- a/src/tests/SOS.TestHarness/Liveness.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -[Flags] -public enum Liveness -{ - Live = 1, - Dump = 2, - AllValid = Live | Dump, -} diff --git a/src/tests/SOS.TestHarness/LldbCliHost.cs b/src/tests/SOS.TestHarness/LldbCliHost.cs deleted file mode 100644 index a376d20d75..0000000000 --- a/src/tests/SOS.TestHarness/LldbCliHost.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The dump (post-mortem) "lldb" host (Linux/macOS): opens a core file under lldb and runs SOS -/// against it. The lldb-driving machinery (spawn, runcommand framing, sentinel draining, dispose) -/// lives in ; this type only opens the core and points SOS at the right DAC. -/// -/// SOS is the native lldb plugin (libsosplugin.so/.dylib), loaded via plugin load; -/// its managed extension is hosted on the runtime named by sethostruntime. SOS commands are -/// dispatched through the plugin's universal sos <command> entry (the lldb analogue of -/// dbgeng's !command), so a test author writes Sos("clrstack") once and it works on every -/// host. -/// -public sealed class LldbCliHost : LldbHostBase -{ - private readonly Flavor _flavor; - private readonly Dac _dac; - private readonly CoreVersion _coreVersion; - - public override string Name => "lldb"; - - public LldbCliHost(string dumpPath, Flavor flavor, Dac dac = Dac.Legacy, CoreVersion coreVersion = CoreVersion.Net10, string? targetExe = null, HostDiagnostics? diagnostics = null) - { - _flavor = flavor; - _dac = dac; - _coreVersion = coreVersion; - StartLldb(diagnostics: diagnostics, captureCrashDumps: true); - - // Load the core. Pass the target executable as the module so lldb can map the program image — - // essential for self-contained single-file bundles (coreclr is embedded in the exe) and for - // createdump-generated crash cores, whose notes alone don't let lldb locate the single-file - // module (SOS then reports "Failed to find runtime module (libcoreclr.so)"). Mirrors the legacy - // SOSRunner, which always passed the host exe. SOS is loaded later in LoadSos. - string create = string.IsNullOrEmpty(targetExe) - ? $"target create --core \"{dumpPath}\"" - : $"target create --core \"{dumpPath}\" \"{targetExe}\""; - Run(create, LoadTimeout); - } - - public override void LoadSos() - { - if (_dac == Dac.CDac) - { - ToolPaths.EnsureLldbPluginCDacOverride(); - } - - Run($"plugin load \"{ToolPaths.LldbPluginPath}\""); - Run($"sethostruntime \"{ToolPaths.HostRuntimeDirectory}\""); - - // Self-contained single-file bundles carry coreclr inside the exe, so there is no runtime - // directory on disk next to which SOS can find the matching DAC. Point SOS's symbol store at the - // runtime pack's native directory (which ships the DAC the publish resolved against); SOS then - // resolves the DAC for the dump's coreclr build-id from there. This is a *local directory* - // (no network), so the session stays hermetic. Other flavors find their DAC next to the on-disk - // runtime and need no override. (cdb does the equivalent via `.cordll -lp`.) - string? dacDir = _dac == Dac.CDac ? ToolPaths.CDacOverrideDirectory : null; - dacDir ??= _flavor == Flavor.SingleFile ? ToolPaths.SingleFileDacDirectory(_coreVersion) : null; - if (dacDir is { Length: > 0 }) - { - Run($"setsymbolserver -directory \"{dacDir}\""); - } - - // Select the DAC for this config's Dac axis: Legacy => `--usecdac false`, CDac (.NET 11+ only) => - // `--usecdac true`. The same dump is reused across both, so only this debug-time toggle differs. - // SOSHARNESS_USECDAC (off by default; never set in CI) is a global clamp that overrides the axis on - // a dev box whose installed runtimes are skewed such that the cDAC can't load. - Run($"runtimes --usecdac {DacPolicy.UseCDac(_dac)}"); - } -} diff --git a/src/tests/SOS.TestHarness/LldbHostBase.cs b/src/tests/SOS.TestHarness/LldbHostBase.cs deleted file mode 100644 index 29c74f1d4b..0000000000 --- a/src/tests/SOS.TestHarness/LldbHostBase.cs +++ /dev/null @@ -1,398 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Text; - -namespace SOS.TestHarness; - -/// -/// Shared machinery for the lldb-CLI hosts (dump and live). Spawns the lldb binary as a child -/// process, imports lldbhelper.py (which adds a runcommand command), and frames every -/// command as runcommand <cmd> so each one is delimited by a sentinel that also carries a real -/// success bit (<END_COMMAND_OUTPUT> / <END_COMMAND_ERROR> from -/// SBCommandReturnObject.Succeeded()). SOS itself is the native lldb plugin -/// (libsosplugin.so/.dylib); its managed extension runs on the runtime named by -/// sethostruntime. Derived hosts differ only in how they create the target (a core file vs. a -/// launched process) and how they advance it. -/// -public abstract class LldbHostBase : IDebuggerHost, IDiagnosticHost -{ - private const string EndMarker = ""; - private const string ErrorMarker = ""; - - private static readonly string? s_trace = Environment.GetEnvironmentVariable("SOSHARNESS_LLDB_TRACE"); - private static readonly object s_traceLock = new(); - - private Process _process = null!; - private StreamWriter _stdin = null!; - private readonly BlockingCollection _lines = new(); - private Thread _reader = null!; - private Thread? _stderrReader; - private HostDiagnostics? _diagnostics; - private string? _commandInFlight; - - public abstract string Name { get; } - - /// Captured stdout/stderr and crash dumps for this host (see ). - public HostDiagnostics? Diagnostics => _diagnostics; - - /// - /// How long to wait for a single command's output before declaring lldb wedged. Dump hosts answer from - /// a static core and are uniformly fast, so the default is tight. The live host overrides this higher: - /// its process continue must let the debuggee actually run to a managed stop point, which under a - /// saturated full-matrix run can be briefly CPU-starved and legitimately slow (observed ~2 min), so a - /// tight timeout would flake on contention rather than catch a real wedge. - /// - protected virtual TimeSpan CommandTimeout => TimeSpan.FromSeconds(120); - - /// - /// How long to wait for the heavy, contention-sensitive startup steps — spawning lldb and draining its - /// banner, and (for the dump host) target create --core. Loading a multi-hundred-MB core under a - /// saturated full-matrix run can take noticeably longer than a single command (observed ~93s isolated, - /// >120s under contention), and it shares none of the wedge risk of an interactive command, so it gets - /// its own, looser budget. Per-command execution keeps the tighter so a - /// genuinely wedged command still surfaces promptly. Override with SOSHARNESS_LLDB_LOAD_TIMEOUT - /// (seconds). - /// - protected virtual TimeSpan LoadTimeout { get; } = - int.TryParse(Environment.GetEnvironmentVariable("SOSHARNESS_LLDB_LOAD_TIMEOUT"), out int s) && s > 0 - ? TimeSpan.FromSeconds(s) - : TimeSpan.FromSeconds(300); - - /// - /// Spawn lldb, import the command helper, and drain the startup banner so the host is ready for - /// commands. Derived constructors call this first, then create/advance their target. - /// runs against the before launch (e.g. to - /// set debuggee environment variables a live host needs inherited). , if - /// supplied, captures the process's stdout/stderr; when is also - /// set the process runs with the .NET crash-dump environment so a fatal fault in the hosted SOS runtime - /// writes a dump (only the dump host opts into this — a live host would otherwise also dump its - /// debuggee's intentional crashes). - /// - protected void StartLldb(Action? configure = null, HostDiagnostics? diagnostics = null, bool captureCrashDumps = false) - { - _diagnostics = diagnostics; - - string helper = Path.Combine(AppContext.BaseDirectory, "lldbhelper.py"); - if (!File.Exists(helper)) - { - throw new FileNotFoundException($"lldb command helper not found at '{helper}'.", helper); - } - - ProcessStartInfo psi = new() - { - FileName = ToolPaths.LldbExe, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - // --no-lldbinit: ignore the dev's ~/.lldbinit so the session is hermetic. - // disable-aslr false: toggling ASLR needs ptrace perms we may not have; keep it off so target - // creation/launch never fails on that. - // prompt-on-quit false: never block waiting for a y/n on shutdown. - psi.ArgumentList.Add("--no-lldbinit"); - psi.ArgumentList.Add("-o"); - psi.ArgumentList.Add("settings set target.disable-aslr false"); - psi.ArgumentList.Add("-o"); - psi.ArgumentList.Add("settings set interpreter.prompt-on-quit false"); - psi.ArgumentList.Add("-o"); - psi.ArgumentList.Add($"command script import {helper}"); - - // Hermetic symbols: scrub any inherited _NT_SYMBOL_PATH (a dev's may point at the Azure-authed - // symweb). We *remove* it rather than point it at a local cache: the native lldb SOS plugin treats - // a set _NT_SYMBOL_PATH as the only search root and stops falling back to the on-disk runtime - // modules, which is how SOS locates the DAC for a locally captured target. Leaving it unset keeps - // that on-disk resolution working. - psi.Environment.Remove("_NT_SYMBOL_PATH"); - - // Run the host with the .NET crash-dump environment so a fatal fault in the SOS managed runtime - // hosted inside lldb writes a full dump we can surface as an artifact. Do this before configure so - // a derived host could still override it if needed. - if (captureCrashDumps) - { - diagnostics?.ConfigureCrashDumps(psi); - } - - configure?.Invoke(psi); - - _process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start lldb"); - _stdin = _process.StandardInput; - _diagnostics?.RecordProcess(_process); - - _reader = new Thread(ReadLoop) { IsBackground = true, Name = "lldb-reader" }; - _reader.Start(); - - // Drain stderr on its own thread: lldb prints crash diagnostics, python errors, and unhandled - // managed-exception traces there. It was previously redirected but never read, so a full stderr - // pipe could even block the host — and, more importantly, the evidence for a crash was discarded. - if (_diagnostics is not null) - { - _stderrReader = new Thread(StderrLoop) { IsBackground = true, Name = "lldb-stderr" }; - _stderrReader.Start(); - } - - // Drain the startup banner up to the marker the helper prints from __lldb_init_module. - DrainToMarker(LoadTimeout); - } - - public abstract void LoadSos(); - - /// Run a raw lldb command verbatim (no SOS dispatch). - public SosOutput Execute(string command) => new(Name, command, Run(command)); - - /// Run a SOS command via the plugin's universal sos <command> dispatcher. - public SosOutput Sos(string command) => new(Name, command, Run("sos " + command)); - - /// Send a command through the runcommand helper and return its output up to the sentinel. - protected string Run(string command) => Run(command, CommandTimeout); - - /// - /// As , but with an explicit timeout — used for the slow, contention-sensitive - /// load steps (e.g. target create --core) that warrant the looser . - /// - protected string Run(string command, TimeSpan timeout) - { - _commandInFlight = command; - try - { - _stdin.WriteLine("runcommand " + command); - _stdin.Flush(); - string outp = DrainToMarker(timeout, command); - AppendTrace($"\n>>> lldb={ToolPaths.LldbExe}\n>>> plugin={ToolPaths.LldbPluginPath}\n>>> rt={ToolPaths.HostRuntimeDirectory}\n(lldb) runcommand {command}\n{outp}\n"); - return outp; - } - catch (Exception ex) when (ex is IOException or ObjectDisposedException) - { - AppendTrace($"--- lldb command failed ---{Environment.NewLine}command={command}{Environment.NewLine}{LldbProcessState()}{Environment.NewLine}"); - throw CreateLldbFailure("lldb command failed", command, ex); - } - finally - { - _commandInFlight = null; - } - } - - /// - /// Collect output lines until the sentinel. Strips lldb's prompt-echo lines ((lldb) ...), which - /// lldb writes for every command when stdin is redirected; SOS output never begins with that prefix, so - /// this is safe. - /// - private string DrainToMarker(TimeSpan timeout, string? command = null) - { - StringBuilder sb = new(); - while (true) - { - if (!_lines.TryTake(out string? line, (int)timeout.TotalMilliseconds, HarnessCancellation.Token)) - { - if (_lines.IsCompleted || HasExited()) - { - AppendTrace($"--- lldb stdout closed ---{Environment.NewLine}command={command ?? ""}{Environment.NewLine}{LldbProcessState()}{Environment.NewLine}"); - throw CreateLldbFailure("lldb stdout closed", command, null); - } - - AppendTrace($"--- lldb command timed out ---{Environment.NewLine}command={command ?? ""}{Environment.NewLine}timeout={timeout}{Environment.NewLine}{LldbProcessState()}{Environment.NewLine}"); - throw CreateLldbFailure("lldb command timed out", command, null); - } - - string trimmed = line.TrimEnd(); - if (trimmed.EndsWith(EndMarker, StringComparison.Ordinal) || trimmed.EndsWith(ErrorMarker, StringComparison.Ordinal)) - { - break; - } - - if (line.StartsWith("(lldb) ", StringComparison.Ordinal)) - { - continue; - } - - sb.AppendLine(line); - } - - return sb.ToString(); - } - - private void ReadLoop() - { - try - { - string? line; - while ((line = _process.StandardOutput.ReadLine()) is not null) - { - _diagnostics?.AppendStdout(line); - _lines.Add(line); - } - } - finally - { - _lines.CompleteAdding(); - AppendTrace($"--- lldb stdout eof ---{Environment.NewLine}{LldbProcessState()}{Environment.NewLine}"); - } - } - - private void StderrLoop() - { - try - { - string? line; - while ((line = _process.StandardError.ReadLine()) is not null) - { - _diagnostics?.AppendStderr(line); - } - } - catch - { - // Best effort: the process may die mid-read. Whatever we captured is still available. - } - } - - private InvalidOperationException CreateLldbFailure(string phase, string? command, Exception? inner) - { - StringBuilder sb = new(); - sb.AppendLine($"phase={phase}"); - sb.AppendLine(LldbProcessState()); - sb.AppendLine($"command={command ?? ""}"); - sb.AppendLine($"commandInFlight={_commandInFlight ?? ""}"); - sb.AppendLine($"ToolPaths.LldbExe={ToolPaths.LldbExe}"); - sb.AppendLine($"ToolPaths.LldbPluginPath={ToolPaths.LldbPluginPath}"); - sb.AppendLine($"ToolPaths.HostRuntimeDirectory={ToolPaths.HostRuntimeDirectory}"); - sb.AppendLine($"crashDumpDirectory={HostDiagnostics.CrashDumpDirectory}"); - sb.AppendLine($"SOSHARNESS_LLDB_TRACE={s_trace ?? ""}"); - - if (_diagnostics is not null) - { - sb.AppendLine($"commandLine={_diagnostics.CommandLine}"); - sb.AppendLine("--- lldb stderr tail ---"); - AppendOrEmpty(sb, _diagnostics.StderrTail()); - sb.AppendLine("--- lldb stdout tail ---"); - AppendOrEmpty(sb, _diagnostics.StdoutTail()); - } - - return new InvalidOperationException(sb.ToString(), inner); - } - - private static void AppendOrEmpty(StringBuilder sb, string text) - { - if (text.Length == 0) - { - sb.AppendLine("(empty)"); - return; - } - - sb.Append(text); - if (!text.EndsWith('\n')) - { - sb.AppendLine(); - } - } - - private bool HasExited() - { - try - { - return _process.HasExited; - } - catch - { - return false; - } - } - - private string LldbProcessState() - { - StringBuilder sb = new(); - try - { - sb.AppendLine($"pid={_process.Id}"); - bool hasExited = _process.HasExited; - sb.AppendLine($"hasExited={hasExited}"); - if (hasExited) - { - sb.AppendLine($"exitCode={_process.ExitCode}"); - } - } - catch (Exception ex) - { - sb.AppendLine($"processStateError={ex.GetType().Name}: {ex.Message}"); - } - - return sb.ToString().TrimEnd(); - } - - private static void AppendTrace(string text) - { - if (s_trace is not { Length: > 0 }) - { - return; - } - - try - { - string? directory = Path.GetDirectoryName(s_trace); - if (!string.IsNullOrEmpty(directory)) - { - Directory.CreateDirectory(directory); - } - - lock (s_traceLock) - { - File.AppendAllText(s_trace, text); - } - } - catch - { - // Trace output is diagnostic only. - } - } - - public void Dispose() - { - try - { - if (!_process.HasExited) - { - // Ask lldb to quit. A *wedged* lldb (busy-spinning on its inferior, not reading stdin) - // never sees this, so don't wait long before escalating to a hard kill. - try - { - _stdin.WriteLine("quit"); - _stdin.Flush(); - } - catch - { - // stdin may already be closed; fall through to the kill path. - } - - if (!_process.WaitForExit(3000)) - { - _process.Kill(entireProcessTree: true); - } - } - } - catch - { - // best effort - } - finally - { - // Reap the child (and its debuggee, killed via the process tree above). Without this the - // killed lldb/debuggee linger as unreaped zombies; across a long multi-version run they - // accumulate, saturate the box, and wedge later live sessions. A bounded wait keeps teardown - // from blocking if the kill is still propagating. - try - { - _process.WaitForExit(10000); - } - catch - { - // best effort - } - - _process.Dispose(); - } - } -} diff --git a/src/tests/SOS.TestHarness/LldbLiveHost.cs b/src/tests/SOS.TestHarness/LldbLiveHost.cs deleted file mode 100644 index 54b37ac6b8..0000000000 --- a/src/tests/SOS.TestHarness/LldbLiveHost.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// The live "lldb" host (Linux/macOS): launches the debuggee under lldb, parks it at the program -/// entry (before CoreCLR is up) with SOS loaded, and advances it on demand. It is the lldb analogue of -/// : a stateful, advancing target owned exclusively by one test. -/// -/// Unlike the Windows engine — which runs in-process and is therefore driven out-of-process through a -/// child — lldb is already its own process, so this host drives it -/// directly through (the shared spawn/runcommand/sentinel machinery). -/// -/// Stop detection is by text: process continue runs synchronously and its output reports either a -/// stop ("Process N stopped") or an exit ("Process N exited"); the precise managed location -/// is confirmed with clrstack, exactly as the dbgeng host confirms with its own clrstack. -/// -public sealed class LldbLiveHost : LldbHostBase, ILiveDebuggerHost -{ - // A navigation resumes the debuggee through however many internal SOS notification breakpoints (JIT / - // prestub) it takes to reach the requested managed method. In the healthy case those auto-continue so - // the method is reached on the first resume; under heavy CPU contention that auto-continue can degrade - // into many separate stops. We therefore bound the walk by a wall clock (see CommandTimeout) rather - // than a small fixed resume count, and keep this only as a safety net against a pathological - // instantly-returning continue so the loop can never spin forever. - private const int MaxResumes = 10000; - - private readonly Flavor _flavor; - private readonly Dac _dac; - private readonly CoreVersion _coreVersion; - - public override string Name => "lldb-live"; - - // Live navigation resumes the debuggee and waits for it to reach a managed stop point; under a - // saturated full-matrix run that can be briefly CPU-starved and take ~2 min, so give it more headroom - // than the (uniformly fast) dump hosts to avoid flaking on contention. Override with SOSHARNESS_LIVE_TIMEOUT - // (seconds). - protected override TimeSpan CommandTimeout { get; } = - int.TryParse(Environment.GetEnvironmentVariable("SOSHARNESS_LIVE_TIMEOUT"), out int s) && s > 0 - ? TimeSpan.FromSeconds(s) - : TimeSpan.FromSeconds(300); - - public LldbLiveHost(string exePath, Flavor flavor, CoreVersion coreVersion = CoreVersion.Net10, Dac dac = Dac.Legacy) - { - _flavor = flavor; - _dac = dac; - _coreVersion = coreVersion; - - // The debuggee inherits the lldb process environment. Disable W^E so SOS's bpmd can patch JIT-ed - // code (see dotnet/diagnostics#3126), matching what the legacy live lldb harness set. For a - // framework-dependent (Core) debuggee, point its apphost at the multi-version test runtime install - // so it binds the runtime matching its target framework (net8 -> 8.0.x, net11 -> the preview). - // - // Capture the host's stdout/stderr for diagnosability, but do NOT enable crash-dump collection - // here: the env would be inherited by the launched debuggee and dump on every intentional crash a - // "run to crash" test triggers. Only the post-mortem lldb host (no debuggee) opts into dumps. - StartLldb(psi => - { - psi.Environment["DOTNET_EnableWriteXorExecute"] = "0"; - if (_flavor == Flavor.Core) - { - psi.Environment["DOTNET_ROOT"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_ROOT(x86)"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_MULTILEVEL_LOOKUP"] = "0"; - } - }, diagnostics: new HostDiagnostics(Name)); - - Run($"target create \"{exePath}\""); - - // Stop at the program entry so we can load SOS and arm bpmd before the app runs. - Run("process launch -s"); - - // A managed fault (divide-by-zero -> SIGFPE, null-deref -> SIGSEGV, etc.) is first delivered to the - // runtime's signal handler, which turns it into a managed exception. We must therefore pass those - // signals through to the debuggee without stopping; an *unhandled* managed exception then tears the - // process down via abort() (SIGABRT), which is the point a live "run to crash" should stop at. - Run("process handle -s false -n false -p true SIGFPE"); - Run("process handle -s false -n false -p true SIGSEGV"); - Run("process handle -s true -n true -p true SIGABRT"); - - LoadSos(); - } - - public override void LoadSos() - { - if (_dac == Dac.CDac) - { - ToolPaths.EnsureLldbPluginCDacOverride(); - } - - Run($"plugin load \"{ToolPaths.LldbPluginPath}\""); - Run($"sethostruntime \"{ToolPaths.HostRuntimeDirectory}\""); - - string? dacDir = _dac == Dac.CDac ? ToolPaths.CDacOverrideDirectory : null; - dacDir ??= _flavor == Flavor.SingleFile ? ToolPaths.SingleFileDacDirectory(_coreVersion) : null; - if (dacDir is { Length: > 0 }) - { - Run($"setsymbolserver -directory \"{dacDir}\""); - } - - // Select the DAC for this config's Dac axis (Legacy => false, CDac on .NET 11+ => true). The - // SOSHARNESS_USECDAC clamp (off by default; never in CI) overrides it on a skewed dev box. - Run($"runtimes --usecdac {DacPolicy.UseCDac(_dac)}"); - } - - /// - /// Set a managed breakpoint on ! and run until it is - /// hit. Throws if the process exits first or the breakpoint is never reached. - /// - public SosOutput RunToBpmd(string module, string method) - { - ClearBreakpoints(); - string bpmdOutput = Sos($"bpmd {module} {method}").Text; - - // bpmd reaches the method in stages (a JIT/prestub notification, then the entry), so resume until - // clrstack confirms we are actually stopped at the requested method. Normally SOS's notification - // breakpoints auto-continue and the method is reached on the very first resume; under a saturated - // full-matrix run that auto-continue can degrade into many individual stops, so bound the walk by - // a wall clock (CommandTimeout) instead of a small fixed count — a slow-but-progressing navigation - // must still complete, not fail at an arbitrary Nth resume. - DateTime deadline = DateTime.UtcNow + CommandTimeout; - int resumes = 0; - while (DateTime.UtcNow < deadline && resumes < MaxResumes) - { - string cont = Execute("process continue").Text; - resumes++; - if (StoppedAtMethod(method)) - { - return new SosOutput(Name, $"bpmd {module} {method}", bpmdOutput); - } - - if (HasExited(cont) || ProcessIsDead()) - { - throw new InvalidOperationException( - $"Debuggee exited before hitting bpmd {module}!{method} (after {resumes} resume(s))."); - } - } - - throw new InvalidOperationException( - $"Did not reach bpmd {module}!{method} within {CommandTimeout} ({resumes} resume(s))."); - } - - /// - /// Run the process until it crashes (the runtime aborts on an unhandled managed exception, i.e. - /// SIGABRT). Throws if it exits cleanly without crashing. - /// - public SosOutput RunToCrash() - { - ClearBreakpoints(); - - DateTime deadline = DateTime.UtcNow + CommandTimeout; - int resumes = 0; - while (DateTime.UtcNow < deadline && resumes < MaxResumes) - { - string cont = Execute("process continue").Text; - resumes++; - if (StoppedOnSignal(cont)) - { - return new SosOutput(Name, "run-to-crash", cont); - } - - if (HasExited(cont) || ProcessIsDead()) - { - throw new InvalidOperationException("Process exited without crashing."); - } - } - - throw new InvalidOperationException($"Process did not crash within {CommandTimeout} ({resumes} resume(s))."); - } - - /// - /// Resume to the next breakpoint the caller has already armed (e.g. via Sos("bpmd …")). Sets and - /// clears nothing itself. Throws if the process exits without hitting one. - /// - public SosOutput RunToBreakpoint() - { - string cont = Execute("process continue").Text; - if (HasExited(cont)) - { - throw new InvalidOperationException("Process exited without hitting a breakpoint."); - } - - if (StoppedOnSignal(cont)) - { - throw new InvalidOperationException($"Hit a fatal signal, not a breakpoint:\n{cont}"); - } - - return new SosOutput(Name, "run-to-breakpoint", cont); - } - - /// Drop any breakpoints left from a previous stop point so they don't re-trigger on resume. - private void ClearBreakpoints() - { - Sos("bpmd -clearall"); - Execute("breakpoint delete --force"); - } - - /// Is the managed call stack currently topped by ? - private bool StoppedAtMethod(string method) - { - string stack = Sos("clrstack").Text; - return stack.Contains(method, StringComparison.Ordinal); - } - - /// - /// True if a process continue reported the debuggee exiting. Covers the normal exit line plus the - /// symptoms lldb prints when the inferior died out from under a command under contention (a lost gdb- - /// remote connection, or a subsequent command complaining the process is gone) — any of which means the - /// debuggee is no longer resumable and we must stop, not keep resuming. - /// - private static bool HasExited(string continueOutput) => - continueOutput.Contains("exited with status", StringComparison.OrdinalIgnoreCase) || - continueOutput.Contains(" exited ", StringComparison.OrdinalIgnoreCase) || - continueOutput.Contains("lost connection", StringComparison.OrdinalIgnoreCase) || - continueOutput.Contains("process must be launched", StringComparison.OrdinalIgnoreCase); - - /// - /// Authoritatively ask lldb whether the debuggee is gone, instead of only scraping the (human-readable, - /// and under contention occasionally mis-framed) process continue text. Queries the live - /// SBProcess state via the script interpreter and treats the terminal states — invalid, - /// unloaded, detached, exited — as dead. Defensive: if the state can't be read/parsed it returns false - /// and callers fall back to . - /// - private bool ProcessIsDead() - { - // lldb.eStateType: 0 invalid, 1 unloaded, 2 connected, 3 attaching, 4 launching, 5 stopped, - // 6 running, 7 stepping, 8 crashed, 9 detached, 10 exited, 11 suspended. - const string Tag = "SOSHARNESS_STATE="; - string outp = Execute( - $"script print('{Tag}%d' % lldb.debugger.GetSelectedTarget().GetProcess().GetState())").Text; - - int idx = outp.LastIndexOf(Tag, StringComparison.Ordinal); - if (idx < 0) - { - return false; // couldn't determine — let text-based HasExited decide - } - - int start = idx + Tag.Length; - int end = start; - while (end < outp.Length && char.IsDigit(outp[end])) - { - end++; - } - - if (end == start || !int.TryParse(outp.AsSpan(start, end - start), out int state)) - { - return false; - } - - return state is 0 or 1 or 9 or 10; // invalid, unloaded, detached, exited - } - - /// True if a process continue stopped on a signal (the runtime's abort = a crash). - private static bool StoppedOnSignal(string continueOutput) => - continueOutput.Contains("stop reason = signal", StringComparison.OrdinalIgnoreCase); -} diff --git a/src/tests/SOS.TestHarness/ReplayContext.cs b/src/tests/SOS.TestHarness/ReplayContext.cs deleted file mode 100644 index 532c16370c..0000000000 --- a/src/tests/SOS.TestHarness/ReplayContext.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace SOS.TestHarness; - -/// The kind of a captured . -public enum ReplayStepKind -{ - /// A navigation (GoToStopPoint/GoToCrash/RunToBreakpoint). - Navigate, - - /// A SOS command run via . - Sos, - - /// A raw debugger command run via . - Execute, -} - -/// One recorded action against a : a navigation or a command, plus the -/// dump file it ran against (null for live targets, which have no dump). -public sealed class ReplayStep -{ - public ReplayStep(ReplayStepKind kind, string text, string? dumpPath) - { - Kind = kind; - Text = text; - DumpPath = dumpPath; - } - - public ReplayStepKind Kind { get; } - public string Text { get; } - public string? DumpPath { get; } -} - -/// -/// A running, per-test record of everything a did — the host/flavor/liveness it -/// was, and the ordered list of navigations and SOS/debugger commands (each tagged with the dump it -/// ran against). It is captured unconditionally and cheaply during the using Target window; a -/// failing test's after-hook reads it back to emit a replay artifact. Tests never touch this -/// directly — capture is automatic. -/// -/// Captures are keyed by the running test's UniqueID in a private table (NOT in xUnit's -/// KeyValueStorage, which is shared across the context hierarchy and would let parallel tests -/// clobber one another). The after-hook removes the entry, so the table never grows past the set of -/// in-flight tests. -/// -public sealed class ReplayContext -{ - private static readonly System.Collections.Concurrent.ConcurrentDictionary s_byTest = new(); - - private readonly List _steps = new(); - private readonly HashSet _hosts = new(); - - private ReplayContext(TestConfig config, bool live) - { - TargetName = config.Target; - Host = config.Host; - Flavor = config.Flavor; - Config = config.ToString(); - Live = live; - } - - public string TargetName { get; } - public Host Host { get; } - public Flavor Flavor { get; } - public string Config { get; } - public bool Live { get; } - public IReadOnlyList Steps => _steps; - - /// The host diagnostics (captured stdout/stderr and crash dumps) of every host this test - /// touched, deduplicated. A failing test's replay renders these. - public IReadOnlyCollection Hosts - { - get { lock (_hosts) { return _hosts.ToArray(); } } - } - - internal void Add(ReplayStepKind kind, string text, string? dumpPath) => - _steps.Add(new ReplayStep(kind, text, dumpPath)); - - /// Note that this test used a host, so its captured diagnostics can be surfaced on failure. - /// No-ops for a null host (e.g. the cdb child host, which is not wired for capture). - internal void AttachHost(HostDiagnostics? host) - { - if (host is null) - { - return; - } - - lock (_hosts) - { - _hosts.Add(host); - } - } - - /// - /// Begin capturing for the current test, replacing any prior capture for it. Keyed by the test's - /// UniqueID, so it is isolated per test even under cross-class parallelism. No-ops (returns - /// null) when not running inside a test. - /// - public static ReplayContext? Start(TestConfig config, bool live) - { - string? id = TestContext.Current.Test?.UniqueID; - if (id is null) - { - return null; - } - - ReplayContext replay = new(config, live); - s_byTest[id] = replay; - return replay; - } - - /// The capture for the current test, or null if none was started. - public static ReplayContext? Current - { - get - { - string? id = TestContext.Current.Test?.UniqueID; - return id is not null && s_byTest.TryGetValue(id, out ReplayContext? replay) ? replay : null; - } - } - - /// Fetch and remove the capture for a test (the after-hook calls this for every test, so - /// the table is bounded by the in-flight test set). - public static ReplayContext? Take(string testUniqueId) => - s_byTest.TryRemove(testUniqueId, out ReplayContext? replay) ? replay : null; -} diff --git a/src/tests/SOS.TestHarness/RepoLayout.cs b/src/tests/SOS.TestHarness/RepoLayout.cs deleted file mode 100644 index 8893befc3c..0000000000 --- a/src/tests/SOS.TestHarness/RepoLayout.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; -using System.Runtime.InteropServices; - -namespace SOS.TestHarness; - -/// -/// Locates the diagnostics repo root and the well-known build output locations the harness -/// consumes (repo-built native SOS, repo-built dotnet-dump, the pre-built debuggees, and the -/// scratch dump directory). The root is found by walking up from the test output directory and -/// looking for the repo markers (global.json alongside Build.cmd), so the harness -/// works regardless of where the test assembly is run from. -/// -public static class RepoLayout -{ - /// The build configuration of the repo-built tools (native SOS, dotnet-dump). This is embedded - /// by MSBuild in the harness assembly; SOSHARNESS_ARTIFACTS_CONFIG is available as a local override. - public static string ArtifactsConfiguration { get; } = - Environment.GetEnvironmentVariable("SOSHARNESS_ARTIFACTS_CONFIG") is { Length: > 0 } c - ? c - : typeof(RepoLayout).Assembly - .GetCustomAttributes() - .Single(a => a.Key == "SOS.TestHarness.Configuration") - .Value!; - - /// The target framework of the running harness, such as net10.0. - public static string TestTargetFramework { get; } = - typeof(RepoLayout).Assembly - .GetCustomAttributes() - .Single(a => a.Key == "SOS.TestHarness.TargetFramework") - .Value!; - - /// The repo root (the directory containing global.json and Build.cmd). - public static string Root { get; } = FindRoot(); - - /// artifacts/bin under the repo root. - public static string ArtifactsBin => Path.Combine(Root, "artifacts", "bin"); - - /// The native build output directory, e.g. artifacts/bin/Windows_NT.x64.Debug. - public static string ArtifactsBinNative => - Path.Combine(ArtifactsBin, $"{TargetOS}.{TargetArch}.{ArtifactsConfiguration}"); - - /// The processor architecture token used in repo artifact paths (x64/x86/arm64). - public static string TargetArch { get; } = RuntimeInformation.ProcessArchitecture switch - { - Architecture.X64 => "x64", - Architecture.X86 => "x86", - Architecture.Arm64 => "arm64", - Architecture.Arm => "arm", - _ => "x64", - }; - - /// The OS token used in repo native artifact paths. The native build lowercases the OS token - /// for non-Windows (e.g. linux.x64.Debug, osx.arm64.Debug) but keeps Windows_NT on - /// Windows, so match that casing here or the native output directory won't be found. - public static string TargetOS { get; } = - OperatingSystem.IsWindows() ? "Windows_NT" : - OperatingSystem.IsMacOS() ? "osx" : "linux"; - - /// The runtime identifier of the current test leg (e.g. win-x64 or - /// linux-musl-x64), embedded by the build so artifact lookup preserves RID distinctions that - /// cannot be inferred from . - public static string Rid { get; } = - typeof(RepoLayout).Assembly - .GetCustomAttributes() - .Single(a => a.Key == "SOS.TestHarness.TargetRid") - .Value!; - - /// The repo's locally-acquired .NET host (.dotnet/dotnet.exe) used to shell out builds. - public static string DotNetExe => Path.Combine(Root, ".dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); - - /// The platform suffix for an apphost executable: .exe on Windows, none elsewhere - /// (Linux/macOS apphosts have no extension). - public static string ExeSuffix => OperatingSystem.IsWindows() ? ".exe" : string.Empty; - - /// Path to a debuggee project under the SOS.UnitTests Debuggees tree. - public static string DebuggeeProject(string name) => - Path.Combine(Root, "src", "tests", "SOS.UnitTests", "Debuggees", name, name + ".csproj"); - - /// The pre-built Core output directory for a debuggee and target framework (e.g. - /// net8.0/net11.0), as produced by Debuggees.proj. - public static string CoreDebuggeeDir(string name, string tfm) => - Path.Combine(ArtifactsBin, name, ArtifactsConfiguration, tfm); - - /// The build-produced self-contained single-file publish directory for a debuggee. - public static string SingleFileDebuggeeDir(string name, string tfm) => - Path.Combine(ArtifactsBin, name, ArtifactsConfiguration, tfm, Rid, "publish"); - - /// - /// The repo's locally-acquired multi-version test .NET install (artifacts/dotnet-test), which - /// eng/InstallRuntimes.proj populates with every RuntimeTestVersions runtime (8/9/10/11). - /// Used as DOTNET_ROOT when launching a debuggee so its apphost resolves the matching runtime - /// version (the repo's .dotnet only carries the build SDK's runtime). - /// - public static string DotnetTestRoot { get; } = Path.Combine(Root, "artifacts", "dotnet-test"); - - /// The multi-version test .NET host (artifacts/dotnet-test/dotnet[.exe]). This is the - /// net11-capable SDK that Debuggees.proj uses to pre-build the debuggees, so local Core fallback - /// builds must use it too — the repo's .dotnet build SDK (e.g. 10.0.x) refuses to target newer - /// frameworks (NETSDK1045). - public static string DotnetTestExe => Path.Combine(DotnetTestRoot, OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"); - - /// Scratch directory for harness-produced artifacts (on-the-fly builds, captured dumps). - public static string Scratch { get; } = - Path.Combine(Root, "artifacts", "tmp", "sos-harness", ArtifactsConfiguration); - - /// - /// A hermetic, local-only symbol path for the SOS host child processes. The dev machine's - /// _NT_SYMBOL_PATH often points at the Azure-authed symweb server, which makes SOS's - /// host init pull in Azure.Identity (and fail loading its closure) and would make tests depend on - /// the network. We point the children at a local cache only — debuggee PDBs are found next to the - /// module, so managed source/line resolution still works. - /// - public static string SymbolCache { get; } = Path.Combine(Scratch, "symcache"); - - private static string FindRoot() - { - string? dir = AppContext.BaseDirectory; - while (dir is not null) - { - if (File.Exists(Path.Combine(dir, "global.json")) && - File.Exists(Path.Combine(dir, "Build.cmd"))) - { - return dir; - } - - dir = Path.GetDirectoryName(dir); - } - - throw new DirectoryNotFoundException( - "Could not locate the diagnostics repo root (global.json + Build.cmd) by walking up from " + - AppContext.BaseDirectory); - } -} diff --git a/src/tests/SOS.TestHarness/SOS.TestHarness.csproj b/src/tests/SOS.TestHarness/SOS.TestHarness.csproj deleted file mode 100644 index 5b9e472167..0000000000 --- a/src/tests/SOS.TestHarness/SOS.TestHarness.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - $(NetCoreAppTestTargetFramework) - enable - enable - true - - - - - - - - - - - - - - - - - - - - - - - <_Parameter1>SOS.TestHarness.Configuration - <_Parameter2>$(Configuration) - - - <_Parameter1>SOS.TestHarness.TargetFramework - <_Parameter2>$(TargetFramework) - - - <_Parameter1>SOS.TestHarness.TargetRid - <_Parameter2>$(TargetRid) - - - - diff --git a/src/tests/SOS.TestHarness/SnapshotStore.cs b/src/tests/SOS.TestHarness/SnapshotStore.cs deleted file mode 100644 index 8acbf5123f..0000000000 --- a/src/tests/SOS.TestHarness/SnapshotStore.cs +++ /dev/null @@ -1,534 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Diagnostics; - -namespace SOS.TestHarness; - -/// -/// Locates and memoizes the runnable debuggee and produces the dump for each (flavor, target, coreVersion, -/// stopPoint). Each (flavor, target, coreVersion) is acquired once and captured once into its -/// own dump directory — so no two tests ever write the same artifact. The DAC axis is not a -/// capture dimension: legacy and cDAC reuse the same dump (only runtimes --usecdac differs at debug -/// time), so it never appears in these keys. -/// -/// Debuggee acquisition follows the repo's build model: -/// -/// Core (net8.0–net11.0) is the pre-built debuggee produced by the repo build -/// (Debuggees.proj) under artifacts/bin/<Name>/<Config>/net{N}.0; the harness -/// consumes it directly (and builds the single project on demand for local development if it isn't there -/// yet), launching it against the multi-version test runtime install so its apphost binds the matching -/// runtime. -/// SingleFile is pre-published by Debuggees.proj once per tested runtime, RID, and -/// configuration. Tests only locate and consume that immutable output. -/// Framework (net462) is produced on the fly in the harness scratch tree, matching the -/// legacy harness's cli build process. -/// -/// -/// Capture mechanism depends on the flavor and stop kind: -/// -/// Snapshot stops (Core / SingleFile) self-snapshot mid-run from inside the debuggee via -/// the repo-built dotnet-dump collect. -/// Crash stop, Core lets the runtime's createdump write the dump. -/// Crash stop, SingleFile on Windows can't use createdump, so it's captured with dbgeng like desktop. -/// Framework (desktop) is always captured externally by . -/// -/// -public static class SnapshotStore -{ - // One acquisition per (flavor, target, coreVersion); thread-safe via Lazy. - private static readonly ConcurrentDictionary<(Flavor Flavor, string Target, CoreVersion CoreVersion), Lazy> s_targetExe = new(); - - // One capture per (flavor, target, gcType, dumpKind, coreVersion) (distinct dump dirs); thread-safe via - // Lazy. The DAC axis is deliberately absent: the same dump is reused for both legacy and cDAC (only - // `runtimes --usecdac` differs at debug time), so capture must not be keyed on it. - private static readonly ConcurrentDictionary<(Flavor Flavor, string Target, GcType GcType, DumpKind DumpKind, CoreVersion CoreVersion), Lazy> s_captured = new(); - - // The out-of-process desktop capturer, located/built once. - private static readonly Lazy s_capturerDll = new(() => SubprocessDll("SOS.TestHarness.Capturer")); - - private static string CapturerDll => s_capturerDll.Value; - - // The out-of-process dbgeng engine host, located/built once. - private static readonly Lazy s_engineHostDll = new(() => SubprocessDll("SOS.TestHarness.EngineHost")); - private static readonly object s_subprocessBuildLock = new(); - - /// Path to the built EngineHost.dll (the subprocess dbgeng backend), produced on first use. - public static string EngineHostDll => s_engineHostDll.Value; - - /// Path to the dump for one stop point of a target in a flavor/GC/dump-kind/version, producing it on first use. - public static string GetDump(Flavor flavor, string targetName, string stopName, GcType gcType = GcType.Workstation, DumpKind dumpKind = DumpKind.Heap, CoreVersion coreVersion = CoreVersion.Net10) - { - TargetDefinition target = TargetCatalog.Get(targetName); - target.Stop(stopName); // validate - - string dumpDir = s_captured - .GetOrAdd((flavor, targetName, gcType, dumpKind, coreVersion), key => new Lazy(() => CaptureTarget(key.Flavor, TargetCatalog.Get(key.Target), key.GcType, key.DumpKind, key.CoreVersion))) - .Value; - - string dump = Path.Combine(dumpDir, stopName + ".dmp"); - if (!File.Exists(dump)) - { - throw new InvalidOperationException( - $"Capture of {flavor}/{gcType}/{dumpKind}/{CoreVersions.Tfm(coreVersion)}/{targetName} did not produce a dump for stop '{stopName}' at '{dump}'."); - } - - return dump; - } - - /// Path to the runnable executable for a target in a flavor/version, producing it on first use. - public static string TargetExe(Flavor flavor, string targetName, CoreVersion coreVersion = CoreVersion.Net10) - { - string exe = s_targetExe - .GetOrAdd((flavor, targetName, coreVersion), k => new Lazy(() => AcquireTarget(k.Flavor, TargetCatalog.Get(k.Target), k.CoreVersion))) - .Value; - EnsureExecutable(exe); - return exe; - } - - private static void EnsureExecutable(string path) - { - if (OperatingSystem.IsWindows()) - { - return; - } - - UnixFileMode mode = File.GetUnixFileMode(path); - UnixFileMode execute = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; - if ((mode & execute) != execute) - { - File.SetUnixFileMode(path, mode | execute); - } - } - - private static string DumpDir(Flavor flavor, string target, GcType gcType, DumpKind dumpKind, CoreVersion coreVersion) => - Path.Combine(RepoLayout.Scratch, "dumps", flavor.ToString().ToLowerInvariant(), - gcType.ToString().ToLowerInvariant(), dumpKind.ToString().ToLowerInvariant(), - CoreVersions.Tfm(coreVersion), target); - - private static string CaptureTarget(Flavor flavor, TargetDefinition target, GcType gcType, DumpKind dumpKind, CoreVersion coreVersion) - { - string dumpDir = DumpDir(flavor, target.Name, gcType, dumpKind, coreVersion); - Directory.CreateDirectory(dumpDir); - - // Resolve (build if needed) the debuggee first, then reuse the cached dumps only if they were - // captured from THIS exe (i.e. are at least as new as it). A rebuilt exe has a fresh PDB whose - // GUID won't match an older dump, so a stale dump must be re-captured. - string exe = TargetExe(flavor, target.Name, coreVersion); - DateTime exeTime = File.GetLastWriteTimeUtc(exe); - if (target.StopPoints.All(s => IsUpToDate(Path.Combine(dumpDir, s.Name + ".dmp"), exeTime))) - { - return dumpDir; - } - - // On Windows, a reduced Core dump of an unsigned runtime needs a machine-wide dbghelp setting. - // Use a Full dump when that setting is absent so normal test runs remain self-contained. - DumpKind captureKind = DumpGenerationRequirements.ResolveCaptureKind(flavor, dumpKind); - - bool isCrash = target.StopPoints.Any(s => s.Kind == StopKind.Crash); - - if (flavor == Flavor.Framework) - { - // Desktop: no diagnostics IPC; dbgeng captures both snapshot (bpmd) and crash (second-chance). - // Run it out-of-process so a dbgeng crash dies with the child, not the test host. - CaptureWithDbgEng(TargetExe(flavor, target.Name, coreVersion), target, dumpDir, gcType, captureKind); - } - else if (isCrash && flavor == Flavor.SingleFile && OperatingSystem.IsWindows()) - { - // Self-contained single-file on Windows doesn't ship/launch createdump, so capture its crash - // with dbgeng like desktop (also out-of-process). On Linux/macOS the bundled runtime's - // createdump handles single-file crashes, so we fall through to CaptureCrashViaCreatedump. - CaptureWithDbgEng(TargetExe(flavor, target.Name, coreVersion), target, dumpDir, gcType, captureKind); - } - else if (isCrash) - { - // .NET Core crash: let the runtime's createdump write the dump. - CaptureCrashViaCreatedump(flavor, target, dumpDir, gcType, captureKind, coreVersion); - } - else - { - // Snapshot stops on Core / SingleFile: self-snapshot mid-run via markers. - SelfCollectCapture(flavor, target, dumpDir, gcType, captureKind, coreVersion); - } - - return dumpDir; - } - - /// Run the Capturer child to produce dumps via in-process dbgeng (desktop, or single-file crash). - private static void CaptureWithDbgEng(string exePath, TargetDefinition target, string dumpDir, GcType gcType, DumpKind dumpKind) - { - ProcessStartInfo psi = new(RepoLayout.DotNetExe) - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - psi.ArgumentList.Add(CapturerDll); - psi.ArgumentList.Add(exePath); - psi.ArgumentList.Add(target.Name); - psi.ArgumentList.Add(dumpDir); - psi.ArgumentList.Add(dumpKind.ToString()); - - // Hermetic, local-only symbols: the Capturer hosts dbgeng+SOS, and the dev's _NT_SYMBOL_PATH may - // point at the Azure-authed symweb, which crashes SOS host init (loading Azure.Identity's closure). - Directory.CreateDirectory(RepoLayout.SymbolCache); - psi.Environment["_NT_SYMBOL_PATH"] = RepoLayout.SymbolCache; - ApplyGcType(psi, gcType); - - using Process p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start Capturer"); - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); - if (p.ExitCode != 0) - { - throw new InvalidOperationException($"Capturer failed for {target.Name} ({p.ExitCode}):\n{stdout}\n{stderr}"); - } - } - - /// - /// Core/SingleFile crash capture: launch the target with the runtime's crash-dump env vars set and let - /// it crash. The runtime's createdump writes the dump to the crash stop's path; the process exits - /// non-zero (it crashed), so we - /// verify the dump exists rather than the exit code. - /// - private static void CaptureCrashViaCreatedump(Flavor flavor, TargetDefinition target, string dumpDir, GcType gcType, DumpKind dumpKind, CoreVersion coreVersion) - { - string exe = TargetExe(flavor, target.Name, coreVersion); - StopPoint crash = target.StopPoints.Single(s => s.Kind == StopKind.Crash); - string dumpPath = Path.Combine(dumpDir, crash.Name + ".dmp"); - - ProcessStartInfo psi = new(exe) - { - WorkingDirectory = Path.GetDirectoryName(exe), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - psi.Environment["DOTNET_DbgEnableMiniDump"] = "1"; - psi.Environment["DOTNET_DbgMiniDumpType"] = CreatedumpType(flavor, dumpKind); - psi.Environment["DOTNET_DbgMiniDumpName"] = dumpPath; - psi.Environment["DOTNET_CreateDumpDiagnostics"] = "1"; - ApplyRuntimeRoot(psi, flavor); - ApplyMacOsDumpConfig(psi); - ApplyGcType(psi, gcType); - - using Process p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to launch target"); - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); - - if (!File.Exists(dumpPath)) - { - throw new InvalidOperationException( - $"createdump did not produce '{dumpPath}' for {target.Project} ({flavor}); exit {p.ExitCode}.\n{stdout}\n{stderr}"); - } - } - - /// Core/SingleFile snapshot capture: run the target once; its markers self-snapshot mid-run. - /// - /// macOS-only dump configuration applied to every debuggee we capture a dump from (createdump on crash, - /// or the in-process dotnet-dump collect self-snapshot). createdump on macOS defaults to a Mach-O - /// core, which SOS/ClrMD cannot read; DOTNET_DbgEnableElfDumpOnMacOS=1 makes it emit an ELF core - /// instead (matching the legacy harness). The diagnostic-IPC socket the runtime opens lives under - /// $TMPDIR, and macOS's default $TMPDIR (/var/folders/…) routinely blows past the - /// 104-byte Unix-domain-socket path limit, so point the debuggee at a short TMPDIR. No-op off macOS. - /// - private static void ApplyMacOsDumpConfig(ProcessStartInfo psi) - { - if (!OperatingSystem.IsMacOS()) - { - return; - } - - psi.Environment["DOTNET_DbgEnableElfDumpOnMacOS"] = "1"; - psi.Environment["TMPDIR"] = "/tmp"; - } - - /// - /// Point a framework-dependent (Core) debuggee at the multi-version test runtime install so its apphost - /// resolves the runtime matching its target framework (e.g. a net8 debuggee binds 8.0.x, net11 binds the - /// installed preview). Self-contained single-file and desktop Framework debuggees carry / don't use a - /// shared runtime, so this is a no-op for them. DOTNET_MULTILEVEL_LOOKUP=0 keeps resolution - /// strictly within the test install (no machine-wide fallback), so the dump's coreclr — and therefore the - /// DAC SOS later loads — is the deterministic, on-disk one. - /// - private static void ApplyRuntimeRoot(ProcessStartInfo psi, Flavor flavor) - { - if (flavor != Flavor.Core) - { - return; - } - - psi.Environment["DOTNET_ROOT"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_ROOT(x86)"] = RepoLayout.DotnetTestRoot; - psi.Environment["DOTNET_MULTILEVEL_LOOKUP"] = "0"; - } - - /// Apply the GC-mode env vars to a debuggee launch. Server forces a deterministic multi-heap - /// GC (a fixed heap count with DATAS off, so it can't collapse back to a single heap). - private static void ApplyGcType(ProcessStartInfo psi, GcType gcType) - { - if (gcType == GcType.Server) - { - psi.Environment["DOTNET_gcServer"] = "1"; - psi.Environment["DOTNET_GCHeapCount"] = "4"; - psi.Environment["DOTNET_GCDynamicAdaptationMode"] = "0"; - } - } - - /// - /// The createdump/DOTNET_DbgMiniDumpType value for a dump kind: Full=4, Heap=2, Mini=1, - /// except single-file crash dumps which must use Full=4. - /// - private static string CreatedumpType(Flavor flavor, DumpKind dumpKind) - { - if (dumpKind == DumpKind.Full || flavor == Flavor.SingleFile) - { - // Single-file crash dumps cannot use createdump's reduced dump modes: Heap/Mini require DAC - // region enumeration, but the single-file app does not have a loadable DAC beside it. Keep the - // test matrix's Heap row, but capture it with the only supported createdump mode. - return "4"; - } - - return dumpKind == DumpKind.Mini ? "1" : "2"; - } - - /// - /// The dotnet-dump collect --type value for a dump kind. Single-file self-snapshots use Full - /// because reduced dumps require the same unsupported DAC region enumeration as single-file crashes. - /// - private static string CollectType(Flavor flavor, DumpKind dumpKind) => - dumpKind == DumpKind.Full || flavor == Flavor.SingleFile ? "Full" : dumpKind.ToString(); - - private static void SelfCollectCapture(Flavor flavor, TargetDefinition target, string dumpDir, GcType gcType, DumpKind dumpKind, CoreVersion coreVersion) - { - string exe = TargetExe(flavor, target.Name, coreVersion); - ProcessStartInfo psi = new(exe) - { - WorkingDirectory = Path.GetDirectoryName(exe), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - psi.Environment["SOSHARNESS_CAPTURE_DIR"] = dumpDir; - // Tell the debuggee's stop-point helper which dotnet-dump to self-collect with (the repo-built one). - psi.Environment["SOSHARNESS_DOTNET"] = RepoLayout.DotNetExe; - psi.Environment["SOSHARNESS_DOTNETDUMP_DLL"] = ToolPaths.DotNetDumpDll; - psi.Environment["SOSHARNESS_DUMP_TYPE"] = CollectType(flavor, dumpKind); - ApplyRuntimeRoot(psi, flavor); - ApplyMacOsDumpConfig(psi); - ApplyGcType(psi, gcType); - - using Process p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to launch target"); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); - - if (p.ExitCode != 0) - { - throw new InvalidOperationException($"Target '{target.Project}' ({flavor}) failed ({p.ExitCode}):\n{stderr}"); - } - } - - /// - /// Resolve the runnable debuggee for a flavor. Core and SingleFile are repo build outputs; Framework - /// is built on demand from the repo debuggee csproj. - /// - private static string AcquireTarget(Flavor flavor, TargetDefinition target, CoreVersion coreVersion) => flavor switch - { - Flavor.Core => AcquireCore(target, coreVersion), - Flavor.Framework => BuildFramework(target), - Flavor.SingleFile => AcquireSingleFile(target, coreVersion), - _ => throw new ArgumentOutOfRangeException(nameof(flavor)), - }; - - /// Consume the repo-built Core debuggee for the requested version; build the single project on - /// demand if it's absent or older than the debuggee source (so a local debuggee edit is picked up). - private static string AcquireCore(TargetDefinition target, CoreVersion coreVersion) - { - string tfm = CoreVersions.Tfm(coreVersion); - string exe = Path.Combine(RepoLayout.CoreDebuggeeDir(target.Project, tfm), target.Project + RepoLayout.ExeSuffix); - string project = RepoLayout.DebuggeeProject(target.Project); - if (IsUpToDate(exe, NewestSourceWriteTime(project))) - { - return exe; - } - - // Missing or stale relative to source — build just this debuggee for the requested framework (lands - // at the same conventional artifacts path). Lock per project (not per framework): different TFMs of - // one csproj share its obj/ and project.assets.json, so concurrent restores would corrupt each other. - lock (BuildLockFor(project)) - { - if (!IsUpToDate(exe, NewestSourceWriteTime(project))) - { - RunToCompletion(RepoLayout.DotnetTestExe, - $"build \"{project}\" -p:BuildProjectFramework={tfm} -c {RepoLayout.ArtifactsConfiguration}"); - } - } - - if (!File.Exists(exe)) - { - throw new InvalidOperationException($"Core build of {target.Project} ({tfm}) did not produce '{exe}'."); - } - - return exe; - } - - /// Consume the self-contained single-file debuggee published by the repo build. - private static string AcquireSingleFile(TargetDefinition target, CoreVersion coreVersion) - { - string tfm = CoreVersions.Tfm(coreVersion); - string outputDir = RepoLayout.SingleFileDebuggeeDir(target.Project, tfm); - string exe = Path.Combine(outputDir, target.Project + RepoLayout.ExeSuffix); - string runtimeVersionFile = Path.Combine(outputDir, "runtime.version"); - string? expectedRuntimeVersion = CoreVersions.RuntimeVersion(coreVersion); - string? actualRuntimeVersion = File.Exists(runtimeVersionFile) - ? File.ReadAllText(runtimeVersionFile).Trim() - : null; - - if (!File.Exists(exe)) - { - throw new FileNotFoundException( - $"Pre-published single-file debuggee '{target.Project}' ({tfm}/{RepoLayout.Rid}) was not found at '{exe}'. " + - "Build src/tests/Debuggees.proj before running SOS tests.", - exe); - } - - if (expectedRuntimeVersion is null || - !string.Equals(actualRuntimeVersion, expectedRuntimeVersion, StringComparison.Ordinal)) - { - throw new InvalidOperationException( - $"Pre-published single-file debuggee '{target.Project}' ({tfm}/{RepoLayout.Rid}) has runtime version " + - $"'{actualRuntimeVersion ?? ""}', but the installed test runtime manifest requires " + - $"'{expectedRuntimeVersion ?? ""}'."); - } - - return exe; - } - - /// Build the Framework debuggee into the scratch tree, reusing the cached exe when it is newer - /// than the debuggee source. - private static string BuildFramework(TargetDefinition target) - { - string project = RepoLayout.DebuggeeProject(target.Project); - string outDir = Path.Combine(RepoLayout.Scratch, "targets", "framework", target.Name); - string exe = Path.Combine(outDir, target.Project + RepoLayout.ExeSuffix); - DateTime sourceWriteTime = NewestSourceWriteTime(project); - - if (IsUpToDate(exe, sourceWriteTime)) - { - return exe; - } - - string config = RepoLayout.ArtifactsConfiguration; - // Desktop SOS resolves source lines from a classic Windows PDB (read via DIA), not a - // portable/embedded one — the repo's global props default DebugType to embedded, so force - // a full (Windows) PDB next to the exe for the source-line tests. - string args = - $"build \"{project}\" -p:BuildProjectFramework=net462 -p:DebugType=full -p:DebugSymbols=true -c {config} -o \"{outDir}\""; - - // Rebuild only when stale (above). Different frameworks of one csproj share its obj/ (and - // project.assets.json), so serialize fallback builds per project. - lock (BuildLockFor(project)) - { - if (!IsUpToDate(exe, sourceWriteTime)) - { - RunToCompletion(RepoLayout.DotnetTestExe, args); - } - } - - if (!File.Exists(exe)) - { - throw new InvalidOperationException($"Framework build of {target.Project} did not produce '{exe}'."); - } - - return exe; - } - - private static readonly ConcurrentDictionary s_projectBuildLocks = new(StringComparer.OrdinalIgnoreCase); - - private static object BuildLockFor(string projectPath) => - s_projectBuildLocks.GetOrAdd(projectPath, _ => new object()); - - /// Newest write time of the debuggee's sources (its .cs files + csproj), so a build is - /// re-run only when the source actually changed (an unchanged build keeps a stable exe/PDB, which keeps - /// the cached dumps — captured against that exe's PDB — valid). - private static DateTime NewestSourceWriteTime(string projectFile) - { - string dir = Path.GetDirectoryName(projectFile)!; - DateTime newest = File.GetLastWriteTimeUtc(projectFile); - foreach (string cs in Directory.EnumerateFiles(dir, "*.cs", SearchOption.AllDirectories)) - { - DateTime t = File.GetLastWriteTimeUtc(cs); - if (t > newest) - { - newest = t; - } - } - - return newest; - } - - /// True if exists and is at least as new as . - private static bool IsUpToDate(string output, DateTime inputUtc) => - File.Exists(output) && File.GetLastWriteTimeUtc(output) >= inputUtc; - - /// - /// Locate a subprocess host (EngineHost / Capturer), building it on demand for local development when - /// the normal repository build has not produced it. - /// - private static string SubprocessDll(string name) - { - string dll = Path.Combine(RepoLayout.ArtifactsBin, name, RepoLayout.ArtifactsConfiguration, RepoLayout.TestTargetFramework, RepoLayout.Rid, name + ".dll"); - string project = Path.Combine(RepoLayout.Root, "src", "tests", name, name + ".csproj"); - - if (!File.Exists(dll)) - { - // Both helper projects reference SOS.TestHarness and can be initialized concurrently by - // different test rows. Serialize their fallback builds so their MSBuild nodes do not write - // the shared harness intermediate assembly at the same time. - lock (s_subprocessBuildLock) - { - if (!File.Exists(dll)) - { - RunToCompletion( - RepoLayout.DotNetExe, - $"build \"{project}\" -c {RepoLayout.ArtifactsConfiguration} -p:TargetArch={RepoLayout.TargetArch}"); - } - } - } - - if (!File.Exists(dll)) - { - throw new InvalidOperationException($"Build of {name} did not produce '{dll}'."); - } - - return dll; - } - - private static void RunToCompletion(string fileName, string arguments) - { - ProcessStartInfo psi = new() - { - FileName = fileName, - Arguments = arguments, - WorkingDirectory = RepoLayout.Root, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - - using Process p = Process.Start(psi) ?? throw new InvalidOperationException($"Failed to start {fileName}"); - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); - if (p.ExitCode != 0) - { - throw new InvalidOperationException($"'{fileName} {arguments}' failed ({p.ExitCode}):\n{stdout}\n{stderr}"); - } - } -} diff --git a/src/tests/SOS.TestHarness/SosAssertException.cs b/src/tests/SOS.TestHarness/SosAssertException.cs deleted file mode 100644 index bebf8cd6b0..0000000000 --- a/src/tests/SOS.TestHarness/SosAssertException.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Thrown when a fluent SOS assertion fails. The message is intentionally rich — -/// it names the host, the command, what was expected, and echoes the full captured -/// output. This is the single biggest day-to-day improvement over the legacy -/// VERIFY: model, whose failures only ever said -/// "Debugger output did not match the expression: ...". -/// -public sealed class SosAssertException : Exception -{ - public SosAssertException(string host, string command, string expectation, string actualOutput) - : base(Build(host, command, expectation, actualOutput)) - { - } - - private static string Build(string host, string command, string expectation, string actualOutput) - { - string body = string.IsNullOrEmpty(actualOutput) ? "(no output)" : actualOutput.TrimEnd(); - return $""" - SOS assertion failed. - host: {host} - command: {command} - expected: {expectation} - --- actual output --- - {body} - --------------------- - """; - } -} diff --git a/src/tests/SOS.TestHarness/SosCell.cs b/src/tests/SOS.TestHarness/SosCell.cs deleted file mode 100644 index b478b7e79d..0000000000 --- a/src/tests/SOS.TestHarness/SosCell.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// One cell of a . It converts implicitly to its value string (so -/// row["Name"] == "System.Int32[]" and Assert.Equal(..., row["Name"]) just work) and -/// adds value helpers like to parse the value as a number, and -/// : -/// -/// SosRow row = table.Single(r => r["Name"] == "System.Int32[]"); -/// ulong obj = row["Object"].AsUInt64(Sos.Addr); -/// -/// Cells sliced out of a table by the parser carry the full captured output for rich failure -/// messages. Free-standing data cells (built by a table data extractor via the public -/// constructor) carry only their source line, which is -/// used as the failure context instead. -/// -public sealed class SosCell -{ - private readonly SosOutput? _output; - private readonly string? _sourceLine; - - public string Column { get; } - public string Value { get; } - - internal SosCell(SosOutput output, string column, string value) - { - _output = output; - Column = column; - Value = value; - } - - /// - /// Build a free-standing cell from a raw — used by table data - /// extractors to attach extra values (registers, GC roots, …) parsed out of the lines between - /// aligned table rows. The source line becomes the failure context for the As* helpers. - /// - public SosCell(string sourceLine, string column, string value) - { - _sourceLine = sourceLine; - Column = column; - Value = value; - } - - /// The cell value, so a cell drops straight into string comparisons / Assert.Equal. - public static implicit operator string(SosCell cell) => cell.Value; - - /// Parse the value as an unsigned 64-bit number per (hex/decimal). - public ulong AsUInt64(SosToken token) => SosConvert.UInt64(Column, Value, token, Fail); - - /// Parse the value as an unsigned 32-bit number per . - public uint AsUInt32(SosToken token) => SosConvert.UInt32(Column, Value, token, Fail); - - /// Parse the value as an unsigned 16-bit number per . - public ushort AsUInt16(SosToken token) => SosConvert.UInt16(Column, Value, token, Fail); - - /// Parse the value as a signed 64-bit number per . - public long AsInt64(SosToken token) => SosConvert.Int64(Column, Value, token, Fail); - - /// Parse the value as a signed 32-bit number per . - public int AsInt32(SosToken token) => SosConvert.Int32(Column, Value, token, Fail); - - /// Parse the value as a signed 16-bit number per . - public short AsInt16(SosToken token) => SosConvert.Int16(Column, Value, token, Fail); - - /// Parse the value as a byte per . - public byte AsByte(SosToken token) => SosConvert.Byte(Column, Value, token, Fail); - - /// Parse the value as a boolean (true/false or 1/0). - public bool AsBoolean() => SosConvert.Boolean(Column, Value, Fail); - - /// True if the value contains . - public bool Contains(string substring) => Value.Contains(substring, StringComparison.Ordinal); - - public override string ToString() => Value; - - private Exception Fail(string expectation) => - _output is not null - ? _output.Fail(expectation) - : new SosAssertException("(table data)", _sourceLine ?? string.Empty, expectation, _sourceLine ?? string.Empty); -} diff --git a/src/tests/SOS.TestHarness/SosColumn.cs b/src/tests/SOS.TestHarness/SosColumn.cs deleted file mode 100644 index 2cbf84cc74..0000000000 --- a/src/tests/SOS.TestHarness/SosColumn.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A column spec for : a header name, an optional -/// every cell must match, and the column's alignment. SOS prints fixed-width columns that are either -/// left- or right-aligned (addresses/SPs are usually right-aligned, type names left); the parser -/// slices cells using that alignment. Implicit conversions let a left-aligned column be written as a -/// bare name or a (name, token) pair; right-aligned columns use : -/// -/// pe.Table("SP", "IP", "Function"); // all left, no validation -/// dso.Table(ColumnAlignment.Right("SP/REG"), ColumnAlignment.Right("Object"), "Name"); -/// dso.Table(ColumnAlignment.Right("Object", Sos.Addr), "Name"); // right-aligned + validated -/// -/// -public readonly struct SosColumn -{ - public SosColumn(string header, SosToken? token = null, bool rightAligned = false) - { - Header = header; - Token = token; - RightAligned = rightAligned; - } - - /// The column header name. - public string Header { get; } - - /// The token every cell must match, or null to skip validation for this column. - public SosToken? Token { get; } - - /// True if the column's values are right-aligned under the header (default left). - public bool RightAligned { get; } - - /// A bare, left-aligned header name with no per-cell validation. - public static implicit operator SosColumn(string header) => new(header); - - /// A left-aligned header name paired with the token its cells must match. - public static implicit operator SosColumn((string Header, SosToken Token) column) => - new(column.Header, column.Token); -} - -/// -/// Builds aligned specs. Only left and right are meaningful for SOS's -/// fixed-width output; is the default behavior (a bare string also yields it), -/// and marks a right-aligned column. -/// -public static class ColumnAlignment -{ - /// A left-aligned column (the default), optionally validated against . - public static SosColumn Left(string header, SosToken? token = null) => new(header, token, rightAligned: false); - - /// A right-aligned column (e.g. addresses/SPs), optionally validated against . - public static SosColumn Right(string header, SosToken? token = null) => new(header, token, rightAligned: true); -} diff --git a/src/tests/SOS.TestHarness/SosConvert.cs b/src/tests/SOS.TestHarness/SosConvert.cs deleted file mode 100644 index 13f81392ed..0000000000 --- a/src/tests/SOS.TestHarness/SosConvert.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Shared value converters for and . The -/// supplies the regex/number format (hex vs decimal); the method name picks -/// the .NET type (named to match BitConverter/Convert: Int32, UInt64, -/// Boolean, …). The caller supplies a fail delegate that turns an expectation message -/// into the right exception (a rich with the full captured output -/// for parser cells, or a line-scoped one for free-standing data cells), so the converters don't -/// depend on a directly. -/// -internal static class SosConvert -{ - public static ulong UInt64(string name, string value, SosToken token, Func fail) - { - if (!token.TryParseNumber(value, out ulong number)) - { - throw fail($"'{name}' to be a parseable {token} value (was \"{value}\")"); - } - - return number; - } - - public static long Int64(string name, string value, SosToken token, Func fail) - { - if (!token.TryParseSigned(value, out long number)) - { - throw fail($"'{name}' to be a parseable signed {token} value (was \"{value}\")"); - } - - return number; - } - - public static uint UInt32(string name, string value, SosToken token, Func fail) => - Checked(name, value, fail, () => checked((uint)UInt64(name, value, token, fail))); - - public static int Int32(string name, string value, SosToken token, Func fail) => - Checked(name, value, fail, () => checked((int)Int64(name, value, token, fail))); - - public static ushort UInt16(string name, string value, SosToken token, Func fail) => - Checked(name, value, fail, () => checked((ushort)UInt64(name, value, token, fail))); - - public static short Int16(string name, string value, SosToken token, Func fail) => - Checked(name, value, fail, () => checked((short)Int64(name, value, token, fail))); - - public static byte Byte(string name, string value, SosToken token, Func fail) => - Checked(name, value, fail, () => checked((byte)UInt64(name, value, token, fail))); - - public static bool Boolean(string name, string value, Func fail) - { - string v = value.Trim(); - if (bool.TryParse(v, out bool b)) - { - return b; - } - - if (v == "1") - { - return true; - } - - if (v == "0") - { - return false; - } - - throw fail($"'{name}' to be a boolean (was \"{value}\")"); - } - - private static T Checked(string name, string value, Func fail, Func convert) - { - try - { - return convert(); - } - catch (OverflowException) - { - throw fail($"'{name}' = \"{value}\" does not fit in {typeof(T).Name}"); - } - } -} diff --git a/src/tests/SOS.TestHarness/SosDataExtractor.cs b/src/tests/SOS.TestHarness/SosDataExtractor.cs deleted file mode 100644 index b803900f62..0000000000 --- a/src/tests/SOS.TestHarness/SosDataExtractor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Claims an internal data line that belongs to — the most recent -/// aligned row (e.g. a GC root from !clrstack -gc or a register from !clrstack -r). -/// Called for each line after the first aligned row is matched. Attach values to the row either as -/// extra scalar columns (, the -r register shape — so -/// row["rip"] works) or as structured sub-records ( with a -/// , the -gc roots shape). Return true to consume the line, or -/// false to let it fall through to normal row matching. -/// -public delegate bool SosDataExtractor(string line, SosRow row); diff --git a/src/tests/SOS.TestHarness/SosDataRow.cs b/src/tests/SOS.TestHarness/SosDataRow.cs deleted file mode 100644 index 12aceb3453..0000000000 --- a/src/tests/SOS.TestHarness/SosDataRow.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// One structured sub-record attached to a by a table data extractor — the -/// "array of structs" shape of internal-data rows. Each !clrstack -gc root line, or each -/// -a/-l/-p parameter/local, becomes one of these: a small bag of named -/// s with the same this[column] + .As*() ergonomics as a table -/// row, but a free-standing set of fields rather than columns shared across rows. Contrast with -/// , which is for the "extra scalar columns" shape (-r -/// registers), where each name occurs once per row. -/// -public sealed class SosDataRow -{ - private readonly string _sourceLine; - private readonly List _cells = new(); - - /// Create an empty record whose failure messages cite . - public SosDataRow(string sourceLine) - { - _sourceLine = sourceLine; - } - - /// The raw output line this record was parsed from. - public string SourceLine => _sourceLine; - - /// All fields of this record, in insertion order. - public IReadOnlyList Cells => _cells; - - /// Add a named field. Throws if is already present. - public SosDataRow Add(string column, string value) - { - if (_cells.Any(c => c.Column == column)) - { - throw new ArgumentException($"Data row already has a field '{column}'. Fields: {string.Join(", ", _cells.Select(c => c.Column))}"); - } - - _cells.Add(new SosCell(_sourceLine, column, value)); - return this; - } - - /// True if this record has the named field. - public bool Has(string column) => _cells.Any(c => c.Column == column); - - /// The field with the given name. - public SosCell this[string column] => - _cells.FirstOrDefault(c => c.Column == column) - ?? throw new ArgumentException($"Data row has no field '{column}'. Fields: {string.Join(", ", _cells.Select(c => c.Column))}"); - - /// The raw source line, so a record renders usefully in any Assert.* failure. - public override string ToString() => _sourceLine; -} diff --git a/src/tests/SOS.TestHarness/SosDataRowExtensions.cs b/src/tests/SOS.TestHarness/SosDataRowExtensions.cs deleted file mode 100644 index 9df889a2b7..0000000000 --- a/src/tests/SOS.TestHarness/SosDataRowExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Output-capturing assertions over a set of records (the "array of structs" -/// internal-data shape — !clrstack -gc roots, or -a/-l/-p -/// parameters/locals). Prefer these over Assert.Contains(records, predicate): on failure they -/// throw echoing the source lines of every record considered, plus a -/// human description of what was sought, instead of a bare "Filter not matched in collection". -/// -public static class SosDataRowExtensions -{ - /// Assert at least one record satisfies and return the first match. - public static SosDataRow AssertContains(this IEnumerable records, Func predicate, string description) - { - SosDataRow[] all = records.ToArray(); - SosDataRow? match = all.FirstOrDefault(predicate); - if (match is null) - { - throw Fail(all, $"at least one record where {description}"); - } - - return match; - } - - /// Assert exactly one record satisfies and return it. - public static SosDataRow AssertSingle(this IEnumerable records, Func predicate, string description) - { - SosDataRow[] all = records.ToArray(); - SosDataRow[] matches = all.Where(predicate).ToArray(); - if (matches.Length != 1) - { - throw Fail(all, $"exactly one record where {description} (found {matches.Length})"); - } - - return matches[0]; - } - - private static SosAssertException Fail(IEnumerable records, string expectation) - { - string body = string.Join('\n', records.Select(r => r.SourceLine)); - return new SosAssertException("(table data)", "records", expectation, body); - } -} diff --git a/src/tests/SOS.TestHarness/SosField.cs b/src/tests/SOS.TestHarness/SosField.cs deleted file mode 100644 index 48223c1172..0000000000 --- a/src/tests/SOS.TestHarness/SosField.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.RegularExpressions; - -namespace SOS.TestHarness; - -/// -/// A single "Key: Value" field pulled from SOS output by or the -/// indexer. It exposes the parsed / -/// , an implicit conversion to the value string (so it drops straight into -/// Assert.Equal/Assert.NotEmpty), rich shape assertions, bool predicates, and numeric -/// parsing: -/// -/// pe["Exception object"].AssertValid(Sos.Addr); // rich shape assert (full output on failure) -/// Assert.NotEmpty(pe["Exception type"]); // presence (implicit string -> IEnumerable) -/// Assert.Equal(TestTargets.NestedExceptions.OuterMessage, pe["Message"]); // value (implicit string) -/// Assert.True(pe["InnerException"].Contains("System.FormatException")); // bool predicate -/// Assert.Equal(0x80131509u, pe.Field("HResult").AsUInt32(Sos.Hex)); // numeric (token says how to parse) -/// -/// Predicates () return plain values to compose with Assert.*; -/// the Assert* helpers throw (with the full captured output) -/// on failure. -/// -public sealed class SosField -{ - private readonly SosOutput _output; - - public string Key { get; } - public string Value { get; } - - internal SosField(SosOutput output, string key, string value) - { - _output = output; - Key = key; - Value = value; - } - - /// The field's value, so a field drops straight into Assert.Equal(expected, pe["x"]). - public static implicit operator string(SosField field) => field.Value; - - /// Assert the value has the shape of (e.g. a hex address). - public SosField AssertValid(SosToken token) - { - if (!token.Matches(Value)) - { - throw _output.Fail($"field '{Key}' to be a {token} value (was \"{Value}\")"); - } - - return this; - } - - /// Assert the value equals exactly (literal text, not a regex). - public SosField Equals(string expected) - { - if (!string.Equals(Value, expected, StringComparison.Ordinal)) - { - throw _output.Fail($"field '{Key}' == \"{expected}\" (was \"{Value}\")"); - } - - return this; - } - - public bool Contains(string substring) => Value.Contains(substring, StringComparison.Ordinal); - public bool Contains(string substring, StringComparison comparison) => Value.Contains(substring, comparison); - - /// - /// Assert the value contains . Prefer this over - /// Assert.True(field.Contains(...)): on failure it throws - /// with the full captured output, instead of a bare "Expected: True". - /// - public SosField AssertContains(string substring) - { - if (!Value.Contains(substring, StringComparison.Ordinal)) - { - throw _output.Fail($"field '{Key}' to contain \"{substring}\" (was \"{Value}\")"); - } - - return this; - } - - /// Assert the value matches as a regex. - public SosField MatchesRegex(string pattern) - { - if (!Regex.IsMatch(Value, pattern)) - { - throw _output.Fail($"field '{Key}' to match /{pattern}/ (was \"{Value}\")"); - } - - return this; - } - - /// Parse the value as an unsigned 64-bit number per (hex/decimal). - public ulong AsUInt64(SosToken token) => SosConvert.UInt64(Key, Value, token, _output.Fail); - - /// Parse the value as an unsigned 32-bit number per . - public uint AsUInt32(SosToken token) => SosConvert.UInt32(Key, Value, token, _output.Fail); - - /// Parse the value as an unsigned 16-bit number per . - public ushort AsUInt16(SosToken token) => SosConvert.UInt16(Key, Value, token, _output.Fail); - - /// Parse the value as a signed 64-bit number per . - public long AsInt64(SosToken token) => SosConvert.Int64(Key, Value, token, _output.Fail); - - /// Parse the value as a signed 32-bit number per . - public int AsInt32(SosToken token) => SosConvert.Int32(Key, Value, token, _output.Fail); - - /// Parse the value as a signed 16-bit number per . - public short AsInt16(SosToken token) => SosConvert.Int16(Key, Value, token, _output.Fail); - - /// Parse the value as a byte per . - public byte AsByte(SosToken token) => SosConvert.Byte(Key, Value, token, _output.Fail); - - /// Parse the value as a boolean (true/false or 1/0). - public bool AsBoolean() => SosConvert.Boolean(Key, Value, _output.Fail); - - /// - /// Extract the first -shaped value embedded in this field's value and - /// parse it to a number — e.g. pull the inner-exception address out of - /// "System.FormatException, Use !printexception 0000026F5649D200 to see more." The token is - /// matched on word boundaries so spurious short hex runs inside words are ignored. - /// - public ulong Extract(SosToken token) - { - Match m = Regex.Match(Value, $@"\b(?:{token.Pattern})\b"); - if (!m.Success || !token.TryParseNumber(m.Value, out ulong number)) - { - throw _output.Fail($"field '{Key}' to contain a {token} value to extract (was \"{Value}\")"); - } - - return number; - } -} diff --git a/src/tests/SOS.TestHarness/SosOutput.cs b/src/tests/SOS.TestHarness/SosOutput.cs deleted file mode 100644 index c4de0239b6..0000000000 --- a/src/tests/SOS.TestHarness/SosOutput.cs +++ /dev/null @@ -1,424 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.RegularExpressions; - -namespace SOS.TestHarness; - -/// -/// The captured output of a single SOS/debugger command, plus a fluent, chainable -/// assertion surface. This is the modern replacement for a wall of VERIFY: -/// regex lines. -/// -/// Three assertion styles are offered side by side, on purpose: -/// -/// Field lookup + value assertions.Field(name) returns a structured -/// (Key/Value). Assert shape inline with a token -/// (.Field("Exception object", Sos.Addr)) and/or an exact value on the returned field -/// (.Field("Exception type").Matches("System.InvalidOperationException"), -/// .Field("Exception object").Matches(0x1234, Sos.Addr), -/// .Field("InnerException").MatchesRegex(@"System\.FormatException")). One assertion per -/// statement keeps each on its own stack-trace line. -/// Structural line assertions.Frame("Program.Main"), -/// .ContainsLineMatching(...). -/// Raw regex escape hatch.Matches(@"HResult:\s+80131509") / -/// .DoesNotMatch(...), for the cases where a pattern really is the clearest thing. -/// -/// All assertions chain (return this) and throw with the -/// full captured output on failure. -/// -public sealed class SosOutput -{ - public string Host { get; } - public string Command { get; } - public string Text { get; } - - private readonly string[] _lines; - - public SosOutput(string host, string command, string text) - { - Host = host; - Command = command; - Text = text ?? string.Empty; - _lines = Text.Replace("\r\n", "\n").Split('\n'); - } - - public IReadOnlyList Lines => _lines; - - public override string ToString() => Text; - - // ---- Field lookup -------------------------------------------------------------------- - - /// - /// Look up a "Name: value" line and return it as a structured . The field - /// carries the value assertions (, , - /// numeric parsing) and converts implicitly to its value string for Assert.Equal. Throws - /// if the field is absent. The indexer is shorthand for this. - /// - public SosField Field(string name) => new(this, name, RequireField(name)); - - /// Shorthand for : pe["Message"]. - public SosField this[string name] => Field(name); - - private string RequireField(string name) => - FindFieldValue(name) ?? throw Fail($"a field named '{name}'"); - - // ---- Tables -------------------------------------------------------------------------- - - /// - /// Parse the columnar block whose header row is (e.g. the - /// printexception StackTrace table's SP/IP/Function), asserting the - /// header row is present. Cells are sliced at fixed header start columns (tables are aligned), so - /// space-containing values (signatures) survive in any column. A column written as - /// (name, token) has every cell validated against that token up front; a bare name is not - /// validated here (use later if you want). Columns may be - /// freely mixed — see . - /// - public SosTable Table(params SosColumn[] columns) => BuildTable(columns, null); - - /// - /// Parse a table whose aligned rows are interleaved with internal data lines that belong - /// to the row above them — e.g. !clrstack -gc (GC roots under each frame) or - /// !clrstack -r (a register dump under each frame). After the first aligned row is matched, - /// each subsequent line is offered to first: if it returns true the - /// line is consumed and any cells it added to the most recent row (via - /// ) show up in ; if it returns - /// false the line falls through to normal row matching (a new row, or the end of the table). - /// Data is never collected before the first row. - /// - public SosTable Table(SosColumn[] columns, SosDataExtractor dataExtractor) => BuildTable(columns, dataExtractor); - - /// - /// Build a table from cells a caller already parsed, for commands whose layout the generic - /// parser can't handle — e.g. !clrthreads, whose header spans two - /// physical lines (the "Lock" of "Lock Count" floats above "Count") and whose trailing column is - /// usually empty. The harness still owns / construction - /// (so the rows carry this output for rich failures); the caller supplies the column names and one - /// string[] of cells per row (each the length of ). - /// - public SosTable TableFromRows(string[] columns, IEnumerable rows) => - new(this, columns, rows as string[][] ?? rows.ToArray()); - - private SosTable BuildTable(SosColumn[] columns, SosDataExtractor? data) - { - string[] headers = columns.Select(c => c.Header).ToArray(); - bool[] rightAligned = columns.Select(c => c.RightAligned).ToArray(); - SosTable table = ParseTable(headers, rightAligned, data); - - for (int c = 0; c < columns.Length; c++) - { - SosToken? token = columns[c].Token; - if (token is null) - { - continue; - } - - for (int r = 0; r < table.Length; r++) - { - string cell = table.Row(r)[c]; - if (!token.Matches(cell)) - { - throw Fail($"every '{headers[c]}' cell to be a {token} value (row {r} was \"{cell}\")"); - } - } - } - - return table; - } - - private SosTable ParseTable(string[] headers, bool[] rightAligned, SosDataExtractor? data) - { - int headerLine = FindHeaderLine(headers, out int[] starts, out int[] ends); - if (headerLine < 0) - { - throw Fail($"a table with header row [{string.Join(", ", headers)}]"); - } - - // Fixed left boundaries of each column, derived from header positions + alignment. A column - // begins at: its own header start if it's left-aligned, else the previous column's header - // end (a right-aligned column's data right-aligns under its header, extending left of it). - int[] bounds = new int[headers.Length]; - bounds[0] = 0; - for (int c = 1; c < headers.Length; c++) - { - bounds[c] = rightAligned[c] ? ends[c - 1] : starts[c]; - } - - List rows = new(); - for (int i = headerLine + 1; i < _lines.Length; i++) - { - string line = _lines[i]; - - // Internal data: once we have a row to attach to, let the extractor claim the line. If it - // does, the cells it added belong to that row, and we don't try to match a new row. - if (data is not null && rows.Count > 0 && data(line, rows[^1])) - { - continue; - } - - // The table ends at the first line that isn't an aligned data row (a blank line, or a - // differently-shaped trailing line). A row must line up with EVERY column's anchor. - if (!LooksLikeRow(line, starts, ends, rightAligned)) - { - break; - } - - rows.Add(new SosRow(this, headers, SliceColumns(line, bounds))); - } - - return new SosTable(this, headers, rows.ToArray()); - } - - /// - /// Find the header row and the character start/end index of each header. SOS tables are - /// fixed-width/aligned; combined with each column's alignment, those positions let cells be - /// sliced so space-containing values (signatures, type names) stay intact in any column. - /// - private int FindHeaderLine(string[] headers, out int[] starts, out int[] ends) - { - starts = Array.Empty(); - ends = Array.Empty(); - for (int i = 0; i < _lines.Length; i++) - { - if (TryLocateHeaders(_lines[i], headers, out int[] s, out int[] e)) - { - starts = s; - ends = e; - return i; - } - } - - return -1; - } - - /// - /// Locate each header as an in-order substring of , each found at or after - /// the previous header's end. Substring matching (rather than whitespace tokenizing) lets headers - /// contain spaces — e.g. !clrstack's "Child SP" and "Call Site" — while a header must sit on - /// a whitespace/edge boundary so a short header like "IP" can't match inside a longer word. - /// - private static bool TryLocateHeaders(string line, string[] headers, out int[] starts, out int[] ends) - { - starts = new int[headers.Length]; - ends = new int[headers.Length]; - int from = 0; - for (int k = 0; k < headers.Length; k++) - { - int at = from; - while (true) - { - at = line.IndexOf(headers[k], at, StringComparison.Ordinal); - if (at < 0) - { - return false; - } - - bool leftEdge = at == 0 || char.IsWhiteSpace(line[at - 1]); - int end = at + headers[k].Length; - bool rightEdge = end >= line.Length || char.IsWhiteSpace(line[end]); - if (leftEdge && rightEdge) - { - starts[k] = at; - ends[k] = end; - from = end; - break; - } - - at = end; - } - } - - return true; - } - - private static string[] SliceColumns(string line, int[] bounds) - { - string[] cells = new string[bounds.Length]; - for (int c = 0; c < bounds.Length; c++) - { - int begin = Math.Min(bounds[c], line.Length); - int end = c + 1 < bounds.Length ? Math.Min(bounds[c + 1], line.Length) : line.Length; - cells[c] = line[begin..Math.Max(begin, end)].Trim(); - } - - return cells; - } - - /// - /// A line is a data row only if it lines up with the column layout at every column's alignment - /// anchor: a left-aligned column has a content character at its header start (and whitespace - /// before it); a right-aligned column has a content character at its header's right edge (and - /// whitespace after it). Checking every column — not just the first — keeps a differently-shaped - /// trailing line (e.g. "StackTraceString: <none>") from being mistaken for a row. - /// - private static bool LooksLikeRow(string line, int[] starts, int[] ends, bool[] rightAligned) - { - if (string.IsNullOrWhiteSpace(line)) - { - return false; - } - - // A left-aligned first column implies a blank left margin (preserves prior behavior). - if (!rightAligned[0]) - { - for (int i = 0; i < starts[0]; i++) - { - if (i >= line.Length || !char.IsWhiteSpace(line[i])) - { - return false; - } - } - } - - for (int c = 0; c < starts.Length; c++) - { - if (rightAligned[c]) - { - int anchor = ends[c] - 1; // data right edge aligns with the header's last char - if (anchor < 0 || anchor >= line.Length || char.IsWhiteSpace(line[anchor])) - { - return false; - } - - if (ends[c] < line.Length && !char.IsWhiteSpace(line[ends[c]])) - { - return false; - } - } - else - { - int anchor = starts[c]; // data left edge aligns with the header start - if (anchor >= line.Length || char.IsWhiteSpace(line[anchor])) - { - return false; - } - - if (anchor > 0 && !char.IsWhiteSpace(line[anchor - 1])) - { - return false; - } - } - } - - return true; - } - - // ---- Structural line assertions ------------------------------------------------------ - - - /// True if the output contains the given literal substring (compose with Assert.True). - public bool Contains(string literal) => Text.Contains(literal, StringComparison.Ordinal); - - /// - /// Assert the output contains . Prefer this over - /// Assert.True(output.Contains(...)): on failure it throws - /// with the host, command, and full captured output, instead of a bare "Expected: True". - /// - public SosOutput AssertContains(string literal) - { - if (!Text.Contains(literal, StringComparison.Ordinal)) - { - throw Fail($"output to contain \"{literal}\""); - } - - return this; - } - - - // ---- Column-oriented helpers --------------------------------------------------------- - - /// - /// Return the whitespace-split tokens of the first data line whose first token is a hex value - /// equal to (tolerant of dbgeng's backtick split). Useful for - /// column-oriented commands like gcwhere/dumpheap. Throws if no such row exists. - /// - public string[] RowByAddress(ulong address) - { - foreach (string line in _lines) - { - string[] tokens = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length > 0 && TryParseHex(tokens[0], out ulong value) && value == address) - { - return tokens; - } - } - - throw Fail($"a row whose first column is address 0x{address:x}"); - } - - /// - /// Header-relative column access for column-oriented output. Finds the header row (the line - /// containing one of as a token), locates that column's - /// index, then returns the value at that index from the data row whose first column is - /// . This tolerates host/flavor layout differences — e.g. SOS prints - /// gcwhere's generation column as "Generation" (col 3) under dotnet-dump but as "Gen" - /// (col 1) under cdb on desktop — without the test hard-coding a column index. - /// - public string Column(ulong address, params string[] headerAliases) - { - int headerIndex = -1; - foreach (string line in _lines) - { - string[] tokens = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - foreach (string alias in headerAliases) - { - int idx = Array.FindIndex(tokens, tok => string.Equals(tok, alias, StringComparison.OrdinalIgnoreCase)); - if (idx >= 0) - { - headerIndex = idx; - break; - } - } - - if (headerIndex >= 0) - { - break; - } - } - - if (headerIndex < 0) - { - throw Fail($"a header column named one of [{string.Join(", ", headerAliases)}]"); - } - - string[] row = RowByAddress(address); - if (headerIndex >= row.Length) - { - throw Fail($"column {headerIndex} ([{string.Join(", ", headerAliases)}]) in row [{string.Join(' ', row)}]"); - } - - return row[headerIndex]; - } - - private static bool TryParseHex(string token, out ulong value) => - ulong.TryParse(token.Replace("`", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out value); - - // ---- internals ----------------------------------------------------------------------- - - private string? FindFieldValue(string name) - { - foreach (string line in _lines) - { - int colon = line.IndexOf(':'); - if (colon <= 0) - { - continue; - } - - string key = line[..colon].Trim(); - if (string.Equals(key, name, StringComparison.Ordinal)) - { - return line[(colon + 1)..].Trim(); - } - } - - return null; - } - - /// - /// Build (don't throw) a for , - /// carrying this output's host, command, and full text. The primitive every fluent assertion and - /// custom command parser uses to produce a rich failure: throw output.Fail("a thread table"). - /// - public SosAssertException Fail(string expectation) => new(Host, Command, expectation, Text); -} diff --git a/src/tests/SOS.TestHarness/SosRow.cs b/src/tests/SOS.TestHarness/SosRow.cs deleted file mode 100644 index 2b50566148..0000000000 --- a/src/tests/SOS.TestHarness/SosRow.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// One data row of a . Cells are addressable by column name -/// (row["Function"]) or by index, so tests can read like the table they assert against: -/// table.Any(row => row["Function"].Contains("ThrowNested")). -/// -/// A table data extractor can attach two distinct shapes of internal-data to a row: -/// -/// Extra scalar columns via — one value per name, promoted -/// onto the row so row["rip"] works alongside row["IP"] (the !clrstack -r -/// registers). -/// Structured sub-records via — a list of multi-field -/// s in (the !clrstack -gc roots, or -/// -a/-l/-p parameters/locals). -/// -/// -public sealed class SosRow -{ - private readonly SosOutput _output; - private readonly string[] _columns; - private readonly string[] _cells; - private readonly List _addedColumns = new(); - private readonly List _data = new(); - - internal SosRow(SosOutput output, string[] columns, string[] cells) - { - _output = output; - _columns = columns; - _cells = cells; - } - - /// The cell in the named column (an original table column or one added via ). - public SosCell this[string column] - { - get - { - int i = Array.IndexOf(_columns, column); - if (i >= 0) - { - return new SosCell(_output, column, _cells[i]); - } - - SosCell? added = _addedColumns.FirstOrDefault(c => c.Column == column); - if (added is not null) - { - return added; - } - - throw new ArgumentException($"Row has no column '{column}'. Columns: {string.Join(", ", Columns)}"); - } - } - - /// The cell at the given column index (original table columns only). - public SosCell this[int index] => new(_output, _columns[index], _cells[index]); - - /// All cell values in column order (original table columns only). - public IReadOnlyList Cells => _cells; - - /// Every column name on this row: the original table columns plus any added via . - public IReadOnlyList Columns => _columns.Concat(_addedColumns.Select(c => c.Column)).ToArray(); - - /// True if this row has the named column (original or added). - public bool HasColumn(string column) => - Array.IndexOf(_columns, column) >= 0 || _addedColumns.Any(c => c.Column == column); - - /// - /// Promote an extra scalar column onto this row (the "extra columns" internal-data shape, e.g. an - /// !clrstack -r register), so row["rip"] reads it like any other column. Throws if a - /// column with this name already exists, so an extractor can never silently shadow a real column. - /// - public void AddColumn(string column, string value) - { - if (HasColumn(column)) - { - throw new ArgumentException($"Row already has a column '{column}'. Columns: {string.Join(", ", Columns)}"); - } - - _addedColumns.Add(new SosCell(_output, column, value)); - } - - /// - /// The structured sub-records attached to this row (the "array of structs" internal-data shape, - /// e.g. the GC roots from !clrstack -gc). Empty unless the table was parsed with an - /// extractor that called . - /// - public IReadOnlyList Data => _data; - - /// Attach a structured sub-record (one parsed internal-data line) to this row. - public void AddData(SosDataRow row) => _data.Add(row); -} diff --git a/src/tests/SOS.TestHarness/SosTable.cs b/src/tests/SOS.TestHarness/SosTable.cs deleted file mode 100644 index 7560d98277..0000000000 --- a/src/tests/SOS.TestHarness/SosTable.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; - -namespace SOS.TestHarness; - -/// -/// A parsed columnar block of SOS output — e.g. the StackTrace (generated) table in -/// printexception with its SP/IP/Function columns. Produced by -/// . Columns are identified by header name and cells are -/// sliced at fixed header start columns (SOS tables are aligned), so values that contain spaces (a -/// function signature like Method(System.String, int)+0x8a) stay intact in any column. -/// -/// The table is itself an of , so LINQ reads -/// naturally: table.Any(row => row["Function"].Contains("ThrowNested")). -/// -public sealed class SosTable : IEnumerable -{ - private readonly SosOutput _output; - private readonly string[] _columns; - private readonly SosRow[] _rows; - - internal SosTable(SosOutput output, string[] columns, string[][] rows) - { - _output = output; - _columns = columns; - _rows = Array.ConvertAll(rows, r => new SosRow(output, columns, r)); - } - - internal SosTable(SosOutput output, string[] columns, SosRow[] rows) - { - _output = output; - _columns = columns; - _rows = rows; - } - - /// The header names, in order. - public IReadOnlyList Columns => _columns; - - /// The number of data rows. - public int Length => _rows.Length; - - /// One data row, by index. - public SosRow Row(int index) => _rows[index]; - - /// - /// Assert that every cell of each column matches the column's token, by position — one token per - /// column, in column order. This is the post-hoc form of the per-column validation - /// does for (name, token) columns, for when the table was - /// parsed with bare header names: table.AssertValid(Sos.Addr, Sos.Addr, Sos.ModuleFunctionWithOffset). - /// - public SosTable AssertValid(params SosToken[] columnTokens) - { - if (columnTokens.Length != _columns.Length) - { - throw _output.Fail($"AssertValid to be given {_columns.Length} token(s) for columns [{string.Join(", ", _columns)}], got {columnTokens.Length}"); - } - - for (int c = 0; c < columnTokens.Length; c++) - { - for (int r = 0; r < _rows.Length; r++) - { - string cell = _rows[r][c]; - if (!columnTokens[c].Matches(cell)) - { - throw _output.Fail($"every '{_columns[c]}' cell to be a {columnTokens[c]} value (row {r} was \"{cell}\")"); - } - } - } - - return this; - } - - /// All cell values in the named column, top to bottom. - public IEnumerable Column(string header) - { - foreach (SosRow row in _rows) - { - yield return row[header].Value; - } - } - - /// - /// Assert at least one row satisfies . Prefer this over - /// Assert.Contains(table, predicate): on failure it throws - /// with the full captured output (a bare predicate Assert.Contains can't say what it wanted - /// or echo the SOS output). names what was sought, e.g. - /// "Function contains NestedExceptions.ThrowNested". - /// - public SosTable AssertContainsRow(Func predicate, string description) - { - if (!_rows.Any(predicate)) - { - throw _output.Fail($"a row where {description}"); - } - - return this; - } - - /// - /// Assert every row satisfies . Prefer this over - /// Assert.All(table, row => Assert.True(...)) when the check is a simple predicate: - /// table.AssertAll(row => row["GC Mode"] == "Preemptive", "every thread is Preemptive"). - /// On failure it throws naming the first offending row and echoing - /// the full captured output. - /// - public SosTable AssertAll(Func predicate, string description) - { - for (int r = 0; r < _rows.Length; r++) - { - if (!predicate(_rows[r])) - { - throw _output.Fail($"every row to satisfy: {description} (row {r} did not: [{string.Join(" | ", _rows[r].Cells)}])"); - } - } - - return this; - } - - /// - /// Assert exactly one row satisfies and return it. Prefer this over - /// LINQ table.Single(predicate): on failure (zero or many matches) it throws - /// with the full captured output. - /// - public SosRow SingleRow(Func predicate, string description) - { - SosRow[] matches = _rows.Where(predicate).ToArray(); - if (matches.Length != 1) - { - throw _output.Fail($"exactly one row where {description} (found {matches.Length})"); - } - - return matches[0]; - } - - public IEnumerator GetEnumerator() => ((IEnumerable)_rows).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => _rows.GetEnumerator(); -} diff --git a/src/tests/SOS.TestHarness/SosToken.cs b/src/tests/SOS.TestHarness/SosToken.cs deleted file mode 100644 index 29957bfb2a..0000000000 --- a/src/tests/SOS.TestHarness/SosToken.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using System.Text.RegularExpressions; - -namespace SOS.TestHarness; - -/// -/// A named, reusable pattern for a class of nondeterministic value that -/// appears in SOS output — pointers, addresses, counts, HResults, etc. These are -/// the modern, readable replacement for the old <HEXVAL>/<DECVAL> -/// regex tokens: instead of hand-writing [A-Fa-f0-9]+ soup, a test says the -/// field "should be a hex value" and the harness checks the shape while ignoring the -/// volatile value. -/// -/// A token also knows how to parse its values to a number (hex vs decimal), so an exact -/// value assertion can say "this field, read as an address, equals 0x1234". -/// -public sealed class SosToken -{ - public string Name { get; } - - /// The inner regex fragment (no anchors), so tokens can be embedded in larger patterns. - public string Pattern { get; } - - private readonly Regex _anchored; - private readonly NumberStyles? _numberStyles; - - public SosToken(string name, string pattern, NumberStyles? numberStyles = null) - { - Name = name; - Pattern = pattern; - _anchored = new Regex("^(?:" + pattern + ")$", RegexOptions.Compiled); - _numberStyles = numberStyles; - } - - /// True if is entirely this kind of token. - public bool Matches(string value) => _anchored.IsMatch(value.Trim()); - - /// - /// Parse to a number using this token's numeric style (hex for - /// addresses/hex, decimal for counts), tolerating dbgeng backticks and thousands separators. - /// Returns false for non-numeric tokens or unparseable input. - /// - public bool TryParseNumber(string value, out ulong number) - { - number = 0; - if (_numberStyles is null) - { - return false; - } - - string normalized = StripHexPrefix(value.Trim().Replace("`", string.Empty).Replace(",", string.Empty)); - return ulong.TryParse(normalized, _numberStyles.Value, CultureInfo.InvariantCulture, out number); - } - - /// Signed counterpart of (for AsInt32/AsInt64). - public bool TryParseSigned(string value, out long number) - { - number = 0; - if (_numberStyles is null) - { - return false; - } - - string normalized = StripHexPrefix(value.Trim().Replace("`", string.Empty).Replace(",", string.Empty)); - return long.TryParse(normalized, _numberStyles.Value, CultureInfo.InvariantCulture, out number); - } - - // NumberStyles.HexNumber doesn't accept a "0x" prefix, but SOS prints some hex columns 0x-prefixed - // (e.g. an OS thread id), so drop it before parsing. Harmless for decimal tokens (they never carry it). - private static string StripHexPrefix(string value) => - value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? value.Substring(2) : value; - - public override string ToString() => $"<{Name}>"; -} - -/// -/// The vocabulary of nondeterministic value shapes. This is deliberately small and -/// named so assertions read like English: Field("HResult", Sos.Hex). -/// -public static class Sos -{ - /// A hex value, optionally 0x-prefixed and/or with dbgeng's backtick high/low split - /// (e.g. 00007ffd`213c498a, 0x2394). - public static readonly SosToken Hex = new("hex", "(?:0[xX])?[0-9A-Fa-f]+(?:`[0-9A-Fa-f]+)?", NumberStyles.HexNumber); - - /// A decimal value, optionally with thousands separators. - public static readonly SosToken Dec = new("dec", "[0-9]+(?:,[0-9]+)*", NumberStyles.None); - - /// A pointer/address — same shape as , but named for intent at call sites. - public static readonly SosToken Addr = new("addr", "(?:0[xX])?[0-9A-Fa-f]+(?:`[0-9A-Fa-f]+)?", NumberStyles.HexNumber); - - /// - /// A decimal value with optional `,`. - /// - public static readonly SosToken Integer = new("integer", "[0-9]+(?:,[0-9]+)*", NumberStyles.None); - - /// - /// A range of memory hex-hex. - /// - public static readonly SosToken MemRange = new("memrange", "[0-9A-Fa-f]+-[0-9A-Fa-f]+", NumberStyles.HexNumber); - - /// - /// A managed frame as SOS prints it: Module!Namespace.Type.Method(args)+0xOFFSET - /// (e.g. NestedExceptions!NestedExceptions.ThrowNested()+0x8a). - /// - public static readonly SosToken ModuleFunctionWithOffset = - new("module!function+offset", @"[^!\s]+!.+\+0x[0-9A-Fa-f]+"); -} diff --git a/src/tests/SOS.TestHarness/Target.cs b/src/tests/SOS.TestHarness/Target.cs deleted file mode 100644 index f2bbe6abe2..0000000000 --- a/src/tests/SOS.TestHarness/Target.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// A debug target a test drives, obtained from . There is one -/// surface for both worlds; only the semantics of navigation differ: -/// -/// Dead (live: false, ) is backed by immutable cached -/// dumps. throws until you navigate to a point; after that you may -/// / among points in any order, repeatedly. -/// Live (live: true, ) is a single advancing process, -/// parked at the debugger's initial breakpoint (before CoreCLR loads). already -/// works there (e.g. bpmd); navigation only moves forward. -/// -/// -public abstract class Target : IDisposable -{ - protected Target(Host host, string targetName, Flavor flavor) - { - Host = host; - TargetName = targetName; - Flavor = flavor; - } - - public Host Host { get; } - public string TargetName { get; } - public Flavor Flavor { get; } - - /// The dump file currently backing this target (dead targets only). - public virtual string DumpPath => - throw new NotSupportedException("A live target has no dump file."); - - /// Navigate to the named stop point. Dead: load that point's dump. Live: run forward to its marker. - public void GoToStopPoint(string stopName) - { - GoToStopPointCore(stopName); - ReplayContext.Current?.Add(ReplayStepKind.Navigate, $"GoToStopPoint(\"{stopName}\")", SafeDumpPath()); - ReplayContext.Current?.AttachHost(CurrentDiagnostics); - } - - /// Navigate to the target's crash. Dead: load the crash dump. Live: run forward to the crash. - public void GoToCrash() - { - GoToCrashCore(); - ReplayContext.Current?.Add(ReplayStepKind.Navigate, "GoToCrash()", SafeDumpPath()); - ReplayContext.Current?.AttachHost(CurrentDiagnostics); - } - - /// Run a SOS command at the current point. - public SosOutput Sos(string command) - { - // Record before running so a throwing command is still captured; the dump (if any) is the one - // we're currently parked on, which is exactly what the command runs against. Attach the host's - // diagnostics up front too, so a command that crashes the host still surfaces its stdout/stderr - // and crash dump in the replay. - ReplayContext.Current?.Add(ReplayStepKind.Sos, command, SafeDumpPath()); - ReplayContext.Current?.AttachHost(CurrentDiagnostics); - return SosCore(command); - } - - /// Run a raw debugger command at the current point. - public SosOutput Execute(string command) - { - ReplayContext.Current?.Add(ReplayStepKind.Execute, command, SafeDumpPath()); - ReplayContext.Current?.AttachHost(CurrentDiagnostics); - return ExecuteCore(command); - } - - /// Navigate to the named stop point (subclass mechanics; records it). - protected abstract void GoToStopPointCore(string stopName); - - /// Navigate to the crash (subclass mechanics; records it). - protected abstract void GoToCrashCore(); - - /// Run a SOS command (subclass mechanics; records it). - protected abstract SosOutput SosCore(string command); - - /// Run a raw debugger command (subclass mechanics; records it). - protected abstract SosOutput ExecuteCore(string command); - - /// The dump backing this target now, or null if it has none yet / is a live target. - private protected string? SafeDumpPath() - { - try - { - return DumpPath; - } - catch - { - return null; - } - } - - /// - /// The diagnostics (captured stdout/stderr and crash dumps) of the host currently backing this target, - /// or null if none is available. The replay attaches this when a command runs so a failing test can - /// show what the underlying debugger process did. - /// - internal virtual HostDiagnostics? CurrentDiagnostics => null; - - public virtual void Dispose() - { - } -} diff --git a/src/tests/SOS.TestHarness/TargetCatalog.cs b/src/tests/SOS.TestHarness/TargetCatalog.cs deleted file mode 100644 index ae88c8eff2..0000000000 --- a/src/tests/SOS.TestHarness/TargetCatalog.cs +++ /dev/null @@ -1,183 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// How a named stop point is realized when producing a dump. -public enum StopKind -{ - /// Mid-run self-snapshot (the debuggee dumps itself and continues). - Snapshot, - - /// The final unhandled-exception crash dump produced by the runtime. - Crash, -} - -/// -/// A named location in a target. The same definition drives both worlds: a dump for the -/// snapshot/shared path, and a bpmd breakpoint on for the live path. -/// -/// Stable name used to key the dump and to ask a live target to stop here. -/// How the dump for this stop is produced. -/// Fully-qualified marker method for live bpmd (null for crash stops). -public sealed record StopPoint(string Name, StopKind Kind, string? Method); - -/// A standalone test target (its own program) and its stop points. -/// Target name used by tests, e.g. "divzero". -/// -/// The target's project/assembly name under the repo's SOS.UnitTests/Debuggees tree, e.g. -/// "DivZero". This is the folder, the csproj, and the produced <Project>.exe / -/// <Project>.dll. -/// -/// Ordered named stop points. -/// -/// The flavors this target supports. Defaults to all; e.g. DynamicMethod uses a .NET-Core-only API -/// (DynamicMethod.CreateDelegate<T>) so it can't build for desktop .NET Framework. -/// -public sealed record TargetDefinition(string Name, string Project, IReadOnlyList StopPoints, Flavor Flavors = Flavor.AllValid) -{ - /// Managed module name for bpmd on .NET Core (e.g. "SosHarnessScenarios.dll"). - public string Module => Project + ".dll"; - - /// - /// Managed module name for bpmd in a given flavor. Desktop .NET Framework's managed - /// module is the EXE itself (e.g. "SosHarnessScenarios.exe"); .NET Core's is the DLL. - /// - public string ModuleFor(Flavor flavor) => flavor == Flavor.Framework ? Project + ".exe" : Project + ".dll"; - - public StopPoint Stop(string name) => - StopPoints.FirstOrDefault(s => s.Name == name) - ?? throw new ArgumentException($"Target '{Name}' has no stop point '{name}'. Known: {string.Join(", ", StopPoints.Select(s => s.Name))}"); - - public string DefaultStopName => StopPoints[0].Name; -} - -/// -/// The debuggee targets the SOS test harness knows about, mapped to the diagnostics repo's existing -/// SOS.UnitTests/Debuggees projects plus the one consolidated marker debuggee the harness adds -/// (). Crash targets reproduce an unhandled exception / fault that the runtime -/// turns into a crash dump; the marker debuggee self-snapshots at named TestHarness.Stop points -/// (live tests set a bpmd breakpoint on the same marker method). -/// -public static class TargetCatalog -{ - // --- Repo crash debuggees (unhandled exception / fault -> crash dump). --- - - public const string NestedException = "nestedexception"; - public const string DivZero = "divzero"; - public const string AsyncMain = "asyncmain"; - public const string DynamicMethod = "dynamicmethod"; - public const string Overflow = "overflow"; - public const string LineNums = "linenums"; - public const string SimpleThrow = "simplethrow"; - public const string Reflection = "reflection"; - - // --- The one consolidated marker debuggee the harness adds (Phase 4). Every snapshot/oracle/live - // scenario is a named stop point on this single program (see SosHarnessScenarios). --- - - public const string Scenarios = "scenarios"; - - // Stop-point names on the Scenarios debuggee (kept as constants so tests don't stringly-type them). - public const string StopThinLock = "thinlock"; - public const string StopHeap = "heap"; - public const string StopArgsLocals = "argslocals"; - public const string StopRoots = "roots"; - public const string StopGen0 = "gen0"; - public const string StopGen1 = "gen1"; - public const string StopGen2 = "gen2"; - public const string StopAllThreads = "allthreads"; - - private const string ScenariosProject = "SosHarnessScenarios"; - - // The Scenarios stop points, shared by the workstation and server-GC targets. Heap stays first so - // GoToFirstStop() lands there; the rest are ordered as they occur, and dumps are keyed by name (not - // array order), so the debuggee's call order can differ. - private static readonly StopPoint[] s_scenarioStops = - { - new(StopHeap, StopKind.Snapshot, $"{ScenariosProject}.AtHeap"), - new(StopThinLock, StopKind.Snapshot, $"{ScenariosProject}.AtThinLock"), - new(StopArgsLocals, StopKind.Snapshot, $"{ScenariosProject}.AtArgsLocals"), - new(StopRoots, StopKind.Snapshot, $"{ScenariosProject}.AtRoots"), - new(StopGen0, StopKind.Snapshot, $"{ScenariosProject}.AtGen0"), - new(StopGen1, StopKind.Snapshot, $"{ScenariosProject}.AtGen1"), - new(StopGen2, StopKind.Snapshot, $"{ScenariosProject}.AtGen2"), - new(StopAllThreads, StopKind.Snapshot, $"{ScenariosProject}.AtAllThreads"), - }; - - private static readonly Dictionary s_targets = new[] - { - new TargetDefinition( - NestedException, - Project: "NestedExceptionTest", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - DivZero, - Project: "DivZero", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - AsyncMain, - Project: "AsyncMain", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - DynamicMethod, - Project: "DynamicMethod", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }, - // DynamicMethod.CreateDelegate() is a .NET-Core-only API, so this debuggee can't build - // for desktop .NET Framework. - Flavors: Flavor.Core | Flavor.SingleFile), - - new TargetDefinition( - Overflow, - Project: "Overflow", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - LineNums, - Project: "LineNums", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - SimpleThrow, - Project: "SimpleThrow", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - new TargetDefinition( - Reflection, - Project: "ReflectionTest", - StopPoints: new[] { new StopPoint("crash", StopKind.Crash, null) }), - - // The consolidated marker debuggee: each scenario is a NoInlining marker method that calls - // TestHarness.Stop(name). Ordered so the heap scenario (live/dead objects) is captured before - // any GC runs for the generation-promotion stops. - new TargetDefinition( - Scenarios, - Project: ScenariosProject, - StopPoints: s_scenarioStops), - }.ToDictionary(t => t.Name); - - public static TargetDefinition Get(string name) => - s_targets.TryGetValue(name, out TargetDefinition? t) - ? t - : throw new ArgumentException($"Unknown target '{name}'. Known: {string.Join(", ", s_targets.Keys)}"); - - /// - /// The flavors a target supports, or when is - /// not a known target (some tests pass other tokens — e.g. a stop name — through the matrix's string - /// dimension, which the flavor filter must tolerate). - /// - public static Flavor FlavorsFor(string name) => - s_targets.TryGetValue(name, out TargetDefinition? t) ? t.Flavors : Flavor.AllValid; - - /// - /// Whether reaching this target's stop points requires a live bpmd notification breakpoint - /// (i.e. it has a stop), as opposed to simply running to a crash. - /// Snapshot navigation can't be performed live on a self-contained single-file image under the lldb - /// host, so the matrix prunes that one row for such targets. - /// Unknown tokens are treated as not requiring bpmd. - /// - public static bool NavigatesViaBpmd(string name) => - s_targets.TryGetValue(name, out TargetDefinition? t) && t.StopPoints.Any(s => s.Kind == StopKind.Snapshot); -} diff --git a/src/tests/SOS.TestHarness/Targets.cs b/src/tests/SOS.TestHarness/Targets.cs deleted file mode 100644 index ac97d7d3d4..0000000000 --- a/src/tests/SOS.TestHarness/Targets.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; - -namespace SOS.TestHarness; - -/// -/// The single entry point tests use to get a debug target. Shared (dump-backed) targets are -/// memoized process-wide by (host, target, stopPoint) so the expensive load happens once -/// and is reused across every test that asks for the same triple. Live targets are exclusive and -/// never memoized — each call hands the caller its own advancing debuggee. -/// -/// Shared hosts (notably the dotnet-dump child processes) must be torn down at the end of the run, -/// or their lingering child processes keep the test host alive. does this; -/// it is wired to and also exposed for an explicit assembly -/// teardown fixture. -/// -public static class Targets -{ - private static readonly ConcurrentDictionary<(Host Host, string Target, string Stop, Flavor Flavor, GcType GcType, DumpKind DumpKind, CoreVersion CoreVersion, Dac Dac), Lazy> s_sessions = new(); - private static readonly ConcurrentBag s_created = new(); - - static Targets() - { - AppDomain.CurrentDomain.ProcessExit += (_, _) => DisposeAll(); - } - - /// - /// Get a debug target for a matrix row (). With - /// = you get a dump-backed - /// (navigate to cached dumps in any order); with - /// you get a launched and parked at the debugger's initial breakpoint. - /// Awaiting is the "gate" — for live it completes once the process is launched and SOS is ready. The - /// row must carry a single Host/Flavor/Liveness/GcType/DumpKind value (the per-case value a theory - /// receives), not a combined AllValid selector. - /// - public static Task GetTargetAsync(TestConfig config) - { - bool live = config.Liveness switch - { - Liveness.Live => true, - Liveness.Dump => false, - _ => throw new ArgumentOutOfRangeException( - nameof(config), config.Liveness, "Expected exactly Liveness.Live or Liveness.Dump."), - }; - - TargetDefinition definition = TargetCatalog.Get(config.Target); - - // Begin capturing this test's replay timeline (host/flavor/liveness + every command/dump). - ReplayContext.Start(config, live); - - if (live) - { - return Task.Run(() => { - string exe = SnapshotStore.TargetExe(config.Flavor, config.Target, config.CoreVersion); - return new LiveTarget(config.Host, definition, config.Flavor, exe, config.CoreVersion, config.Dac); - }); - } - - // Dead targets are cheap cursors; the heavy work (capture + load) happens on first GoTo, - // memoized per point so parallel tests navigating to the same point share one dump host. - return Task.FromResult(new DeadTarget(config)); - } - - /// - /// Resolve (memoized, process-wide) the read-only dump session for one point — produced and SOS - /// loaded on first use, then reused by every that navigates here. - /// - internal static DumpSession ResolveSession(TestConfig config, string stop) - { - return s_sessions - .GetOrAdd((config.Host, config.Target, stop, config.Flavor, config.GcType, config.DumpKind, config.CoreVersion, config.Dac), - key => new Lazy(() => CreateSession(key))) - .Value; - } - - private static DumpSession CreateSession((Host Host, string Target, string Stop, Flavor Flavor, GcType GcType, DumpKind DumpKind, CoreVersion CoreVersion, Dac Dac) key) - { - string dump = SnapshotStore.GetDump(key.Flavor, key.Target, key.Stop, key.GcType, key.DumpKind, key.CoreVersion); - DumpSession session = new(key.Host, key.Target, key.Stop, key.Flavor, dump, key.CoreVersion, key.Dac); - s_created.Add(session); - return session; - } - - /// Dispose every memoized dump session (kills dotnet-dump children, closes dbgeng hosts). - public static void DisposeAll() - { - while (s_created.TryTake(out DumpSession? session)) - { - try - { - session.Dispose(); - } - catch - { - // best effort teardown - } - } - - // Close any pooled (dotnet-dump) host still open. cdb children were disposed above via - // each SharedTarget.Dispose(). - HostSlot.DotNetDump.CloseCurrent(); - } -} diff --git a/src/tests/SOS.TestHarness/TestConfig.cs b/src/tests/SOS.TestHarness/TestConfig.cs deleted file mode 100644 index 718f4e1e17..0000000000 --- a/src/tests/SOS.TestHarness/TestConfig.cs +++ /dev/null @@ -1,353 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; -using Xunit.Sdk; - -namespace SOS.TestHarness; - -/// -/// One row of the test matrix: the full set of axes that define a single debug-target configuration a -/// theory runs against. Replaces the old positional (Host, Flavor, Liveness) tuple so adding axes -/// (GC type, dump kind, ...) doesn't widen every signature. A test takes a single -/// parameter and hands it straight to . -/// -/// Implements so each row has a stable, individually-runnable test -/// id and a legible display name (see ), and has value equality so -/// can de-duplicate rows that collapse onto the same configuration. -/// -public sealed record TestConfig : IXunitSerializable -{ - private string _target = string.Empty; - private Host _host; - private Flavor _flavor; - private Liveness _liveness; - private GcType _gcType; - private CoreVersion _coreVersion; - private Dac _dac; - private DumpKind _dumpKind; - - /// The debuggee target name (e.g. ). - public string Target { get => _target; init => _target = value; } - - /// The debugger host (cdb / dotnet-dump / lldb). - public Host Host { get => _host; init => _host = value; } - - /// The runtime flavor (Core / SingleFile / Framework). - public Flavor Flavor { get => _flavor; init => _flavor = value; } - - /// Live process vs. post-mortem dump. - public Liveness Liveness { get => _liveness; init => _liveness = value; } - - /// Workstation vs. server GC. - public GcType GcType { get => _gcType; init => _gcType = value; } - - /// The .NET Core runtime version the target is built and dumped against (a single flag). - public CoreVersion CoreVersion { get => _coreVersion; init => _coreVersion = value; } - - /// Which DAC SOS debugs with (Legacy / CDac). cDAC is only valid on .NET 11+ (see ). - public Dac Dac { get => _dac; init => _dac = value; } - - /// The dump kind (Heap / Mini). Always for live targets (no dump). - public DumpKind DumpKind { get => _dumpKind; init => _dumpKind = value; } - - /// Parameterless ctor required by ; do not use directly. - public TestConfig() - { - } - - public TestConfig(string target, Host host, Flavor flavor, Liveness liveness, - GcType gcType = GcType.Workstation, DumpKind dumpKind = DumpKind.Heap, - CoreVersion coreVersion = CoreVersion.Net10, Dac dac = Dac.Legacy) - { - Target = target; - Host = host; - Flavor = flavor; - Liveness = liveness; - GcType = gcType; - DumpKind = dumpKind; - CoreVersion = coreVersion; - Dac = dac; - } - - /// True for a live process target; false for a post-mortem dump. - public bool IsLive => Liveness == Liveness.Live; - - /// - /// Generate the cross-product of the requested axes as a single-column theory source, filtered to the - /// valid configurations for the current platform (see ). - /// - /// Axis defaults are deliberate: / default to - /// AllValid (full coverage), but defaults to - /// , to , and - /// to . Live debugging is slow (a debugger - /// ptrace-attached to a running process, one session per core) and almost every command behaves - /// identically against a dump, so live coverage is opt-in: a test that uniquely benefits from a - /// live process (e.g. a stack walk reading live thread contexts, a live GC heap/root scan) passes - /// liveness: Liveness.AllValid to run dump and live; everything else stays dump-only. - /// Server GC and Mini dumps are likewise opt-in so the matrix doesn't explode. - /// - /// Each axis can be narrowed at run time by a comma-separated env allow-list: - /// SOSHARNESS_ONLY_HOSTS, _FLAVORS, _LIVENESS, _GCTYPE, _DUMPKIND, - /// _COREVERSIONS (e.g. Net10,Net11), _DAC (e.g. Legacy). - /// - /// Out-of-support Core versions are excluded from the default matrix; set - /// SOSHARNESS_TEST_OUT_OF_SUPPORT_CORE=1 to include them, or name them explicitly in - /// SOSHARNESS_ONLY_COREVERSIONS (which bypasses the exclusion). - public static TheoryData BuildMatrix( - string[] targets, - Flavor flavor = Flavor.AllValid, - Host host = Host.AllValid, - Liveness liveness = Liveness.Dump, - GcType gcType = GcType.Workstation, - DumpKind dumpKind = DumpKind.Heap, - CoreVersion coreVersion = CoreVersion.All, - Dac dac = Dac.All) - { - TheoryData data = new(); - foreach (TestConfig cfg in Permutations(targets, flavor, host, liveness, gcType, dumpKind, coreVersion, dac)) - { - data.Add(cfg); - } - - return data; - } - - /// - /// The raw valid configurations for the requested axes (what wraps into a - /// theory source). Exposed for theories that need to pair each config with an extra, non-axis column — - /// e.g. a stop-point name — into their own TheoryData<TestConfig, ...>. - /// - public static IEnumerable Permutations( - string[] targets, - Flavor flavor = Flavor.AllValid, - Host host = Host.AllValid, - Liveness liveness = Liveness.Dump, - GcType gcType = GcType.Workstation, - DumpKind dumpKind = DumpKind.Heap, - CoreVersion coreVersion = CoreVersion.All, - Dac dac = Dac.All) - { - HashSet seen = new(); - - // Only ever expand versions the harness actually builds/installs; a requested bit outside the - // available set is silently dropped (the axis disables, it never positively enables — see CoreVersion). - CoreVersion requestedVersions = coreVersion & CoreVersions.Available; - - // Exclude out-of-support versions from the default matrix. They still run when opted in or when - // explicitly named in SOSHARNESS_ONLY_COREVERSIONS (the explicit allow-list is authoritative). - bool explicitVersions = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SOSHARNESS_ONLY_COREVERSIONS")); - if (!CoreVersions.TestOutOfSupportCore && !explicitVersions) - { - requestedVersions &= ~CoreVersions.OutOfSupport; - } - - foreach (string target in targets) - { - foreach (Host h in SingleFlags(host, "SOSHARNESS_ONLY_HOSTS")) - { - foreach (Flavor f in SingleFlags(flavor, "SOSHARNESS_ONLY_FLAVORS")) - { - foreach (Liveness l in SingleFlags(liveness, "SOSHARNESS_ONLY_LIVENESS")) - { - foreach (GcType g in SingleFlags(gcType, "SOSHARNESS_ONLY_GCTYPE")) - { - foreach (DumpKind d in SingleFlags(dumpKind, "SOSHARNESS_ONLY_DUMPKIND")) - { - foreach (CoreVersion cv in SingleFlags(requestedVersions, "SOSHARNESS_ONLY_COREVERSIONS")) - { - foreach (Dac da in SingleFlags(dac, "SOSHARNESS_ONLY_DAC")) - { - // A live target has no dump kind. Collapse it to the canonical - // value so we emit one live row, not one per dump-kind permutation. - DumpKind dk = l == Liveness.Live ? DumpKind.Heap : d; - - // Desktop .NET Framework has no .NET Core version — the axis is inert - // there. Collapse it to CoreVersion.None so every Framework row folds - // into one (via the seen-set dedup below) instead of fanning out an - // identical desktop-Framework config per Core version. - CoreVersion cvEffective = f == Flavor.Framework ? CoreVersion.None : cv; - - TestConfig cfg = new(target, h, f, l, g, dk, cvEffective, da); - if (IsValid(cfg) && seen.Add(cfg)) - { - yield return cfg; - } - } - } - } - } - } - } - } - } - } - - /// - /// Whether a configuration is valid on the current platform. Centralizes every constraint that the old - /// nested-loop BuildMatrix scattered across per-axis continues. - /// - private static bool IsValid(TestConfig c) - { - // Host platform constraints: cdb is Windows-only, lldb is non-Windows-only. - if (c.Host == Host.Cdb && !OperatingSystem.IsWindows()) - { - return false; - } - - if (c.Host == Host.Lldb && OperatingSystem.IsWindows()) - { - return false; - } - - // Desktop .NET Framework is Windows-only. - if (c.Flavor == Flavor.Framework && !OperatingSystem.IsWindows()) - { - return false; - } - - // dotnet-dump is post-mortem only; it has no live host. - if (c.IsLive && c.Host == Host.DotnetDump) - { - return false; - } - - // Live bpmd can't bind in a self-contained single-file image under the lldb host: CoreCLR is - // statically linked into the symbol-stripped app image, so lldb has no symbol on which to set the - // JIT/prestub notification breakpoint (.NET Core keeps CoreCLR as a distinct libcoreclr.so, so it - // works there). Prune the (lldb, single-file, live) row for targets navigated via a managed stop - // point; crash targets, which just run to the fault, keep their live single-file coverage. - if (c.IsLive && c.Host == Host.Lldb && c.Flavor == Flavor.SingleFile && TargetCatalog.NavigatesViaBpmd(c.Target)) - { - return false; - } - - // The target must support the requested flavor (e.g. DynamicMethod can't build for Framework). - if ((TargetCatalog.FlavorsFor(c.Target) & c.Flavor) == 0) - { - return false; - } - - // Server GC is forced via .NET-Core GC env vars (DATAS off + fixed heap count); desktop .NET - // Framework doesn't honor them, so Server is a Core/SingleFile-only axis. - if (c.GcType == GcType.Server && c.Flavor == Flavor.Framework) - { - return false; - } - - // Server GC for a LIVE target would require injecting the GC env vars into the dbgeng-launched - // debuggee process; that isn't wired yet (no consumer), so Server is dump-only for now. - if (c.GcType == GcType.Server && c.IsLive) - { - return false; - } - - // Runtime createdump only supports full dumps for single-file apps when it needs the DAC to - // enumerate reduced-dump regions. Don't generate Mini rows for single-file targets. - if (c.DumpKind == DumpKind.Mini && c.Flavor == Flavor.SingleFile) - { - return false; - } - - // The cDAC (managed contract DAC) is a .NET Core concept; desktop .NET Framework has no cDAC, so - // `runtimes --usecdac true` fails on clr.dll ("no matching cDAC is available for this runtime"). - // Prune the CDac axis for the Framework flavor (its CoreVersion label is meaningless anyway). - if (c.Dac == Dac.CDac && c.Flavor == Flavor.Framework) - { - return false; - } - - // The cDAC (managed contract DAC) only exists on .NET 11+; on earlier runtimes only the legacy - // native DAC is available, so prune the CDac axis there. The same dump is reused across DAC values - // (only `runtimes --usecdac` differs at debug time), so this just removes the invalid debug-time - // variant, never a capture. - if (c.Dac == Dac.CDac && (uint)c.CoreVersion < (uint)CoreVersion.Net11) - { - return false; - } - - // The universal cDAC can identify a single-file runtime and inspect its GC heap, but it cannot - // currently expose the managed execution metadata that SOS commands require (AppDomain/module - // details, MethodDescs, exception stack traces, or stack walks). Keep cDAC coverage on Core, - // where the full command surface is supported, and test SingleFile with its matching legacy DAC. - if (c.Dac == Dac.CDac && c.Flavor == Flavor.SingleFile) - { - return false; - } - - return true; - } - - private static IEnumerable SingleFlags(T value) where T : struct, Enum - { - foreach (T candidate in Enum.GetValues()) - { - long v = Convert.ToInt64(candidate); - if (v != 0 && (v & (v - 1)) == 0 && (Convert.ToInt64(value) & v) != 0) - { - yield return candidate; - } - } - } - - /// - /// Like , but additionally narrowed by an optional comma-separated - /// allow-list in (enum names, case-insensitive). Lets a run be staged onto a - /// subset of the matrix during bring-up, e.g. SOSHARNESS_ONLY_FLAVORS=Core, - /// SOSHARNESS_ONLY_HOSTS=Cdb,DotnetDump, SOSHARNESS_ONLY_GCTYPE=Server. - /// - private static IEnumerable SingleFlags(T value, string envVar) where T : struct, Enum - { - string? only = Environment.GetEnvironmentVariable(envVar); - HashSet? allowed = string.IsNullOrEmpty(only) - ? null - : new HashSet(only.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), StringComparer.OrdinalIgnoreCase); - - foreach (T candidate in SingleFlags(value)) - { - if (allowed is null || allowed.Contains(candidate.ToString())) - { - yield return candidate; - } - } - } - - void IXunitSerializable.Serialize(IXunitSerializationInfo info) - { - info.AddValue(nameof(Target), Target, typeof(string)); - info.AddValue(nameof(Host), Host, typeof(Host)); - info.AddValue(nameof(Flavor), Flavor, typeof(Flavor)); - info.AddValue(nameof(Liveness), Liveness, typeof(Liveness)); - info.AddValue(nameof(GcType), GcType, typeof(GcType)); - info.AddValue(nameof(DumpKind), DumpKind, typeof(DumpKind)); - info.AddValue(nameof(CoreVersion), CoreVersion, typeof(CoreVersion)); - info.AddValue(nameof(Dac), Dac, typeof(Dac)); - } - - void IXunitSerializable.Deserialize(IXunitSerializationInfo info) - { - _target = info.GetValue(nameof(Target))!; - _host = info.GetValue(nameof(Host)); - _flavor = info.GetValue(nameof(Flavor)); - _liveness = info.GetValue(nameof(Liveness)); - _gcType = info.GetValue(nameof(GcType)); - _dumpKind = info.GetValue(nameof(DumpKind)); - _coreVersion = info.GetValue(nameof(CoreVersion)); - _dac = info.GetValue(nameof(Dac)); - } - - /// - /// A legible, deterministic id used for the theory display name and de-duplication, e.g. - /// scenarios/Cdb/Core/net10/Dump/Workstation/Heap (the runtime version is always shown; the dump - /// kind is omitted for live rows; a /cdac suffix marks the cDAC variant. Legacy DAC is the - /// implicit default and isn't tokenized, so single-DAC ids stay terse. - /// - public override string ToString() - { - string version = CoreVersion == CoreVersion.None ? "/netfx" : "/net" + CoreVersions.Major(CoreVersion); - string dump = IsLive ? string.Empty : "/" + DumpKind; - string dac = Dac == Dac.CDac ? "/cdac" : string.Empty; - return $"{Target}/{Host}/{Flavor}{version}/{Liveness}/{GcType}{dump}{dac}"; - } - -} diff --git a/src/tests/SOS.TestHarness/ToolPaths.cs b/src/tests/SOS.TestHarness/ToolPaths.cs deleted file mode 100644 index 300a12d53a..0000000000 --- a/src/tests/SOS.TestHarness/ToolPaths.cs +++ /dev/null @@ -1,435 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace SOS.TestHarness; - -/// -/// Resolves the external tools the harness drives, pointing every one at the diagnostics repo's -/// own build outputs (so the harness always validates freshly-built SOS, not a stale machine-wide -/// install): -/// -/// dbgeng.dll comes from the restored cdb-sos package, so no WinDbg installation -/// is required. -/// Native sos.dll comes from the repo's native build output. -/// dotnet-dump is the repo-built tool, run as dotnet dotnet-dump.dll. -/// -/// -public static class ToolPaths -{ - /// - /// Directory containing dbgeng.dll, taken from the restored cdb-sos package's - /// runtimes/win-<arch>/native directory. Windows-only; resolved lazily so non-Windows - /// hosts never trigger it. - /// - public static string DbgEngDirectory => s_dbgEngDirectory.Value; - - /// Repo-built native SOS (sos.dll) from . - /// Windows-only (the dbgeng .load target); resolved lazily. - public static string SosPath => s_sosPath.Value; - - /// Repo-built dotnet-dump managed entry point, run as dotnet <dll>. - public static string DotNetDumpDll => s_dotNetDumpDll.Value; - - /// - /// The native lldb plugin (libsosplugin.so on Linux, libsosplugin.dylib on macOS) that - /// SOS loads into lldb via plugin load, taken from . - /// Non-Windows; resolved lazily. - /// - public static string LldbPluginPath => s_lldbPluginPath.Value; - - /// - /// The lldb executable the harness drives. Resolution mirrors eng/build.sh: the - /// LLDB_PATH env var first, then (on macOS) Xcode's lldb at - /// $(xcode-select -p)/usr/bin/lldb (it carries the debugging entitlements), then a plain - /// lldb on PATH. Non-Windows; resolved lazily. - /// - public static string LldbExe => s_lldbExe.Value; - - /// - /// The .NET runtime directory SOS hosts its managed extension on (the sethostruntime target). - /// Points at the repo's locally-acquired .dotnet shared runtime (highest net10 present), so the - /// host runtime is deterministic and hermetic rather than auto-detected from PATH. - /// - public static string HostRuntimeDirectory => s_hostRuntimeDirectory.Value; - - /// - /// Full path to the createdump executable the debugger host's hosted .NET runtime can use to - /// write a crash dump. NativeAOT components (notably the universal cDAC) may not have a neighboring - /// createdump, so the crash-dump environment points explicitly at the one from the host runtime. - /// - public static string CreateDumpPath => s_createDumpPath.Value; - - /// - /// Directory containing the DAC (mscordaccore.dll / libmscordaccore.so / - /// libmscordaccore.dylib) that matches the runtime a self-contained single-file debuggee of the - /// given bundles. Self-contained single-file apps carry the runtime - /// inside the exe, so a native debugger can't find the DAC next to a runtime on disk and (hermetically) - /// can't download it. The cdb host loads it explicitly via .cordll -lp; the lldb host adds it as a - /// local symbol-store directory via setsymbolserver -directory. The version is the runtime patch - /// the single-file publish resolved against (from the install manifest and test runtime installation, - /// or the matching runtime pack cache). Returns null if it can't be located. - /// - public static string? SingleFileDacDirectory(CoreVersion coreVersion) => - s_singleFileDacDirectory.GetOrAdd(coreVersion, ResolveSingleFileDacDirectory); - - /// - /// Optional local directory containing an override universal cDAC - /// (libmscordaccore_universal.so / platform equivalent) for cDAC test rows. Defaults to - /// artifacts/cdac-override/<Configuration> and can be overridden with - /// SOSHARNESS_CDAC_DIR. - /// - public static string? CDacOverrideDirectory => s_cdacOverrideDirectory.Value; - - /// - /// Native SOS resolves the universal cDAC from the SOS module directory. Keep the lldb plugin output - /// directory aligned with the configured override before cDAC rows load SOS. - /// - public static void EnsureLldbPluginCDacOverride() - { - string? overrideDirectory = CDacOverrideDirectory; - if (overrideDirectory is null) - { - return; - } - - string source = Path.Combine(overrideDirectory, CDacFileName); - string destination = Path.Combine(Path.GetDirectoryName(LldbPluginPath)!, CDacFileName); - lock (s_cdacCopyLock) - { - if (!File.Exists(destination) || !FilesEqual(source, destination)) - { - File.Copy(source, destination, overwrite: true); - } - } - } - - // Lazy so each host only resolves the tools it actually needs: the non-Windows lldb/dotnet-dump hosts - // never touch the Windows-only dbgeng/sos.dll resolvers (which would throw for lack of those payloads), - // and the Windows cdb host never touches the lldb resolvers. - private static readonly Lazy s_dbgEngDirectory = new(ResolveDbgEngDirectory); - private static readonly Lazy s_sosPath = new(ResolveSosPath); - private static readonly Lazy s_dotNetDumpDll = new(ResolveDotNetDumpDll); - private static readonly Lazy s_lldbPluginPath = new(ResolveLldbPluginPath); - private static readonly Lazy s_lldbExe = new(ResolveLldbExe); - private static readonly Lazy s_hostRuntimeDirectory = new(ResolveHostRuntimeDirectory); - private static readonly Lazy s_createDumpPath = new(ResolveCreateDumpPath); - private static readonly Lazy s_cdacOverrideDirectory = new(ResolveCDacOverrideDirectory); - private static readonly System.Collections.Concurrent.ConcurrentDictionary s_singleFileDacDirectory = new(); - private static readonly object s_cdacCopyLock = new(); - - private static string ResolveDbgEngDirectory() - { - string relativeNative = Path.Combine("runtimes", $"win-{RepoLayout.TargetArch}", "native"); - - foreach (string root in NuGetPackageRoots()) - { - string pkg = Path.Combine(root, "cdb-sos"); - if (!Directory.Exists(pkg)) - { - continue; - } - - // Prefer the highest version present. - foreach (string versionDir in Directory.GetDirectories(pkg).OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase)) - { - string native = Path.Combine(versionDir, relativeNative); - if (File.Exists(Path.Combine(native, "dbgeng.dll"))) - { - return native; - } - } - } - - throw new FileNotFoundException( - "Could not locate dbgeng.dll from the cdb-sos package. Restore the harness test project so " + - "its PackageDownload populates the NuGet cache."); - } - - private static string ResolveSosPath() - { - string path = Path.Combine(RepoLayout.ArtifactsBinNative, "sos.dll"); - if (!File.Exists(path)) - { - throw new FileNotFoundException( - $"Repo-built native SOS not found at '{path}'. Build the repo (Build.cmd) so the native " + - "SOS is produced for this configuration/architecture.", path); - } - - return path; - } - - private static string ResolveLldbPluginPath() - { - string name = OperatingSystem.IsMacOS() ? "libsosplugin.dylib" : "libsosplugin.so"; - string path = Path.Combine(RepoLayout.ArtifactsBinNative, name); - if (!File.Exists(path)) - { - throw new FileNotFoundException( - $"Repo-built lldb SOS plugin not found at '{path}'. Build the repo (./build.sh) so the native " + - "lldb plugin is produced for this configuration/architecture.", path); - } - - return path; - } - - private static string ResolveLldbExe() - { - // 1) Explicit override (what eng/build.sh exports), if it points at a real file. - string? env = Environment.GetEnvironmentVariable("LLDB_PATH"); - if (!string.IsNullOrEmpty(env) && File.Exists(env)) - { - return env; - } - - // 2) macOS: Xcode's lldb is signed with the debugging entitlements needed to drive a process and - // to load core dumps, so prefer it over anything else. - if (OperatingSystem.IsMacOS()) - { - string? developerDir = TryRun("xcode-select", "-p"); - if (!string.IsNullOrWhiteSpace(developerDir)) - { - string candidate = Path.Combine(developerDir.Trim(), "usr", "bin", "lldb"); - if (File.Exists(candidate)) - { - return candidate; - } - } - } - - // 3) A plain `lldb` on PATH. - string? onPath = FindOnPath("lldb"); - if (onPath is not null) - { - return onPath; - } - - throw new FileNotFoundException( - "Could not locate an 'lldb' executable. Set LLDB_PATH, install lldb on PATH, or (on macOS) " + - "install Xcode."); - } - - private static string ResolveHostRuntimeDirectory() - { - // SOS hosts its managed extension on a .NET runtime; point it at the repo's locally-acquired - // .dotnet shared runtime so it's deterministic. Any recent runtime works as a host (it need not - // match the target's runtime), so pick the highest net10 present. - string sharedRoot = Path.Combine(RepoLayout.Root, ".dotnet", "shared", "Microsoft.NETCore.App"); - if (Directory.Exists(sharedRoot)) - { - string? best = Directory.GetDirectories(sharedRoot) - .Select(Path.GetFileName) - .Where(v => v is not null && v.StartsWith("10.0.", StringComparison.Ordinal)) - .OrderByDescending(v => v, StringComparer.OrdinalIgnoreCase) - .FirstOrDefault(); - if (best is not null) - { - return Path.Combine(sharedRoot, best); - } - } - - throw new DirectoryNotFoundException( - $"Could not locate a net10 host runtime under '{sharedRoot}'. Run ./build.sh so the repo's " + - ".dotnet runtime is acquired."); - } - - private static string ResolveCreateDumpPath() - { - string exe = OperatingSystem.IsWindows() ? "createdump.exe" : "createdump"; - string candidate = Path.Combine(HostRuntimeDirectory, exe); - if (File.Exists(candidate)) - { - return candidate; - } - - throw new FileNotFoundException( - $"createdump not found at '{candidate}'. Run ./build.sh so the repo's .dotnet runtime is acquired.", candidate); - } - - private static string? FindOnPath(string fileName) - { - string? path = Environment.GetEnvironmentVariable("PATH"); - if (string.IsNullOrEmpty(path)) - { - return null; - } - - foreach (string dir in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) - { - string candidate = Path.Combine(dir, fileName); - if (File.Exists(candidate)) - { - return candidate; - } - } - - return null; - } - - private static string? TryRun(string fileName, string arguments) - { - try - { - using System.Diagnostics.Process? p = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = fileName, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }); - if (p is null) - { - return null; - } - - string output = p.StandardOutput.ReadToEnd(); - p.WaitForExit(5000); - return p.ExitCode == 0 ? output : null; - } - catch - { - return null; - } - } - - private static string ResolveDotNetDumpDll() - { - // dotnet-dump targets net8.0 and is published under the repo artifacts; prefer the published - // copy (self-contained closure) and fall back to the plain build output. - string baseDir = Path.Combine(RepoLayout.ArtifactsBin, "dotnet-dump", RepoLayout.ArtifactsConfiguration, "net8.0"); - string published = Path.Combine(baseDir, "publish", "dotnet-dump.dll"); - if (File.Exists(published)) - { - return published; - } - - string built = Path.Combine(baseDir, "dotnet-dump.dll"); - if (File.Exists(built)) - { - return built; - } - - throw new FileNotFoundException( - $"Repo-built dotnet-dump not found under '{baseDir}'. Build the repo (Build.cmd) so dotnet-dump " + - "is produced.", published); - } - - private static string? ResolveSingleFileDacDirectory(CoreVersion coreVersion) - { - string rid = RepoLayout.Rid; // win-x64 / linux-x64 / osx-arm64 / ... - string packId = $"microsoft.netcore.app.runtime.{rid}"; - string relativeNative = Path.Combine("runtimes", rid, "native"); - string dacFileName = DacFileName; // mscordaccore.dll / libmscordaccore.so / libmscordaccore.dylib - int major = CoreVersions.Major(coreVersion); - - // Preferred: the exact runtime version the single-file publish resolved against (what the install - // manifest recorded for this framework), read straight from the test runtime installation. The - // corresponding runtime pack is not necessarily restored into the user's NuGet cache. - string? pinned = CoreVersions.RuntimeVersion(coreVersion); - if (!string.IsNullOrEmpty(pinned)) - { - string sharedRuntime = Path.Combine( - RepoLayout.DotnetTestRoot, - "shared", - "Microsoft.NETCore.App", - pinned!); - if (File.Exists(Path.Combine(sharedRuntime, dacFileName))) - { - return sharedRuntime; - } - - foreach (string root in NuGetPackageRoots()) - { - string native = Path.Combine(root, packId, pinned!, relativeNative); - if (File.Exists(Path.Combine(native, dacFileName))) - { - return native; - } - } - } - - // Fallback: the highest patch of this major present in the runtime-pack cache. - string majorPrefix = $"{major}.0."; - foreach (string root in NuGetPackageRoots()) - { - string pkg = Path.Combine(root, packId); - if (!Directory.Exists(pkg)) - { - continue; - } - - string? best = Directory.GetDirectories(pkg) - .Select(Path.GetFileName) - .Where(v => v is not null && v.StartsWith(majorPrefix, StringComparison.Ordinal)) - .OrderByDescending(v => v, StringComparer.OrdinalIgnoreCase) - .FirstOrDefault(); - if (best is not null) - { - string native = Path.Combine(pkg, best, relativeNative); - if (File.Exists(Path.Combine(native, dacFileName))) - { - return native; - } - } - } - - return null; - } - - private static string? ResolveCDacOverrideDirectory() - { - string? env = Environment.GetEnvironmentVariable("SOSHARNESS_CDAC_DIR"); - string directory = string.IsNullOrEmpty(env) - ? Path.Combine(RepoLayout.Root, "artifacts", "cdac-override", RepoLayout.ArtifactsConfiguration) - : env; - - return File.Exists(Path.Combine(directory, CDacFileName)) ? directory : null; - } - - private static bool FilesEqual(string leftPath, string rightPath) - { - FileInfo leftInfo = new(leftPath); - FileInfo rightInfo = new(rightPath); - if (leftInfo.Length != rightInfo.Length) - { - return false; - } - - using FileStream left = File.OpenRead(leftPath); - using FileStream right = File.OpenRead(rightPath); - int leftByte; - while ((leftByte = left.ReadByte()) != -1) - { - if (leftByte != right.ReadByte()) - { - return false; - } - } - - return right.ReadByte() == -1; - } - - /// The platform-specific DAC module file name: mscordaccore.dll on Windows, - /// libmscordaccore.dylib on macOS, libmscordaccore.so elsewhere. - private static string DacFileName => - OperatingSystem.IsWindows() ? "mscordaccore.dll" : - OperatingSystem.IsMacOS() ? "libmscordaccore.dylib" : "libmscordaccore.so"; - - private static string CDacFileName => - OperatingSystem.IsWindows() ? "mscordaccore_universal.dll" : - OperatingSystem.IsMacOS() ? "libmscordaccore_universal.dylib" : "libmscordaccore_universal.so"; - - private static IEnumerable NuGetPackageRoots() - { - string? env = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); - if (!string.IsNullOrEmpty(env)) - { - yield return env; - } - - yield return Path.Combine(UserProfile, ".nuget", "packages"); - } - - private static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); -} diff --git a/src/tests/SOS.TestHarness/lldbhelper.py b/src/tests/SOS.TestHarness/lldbhelper.py deleted file mode 100644 index 76934fd388..0000000000 --- a/src/tests/SOS.TestHarness/lldbhelper.py +++ /dev/null @@ -1,41 +0,0 @@ -# Licensed to the .NET Foundation under one or more agreements. -# The .NET Foundation licenses this file to you under the MIT license. -# -# lldb command framing for the SOS test harness (the LldbCliHost backend). -# -# A bare lldb REPL gives no reliable per-command "done" delimiter and no success/failure status, so the -# harness never issues commands directly: it imports this helper and runs every command as -# `runcommand `. The helper executes the real command through the command interpreter, streams its -# output, and then prints a sentinel line the host keys on -- when the command -# succeeded, when it failed (so the host gets a real success bit, not a screen-scrape). - -import lldb -import sys - -END_OUTPUT = "" -END_ERROR = "" - - -def __lldb_init_module(debugger, internal_dict): - debugger.HandleCommand("command script add -f lldbhelper.runcommand runcommand") - # Emit the marker once at import time so the host can drain the startup banner up to a known point - # and know the helper is ready before it sends the first command. - sys.stdout.write(END_OUTPUT + "\n") - sys.stdout.flush() - - -def runcommand(debugger, command, result, internal_dict): - interpreter = debugger.GetCommandInterpreter() - - ret = lldb.SBCommandReturnObject() - interpreter.HandleCommand(command, ret) - - # GetOutput()/GetError() already include trailing newlines; use write (not print) to avoid doubling - # them, so the sentinel always lands on its own line. - if ret.GetOutputSize() > 0: - sys.stdout.write(ret.GetOutput()) - if ret.GetErrorSize() > 0: - sys.stdout.write(ret.GetError()) - - sys.stdout.write((END_OUTPUT if ret.Succeeded() else END_ERROR) + "\n") - sys.stdout.flush() diff --git a/src/tests/SOS.Tests/COVERAGE.md b/src/tests/SOS.Tests/COVERAGE.md deleted file mode 100644 index ca77b10204..0000000000 --- a/src/tests/SOS.Tests/COVERAGE.md +++ /dev/null @@ -1,126 +0,0 @@ -# SOS test coverage audit - -This audit compares PR #5979 at `40ccc0262a199a1576ed3bec25d82152a32c1bc4` -with the legacy `SOS.UnitTests` entry points and 29 scripts present at the same -commit. The new project contains 89 command-focused test methods after the -focused `dumpgen` addition described below. - -## Result - -The repository's baseline coverage equals or exceeds the previous baseline: -PR #5979 does not delete or disable any legacy test, and adds the new matrix and -89 tests. The stronger claim that `SOS.Tests` alone replaces every legacy -scenario is not supported. Most command behavior is covered more strictly, but -specialized debuggees and debugger transitions remain in `SOS.UnitTests`. -Those tests must remain until their rows marked **gap** below are migrated. - -Status meanings: - -- **Covered**: the same observable command behavior has a direct new test. -- **Improved**: the new test adds a stronger oracle, more options, or broader - host/flavor/runtime coverage. -- **Retained**: the specialized scenario remains intentionally owned by the - legacy suite; the command may also have generic new coverage. -- **Gap**: `SOS.Tests` has no equivalent for a material legacy behavior. The - legacy test is still active, so this is a migration gap rather than a - regression in the PR baseline. - -No legacy script is retired by #5979. Rows that are fully covered or improved -are candidates for later retirement only after CI proves the intended matrix -replacement. - -## Legacy-to-new map - -| Legacy script | Status | New evidence and remaining legacy value | -| --- | --- | --- | -| `AsyncMain.script` | Gap | General stack shape is covered by `ClrStackLinesTests` and `ClrStackAllThreadsTests`; the async-`Main` frame identity has no new oracle. | -| `ClrStackWithNumberOfFrames.script` | Improved | `ClrStackFrameCountTests.ClrStack_FrameCount` compares each `-c N` result with the exact prefix of an unlimited walk and checks an over-limit request across four crash targets. | -| `ConcurrentDictionaries.script` | Improved | `SpecializedInspectionTests.Dcd_DumpsConcurrentDictionary`, `DumpArrayTests`, and `ObjectFieldsTests` provide typed data assertions. Legacy still covers dcd argument errors and its wider generic key/value set. | -| `DivZero.script` | Improved | `PrintExceptionTests`, `ClrThreadsTests`, `ClrStackLinesTests`, `ClrStackICorDebugTests`, and frame-count tests split the monolithic script into data and structure oracles. | -| `DualRuntimes.script` | Retained | Generic stacks, threads, heaps, and runtime listing are covered; loading and switching between two runtimes in one process remains a specialized legacy scenario. | -| `DumpGCData.script` | Covered | `DiagnosticCommandTests.DumpGcData_ReportsGcStatistics` directly exercises `dumpgcdata`. | -| `DumpGen.script` | Improved | `GcInspectionTests.DumpGen_ListsGenerationObjects` asserts a known gen0 object. `DumpGen_ArgumentsAndFilters` adds missing/invalid generation and valid `-type`/`-mt` coverage. Exact legacy gen1/gen2/LOH/POH populations remain retained. | -| `DynamicMethod.script` | Retained | `DumpIlTests` validates IL addresses and instructions and ICorDebug is covered elsewhere; the emitted dynamic-method target remains a legacy scenario. | -| `FindRootsOlderGeneration.script` | Gap | `GcRoot_FindsRootsForLive_NoneForDead` improves ordinary root correctness, but the live `findroots -gen any` notification/continue sequence and older-generation result are not represented. | -| `GCPOH.script` | Improved | `DumpHeapGenerationsTests`, `DumpArrayTests`, `GcHandles`, `VerifyHeap`, `EeHeapTests`, and stack-root tests use deterministic objects and structured assertions; the original POH script remains active. | -| `GCTests.script` | Improved | Object fields, `gcwhere`, stack objects, heap statistics, roots, handles, finalization, and verification are split into focused tests with exact object oracles. | -| `InterpreterStackInterleavedTest.script` | Retained | No generic target can replace the interleaved interpreter/JIT frame sequence; legacy ownership is intentional. | -| `InterpreterStackTest.script` | Retained | Interpreter-frame ordering remains specialized legacy coverage. | -| `LineNums.script` | Improved | `ClrStackLinesTests` checks source file/line behavior and `PrintExceptionTests` checks exception structure/data across the matrix. | -| `MiniDumpLocalVarLookup.script` | Gap | `ClrStackArgsLocalsTests` and `ClrStackICorDebugTests` improve variable data checks, but they use full/heap dumps rather than proving local recovery from a Mini dump. | -| `NestedExceptionTest.script` | Improved | `PrintExceptionTests` verifies exact outer/inner types, messages, HResult, frame data, and inner-address round-trip; stack variants are independently covered. | -| `OtherCommands.script` | Improved | Its broad command set is split across object, module, domain, heap, runtime, memory, code-info, and diagnostic test classes with structured round-trips. | -| `Overflow.script` | Retained | Stack-overflow exception behavior remains a specialized crash/live legacy target; ordinary exception output is covered by `PrintExceptionTests`. | -| `Reflection.script` | Retained | Target-invocation exception and reflection boundary behavior remains legacy; generic nested exception and stack behavior is improved in the new project. | -| `SimpleThrow.script` | Covered | Exception fields, HResult, source lines, threads, and managed stack shape are directly covered by the new exception and stack classes on deterministic crash targets. | -| `StackAndOtherTests.script` | Improved | `RuntimeInfoTests` and the `ClrStack*Tests` classes separately cover runtime selection, plain/line/full/all/register/args/locals stack modes with stronger comparisons. | -| `StackTests.script` | Improved | `ClrStackTests`, `ClrStackFullTests`, `ClrStackAllThreadsTests`, `ClrStackArgsLocalsTests`, `StackInspectionTests`, and `RuntimeInfoTests` replace shape-only checks with tables and address round-trips. | -| `StackTraceFaultingExceptionFrame.script` | Gap | Exception and ordinary stack data are covered; no new assertion requires the synthetic `[FaultingExceptionFrame: ...]` row. | -| `StackTraceSoftwareExceptionFrame.script` | Gap | Exception and ordinary stack data are covered; no new assertion requires the synthetic `[SoftwareExceptionFrame: ...]` row. | -| `TaskNestedException.script` | Gap | New tests cover one inner-exception round-trip, not the AggregateException/task chain and source lines. | -| `TestExtensions.script` | Retained | This validates extension-command interception and dispatch rather than SOS product output; it remains a legacy harness integration test. | -| `ThreadApartment.script` | Gap | `ClrThreadsTests` validates thread rows and counts, but does not assert Windows STA/MTA apartment flags. | -| `VarargPInvokeInteropMD.script` | Retained | Vararg P/Invoke, IL stubs, native breakpointing, `ip2md`, and `clru -il` form one specialized interop scenario. Generic `ip2md` and `clru` have new structured tests. | -| `WebApp.script` | Retained | Timers, ASP.NET/WebApp stacks, args/registers, and GC stress-log behavior remain in the specialized Windows legacy target; generic command equivalents are covered. | - -The focused gap fixed in this review is `dumpgen` argument and filtering -behavior. The new test uses the source-generated scenario marker, resolves its -real MethodTable through `dumpobj`, and proves both `-type` and `-mt` select it. -The remaining gaps require purpose-built targets, Mini-dump policy, or -platform-specific debugger sequencing and are not appropriate baseline-harness -refactors. - -## Product mutation experiment - -### Selection - -A fresh Copilot session performed selection independently from the harness -implementation. It formed eligible candidate IDs from real SOS product output -behaviors with assertions in both suites, excluding harness-only defects and -sites unavailable on macOS arm64. With seed -`0x40ccc0262a199a15`, it sorted candidates by: - -```text -SHA256(seed + NUL + candidate-id) -``` - -and selected the first five: - -1. `dumpmt-type-name` -2. `clrstack-frame-limit` -3. `ip2md-methoddesc` -4. `dumparray-element-count` -5. `printexception-hresult` - -Each mutation was made in `src/SOS/Strike/strike.cpp`, native SOS was rebuilt, -and the mutated `libsos.dylib` was staged beside the repository-built -dotnet-dump. Mutations were tested one at a time and restored before the next. -None is part of the final diff. - -### Host constraint and comparison validity - -On this macOS arm64 machine, the unchanged legacy test driver enters mandatory -live/native LLDB phases that crash before reaching the script assertions. For -this experiment only, an uncommitted adapter selected one Core runtime row and -skipped those LLDB phases while leaving the legacy scripts, dotnet-dump -commands, dumps, and regex assertions unchanged. The corresponding new tests -were narrowed to Core, dotnet-dump, dump, workstation GC, Heap, and legacy DAC. -This compares the same repository-built SOS backend and equivalent product -behavior, without treating an environmental LLDB crash as mutation detection. - -The unmodified legacy probes passed before mutation. Final baseline runs of the -new probes pass after all mutations are restored. - -### Results - -| Mutation | Temporary product defect | Legacy result | New result | Assessment | -| --- | --- | --- | --- | --- | -| `dumpmt-type-name` | Force `dumpmt`'s type name to print ``. | `OtherCommands` failed because `Name: SymbolTestApp.Program` disappeared. | `DumpObj_Mt_Class_Md_Chain` failed because `ThinLockMarker` became ``. | Equal detection; new test uses a deterministic named marker and command round-trip. | -| `clrstack-frame-limit` | Make `clrstack -c N` emit one extra frame. | `ClrStackWithNumberOfFrames` failed its output-line cardinality regex. | `ClrStack_FrameCount` failed all four selected targets; for `-c 1`, expected 1 row and observed 2. | Equal detection, with clearer new diagnostics and multi-target evidence. | -| `ip2md-methoddesc` | Print `pMD + 1` in the `ip2md` MethodDesc field. | `OtherCommands` failed its downstream code-size/method-info assertion after consuming the bad address. | `Ip2md_ResolvesJittedMethodWithSource` failed directly: expected `4535571072`, actual `4535571073`. | Equal detection; the new failure localizes the corrupted field. | -| `dumparray-element-count` | Report `dwNumComponents + 1` from `dumparray`. | `ConcurrentDictionaries` failed `Number of elements 4`. | `DumpArray_StructureStartLengthDetails` failed: expected 8, actual 9. | Equal detection; the new test also checks rank, type, listed rows, slicing, and details. | -| `printexception-hresult` | Report `HResult + 1`. | `NestedExceptionTest` failed `HResult: 80131509`. | `PrintException_Data` failed: expected `0x80131509`, actual `0x8013150a`. | Equal detection; the new test parses the field as a typed `UInt32`. | - -All five new probes were at least as effective as the corresponding legacy -probe: every defect detected by a legacy assertion was also detected by the new -test, usually at a more specific field or row. diff --git a/src/tests/SOS.Tests/ClrStackAllThreadsTests.cs b/src/tests/SOS.Tests/ClrStackAllThreadsTests.cs deleted file mode 100644 index 895387a896..0000000000 --- a/src/tests/SOS.Tests/ClrStackAllThreadsTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for !clrstack -all (every managed thread's stack). The legacy scripts ran -all -/// on WebApp/DualRuntimes/FindRoots but only shape-checked it. The ManagedThreads target parks a fixed -/// number of worker threads at a known method (WorkerPark) via a barrier, so the enumeration is -/// deterministic: there must be exactly the expected number of workers parked in WorkerPark plus the -/// main thread (… AtAllThreads → Main). Self-consistency: the current thread's section in -all -/// matches plain clrstack (same frame IPs). -/// -public sealed class ClrStackAllThreadsTests -{ - private const int ExpectedWorkers = 3; - - public static TheoryData Matrix { get; } = TestMatrices.StackWalk([TargetCatalog.Scenarios]); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_AllThreads(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(TargetCatalog.StopAllThreads); - - IReadOnlyList threads = target.ClrstackAllThreads(); - - // Every worker thread is parked in WorkerPark; there are exactly ExpectedWorkers of them. - int workers = threads.Count(t => t.Frames.Any(f => f.Function.Contains("SosHarnessScenarios.WorkerPark", StringComparison.Ordinal))); - Assert.Equal(ExpectedWorkers, workers); - - // Exactly one thread is the main thread, in AtAllThreads below Main. - TargetExtensions.ThreadStack main = Assert.Single( - threads, t => t.Frames.Any(f => f.Function.Contains("SosHarnessScenarios.AtAllThreads", StringComparison.Ordinal))); - Assert.Contains(main.Frames, f => f.Function.Contains("SosHarnessScenarios.Main", StringComparison.Ordinal)); - - // -all enumerates at least the main thread plus the workers. - Assert.True(threads.Count >= ExpectedWorkers + 1, $"expected >= {ExpectedWorkers + 1} threads, got {threads.Count}."); - - // Each OS thread id is distinct. - Assert.Equal(threads.Count, threads.Select(t => t.OsThreadId).Distinct().Count()); - - // Self-consistency: plain clrstack is the current (main) thread, so its frame IPs match the - // main thread's section in -all. - List plainIps = target.Clrstack() - .Select(r => r["IP"].Value.ToUpperInvariant()) - .ToList(); - List mainIps = main.Frames - .Where(f => f.IP.Length > 0) - .Select(f => f.IP.ToUpperInvariant()) - .ToList(); - Assert.Equal(plainIps, mainIps); - } -} diff --git a/src/tests/SOS.Tests/ClrStackArgsLocalsTests.cs b/src/tests/SOS.Tests/ClrStackArgsLocalsTests.cs deleted file mode 100644 index 3f0ff2e2a8..0000000000 --- a/src/tests/SOS.Tests/ClrStackArgsLocalsTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for !clrstack -p (parameters), -l (locals) and -a (both). The legacy -/// scripts only ever ran -a (and transitively covered -p/-l); here all three are -/// exercised from one stop point. Correctness comes from cross-variant self-consistency — -a's -/// per-frame PARAMETERS equal -p's and its LOCALS equal -l's, while -p shows no -/// locals and -l no parameters — plus sensible value hardcoding and an SOS-native value oracle: -/// a uniquely-typed object's slot value equals its !dumpheap address. -/// -public sealed class ClrStackArgsLocalsTests -{ - public static TheoryData Matrix { get; } - = TestMatrices.StackWalkFullDumpOnCoreVersions( - [ - TargetCatalog.DivZero, - TargetCatalog.Scenarios, - ], - CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_ArgsLocals(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - if (config.Target == TargetCatalog.Scenarios) - { - target.GoToStopPoint(TargetCatalog.StopArgsLocals); - } - else - { - target.GoToFirstStop(); - } - - SosTable p = target.ClrstackArgsLocals(ArgsLocals.Parameters); - SosTable l = target.ClrstackArgsLocals(ArgsLocals.Locals); - SosTable a = target.ClrstackArgsLocals(ArgsLocals.Both); - - // Same stackwalk -> same frames in all three. - Assert.Equal(a.Length, p.Length); - Assert.Equal(a.Length, l.Length); - - // -a == -p (parameters) merged with -l (locals), frame by frame; -p has no locals and -l no - // parameters. - for (int i = 0; i < a.Length; i++) - { - SosRow fa = a.Row(i); - SosRow fp = p.Row(i); - SosRow fl = l.Row(i); - Assert.Equal(fa["IP"].Value, fp["IP"].Value); - Assert.Equal(fa["IP"].Value, fl["IP"].Value); - - Assert.Equal(Values(fa, "PARAMETERS"), Values(fp, "PARAMETERS")); - Assert.Equal(Values(fa, "LOCALS"), Values(fl, "LOCALS")); - Assert.Empty(Records(fp, "LOCALS")); - Assert.Empty(Records(fl, "PARAMETERS")); - } - - // At least one frame actually had parameters and one had locals (the merge above is meaningful). - a.AssertContainsRow(f => Records(f, "PARAMETERS").Any(), "a frame has PARAMETERS"); - a.AssertContainsRow(f => Records(f, "LOCALS").Any(), "a frame has LOCALS"); - - switch (config.Target) - { - case TargetCatalog.DivZero: - // F3's locals are a=1, b=2 (ref-passed, so on the stack); F2's are p=3, q=4. - AssertLocalValues(a, ".F3(", [1, 2]); - AssertLocalValues(a, ".F2(", [3, 4]); - break; - - case TargetCatalog.Scenarios: - // The named int param `number` is 0x2a; a local holds the primitive 0x63. - SosRow method = Frame(a, ".ArgsLocalsMethod("); - Assert.Equal(0x2aul, Named(method, "number").AsUInt64(Sos.Hex)); - Records(method, "LOCALS").AssertContains(r => r["Value"].AsUInt64(Sos.Hex) == 0x63, "a LOCAL with value 0x63"); - - // SOS-native value oracle: the uniquely-typed arg and local slots hold the very objects - // !dumpheap finds for those types. - ulong argObj = target.FindUniqueObject("ArgUniqueMarker"); - ulong localObj = target.FindUniqueObject("LocalUniqueMarker"); - Assert.Equal(argObj, Named(method, "arg").AsUInt64(Sos.Hex)); - Records(method, "LOCALS").AssertContains(r => r["Value"].AsUInt64(Sos.Hex) == localObj, "a LOCAL referencing the LocalUniqueMarker object"); - break; - - default: - // A matrix target with no value assertions would otherwise silently pass on the - // structural checks alone; force adding a case when a target is added. - throw new ArgumentOutOfRangeException(nameof(config.Target), config.Target, "No clrstack args/locals value assertions defined for this target."); - } - } - - private static IEnumerable Records(SosRow frame, string section) => - frame.Data.Where(d => d["Section"].Value == section); - - private static IReadOnlyList Values(SosRow frame, string section) => - Records(frame, section).Select(r => r["Value"].Value).ToList(); - - private static SosRow Frame(SosTable table, string functionSubstring) => - table.SingleRow( - r => !r["InternalFrame"].AsBoolean() && r["Function"].Value.Contains(functionSubstring, StringComparison.Ordinal), - $"a managed frame whose Function contains '{functionSubstring}'"); - - // The parameter named in this frame. - private static SosCell Named(SosRow frame, string name) => - Records(frame, "PARAMETERS").AssertSingle(r => r["Name"].Value == name, $"a PARAMETER named '{name}'")["Value"]; - - private static void AssertLocalValues(SosTable table, string functionSubstring, int[] expected) - { - SosRow frame = Frame(table, functionSubstring); - List locals = Records(frame, "LOCALS").Where(r => r["HasData"].AsBoolean()).Select(r => r["Value"].AsUInt64(Sos.Hex)).ToList(); - foreach (int e in expected) - Assert.Contains((ulong)e, locals); - } -} diff --git a/src/tests/SOS.Tests/ClrStackFrameCountTests.cs b/src/tests/SOS.Tests/ClrStackFrameCountTests.cs deleted file mode 100644 index 21ec9c5eaa..0000000000 --- a/src/tests/SOS.Tests/ClrStackFrameCountTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for !clrstack -c <n> (limit the number of printed frames), as the legacy -/// ClrStackWithNumberOfFrames.script did with DivZero. Self-consistency oracle: -c N is exactly -/// the first N rows of the full clrstack (SOS counts every printed row toward the limit, -/// internal frames included), and N larger than the stack prints the whole stack without truncating. -/// Exercised over deep-stacked debuggees. -/// -public sealed class ClrStackFrameCountTests -{ - public static TheoryData Matrix { get; } - = TestMatrices.StackWalk( - [ - TargetCatalog.DivZero, - TargetCatalog.NestedException, - TargetCatalog.LineNums, - TargetCatalog.DynamicMethod, - ]); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_FrameCount(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToFirstStop(); - - SosTable full = target.Clrstack(); - Assert.True(full.Length >= 2, "expected a deep enough stack to exercise -c"); - - // -c N is the first N rows of the full stack, for N within the stack... - for (int n = 1; n <= full.Length; n++) - { - SosTable limited = target.ClrstackFrames(n); - AssertSameFrames(full, limited, n); - } - - // ...and N past the end prints the whole stack, no truncation, no padding. - SosTable over = target.ClrstackFrames(full.Length + 5); - AssertSameFrames(full, over, full.Length); - } - - private static void AssertSameFrames(SosTable full, SosTable limited, int expectedCount) - { - Assert.Equal(expectedCount, limited.Length); - for (int i = 0; i < expectedCount; i++) - { - Assert.Equal(full.Row(i)["Child SP"].Value, limited.Row(i)["Child SP"].Value); - Assert.Equal(full.Row(i)["IP"].Value, limited.Row(i)["IP"].Value); - Assert.Equal(full.Row(i)["Call Site"].Value, limited.Row(i)["Call Site"].Value); - } - } -} diff --git a/src/tests/SOS.Tests/ClrStackFullTests.cs b/src/tests/SOS.Tests/ClrStackFullTests.cs deleted file mode 100644 index 5da1210f82..0000000000 --- a/src/tests/SOS.Tests/ClrStackFullTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for !clrstack -f (full / native-interleaved stack). The legacy scripts ran -f -/// on WebApp/StackTests but only shape-checked it. Here the oracle is cross-variant: every managed -/// frame from plain clrstack is preserved in -f (matched by IP) and rendered in the full -/// assembly-qualified Assembly.dll!Method + offset format. The native dimension is host-specific -/// (by design): under cdb, -f must be strictly larger than plain and contain real native runtime -/// frames (coreclr!/clr!/ntdll!/…); under the managed-only dotnet-dump host it contains no native frames. -/// -public sealed class ClrStackFullTests -{ - public static TheoryData Matrix { get; } - = TestMatrices.StackWalk( - [ - TargetCatalog.SimpleThrow, - TargetCatalog.DivZero, - TargetCatalog.NestedException, - ]); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_Full(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToFirstStop(); - - SosTable plain = target.Clrstack(); - IReadOnlyList full = target.ClrstackFull(); - - // The IPs of the managed (non-internal) frames in plain clrstack. - HashSet plainManagedIps = plain - .Where(r => !r["InternalFrame"].AsBoolean()) - .Select(r => r["IP"].Value.ToUpperInvariant()) - .ToHashSet(); - Assert.NotEmpty(plainManagedIps); - - HashSet fullIps = full - .Where(f => f.IP.Length > 0) - .Select(f => f.IP.ToUpperInvariant()) - .ToHashSet(); - - // Every managed frame from plain clrstack is preserved in -f. - foreach (string ip in plainManagedIps) - Assert.Contains(ip, fullIps); - - // -f renders managed frames assembly-qualified (Assembly.dll!Method + offset). - Assert.Contains(full, f => f.IsManaged); - - bool nativeHost = config.Host == Host.Cdb || config.Host == Host.Lldb; - if (nativeHost) - { - // Native interleaving: strictly more frames than plain (native frames are woven in). - Assert.True(full.Count > plain.Length, - $"-f ({full.Count}) should have more frames than plain clrstack ({plain.Length}) under a native host."); - - // Real native runtime frames (coreclr!/clr!/ntdll!/…) are only identifiable when the runtime - // is its own module. In a self-contained single-file publish the runtime is statically linked - // into the app executable, so those frames render under the app module name (e.g. - // SimpleThrow!…) and can't be matched by module name. - if (config.Flavor != Flavor.SingleFile) - { - Assert.Contains(full, f => f.IsNativeRuntime); - } - } - else - { - // dotnet-dump is managed-only: no native runtime frames. - Assert.DoesNotContain(full, f => f.IsNativeRuntime); - } - } -} diff --git a/src/tests/SOS.Tests/ClrStackICorDebugTests.cs b/src/tests/SOS.Tests/ClrStackICorDebugTests.cs deleted file mode 100644 index 1dc207890a..0000000000 --- a/src/tests/SOS.Tests/ClrStackICorDebugTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for the EXPERIMENTAL !clrstack -i (ICorDebug) and -i -a (variables). The -/// legacy scripts ran -i/-i -a on DivZero/DynamicMethod but only shape-checked them. It is -/// far richer than the non-i path: it recovers real local names and decodes values, which lets us -/// assert concrete parameter/local values and cross-check object references against !dumpheap (the -/// SOS-native oracle). -/// -/// Self-contained single-file is excluded from the matrix: ICorDebug/DBI cannot decode a single-file -/// image's locals (they come back as unnamed IsError slots), and this test specifically validates -/// decoded local names and values. -/// -public sealed class ClrStackICorDebugTests -{ - public static TheoryData Matrix { get; } - = TestConfig.BuildMatrix([TargetCatalog.DivZero, TargetCatalog.Scenarios], Flavor.Core | Flavor.Framework); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_ICorDebug(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - if (config.Target == TargetCatalog.Scenarios) - { - target.GoToStopPoint(TargetCatalog.StopArgsLocals); - } - else - { - target.GoToFirstStop(); - } - - // Basic -i: the expected managed methods are present as [DEFAULT] frames. - IReadOnlyList frames = target.ClrstackICorDebug(variables: false); - Assert.Contains(frames, f => f.IsManaged); - foreach (string method in ExpectedMethods(config.Target)) - Assert.Contains(frames, f => f.IsManaged && f.CallSite.Contains(method, StringComparison.Ordinal)); - - // -i -a: same frames, now with parameters and locals decoded. - IReadOnlyList withVars = target.ClrstackICorDebug(variables: true); - foreach (string method in ExpectedMethods(config.Target)) - Assert.Contains(withVars, f => f.IsManaged && f.CallSite.Contains(method, StringComparison.Ordinal)); - - if (config.Target == TargetCatalog.Scenarios) - { - AssertArgsLocalsVariables(target, withVars); - } - } - - private static void AssertArgsLocalsVariables(Target target, IReadOnlyList frames) - { - TargetExtensions.IcorFrame method = Assert.Single(frames, f => f.IsManaged && f.CallSite.Contains("SosHarnessScenarios.ArgsLocalsMethod", StringComparison.Ordinal)); - - // ICorDebug recovers parameter and local names and decodes primitive values (in decimal). Object - // values print as "@ 0x", and that address is the very object !dumpheap reports for the - // uniquely-named type. - Assert.Equal("42", Param(method, "number").Value); - TargetExtensions.IcorVar arg = Param(method, "arg"); - Assert.True(arg.HasAddress); - Assert.Equal(target.FindUniqueObject("ArgUniqueMarker"), arg.Address); - - Assert.Equal("99", Local(method, "localInt").Value); - TargetExtensions.IcorVar localObj = Local(method, "localObj"); - Assert.True(localObj.HasAddress); - Assert.Equal(target.FindUniqueObject("LocalUniqueMarker"), localObj.Address); - } - - // The methods we require on each target's ICorDebug stack. - private static IReadOnlyList ExpectedMethods(string target) => target switch - { - TargetCatalog.DivZero => ["C.DivideByZero", "C.F3", "C.F2", "C.Main"], - TargetCatalog.Scenarios => ["SosHarnessScenarios.ArgsLocalsMethod", "SosHarnessScenarios.AtArgsLocals", "SosHarnessScenarios.Main"], - _ => throw new ArgumentOutOfRangeException(nameof(target)), - }; - - private static TargetExtensions.IcorVar Param(TargetExtensions.IcorFrame frame, string name) => - Assert.Single(frame.Parameters, v => v.Name == name); - - private static TargetExtensions.IcorVar Local(TargetExtensions.IcorFrame frame, string name) => - Assert.Single(frame.Locals, v => v.Name == name); -} diff --git a/src/tests/SOS.Tests/ClrStackLinesTests.cs b/src/tests/SOS.Tests/ClrStackLinesTests.cs deleted file mode 100644 index 7f008ab96b..0000000000 --- a/src/tests/SOS.Tests/ClrStackLinesTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for plain !clrstack (base managed stack with source [file @ line]) and -/// -n (suppress source line numbers). The legacy scripts hardcoded method names and exact -/// source file/line (e.g. SimpleThrow.script asserted UserObject.cs @ 19); we do the same kind -/// of sensible hardcoding — assert the expected managed methods appear in caller order and resolve to -/// the expected source file — then use cross-variant self-consistency for -n: it produces the -/// identical frames (same IP / Function per row) but with the source annotation stripped. -/// -/// Two harness fidelity fixes make source lines resolve on every config: the dbgeng host now enables -/// SYMOPT_LOAD_LINES (.lines -e), and net48 targets emit a full Windows PDB (see -/// testtargets/Directory.Build.props) — without these, cdb / desktop showed no [file @ line]. -/// -public sealed class ClrStackLinesTests -{ - private sealed record Frame(string Function, string SourceFile); - - // Hardcoded like the legacy scripts: distinctive method substrings expected on each target's stack - // (in caller order) and the source file each resolves to. - private static IReadOnlyList ExpectedFrames(string target) => target switch - { - TargetCatalog.SimpleThrow => - [new("UseObject", "UserObject.cs"), new("Simple.Main", "SimpleThrow.cs")], - TargetCatalog.LineNums => - [new(".Bar(", "Program.cs"), new(".Foo(", "Program.cs"), new(".Main(", "Program.cs")], - TargetCatalog.DivZero => - [new(".DivideByZero(", "DivZero.cs"), new(".F3(", "DivZero.cs"), new(".F2(", "DivZero.cs")], - TargetCatalog.NestedException => - [new(".Main(", "NestedExceptionTest.cs")], - TargetCatalog.Scenarios => - [new(".ArgsLocalsMethod(", "SosHarnessScenarios.cs"), new(".Main(", "SosHarnessScenarios.cs")], - _ => throw new ArgumentOutOfRangeException(nameof(target), target, "no expected frames"), - }; - - public static TheoryData Matrix { get; } - = TestMatrices.StackWalk( - [ - TargetCatalog.SimpleThrow, - TargetCatalog.LineNums, - TargetCatalog.DivZero, - TargetCatalog.NestedException, - TargetCatalog.Scenarios, - ]); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrStack_SourceLines(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - if (config.Target == TargetCatalog.Scenarios) - { - target.GoToStopPoint(TargetCatalog.StopArgsLocals); - } - else - { - target.GoToFirstStop(); - } - - SosTable plain = target.Clrstack(); - - // The expected managed methods appear, in caller order, each resolving to its source file with - // a real (positive) line number. - int searchFrom = 0; - foreach (Frame expected in ExpectedFrames(config.Target)) - { - int at = IndexOfFrame(plain, expected.Function, searchFrom); - Assert.True(at >= 0, $"Expected frame '{expected.Function}' at/after row {searchFrom} in:\n{Dump(plain)}"); - SosRow row = plain.Row(at); - - Assert.Equal(expected.SourceFile, Path.GetFileName(row["SourceFile"].Value)); - Assert.True(row["LineNumber"].AsInt32(Sos.Integer) > 0, $"Expected a positive line number for '{expected.Function}'."); - searchFrom = at + 1; - } - - // -n produces the identical frames (same count, same IP and Function per row) but with the - // source annotation suppressed everywhere. - SosTable noLines = target.Clrstack(suppressLines: true); - Assert.Equal(plain.Length, noLines.Length); - for (int i = 0; i < plain.Length; i++) - { - Assert.Equal(plain.Row(i)["IP"].Value, noLines.Row(i)["IP"].Value); - Assert.Equal(plain.Row(i)["Function"].Value, noLines.Row(i)["Function"].Value); - } - - Assert.All(noLines, r => - { - Assert.Empty(r["SourceFile"].Value); - Assert.Empty(r["LineNumber"].Value); - }); - - // Sanity: plain really did carry source info (so the -n strip above is meaningful). - Assert.Contains(plain, r => r["SourceFile"].Value.Length > 0); - } - - private static int IndexOfFrame(SosTable table, string functionSubstring, int from) - { - for (int i = from; i < table.Length; i++) - { - SosRow row = table.Row(i); - if (!row["InternalFrame"].AsBoolean() && row["Function"].Value.Contains(functionSubstring, StringComparison.Ordinal)) - return i; - } - - return -1; - } - - private static string Dump(SosTable table) => - string.Join("\n", table.Select(r => $"{r["Child SP"].Value} {r["IP"].Value} {r["Call Site"].Value}")); -} diff --git a/src/tests/SOS.Tests/ClrStackTests.cs b/src/tests/SOS.Tests/ClrStackTests.cs deleted file mode 100644 index b9da87f04c..0000000000 --- a/src/tests/SOS.Tests/ClrStackTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// Coverage for !clrstack -r (register display). The legacy SOS scripts only verified the -/// shape of the output (the "OS Thread Id" banner, the "Child SP / IP / Call Site" header, -/// and that each frame prints a register block). We assert the shape too (see -/// ), then go further and check values: -/// SOS fills the table's Child SP / IP columns from the same per-frame register context it dumps -/// (strike.cpp GetFrameLocation / PrintManagedFrameContext read one context with -/// GetFullContextFlags). So for every frame the IP column must equal the instruction-pointer -/// register (rip/eip/pc), and for every real (non-internal) managed frame the Child SP column must -/// equal the stack-pointer register (rsp/esp/sp). Internal clr!Frames report the Frame address as -/// Child SP instead of the context SP, so only their IP is matched. -/// -public sealed class ClrStackTests -{ - // Instruction-pointer / stack-pointer register names across the platforms SOS prints in - // strike.cpp PrintManagedFrameContext: amd64 rip/rsp, x86 eip/esp, arm & arm64 pc/sp. - private static readonly string[] s_ipRegisters = ["rip", "eip", "pc"]; - private static readonly string[] s_spRegisters = ["rsp", "esp", "sp"]; - - // The legacy !clrstack -r scripts ran against NestedExceptionTest (StackTests.script), - // SymbolTestApp (StackAndOtherTests.script), and WebApp3 (WebApp.script). SymbolTestApp and - // WebApp3 are deferred (multi-assembly / ASP.NET+DualRuntimes). Since -r is target-agnostic, we - // exercise it over a diverse set of already-ported debuggees with deliberately different stack - // shapes / stop kinds, so the SP/IP <-> register invariant is checked across varied frames: - // NestedException - unhandled exception crash (the real -r debuggee) - // DivZero - hardware fault (div by zero), deep non-async call chain - // AsyncMain - async state-machine frames - // DynamicMethod - a dynamic (IL-emitted) method on the stack - // Scenarios - snapshot (marker) stop rather than a crash - public static TheoryData RegistersMatrix { get; } - = TestMatrices.StackWalk( - [ - TargetCatalog.NestedException, - TargetCatalog.DivZero, - TargetCatalog.AsyncMain, - TargetCatalog.DynamicMethod, - TargetCatalog.Scenarios, - ], - // Live opt-in: !clrstack is one of the few commands that genuinely exercises a different path - // live (it unwinds a live thread's stack and reads its register context), so this base - // stackwalk runs dump AND live. Most other clrstack variations stay dump-only. - liveness: Liveness.AllValid); - - [SosTheory] - [MemberData(nameof(RegistersMatrix))] - public async Task ClrStack_Registers(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToFirstStop(); - - SosTable table = target.ClrstackRegisters(); - - bool sawManagedFrame = false; - foreach (SosRow row in table) - { - // The IP column is filled from the frame context's instruction pointer, which -r also - // dumps as a register column, so they must agree for every frame (internal frames included). - ulong ip = row["IP"].AsUInt64(Sos.Addr); - ulong ipRegister = Register(row, s_ipRegisters); - Assert.Equal(ip, ipRegister); - - // Internal clr!Frames report the Frame address as Child SP (not the context SP), so only - // real managed frames have a Child SP that equals the stack-pointer register. - bool internalFrame = row["InternalFrame"].AsBoolean(); - if (!internalFrame) - { - sawManagedFrame = true; - ulong sp = row["Child SP"].AsUInt64(Sos.Addr); - ulong spRegister = Register(row, s_spRegisters); - Assert.Equal(sp, spRegister); - } - } - - Assert.True(sawManagedFrame, "clrstack -r produced no non-internal managed frame to match Child SP against."); - } - - // The value of the row's register column whose name is one of . - private static ulong Register(SosRow row, string[] names) - { - string? name = names.FirstOrDefault(row.HasColumn); - Assert.NotNull(name); - return row[name!].AsUInt64(Sos.Addr); - } - - // !clrstack -gc is not covered by the legacy scripts at all. It must be exercised over stops that - // hold object references in ordinary live locals: whether an object is a GC-reported *stack* root - // depends on the capture point, so crash/throw targets are unreliable (e.g. an exception is only a - // -gc root while the runtime's managed dispatch frame is live, which createdump captures but a - // 2nd-chance/dbgeng capture does not — dso still finds it by scanning stack memory, but -gc - // legitimately reports nothing). The Scenarios marker stops avoid that; we use two with different - // root shapes, both BEFORE any GC.Collect so the live path is safe: - // roots - a normal object + a pinned byte[] + an interior int[] ref live across the marker - // argslocals - a uniquely-typed reference arg and local live across the marker - public static TheoryData GcRootsMatrix { get; } = BuildGcRootsMatrix(); - - // The stop point is an extra (non-axis) column paired with each config, so this is built by hand from - // the raw config permutations rather than the single-column BuildMatrix. - private static TheoryData BuildGcRootsMatrix() - { - TheoryData data = new(); - // Live opt-in: !clrstack -gcroots is fundamentally different from !clrstack (it scans the live - // stack and registers for GC-reported roots), so it runs dump AND live. - foreach (TestConfig config in TestMatrices.StackWalkConfigs([TargetCatalog.Scenarios], liveness: Liveness.AllValid)) - { - data.Add(config, TargetCatalog.StopRoots); - data.Add(config, TargetCatalog.StopArgsLocals); - } - - return data; - } - - [SosTheory] - [MemberData(nameof(GcRootsMatrix))] - public async Task ClrStack_GcRoots(TestConfig config, string stopName) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(stopName); - - // The objects dumpstackobjects finds by scanning stack memory. - SosTable dsoTable = target.DumpStackObjects(); - HashSet dsoObjects = dsoTable.Select(r => r["Object"].AsUInt64(Sos.Addr)).ToHashSet(); - - SosTable gc = target.ClrstackGcRoots(); - - int compared = 0; - foreach (SosRow frame in gc) - { - foreach (SosDataRow root in frame.Data) - { - // Pinned / Interior are always present (default False even when not printed). - bool interior = root["Interior"].AsBoolean(); - _ = root["Pinned"].AsBoolean(); - ulong obj = root["Object"].AsUInt64(Sos.Addr); - - // A non-interior, non-null root that lives in a stack slot holds an object pointer in - // stack memory, so dumpstackobjects (which scans that memory) must have found it too. - // Interior pointers don't point at an object head, so dso won't list them — skip them. - if (interior || obj == 0 || root["Address"].Value.Length == 0) - continue; - - Assert.Contains(obj, dsoObjects); - compared++; - } - } - - Assert.True(compared > 0, "clrstack -gc produced no non-interior stack-slot roots to compare against dumpstackobjects."); - } - - // GcRoots is the dedicated target that deliberately keeps one of each root flavor alive across - // the marker, so the parser's handling of the optional (pinned)/(interior) flags and the - // sometimes-absent type is actually exercised — and so we can assert the SosDataRow always - // carries Pinned/Interior (defaulting to False when the flag isn't printed). - public static TheoryData GcRootsFlagMatrix { get; } = TestMatrices.StackWalkFullDumpOnCoreVersions([TargetCatalog.Scenarios], CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10); - - [SosTheory] - [MemberData(nameof(GcRootsFlagMatrix))] - public async Task ClrStack_GcRoots_Flags(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint("roots"); - - SosTable gc = target.ClrstackGcRoots(); - List roots = gc.SelectMany(f => f.Data).ToList(); - Assert.NotEmpty(roots); - - // Every record always carries both flag fields (False when SOS didn't print the flag). - Assert.All(roots, r => - { - Assert.True(r.Has("Pinned")); - Assert.True(r.Has("Interior")); - }); - - // The fixed byte[] is a pinned root: (pinned), not interior, and SOS still prints its type - // (it points at the object head, so it isn't interior). - SosDataRow pinned = roots.AssertSingle(r => r["Pinned"].AsBoolean(), "a pinned root"); - Assert.False(pinned["Interior"].AsBoolean()); - Assert.NotEqual(0ul, pinned["Object"].AsUInt64(Sos.Addr)); - Assert.Contains("Byte[]", pinned["Type"].Value); - - // The ref into the int[] is an interior root: (interior), and SOS prints no type for it - // (an interior pointer doesn't point at an object head). - roots.AssertContains(r => r["Interior"].AsBoolean() && r["Type"].Value.Length == 0, "an interior root with no Type"); - - // A normal object root: neither flag set, and a type is present. - roots.AssertContains( - r => !r["Pinned"].AsBoolean() && !r["Interior"].AsBoolean() && - r["Object"].AsUInt64(Sos.Addr) != 0 && r["Type"].Contains("System.Object"), - "a normal System.Object root (neither pinned nor interior)"); - } -} diff --git a/src/tests/SOS.Tests/ClrThreadsParsing.cs b/src/tests/SOS.Tests/ClrThreadsParsing.cs deleted file mode 100644 index bd1ffadeda..0000000000 --- a/src/tests/SOS.Tests/ClrThreadsParsing.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; - -namespace SOS.Tests; - -/// -/// Parser for the tabular section of !clrthreads. The generic -/// can't read it: SOS prints the header across two physical lines (the "Lock" of the "Lock Count" -/// column floats on the line above "Count" — see strike.cpp's -/// WriteColumn(8, "Lock") before the WriteRow(...)), and the trailing Exception -/// column is empty for most threads, which would make the fixed-width row detector reject every row. -/// -/// Every column up to Apt is a single whitespace token (the GC Alloc Context is -/// addr:addr with no space), so we tokenize each data line: the first N tokens are the fixed -/// columns and everything after is the free-form Exception column (thread tags like -/// (Finalizer)/(GC)/(Threadpool Worker) and any last-thrown exception). -/// -internal static class ClrThreadsParsing -{ - // Columns in SOS print order (strike.cpp PrintThreadsFromThreadStore). "Lock Count" rejoins the - // two-line header; "Exception" is the trailing free-form column. - private static readonly string[] s_columns = - ["DBG", "ID", "OSID", "ThreadOBJ", "State", "GC Mode", "GC Alloc Context", "Domain", "Lock Count", "Apt", "Exception"]; - - // When the runtime is hosted, SOS inserts a "Fiber" column before "Exception". - private static readonly string[] s_hostedColumns = - ["DBG", "ID", "OSID", "ThreadOBJ", "State", "GC Mode", "GC Alloc Context", "Domain", "Lock Count", "Apt", "Fiber", "Exception"]; - - /// - /// Parse the !clrthreads thread table into a . Rows are addressable by - /// column name (row["GC Mode"], row["Lock Count"], row["Exception"]); cells - /// convert implicitly to their string value, so row["GC Mode"] == "Preemptive" works. - /// - public static SosTable AsThreadsTable(this SosOutput output) - { - string[] columns = IsHosted(output) ? s_hostedColumns : s_columns; - int fixedCount = columns.Length - 1; // everything before the trailing Exception column - - IReadOnlyList lines = output.Lines; - int header = FindHeaderLine(lines); - if (header < 0) - { - throw output.Fail("a !clrthreads thread table (a header row with DBG / ThreadOBJ / Apt)"); - } - - List rows = new(); - for (int i = header + 1; i < lines.Count; i++) - { - string line = lines[i]; - if (string.IsNullOrWhiteSpace(line)) - { - break; // blank line terminates the table - } - - string[] tokens = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length < fixedCount) - { - break; // not a data row — end of the table - } - - string[] cells = new string[columns.Length]; - for (int c = 0; c < fixedCount; c++) - { - cells[c] = tokens[c]; - } - - // The remaining tokens (if any) are the free-form Exception column; empty when absent. - cells[fixedCount] = tokens.Length > fixedCount - ? string.Join(' ', tokens[fixedCount..]) - : string.Empty; - - rows.Add(cells); - } - - return output.TableFromRows(columns, rows); - } - - private static bool IsHosted(SosOutput output) - { - foreach (string line in output.Lines) - { - string t = line.Trim(); - if (t.StartsWith("Hosted Runtime:", StringComparison.Ordinal)) - { - return t["Hosted Runtime:".Length..].Trim() != "no"; - } - } - - return false; - } - - private static int FindHeaderLine(IReadOnlyList lines) - { - for (int i = 0; i < lines.Count; i++) - { - string t = lines[i].TrimStart(); - if (t.StartsWith("DBG", StringComparison.Ordinal) && - lines[i].Contains("ThreadOBJ", StringComparison.Ordinal) && - lines[i].Contains("Apt", StringComparison.Ordinal)) - { - return i; - } - } - - return -1; - } -} diff --git a/src/tests/SOS.Tests/ClrThreadsTests.cs b/src/tests/SOS.Tests/ClrThreadsTests.cs deleted file mode 100644 index 9e70d15b63..0000000000 --- a/src/tests/SOS.Tests/ClrThreadsTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// clrthreads across the full host × target × flavor matrix, with the assertion inline. -/// Demonstrates the cross-product MemberData shape and shared-dump reuse. This works uniformly -/// (including desktop .NET Framework under cdb) because the cdb host unloads any SOS cdb -/// auto-loaded and forces OUR modern SOS - which exposes clrthreads on every flavor. -/// (If we let cdb's auto-loaded desktop Framework SOS answer, it would only expose threads.) -/// -public sealed class ClrThreadsTests -{ - /// - /// A matrix of all combinations of hosts, targets, and flavors. - /// Hosts.DumpHosts = [cdb, dotnet-dump] || [lldb, dotnet-dump] - /// Targets = debuggee targets to test - /// Flavors = e.g. [Flavor.Core, Flavor.SingleFile, Flavor.Framework] - /// - public static TheoryData Matrix { get; } - // Live opt-in: !clrthreads enumerates the live thread list, so this base test runs - // dump AND live as the representative live-thread-enumeration check. - = TestConfig.BuildMatrix( - [TargetCatalog.NestedException, TargetCatalog.Scenarios], - liveness: Liveness.AllValid, - dumpKind: DumpKind.All); - - [SosTheory] - [MemberData(nameof(Matrix))] - public async Task ClrThreads_ReportsThreadCount(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToFirstStop(); - - SosOutput clrthreads = target.Sos("clrthreads"); - - clrthreads["ThreadCount"].AssertValid(Sos.Dec); - Assert.NotEqual(0u, clrthreads["ThreadCount"].AsUInt32(Sos.Dec)); - - SosTable table = clrthreads.AsThreadsTable(); - Assert.NotEmpty(table); - - // The structured ThreadCount field equals the number of rows the table parsed (ties the - // summary field to the per-thread rows). - Assert.Equal(clrthreads["ThreadCount"].AsInt32(Sos.Dec), table.Length); - - // Every fixed column sliced into the right shape — proof the two-line "Lock Count" header and - // the column alignment were handled. A misaligned column would put a non-address in ThreadOBJ, - // or something other than the two GC modes / known apartment states in their columns. - table.AssertAll(row => Sos.Addr.Matches(row["ThreadOBJ"]), "ThreadOBJ is an address"); - table.AssertAll(row => row["GC Mode"] == "Preemptive" || row["GC Mode"] == "Cooperative", "GC Mode is Preemptive or Cooperative"); - table.AssertAll(row => row["Apt"].Value is "MTA" or "STA" or "NTA" or "Ukn", "Apt is a known apartment state"); - - // The trailing free-form Exception column parses: every managed process has a finalizer thread, - // tagged "(Finalizer)" there. - table.AssertContainsRow(row => row["Exception"].Contains("Finalizer"), "the (Finalizer) thread is tagged in the Exception column"); - } -} diff --git a/src/tests/SOS.Tests/ClrUTests.cs b/src/tests/SOS.Tests/ClrUTests.cs deleted file mode 100644 index 32a8acc11d..0000000000 --- a/src/tests/SOS.Tests/ClrUTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SOS.TestHarness; -using Xunit; - -namespace SOS.Tests; - -/// -/// !clru (alias of !u) annotated disassembly and all of its parameters. clru depends on the -/// debugger's native disassembler, which only the dbgeng (cdb) host provides — dotnet-dump reports it as an -/// unrecognized command — so this runs on a cdb-only matrix. The disassembly is architecture-specific, so -/// the assertions are structural (the banner, method name, Begin/size, a non-empty instruction list, -/// source-line presence) plus the interleaving that -gcinfo/-ehinfo/-il add — which is -/// cross-checked against the standalone !gcinfo/!ehinfo output. -/// -public sealed class ClrUTests -{ - public static TheoryData Matrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.Cdb); - - // !clru's IL/source-line/EH interleaving relies on per-method debug info the DAC only sees in a Full - // dump on net8-net10 (present in Heap dumps from net11 on); capture Full there. AcceptsInstructionPointer - // needs none of that, so it stays on the default Heap Matrix. - public static TheoryData FullDumpMatrix => TestMatrices.FullDumpOnCoreVersions([TargetCatalog.Scenarios], CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10, Flavor.AllValid, Host.Cdb); - - [WindowsTheory] - [MemberData(nameof(FullDumpMatrix))] - public async Task ClrU_StructureLinesOffsets(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(TargetCatalog.StopHeap); - EEMatch atHeap = Method(target, config.Flavor, "AtHeap"); - - // Default: banner, method name, Begin/size, a real instruction stream, and source annotations. - ClrUResult plain = target.ClrU(atHeap.MethodDesc!.Value); - Assert.True(plain.HasNormalJitBanner); - Assert.Contains("AtHeap", plain.MethodName, StringComparison.Ordinal); - Assert.NotEqual(0ul, plain.Begin); - Assert.True(plain.Size > 0); - Assert.NotEmpty(plain.Instructions); - Assert.True(plain.SourceLineCount > 0, "expected source-line annotations by default"); - Assert.False(plain.HasOffsets); - - // -n suppresses the source annotations but still disassembles. - ClrUResult noLines = target.ClrU(atHeap.MethodDesc.Value, noLines: true); - Assert.Equal(0, noLines.SourceLineCount); - Assert.NotEmpty(noLines.Instructions); - - // -o prefixes every instruction with its offset. - ClrUResult offsets = target.ClrU(atHeap.MethodDesc.Value, offsets: true); - Assert.True(offsets.HasOffsets); - Assert.Equal(0, offsets.Instructions[0].Offset); - } - - [WindowsTheory] - [MemberData(nameof(FullDumpMatrix))] - public async Task ClrU_InterleavesGcInfoEhInfoIl(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(TargetCatalog.StopHeap); - EEMatch atHeap = Method(target, config.Flavor, "AtHeap"); - EEMatch lockHolder = Method(target, config.Flavor, "LockHolder"); - - // -il interleaves the MSIL. - Assert.Contains("IL_", target.ClrU(atHeap.MethodDesc!.Value, il: true).Output.Text, StringComparison.Ordinal); - - // -gcinfo interleaves the same interruptibility info that standalone gcinfo prints. - ClrUResult withGc = target.ClrU(atHeap.MethodDesc.Value, gcInfo: true); - Assert.Contains("interruptible", withGc.Output.Text, StringComparison.Ordinal); - Assert.NotEmpty(withGc.Instructions); // still a real disassembly, not just the gc dump - Assert.NotEmpty(target.GcInfo(atHeap.MethodDesc.Value).Transitions); - - // -ehinfo interleaves the EH clause markers of a method that has a (finally) handler. - ClrUResult withEh = target.ClrU(lockHolder.MethodDesc!.Value, ehInfo: true); - Assert.Contains("EHHandler", withEh.Output.Text, StringComparison.Ordinal); - Assert.Contains("FINALLY", withEh.Output.Text, StringComparison.Ordinal); - Assert.Contains(target.EhInfo(lockHolder.MethodDesc.Value).Clauses, c => c.Kind.Contains("FINALLY", StringComparison.Ordinal)); - } - - [WindowsTheory] - [MemberData(nameof(Matrix))] - public async Task ClrU_AcceptsInstructionPointer(TestConfig config) - { - using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(TargetCatalog.StopHeap); - EEMatch atHeap = Method(target, config.Flavor, "AtHeap"); - - // clru disassembles the same method whether given its MethodDesc or an IP inside it. - ClrUResult byMd = target.ClrU(atHeap.MethodDesc!.Value); - ClrUResult byIp = target.ClrU(atHeap.JittedCodeAddress!.Value); - Assert.Equal(byMd.Begin, byIp.Begin); - Assert.Equal(byMd.MethodName, byIp.MethodName); - } - - private static EEMatch Method(Target target, Flavor flavor, string name) - { - string module = TargetCatalog.Get(TargetCatalog.Scenarios).ModuleFor(flavor); - return target.Name2EE($"{module}!SosHarnessScenarios.{name}").Single; - } -} diff --git a/src/tests/SOS.Tests/CodeCommandParsing.cs b/src/tests/SOS.Tests/CodeCommandParsing.cs deleted file mode 100644 index b3c69066d1..0000000000 --- a/src/tests/SOS.Tests/CodeCommandParsing.cs +++ /dev/null @@ -1,291 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using System.Text.RegularExpressions; -using SOS.TestHarness; - -namespace SOS.Tests; - -/// -/// Structured parsers for the code-inspection SOS commands: !clru/!u (annotated -/// disassembly), !ehinfo, !gcinfo, and the related !dumpil (MSIL). The disassembly -/// itself is architecture-specific, so models only the stable structure (the -/// "Normal JIT generated code" banner, method name, Begin/size, the instruction lines, source-line -/// annotations, and the markers that -gcinfo/-ehinfo/-il interleave). Behaviour follows -/// the native implementations in src/SOS/Strike/strike.cpp (DECLARE_API(u/EHInfo/GCInfo/DumpIL)). -/// clru needs the debugger's native disassembler, so it exists only on the dbgeng (cdb) host. -/// -internal static class CodeCommandParsing -{ - public static ClrUResult ClrU( - this Target target, ulong address, bool noLines = false, bool offsets = false, - bool gcInfo = false, bool ehInfo = false, bool il = false, bool map = false) - { - string flags = string.Concat( - noLines ? "-n " : "", offsets ? "-o " : "", gcInfo ? "-gcinfo " : "", - ehInfo ? "-ehinfo " : "", il ? "-il " : "", map ? "-map " : ""); - return new ClrUResult(target.Sos($"clru {flags}{address:x}")); - } - - public static EhInfoResult EhInfo(this Target target, ulong address) => - new(target.Sos($"ehinfo {address:x}")); - - public static GcInfoResult GcInfo(this Target target, ulong address) => - new(target.Sos($"gcinfo {address:x}")); - - public static DumpIlResult DumpIl(this Target target, ulong address, bool ilPointer = false) => - new(target.Sos(ilPointer ? $"dumpil -i {address:x}" : $"dumpil {address:x}")); - - internal static ulong Hex(string value) => - ulong.Parse(value.Replace("`", string.Empty), NumberStyles.HexNumber, CultureInfo.InvariantCulture); -} - -/// One disassembled instruction line from !clru. -public readonly record struct DisasmLine(int? Offset, ulong Address, string Bytes, string Mnemonic, string Operands); - -/// Parsed !clru / !u output. Disassembly is arch-specific so only the stable -/// structure is modelled; is kept for interleave-marker assertions. -public sealed class ClrUResult -{ - private static readonly Regex s_begin = - new(@"^Begin\s+([0-9a-fA-F`]+),\s+size\s+([0-9a-fA-F]+)\s*$", RegexOptions.Compiled); - private static readonly Regex s_source = new(@"@\s+(\d+):\s*$", RegexOptions.Compiled); - private static readonly Regex s_instruction = new( - @"^(?:(?[0-9a-fA-F]{4,8})\s+)?(?[0-9a-fA-F]+`[0-9a-fA-F]+)\s+(?[0-9a-fA-F]+)\s+(?\S+)(?:\s+(?.*\S))?\s*$", - RegexOptions.Compiled); - - public ClrUResult(SosOutput output) - { - Output = output; - HasNormalJitBanner = output.Contains("Normal JIT generated code"); - - List instructions = new(); - int sourceLines = 0; - foreach (string raw in output.Lines) - { - string line = raw.TrimEnd(); - - Match b = s_begin.Match(line); - if (b.Success) - { - Begin = CodeCommandParsing.Hex(b.Groups[1].Value); - Size = (int)CodeCommandParsing.Hex(b.Groups[2].Value); - continue; - } - - if (s_source.IsMatch(line)) - { - sourceLines++; - continue; - } - - Match m = s_instruction.Match(line); - if (m.Success) - { - int? off = m.Groups["off"].Success ? (int)CodeCommandParsing.Hex(m.Groups["off"].Value) : null; - instructions.Add(new DisasmLine( - off, - CodeCommandParsing.Hex(m.Groups["addr"].Value), - m.Groups["bytes"].Value, - m.Groups["mn"].Value, - m.Groups["ops"].Value.Trim())); - continue; - } - - // The method name is the non-empty line that precedes Begin and isn't the banner. - if (line.Length > 0 && Begin == 0 && line != "Normal JIT generated code") - { - MethodName = line.Trim(); - } - } - - Instructions = instructions; - SourceLineCount = sourceLines; - } - - public SosOutput Output { get; } - public bool HasNormalJitBanner { get; } - public string MethodName { get; private set; } = string.Empty; - public ulong Begin { get; private set; } - public int Size { get; private set; } - public IReadOnlyList Instructions { get; } - - /// How many file @ line: source annotations were printed (0 with -n). - public int SourceLineCount { get; } - - /// True if the instruction lines carry the -o offset prefix. - public bool HasOffsets => Instructions.Count > 0 && Instructions.All(i => i.Offset is not null); -} - -/// One exception-handling clause from !ehinfo. -public readonly record struct EhClause(int Index, string Kind, ulong ClauseStart, ulong ClauseEnd, ulong HandlerStart, ulong HandlerEnd); - -/// Parsed !ehinfo output: the method identity plus its EH clauses (empty for a method with -/// no try/catch). -public sealed class EhInfoResult -{ - private static readonly Regex s_handler = new(@"^EHHandler\s+(\d+):\s+(?.+?)\s*$", RegexOptions.Compiled); - private static readonly Regex s_range = - new(@"^(?