Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/tests/SOS.TestHarness/TestConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,12 @@ internal static bool IsValid(TestConfig c)
}

/// <summary>
/// The cDAC is available only for .NET Core 11+; desktop Framework and single-file command coverage
/// continue to use the legacy DAC.
/// The cDAC is available only for .NET Core 11+; desktop Framework and earlier Core versions use the
/// legacy DAC. This is independent of host-platform constraints such as musl SingleFile support.
/// </summary>
internal static bool IsDacSupported(TestConfig config) =>
config.Dac != Dac.CDac ||
(config.Flavor is not Flavor.Framework and not Flavor.SingleFile &&
(uint)config.CoreVersion >= (uint)CoreVersion.Net11);
(config.Flavor != Flavor.Framework && (uint)config.CoreVersion >= (uint)CoreVersion.Net11);

internal static bool IsFlavorSupportedOnRid(Flavor flavor, string rid) =>
flavor != Flavor.SingleFile || !rid.StartsWith("linux-musl-", StringComparison.Ordinal);
Expand Down
24 changes: 15 additions & 9 deletions src/tests/SOS.Tests/COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

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.
commit. At that baseline, the new project contained 89 command-focused test
methods after the focused `dumpgen` addition described below; this layer adds
the command and parser coverage used for the retirements recorded here.

## Result

Expand All @@ -19,15 +20,19 @@ 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.
- **Retired**: every material legacy assertion and debugger path is covered by
focused tests, so the legacy script and registration have been removed.
- **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.
No legacy script was retired by #5979. This layer retires `DivZero.script`,
`NestedExceptionTest.script`, and `SimpleThrow.script` after moving their exact
exception, source-line, stack, thread, live/dump, and CLRMA assertions into the
command-focused suite. `Reflection.script` remains active because its reflected
target-invocation boundary is still specialized legacy behavior.

## Legacy-to-new map

Expand All @@ -36,7 +41,7 @@ replacement.
| `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. |
| `DivZero.script` | Retired | `PrintExceptionTests.PrintException_NoInnerException` verifies the exact exception type, message, HResult, no-inner state, generated frame, `-nested`, and `-lines` output across live and dump rows. `ClrThreadsTests`, `ClrStackLinesTests`, and `ClrStackICorDebugTests` cover the remaining thread and stack behavior. |
| `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. |
Expand All @@ -48,11 +53,11 @@ replacement.
| `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. |
| `NestedExceptionTest.script` | Retired | `PrintExceptionTests` verifies exact outer/inner types, messages, HResults, `-nested`, `-lines`, frame data, and inner-address round-trip across live and dump rows. `LiveBpmdTests`, `ClrThreadsTests`, the stack tests, and `DiagnosticCommandTests.Clrma_ReportsCurrentExceptionChain` cover the breakpoint, thread, stack, and CLRMA paths. |
| `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. |
| `SimpleThrow.script` | Retired | `PrintExceptionTests.PrintException_NoInnerException` verifies the exact exception type, message, HResult, no-inner state, generated frame, `-nested`, and `-lines` output across live and dump rows. `ClrThreadsTests` and the stack tests cover the remaining thread and managed-stack behavior. |
| `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. |
Expand All @@ -65,7 +70,8 @@ replacement.

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.
address and real MethodTable from `dumpheap`, 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.
Expand Down
5 changes: 4 additions & 1 deletion src/tests/SOS.Tests/ClrStackICorDebugTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ namespace SOS.Tests;
public sealed class ClrStackICorDebugTests
{
public static TheoryData<TestConfig> Matrix { get; }
= TestConfig.BuildMatrix([TargetCatalog.DivZero, TargetCatalog.Scenarios], Flavor.Core | Flavor.Framework);
= TestMatrices.StackWalk(
[TargetCatalog.DivZero, TargetCatalog.Scenarios],
Flavor.Core | Flavor.Framework,
filter: TestMatrices.SupportsICorDebugStackWalk);

[SosTheory]
[MemberData(nameof(Matrix))]
Expand Down
32 changes: 23 additions & 9 deletions src/tests/SOS.Tests/ClrStackLinesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,28 @@ namespace SOS.Tests;
/// </summary>
public sealed class ClrStackLinesTests
{
private sealed record Frame(string Function, string SourceFile);
private sealed record Frame(string Function, string SourceFile, params int[] SourceLines);

// 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<Frame> ExpectedFrames(string target) => target switch
private static IReadOnlyList<Frame> ExpectedFrames(TestConfig config) => config.Target switch
{
TargetCatalog.SimpleThrow =>
[new("UseObject", "UserObject.cs"), new("Simple.Main", "SimpleThrow.cs")],
[new("UseObject", "UserObject.cs", 19), new("Simple.Main", "SimpleThrow.cs", 12)],
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")],
[
new(".DivideByZero(", "DivZero.cs", 15),
new(".F3(", "DivZero.cs", 24),
new(".F2(", "DivZero.cs", 36),
new(".Main(", "DivZero.cs", config.Flavor == Flavor.Framework ? 56 : 57),
],
TargetCatalog.NestedException =>
[new(".Main(", "NestedExceptionTest.cs")],
[new(".Main(", "NestedExceptionTest.cs", config.Flavor == Flavor.Framework ? [11, 20] : [20])],
TargetCatalog.Scenarios =>
[new(".ArgsLocalsMethod(", "SosHarnessScenarios.cs"), new(".Main(", "SosHarnessScenarios.cs")],
_ => throw new ArgumentOutOfRangeException(nameof(target), target, "no expected frames"),
_ => throw new ArgumentOutOfRangeException(nameof(config), config.Target, "no expected frames"),
};

public static TheoryData<TestConfig> Matrix { get; }
Expand All @@ -47,7 +52,8 @@ private sealed record Frame(string Function, string SourceFile);
TargetCatalog.DivZero,
TargetCatalog.NestedException,
TargetCatalog.Scenarios,
]);
],
liveness: Liveness.AllValid);

[SosTheory]
[MemberData(nameof(Matrix))]
Expand All @@ -68,14 +74,22 @@ public async Task ClrStack_SourceLines(TestConfig config)
// 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))
foreach (Frame expected in ExpectedFrames(config))
{
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}'.");
int lineNumber = row["LineNumber"].AsInt32(Sos.Integer);
if (expected.SourceLines.Length == 0)
{
Assert.True(lineNumber > 0, $"Expected a positive line number for '{expected.Function}'.");
}
else
{
Assert.Contains(lineNumber, expected.SourceLines);
}
searchFrom = at + 1;
}

Expand Down
10 changes: 8 additions & 2 deletions src/tests/SOS.Tests/ClrStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ private static TheoryData<TestConfig, string> BuildGcRootsMatrix()
TheoryData<TestConfig, string> 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))
foreach (TestConfig config in TestMatrices.StackWalkConfigs(
[TargetCatalog.Scenarios],
liveness: Liveness.AllValid,
filter: TestMatrices.SupportsGcRootEnumeration))
{
data.Add(config, TargetCatalog.StopRoots);
data.Add(config, TargetCatalog.StopArgsLocals);
Expand Down Expand Up @@ -158,7 +161,10 @@ public async Task ClrStack_GcRoots(TestConfig config, string stopName)
// 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<TestConfig> GcRootsFlagMatrix { get; } = TestMatrices.StackWalkFullDumpOnCoreVersions([TargetCatalog.Scenarios], CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10);
public static TheoryData<TestConfig> GcRootsFlagMatrix { get; } = TestMatrices.StackWalkFullDumpOnCoreVersions(
[TargetCatalog.Scenarios],
CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10,
filter: TestMatrices.SupportsGcRootEnumeration);

[SosTheory]
[MemberData(nameof(GcRootsFlagMatrix))]
Expand Down
9 changes: 7 additions & 2 deletions src/tests/SOS.Tests/ClrUTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public async Task ClrU_StructureLinesOffsets(TestConfig config)
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");
// The x86 desktop DAC does not surface managed source annotations through clru even with full PDBs.
if (IntPtr.Size != 4 || config.Flavor != Flavor.Framework)
{
Assert.True(plain.SourceLineCount > 0, "expected source-line annotations by default");
}
Assert.False(plain.HasOffsets);

// -n suppresses the source annotations but still disassembles.
Expand Down Expand Up @@ -66,7 +70,8 @@ public async Task ClrU_InterleavesGcInfoEhInfoIl(TestConfig config)

// -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);
string transitionMarker = IntPtr.Size == 4 ? "becoming live" : "interruptible";
Assert.Contains(transitionMarker, withGc.Output.Text, StringComparison.OrdinalIgnoreCase);
Assert.NotEmpty(withGc.Instructions); // still a real disassembly, not just the gc dump
Assert.NotEmpty(target.GcInfo(atHeap.MethodDesc.Value).Transitions);

Expand Down
33 changes: 22 additions & 11 deletions src/tests/SOS.Tests/CodeCommandParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static ClrUResult ClrU(
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}"));
return new ClrUResult(target.Sos($"clru {flags}{address:x}"), offsets);
}

public static EhInfoResult EhInfo(this Target target, ulong address) =>
Expand All @@ -52,12 +52,17 @@ public sealed class ClrUResult
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(
@"^(?:(?<off>[0-9a-fA-F]{4,8})\s+)?(?<addr>[0-9a-fA-F]+`[0-9a-fA-F]+)\s+(?<bytes>[0-9a-fA-F]+)\s+(?<mn>\S+)(?:\s+(?<ops>.*\S))?\s*$",
@"^(?<addr>[0-9a-fA-F]+(?:`[0-9a-fA-F]+)?)\s+(?<bytes>[0-9a-fA-F]+)\s+(?<mn>\S+)(?:\s+(?<ops>.*\S))?\s*$",
RegexOptions.Compiled);
private static readonly Regex s_instructionWithOffset = new(
@"^(?<off>[0-9a-fA-F]{4,8})\s+(?<addr>[0-9a-fA-F]+(?:`[0-9a-fA-F]+)?)\s+(?<bytes>[0-9a-fA-F]+)\s+(?<mn>\S+)(?:\s+(?<ops>.*\S))?\s*$",
RegexOptions.Compiled);
private readonly bool _hasOffsets;

public ClrUResult(SosOutput output)
public ClrUResult(SosOutput output, bool hasOffsets = false)
{
Output = output;
_hasOffsets = hasOffsets;
HasNormalJitBanner = output.Contains("Normal JIT generated code");

List<DisasmLine> instructions = new();
Expand All @@ -80,7 +85,7 @@ public ClrUResult(SosOutput output)
continue;
}

Match m = s_instruction.Match(line);
Match m = (hasOffsets ? s_instructionWithOffset : s_instruction).Match(line);
if (m.Success)
{
int? off = m.Groups["off"].Success ? (int)CodeCommandParsing.Hex(m.Groups["off"].Value) : null;
Expand Down Expand Up @@ -115,7 +120,7 @@ public ClrUResult(SosOutput output)
public int SourceLineCount { get; }

/// <summary>True if the instruction lines carry the <c>-o</c> offset prefix.</summary>
public bool HasOffsets => Instructions.Count > 0 && Instructions.All(i => i.Offset is not null);
public bool HasOffsets => _hasOffsets && Instructions.Count > 0 && Instructions.All(i => i.Offset is not null);
}

/// <summary>One exception-handling clause from <c>!ehinfo</c>.</summary>
Expand Down Expand Up @@ -198,17 +203,19 @@ public sealed class GcInfoResult
{
private static readonly Regex s_entry = new(@"^entry point\s+([0-9a-fA-F`]+)", RegexOptions.Compiled);
private static readonly Regex s_gcinfo = new(@"^GC info\s+([0-9a-fA-F`]+)", RegexOptions.Compiled);
private static readonly Regex s_codeSize = new(@"^Code size:\s+(\d+)", RegexOptions.Compiled);
private static readonly Regex s_codeSize = new(
@"^(?:Code size:\s+(?<decimal>\d+)|method\s+size\s*=\s*(?<hex>[0-9a-fA-F]+))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex s_transition =
new(@"^[0-9a-fA-F]{4,}\s+(interruptible|not interruptible|[+\-].+)$", RegexOptions.Compiled);
new(@"^[0-9a-fA-F]{4,}\s+(interruptible|not interruptible|[+\-].+|reg .+ becoming (?:live|dead))$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

public GcInfoResult(SosOutput output)
{
Output = output;
List<string> transitions = new();
foreach (string raw in output.Lines)
{
string line = raw.TrimEnd();
string line = raw.Trim();
Match e = s_entry.Match(line);
if (e.Success)
{
Expand All @@ -226,13 +233,17 @@ public GcInfoResult(SosOutput output)
Match c = s_codeSize.Match(line);
if (c.Success)
{
CodeSize = int.Parse(c.Groups[1].Value, CultureInfo.InvariantCulture);
CodeSize = c.Groups["decimal"].Success
? int.Parse(c.Groups["decimal"].Value, CultureInfo.InvariantCulture)
: int.Parse(c.Groups["hex"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
continue;
}

if (s_transition.IsMatch(line))
int encodedPrefix = line.LastIndexOf('|');
string transition = encodedPrefix >= 0 ? line[(encodedPrefix + 1)..].Trim() : line;
if (s_transition.IsMatch(transition))
{
transitions.Add(line.Trim());
transitions.Add(transition);
}
}

Expand Down
Loading
Loading