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("", slotEnd, StringComparison.Ordinal);
- if (valEnd < 0)
- {
- return;
- }
-
- into[slot] = line.Substring(slotEnd + 1, valEnd - slotEnd - 1).Trim();
- }
-
- private static CoreVersion ReadSupportedFrameworksFromProps()
- {
- CoreVersion result = 0;
- string props = Path.Combine(RepoLayout.Root, "Directory.Build.props");
- if (File.Exists(props))
- {
- foreach (string line in File.ReadLines(props))
- {
- const string tag = "";
- int open = line.IndexOf(tag, StringComparison.Ordinal);
- if (open < 0)
- {
- continue;
- }
-
- int close = line.IndexOf("", open, StringComparison.Ordinal);
- string value = line.Substring(open + tag.Length, close - open - tag.Length);
- foreach (string tfm in value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
- {
- if (tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase) &&
- tfm.EndsWith(".0", StringComparison.Ordinal) &&
- int.TryParse(tfm.AsSpan(3, tfm.Length - 5), out int major))
- {
- result |= (CoreVersion)(1u << major);
- }
- }
-
- break;
- }
- }
-
- // Last-ditch default matching the current servicing+preview set.
- return result != 0 ? result : CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10 | CoreVersion.Net11;
- }
-}
diff --git a/src/tests/SOS.TestHarness/Dac.cs b/src/tests/SOS.TestHarness/Dac.cs
deleted file mode 100644
index 63a6c0d2e5..0000000000
--- a/src/tests/SOS.TestHarness/Dac.cs
+++ /dev/null
@@ -1,28 +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;
-
-///
-/// 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(@"^(?