From d6f25763c0f88d26d7ee5b5a4381f3c1cc6ee463 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 2 Sep 2026 19:52:14 -0400 Subject: [PATCH] Migrate SOS command coverage Carry the reviewed command tests and cross-platform capability filters onto the stabilized harness layer. Retire the three fully superseded legacy exception scripts while retaining Reflection coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72ff9a52-4eb6-46f7-a3f9-39b5f99de529 --- src/tests/SOS.TestHarness/TestConfig.cs | 7 +- src/tests/SOS.Tests/COVERAGE.md | 24 ++- src/tests/SOS.Tests/ClrStackICorDebugTests.cs | 5 +- src/tests/SOS.Tests/ClrStackLinesTests.cs | 32 ++- src/tests/SOS.Tests/ClrStackTests.cs | 10 +- src/tests/SOS.Tests/ClrUTests.cs | 9 +- src/tests/SOS.Tests/CodeCommandParsing.cs | 33 ++- .../SOS.Tests/CodeCommandParsingTests.cs | 68 ++++++ src/tests/SOS.Tests/CodeInfoTests.cs | 3 +- src/tests/SOS.Tests/DiagnosticCommandTests.cs | 18 ++ src/tests/SOS.Tests/DumpHeapStringsTests.cs | 2 +- src/tests/SOS.Tests/DumpHeapThinLockTests.cs | 6 + src/tests/SOS.Tests/EeHeapParsing.cs | 4 +- src/tests/SOS.Tests/EeHeapTests.cs | 4 +- src/tests/SOS.Tests/GcInspectionTests.cs | 28 ++- src/tests/SOS.Tests/GcWhereTests.cs | 5 +- src/tests/SOS.Tests/MemoryAndDecodeTests.cs | 29 ++- src/tests/SOS.Tests/MiscCommandTests.cs | 47 ++-- .../SOS.Tests/NativeAddressSpaceTests.cs | 22 +- src/tests/SOS.Tests/ObjectInspectionTests.cs | 2 +- src/tests/SOS.Tests/PrintExceptionTests.cs | 200 ++++++++++++++++-- src/tests/SOS.Tests/README.md | 12 +- src/tests/SOS.Tests/RuntimeInfoTests.cs | 10 +- .../SOS.Tests/SpecializedInspectionTests.cs | 16 ++ src/tests/SOS.Tests/StackInspectionTests.cs | 3 +- .../SOS.Tests/TestConfigValidityTests.cs | 5 +- src/tests/SOS.Tests/TestMatrices.cs | 88 ++++++-- .../SOS.Tests/TestMatrixCapabilityTests.cs | 84 ++++++++ .../SosHarnessScenarios.cs | 9 + src/tests/SOS.UnitTests/SOS.cs | 28 --- .../SOS.UnitTests/Scripts/DivZero.script | 70 ------ .../Scripts/NestedExceptionTest.script | 144 ------------- .../SOS.UnitTests/Scripts/SimpleThrow.script | 66 ------ 33 files changed, 673 insertions(+), 420 deletions(-) create mode 100644 src/tests/SOS.Tests/CodeCommandParsingTests.cs create mode 100644 src/tests/SOS.Tests/TestMatrixCapabilityTests.cs delete mode 100644 src/tests/SOS.UnitTests/Scripts/DivZero.script delete mode 100644 src/tests/SOS.UnitTests/Scripts/NestedExceptionTest.script delete mode 100644 src/tests/SOS.UnitTests/Scripts/SimpleThrow.script diff --git a/src/tests/SOS.TestHarness/TestConfig.cs b/src/tests/SOS.TestHarness/TestConfig.cs index ab83e01ca5..89213466b8 100644 --- a/src/tests/SOS.TestHarness/TestConfig.cs +++ b/src/tests/SOS.TestHarness/TestConfig.cs @@ -277,13 +277,12 @@ internal static bool IsValid(TestConfig c) } /// - /// 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. /// 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); diff --git a/src/tests/SOS.Tests/COVERAGE.md b/src/tests/SOS.Tests/COVERAGE.md index ca77b10204..2bb7848d3c 100644 --- a/src/tests/SOS.Tests/COVERAGE.md +++ b/src/tests/SOS.Tests/COVERAGE.md @@ -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 @@ -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 @@ -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. | @@ -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. | @@ -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. diff --git a/src/tests/SOS.Tests/ClrStackICorDebugTests.cs b/src/tests/SOS.Tests/ClrStackICorDebugTests.cs index 1dc207890a..042f91774a 100644 --- a/src/tests/SOS.Tests/ClrStackICorDebugTests.cs +++ b/src/tests/SOS.Tests/ClrStackICorDebugTests.cs @@ -20,7 +20,10 @@ namespace SOS.Tests; public sealed class ClrStackICorDebugTests { public static TheoryData 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))] diff --git a/src/tests/SOS.Tests/ClrStackLinesTests.cs b/src/tests/SOS.Tests/ClrStackLinesTests.cs index 7f008ab96b..c947820946 100644 --- a/src/tests/SOS.Tests/ClrStackLinesTests.cs +++ b/src/tests/SOS.Tests/ClrStackLinesTests.cs @@ -20,23 +20,28 @@ namespace SOS.Tests; /// 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 ExpectedFrames(string target) => target switch + private static IReadOnlyList 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 Matrix { get; } @@ -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))] @@ -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; } diff --git a/src/tests/SOS.Tests/ClrStackTests.cs b/src/tests/SOS.Tests/ClrStackTests.cs index b9da87f04c..6938ea323e 100644 --- a/src/tests/SOS.Tests/ClrStackTests.cs +++ b/src/tests/SOS.Tests/ClrStackTests.cs @@ -108,7 +108,10 @@ 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)) + foreach (TestConfig config in TestMatrices.StackWalkConfigs( + [TargetCatalog.Scenarios], + liveness: Liveness.AllValid, + filter: TestMatrices.SupportsGcRootEnumeration)) { data.Add(config, TargetCatalog.StopRoots); data.Add(config, TargetCatalog.StopArgsLocals); @@ -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 GcRootsFlagMatrix { get; } = TestMatrices.StackWalkFullDumpOnCoreVersions([TargetCatalog.Scenarios], CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10); + public static TheoryData GcRootsFlagMatrix { get; } = TestMatrices.StackWalkFullDumpOnCoreVersions( + [TargetCatalog.Scenarios], + CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10, + filter: TestMatrices.SupportsGcRootEnumeration); [SosTheory] [MemberData(nameof(GcRootsFlagMatrix))] diff --git a/src/tests/SOS.Tests/ClrUTests.cs b/src/tests/SOS.Tests/ClrUTests.cs index 32a8acc11d..4bdd060414 100644 --- a/src/tests/SOS.Tests/ClrUTests.cs +++ b/src/tests/SOS.Tests/ClrUTests.cs @@ -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. @@ -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); diff --git a/src/tests/SOS.Tests/CodeCommandParsing.cs b/src/tests/SOS.Tests/CodeCommandParsing.cs index b3c69066d1..25dbbbb7a5 100644 --- a/src/tests/SOS.Tests/CodeCommandParsing.cs +++ b/src/tests/SOS.Tests/CodeCommandParsing.cs @@ -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) => @@ -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( - @"^(?:(?[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*$", + @"^(?[0-9a-fA-F]+(?:`[0-9a-fA-F]+)?)\s+(?[0-9a-fA-F]+)\s+(?\S+)(?:\s+(?.*\S))?\s*$", RegexOptions.Compiled); + private static readonly Regex s_instructionWithOffset = 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); + 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 instructions = new(); @@ -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; @@ -115,7 +120,7 @@ public ClrUResult(SosOutput output) 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); + public bool HasOffsets => _hasOffsets && Instructions.Count > 0 && Instructions.All(i => i.Offset is not null); } /// One exception-handling clause from !ehinfo. @@ -198,9 +203,11 @@ 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+(?\d+)|method\s+size\s*=\s*(?[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) { @@ -208,7 +215,7 @@ public GcInfoResult(SosOutput output) List 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) { @@ -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); } } diff --git a/src/tests/SOS.Tests/CodeCommandParsingTests.cs b/src/tests/SOS.Tests/CodeCommandParsingTests.cs new file mode 100644 index 0000000000..87a7c2b45a --- /dev/null +++ b/src/tests/SOS.Tests/CodeCommandParsingTests.cs @@ -0,0 +1,68 @@ +// 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; + +public sealed class CodeCommandParsingTests +{ + [Fact] + public void ClrUParsesX86InstructionWithoutOffset() + { + ClrUResult result = new(new SosOutput("cdb", "clru", """ + Normal JIT generated code + Type.Method() + Begin 088837b0, size 10 + 088837b0 03c1 add eax,ecx + """)); + + DisasmLine instruction = Assert.Single(result.Instructions); + Assert.Null(instruction.Offset); + Assert.Equal(0x088837b0ul, instruction.Address); + Assert.Equal("03c1", instruction.Bytes); + Assert.Equal("add", instruction.Mnemonic); + Assert.Equal("eax,ecx", instruction.Operands); + Assert.False(result.HasOffsets); + } + + [Fact] + public void ClrUParsesX86InstructionWithOffset() + { + ClrUResult result = new(new SosOutput("cdb", "clru -o", """ + Normal JIT generated code + Type.Method() + Begin 088837b0, size 10 + 0000 088837b0 03c1 add eax,ecx + """), hasOffsets: true); + + DisasmLine instruction = Assert.Single(result.Instructions); + Assert.Equal(0, instruction.Offset); + Assert.Equal(0x088837b0ul, instruction.Address); + Assert.Equal("03c1", instruction.Bytes); + Assert.Equal("add", instruction.Mnemonic); + Assert.True(result.HasOffsets); + } + + [Fact] + public void GcInfoParsesX86LegacyEncoding() + { + GcInfoResult result = new(new SosOutput("cdb", "gcinfo", """ + entry point 08755230 + GC info 0A7596E4 + Method info block: + method size = 0022 + Pointer table: + F1 4F FF ...| 0017 reg ECX becoming live + 0E 00 39 ...| 001D reg ECX becoming dead + """)); + + Assert.Equal(0x08755230ul, result.EntryPoint); + Assert.Equal(0x0A7596E4ul, result.GcInfoAddress); + Assert.Equal(0x22, result.CodeSize); + Assert.Equal(2, result.Transitions.Count); + Assert.Contains("becoming live", result.Transitions[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("becoming dead", result.Transitions[1], StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/tests/SOS.Tests/CodeInfoTests.cs b/src/tests/SOS.Tests/CodeInfoTests.cs index 942b8066e4..728701cbd7 100644 --- a/src/tests/SOS.Tests/CodeInfoTests.cs +++ b/src/tests/SOS.Tests/CodeInfoTests.cs @@ -64,7 +64,8 @@ public async Task GcInfo_ReportsEncoding(TestConfig config) Assert.NotEqual(0ul, gc.GcInfoAddress); Assert.True(gc.CodeSize > 0, "expected a positive code size"); Assert.NotEmpty(gc.Transitions); - Assert.Contains(gc.Transitions, t => t.Contains("interruptible", StringComparison.Ordinal)); + string transitionMarker = IntPtr.Size == 4 ? "becoming live" : "interruptible"; + Assert.Contains(gc.Transitions, t => t.Contains(transitionMarker, StringComparison.OrdinalIgnoreCase)); // gcinfo accepts an IP and reports the same entry point. GcInfoResult byIp = target.GcInfo(atHeap.JittedCodeAddress!.Value); diff --git a/src/tests/SOS.Tests/DiagnosticCommandTests.cs b/src/tests/SOS.Tests/DiagnosticCommandTests.cs index 6fa7e59f83..f32de37d31 100644 --- a/src/tests/SOS.Tests/DiagnosticCommandTests.cs +++ b/src/tests/SOS.Tests/DiagnosticCommandTests.cs @@ -21,6 +21,8 @@ public sealed class DiagnosticCommandTests // (dotnet-dump) hosts. The lldb SOS plugin never registered it (true of the legacy suite too — clrma // ran only under the dotnet-dump host there), so lldb is excluded from the matrix rather than skipped. public static TheoryData ClrmaMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.Cdb | Host.DotnetDump); + public static TheoryData ClrmaExceptionMatrix => + TestConfig.BuildMatrix([TargetCatalog.NestedException], Flavor.AllValid, Host.DotnetDump, dumpKind: DumpKind.All); [SosTheory] [MemberData(nameof(Matrix))] @@ -87,4 +89,20 @@ public async Task Clrma_DrivesManagedAnalysis(TestConfig config) clrma.AssertContains("Managed analysis provider"); clrma.AssertContains("OSThreadId:"); } + + [SosTheory] + [MemberData(nameof(ClrmaExceptionMatrix))] + public async Task Clrma_ReportsCurrentExceptionChain(TestConfig config) + { + using Target target = await Targets.GetTargetAsync(config); + target.GoToFirstStop(); + + SosOutput clrma = target.Sos("clrma"); + clrma.AssertContains("Current exception:"); + Assert.Matches(@"Exception type:\s+System\.InvalidOperationException", clrma.Text); + Assert.Matches(@"HResult:\s+80131509", clrma.Text); + clrma.AssertContains("InnerException:"); + Assert.Matches(@"Exception type:\s+System\.FormatException", clrma.Text); + Assert.Matches(@"HResult:\s+80131537", clrma.Text); + } } diff --git a/src/tests/SOS.Tests/DumpHeapStringsTests.cs b/src/tests/SOS.Tests/DumpHeapStringsTests.cs index 2af1516388..a5008a57aa 100644 --- a/src/tests/SOS.Tests/DumpHeapStringsTests.cs +++ b/src/tests/SOS.Tests/DumpHeapStringsTests.cs @@ -15,7 +15,7 @@ namespace SOS.Tests; /// public sealed class DumpHeapStringsTests { - public static TheoryData Matrix => TestConfig.BuildMatrix([TargetCatalog.NestedException]); + public static TheoryData Matrix => TestMatrices.HeapEnumeration([TargetCatalog.NestedException]); [SosTheory] [MemberData(nameof(Matrix))] diff --git a/src/tests/SOS.Tests/DumpHeapThinLockTests.cs b/src/tests/SOS.Tests/DumpHeapThinLockTests.cs index 529bd9a0f4..89b4585ac0 100644 --- a/src/tests/SOS.Tests/DumpHeapThinLockTests.cs +++ b/src/tests/SOS.Tests/DumpHeapThinLockTests.cs @@ -22,6 +22,12 @@ public sealed class DumpHeapThinLockTests [MemberData(nameof(Matrix))] public async Task DumpHeap_ThinLock(TestConfig config) { + if (RepoLayout.Rid.StartsWith("linux-musl-", StringComparison.Ordinal)) + { + HarnessSkipException.Now( + "Thin-lock data is unavailable from the DAC on Linux musl, so dumpheap -thinlock omits the table."); + } + using Target target = await Targets.GetTargetAsync(config); target.GoToStopPoint(TargetCatalog.StopThinLock); diff --git a/src/tests/SOS.Tests/EeHeapParsing.cs b/src/tests/SOS.Tests/EeHeapParsing.cs index 804ed8cfe3..0130285111 100644 --- a/src/tests/SOS.Tests/EeHeapParsing.cs +++ b/src/tests/SOS.Tests/EeHeapParsing.cs @@ -159,7 +159,7 @@ public EeHeap(SosOutput output) /// The raw command output, for custom assertions / failure messages. public SosOutput Output => _output; - /// True for the modern regions/DATAS layout; false for the segment/ephemeral layout. + /// True when generation-specific region headers are present; false for the segment/ephemeral layout. public bool IsRegions { get; private set; } /// The DATAS mode value (e.g. "0"/""), or null when there is no DATAS banner (segment layout). @@ -198,7 +198,6 @@ private List Parse() if (line.StartsWith("DATAS", StringComparison.Ordinal)) { - IsRegions = true; int eq = line.IndexOf('='); Datas = eq >= 0 ? line[(eq + 1)..].Trim() : string.Empty; continue; @@ -250,6 +249,7 @@ private List Parse() Match genHeader = s_genHeader.Match(line); if (genHeader.Success) { + IsRegions = true; target = genHeader.Groups[1].Value switch { "0" => heap.Gen0, diff --git a/src/tests/SOS.Tests/EeHeapTests.cs b/src/tests/SOS.Tests/EeHeapTests.cs index c1b9895242..ab41ceac64 100644 --- a/src/tests/SOS.Tests/EeHeapTests.cs +++ b/src/tests/SOS.Tests/EeHeapTests.cs @@ -47,7 +47,9 @@ public async Task EeHeap_ServerGc_IsMultiHeap(TestConfig config) // Each heap is independently well-formed with its own gen0/1/2 segments. Assert.All(ee.Heaps, h => { - Assert.NotEmpty(h.Gen0.Concat(h.Gen1).Concat(h.Gen2)); + Assert.NotEmpty(h.Ranges(GcGeneration.Gen0)); + Assert.NotEmpty(h.Ranges(GcGeneration.Gen1)); + Assert.NotEmpty(h.Ranges(GcGeneration.Gen2)); Assert.NotNull(h.Address); }); } diff --git a/src/tests/SOS.Tests/GcInspectionTests.cs b/src/tests/SOS.Tests/GcInspectionTests.cs index e20eee5939..37d8e14243 100644 --- a/src/tests/SOS.Tests/GcInspectionTests.cs +++ b/src/tests/SOS.Tests/GcInspectionTests.cs @@ -99,8 +99,24 @@ public async Task GcHeapStat_ReportsGenerationSizes(TestConfig config) [MemberData(nameof(Matrix))] public async Task VerifyHeap_ReportsNoCorruption(TestConfig config) { + if (RepoLayout.Rid.StartsWith("linux-musl-", StringComparison.Ordinal)) + { + HarnessSkipException.Now( + "The DAC reports incorrect sync-block-related verifyheap failures on Linux musl."); + } + + if (OperatingSystem.IsMacOS() && + config.Host == Host.DotnetDump && + config.Dac == Dac.Legacy && + config.Flavor == Flavor.Core && + config.CoreVersion == CoreVersion.Net11) + { + HarnessSkipException.Now( + "https://github.com/dotnet/diagnostics/issues/5985: legacy DAC SyncBlock data is unavailable for macOS .NET 11 dumps."); + } + using Target target = await Targets.GetTargetAsync(config); - target.GoToStopPoint(TargetCatalog.StopHeap); + target.GoToStopPoint(TargetCatalog.StopGen1); SosOutput verify = target.Sos("verifyheap"); Assert.Matches(@"\b0 errors\b", verify.Text); @@ -129,8 +145,14 @@ public async Task DumpGen_ArgumentsAndFilters(TestConfig config) target.Sos("dumpgen").AssertContains("Generation argument is missing"); target.Sos("dumpgen invalid").AssertContains("invalid is not a supported generation"); - ulong marker = target.FindUniqueObject("ThinLockMarker"); - ulong methodTable = target.DumpObj(marker).MethodTable; + DumpHeapResult markerDump = target.DumpHeap("-type ThinLockMarker"); + SosRow markerStat = markerDump.Statistics + .SingleRow(r => r["Class Name"].Value == "ThinLockMarker", "the ThinLockMarker method table"); + Assert.Equal(1, markerStat["Count"].AsInt32(Sos.Integer)); + ulong methodTable = markerStat["MT"].AsUInt64(Sos.Addr); + SosRow markerRow = markerDump.Objects + .SingleRow(r => r["MT"].AsUInt64(Sos.Addr) == methodTable, "the ThinLockMarker object"); + ulong marker = markerRow["Address"].AsUInt64(Sos.Addr); target.Sos("dumpgen gen0 -type ThinLockMarker") .AssertContains("ThinLockMarker"); diff --git a/src/tests/SOS.Tests/GcWhereTests.cs b/src/tests/SOS.Tests/GcWhereTests.cs index 69b62d9570..7a11702f5c 100644 --- a/src/tests/SOS.Tests/GcWhereTests.cs +++ b/src/tests/SOS.Tests/GcWhereTests.cs @@ -17,8 +17,9 @@ public sealed class GcWhereTests // gcwhere's structure check is dump-only (the generation layout is identical in a dump). The // generation-promotion check (GcWhere_Moves) is the live-worthy one: it drives bpmd through the // gen0->gen1->gen2 promotion (GC.Collect(2) between markers) on a live process, so it opts into live. - public static TheoryData Matrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios]); - public static TheoryData LiveMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], liveness: Liveness.AllValid); + public static TheoryData Matrix => TestMatrices.CurrentThreadCommands([TargetCatalog.Scenarios]); + public static TheoryData LiveMatrix => + TestMatrices.CurrentThreadCommands([TargetCatalog.Scenarios], Liveness.AllValid); [SosTheory] [MemberData(nameof(Matrix))] diff --git a/src/tests/SOS.Tests/MemoryAndDecodeTests.cs b/src/tests/SOS.Tests/MemoryAndDecodeTests.cs index 3fc9c5f387..d644e77eb0 100644 --- a/src/tests/SOS.Tests/MemoryAndDecodeTests.cs +++ b/src/tests/SOS.Tests/MemoryAndDecodeTests.cs @@ -16,7 +16,7 @@ namespace SOS.Tests; public sealed class MemoryAndDecodeTests { public static TheoryData ScenariosMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios]); - public static TheoryData NestedExceptionMatrix => TestConfig.BuildMatrix([TargetCatalog.NestedException]); + public static TheoryData NestedExceptionMatrix => TestMatrices.HeapEnumeration([TargetCatalog.NestedException]); public static TheoryData DotnetDumpMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.DotnetDump); [SosTheory] @@ -29,10 +29,31 @@ public async Task MemoryDumpers_ShowKnownFieldBytes(TestConfig config) // The memory dumpers are dotnet-dump REPL commands (cdb uses the native d*/dp/db). FieldMarker's // LongField is a known 64-bit value, so it appears verbatim in the pointer/qword dumps. ulong marker = target.FindUniqueObject("FieldMarker"); - string longHex = ((ulong)TestTargets.SosHarnessScenarios.FieldMarkerLong).ToString("x"); + ulong longValue = (ulong)TestTargets.SosHarnessScenarios.FieldMarkerLong; + string longHex = longValue.ToString("x"); + + string pointerDump = target.Sos($"dp {marker:x}").Text; + if (IntPtr.Size == 4) + { + Assert.Contains(unchecked((uint)longValue).ToString("x8"), pointerDump, StringComparison.OrdinalIgnoreCase); + Assert.Contains(unchecked((uint)(longValue >> 32)).ToString("x8"), pointerDump, StringComparison.OrdinalIgnoreCase); + } + else + { + Assert.Contains(longHex, pointerDump, StringComparison.OrdinalIgnoreCase); + } + + string qwordDump = target.Sos($"dq {marker:x}").Text; + if (IntPtr.Size == 4) + { + Assert.Contains(unchecked((uint)longValue).ToString("x8"), qwordDump, StringComparison.OrdinalIgnoreCase); + Assert.Contains(unchecked((uint)(longValue >> 32)).ToString("x8"), qwordDump, StringComparison.OrdinalIgnoreCase); + } + else + { + Assert.Contains(longHex, qwordDump, StringComparison.OrdinalIgnoreCase); + } - Assert.Contains(longHex, target.Sos($"dp {marker:x}").Text, StringComparison.OrdinalIgnoreCase); - Assert.Contains(longHex, target.Sos($"dq {marker:x}").Text, StringComparison.OrdinalIgnoreCase); target.Sos($"db {marker:x}").AssertContains(":"); // byte dump prints ": " } diff --git a/src/tests/SOS.Tests/MiscCommandTests.cs b/src/tests/SOS.Tests/MiscCommandTests.cs index c111375e35..78a25d6aae 100644 --- a/src/tests/SOS.Tests/MiscCommandTests.cs +++ b/src/tests/SOS.Tests/MiscCommandTests.cs @@ -26,30 +26,37 @@ public async Task SessionCommands_Execute(TestConfig config) // dbgout toggles internal debug logging and reports the new state. target.Sos("dbgout").AssertContains("Debug output logging"); - // sosflush resets SOS's cached state; it produces no output but must run cleanly. The cDAC - // implements IXCLRDataProcess::Flush (dotnet/runtime Legacy/SOSDacImpl.IXCLRDataProcess.cs), so this - // works on every host/DAC including net11 + cDAC. - AssertRuns(target.Sos("sosflush")); - - // enummem is not surfaced by the lldb SOS plugin; return early rather - // than skipping — sosflush above has already been verified on this config. - if (config.Host == Host.Lldb) + try { - return; - } + // sosflush resets SOS's cached state; it produces no output but must run cleanly. The cDAC + // implements IXCLRDataProcess::Flush (dotnet/runtime Legacy/SOSDacImpl.IXCLRDataProcess.cs), so this + // works on every host/DAC including net11 + cDAC. + AssertRuns(target.Sos("sosflush")); + + // enummem is not surfaced by the lldb SOS plugin; return early rather + // than skipping — sosflush above has already been verified on this config. + if (config.Host == Host.Lldb) + { + return; + } + + // enummem (EnumMemoryRegions) is E_NOTIMPL under the cDAC on the dotnet-dump host — by design, the + // cDAC doesn't implement the memory-region enumeration contract, surfaced as "Unrecognized SOS + // command". The native (cdb) host services enummem itself, so + // it's only unavailable on dotnet-dump. Return early rather than skipping — sosflush above has + // already been verified on this config. cDAC only exists on net11+, so no version check is needed. + if (config.Dac == Dac.CDac && config.Host == Host.DotnetDump) + { + return; + } - // enummem (EnumMemoryRegions) is E_NOTIMPL under the cDAC on the dotnet-dump host — by design, the - // cDAC doesn't implement the memory-region enumeration contract, surfaced as "Unrecognized SOS - // command". The native (cdb) host services enummem itself, so - // it's only unavailable on dotnet-dump. Return early rather than skipping — sosflush above has - // already been verified on this config. cDAC only exists on net11+, so no version check is needed. - if (config.Dac == Dac.CDac && config.Host == Host.DotnetDump) + // enummem produces no output but must be a recognised command that runs cleanly. + AssertRuns(target.Sos("enummem")); + } + finally { - return; + target.Sos("dbgout -off").AssertContains("Debug output logging"); } - - // enummem produces no output but must be a recognised command that runs cleanly. - AssertRuns(target.Sos("enummem")); } private static void AssertRuns(SosOutput output) diff --git a/src/tests/SOS.Tests/NativeAddressSpaceTests.cs b/src/tests/SOS.Tests/NativeAddressSpaceTests.cs index f58b47273c..7c7f28b089 100644 --- a/src/tests/SOS.Tests/NativeAddressSpaceTests.cs +++ b/src/tests/SOS.Tests/NativeAddressSpaceTests.cs @@ -21,12 +21,22 @@ public async Task NotReachableInRange_ScansPointerRange(TestConfig config) using Target target = await Targets.GetTargetAsync(config); target.GoToStopPoint(TargetCatalog.StopHeap); - // notreachableinrange treats [start,end) as an array of object pointers (it backs !finalizerqueue) - // and reports the dead ones. Point it at a known live object's span: the command computes the live - // set and emits the dumpheap-style listing, proving the scan path works. - ulong marker = target.FindUniqueObject("FieldMarker"); - SosOutput scan = target.Sos($"notreachableinrange {marker:x} {marker + 0x200:x}"); - scan.AssertContains("Calculating live objects"); + // notreachableinrange treats [start,end) as an array of object pointers (it backs !finalizerqueue). + // ObjectReference[] contains one-field value types, so dumparray reports the address of an + // actual object-reference slot rather than the referenced object or an object header. + ulong references = target.FindUniqueObject("ObjectReference[]"); + ulong slot = target.DumpArray(references).Elements[0].Address; + + SosOutput scan = target.Sos($"notreachableinrange {slot:x} {slot + (ulong)IntPtr.Size:x}"); + string[] results = scan.Lines + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Where(line => !IsLivenessProgress(line)) + .ToArray(); + Assert.Empty(results); } + internal static bool IsLivenessProgress(string line) => + line.StartsWith("Calculating live objects", StringComparison.Ordinal) || + line is "Caching GC roots, this may take a while." or + "Subsequent runs of this command will be faster."; } diff --git a/src/tests/SOS.Tests/ObjectInspectionTests.cs b/src/tests/SOS.Tests/ObjectInspectionTests.cs index 05200a5de8..9d8f70ffd4 100644 --- a/src/tests/SOS.Tests/ObjectInspectionTests.cs +++ b/src/tests/SOS.Tests/ObjectInspectionTests.cs @@ -58,7 +58,7 @@ public async Task DumpObj_Mt_Class_Md_Chain(TestConfig config) // dumpobj: identity of the object. DumpObjResult obj = target.DumpObj(marker); Assert.Equal("ThinLockMarker", obj.Name); - Assert.Equal(24, obj.Size); + Assert.Equal(3 * IntPtr.Size, obj.Size); ulong mt = obj.MethodTable; Assert.NotEqual(0ul, mt); diff --git a/src/tests/SOS.Tests/PrintExceptionTests.cs b/src/tests/SOS.Tests/PrintExceptionTests.cs index e88ae0b448..50edffba14 100644 --- a/src/tests/SOS.Tests/PrintExceptionTests.cs +++ b/src/tests/SOS.Tests/PrintExceptionTests.cs @@ -13,17 +13,26 @@ namespace SOS.Tests; /// public sealed class PrintExceptionTests { - /// - /// A matrix of all combinations of hosts, targets, and flavors. - /// Hosts.DumpHosts = [cdb, dotnet-dump] || [lldb, dotnet-dump] - /// Flavors = e.g. [Flavor.Core, Flavor.SingleFile, Flavor.Framework] - /// - public static TheoryData Matrix { get; } = - TestConfig.BuildMatrix([TargetCatalog.NestedException], dumpKind: DumpKind.All); + public static TheoryData NestedMatrix { get; } = + TestMatrices.CurrentThreadCommands( + [TargetCatalog.NestedException], + Liveness.AllValid, + DumpKind.All); + public static TheoryData NoInnerMatrix { get; } = + TestMatrices.CurrentThreadCommands( + [TargetCatalog.DivZero, TargetCatalog.SimpleThrow], + Liveness.AllValid, + DumpKind.All); + + public static TheoryData ReflectionMatrix { get; } = + TestMatrices.CurrentThreadCommands( + [TargetCatalog.Reflection], + Liveness.AllValid, + DumpKind.All); [SosTheory] - [MemberData(nameof(Matrix))] + [MemberData(nameof(NestedMatrix))] public async Task PrintException_Structure(TestConfig config) { using Target target = await Targets.GetTargetAsync(config); @@ -35,16 +44,42 @@ public async Task PrintException_Structure(TestConfig config) Assert.NotEmpty(pe["Message"].Value); Assert.NotEmpty(pe["InnerException"].Value); // this is present because we are looking at exception with a nested exception pe.AssertContains("StackTrace (generated):"); - - SosTable table = pe.Table(("SP", Sos.Addr), ("IP", Sos.Addr), ("Function", Sos.ModuleFunctionWithOffset)); - Assert.NotEmpty(table); + AssertFrameSequence(pe, ["NestedExceptionTest.Program.Main"]); pe["HResult"].AssertValid(Sos.Hex); pe.AssertContains("There are nested exceptions on this thread"); + + SosOutput nested = target.Sos("printexception -nested"); + nested.AssertContains("Nested exception"); + nested.AssertContains("System.InvalidOperationException"); + nested.AssertContains("Invalid operation exception, outer"); + nested.AssertContains("System.FormatException"); + nested.AssertContains("Bad format exception, inner"); + Assert.Matches(@"HResult:\s+80131509", nested.Text); + Assert.Matches(@"HResult:\s+80131537", nested.Text); + if (nested.Text.Split("NestedExceptionTest.Program.Main").Length - 1 < 2) + { + throw nested.Fail("outer and inner NestedExceptionTest.Program.Main frames"); + } + + SosOutput lines = target.Sos("printexception -lines"); + lines.AssertContains("NestedExceptionTest.Program.Main"); + AssertFrameSequence( + lines, + ["NestedExceptionTest.Program.Main"], + ["NestedExceptionTest.cs @ "]); + if (config.Flavor == Flavor.Framework) + { + Assert.Matches(@"NestedExceptionTest\.cs @ (11|20)", lines.Text); + } + else + { + lines.AssertContains("NestedExceptionTest.cs @ 20"); + } } [SosTheory] - [MemberData(nameof(Matrix))] + [MemberData(nameof(NestedMatrix))] public async Task PrintException_Data(TestConfig config) { using Target target = await Targets.GetTargetAsync(config); @@ -55,8 +90,7 @@ public async Task PrintException_Data(TestConfig config) Assert.Equal("Invalid operation exception, outer", pe["Message"]); pe["InnerException"].AssertContains("System.FormatException"); - SosTable table = pe.Table(("SP", Sos.Addr), ("IP", Sos.Addr), ("Function", Sos.ModuleFunctionWithOffset)); - table.AssertContainsRow(row => row["Function"].Contains("NestedExceptionTest.Program.Main"), "Function contains NestedExceptionTest.Program.Main"); + AssertFrameSequence(pe, ["NestedExceptionTest.Program.Main"]); Assert.Equal(0x80131509u, pe.Field("HResult").AsUInt32(Sos.Hex)); @@ -64,5 +98,143 @@ public async Task PrintException_Data(TestConfig config) SosOutput inner = target.Sos($"printexception {innerExceptionAddr:x}"); Assert.Equal("System.FormatException", inner["Exception type"]); Assert.Equal("Bad format exception, inner", inner["Message"]); + Assert.Equal("", inner["InnerException"]); + Assert.Equal(0x80131537u, inner.Field("HResult").AsUInt32(Sos.Hex)); + AssertFrameSequence(inner, ["NestedExceptionTest.Program.Main"]); + } + + [SosTheory] + [MemberData(nameof(NoInnerMatrix))] + public async Task PrintException_NoInnerException(TestConfig config) + { + using Target target = await Targets.GetTargetAsync(config); + target.GoToFirstStop(); + + (string exceptionType, string message, uint hresult, string[] functions, string[] sourceLines) = config.Target switch + { + TargetCatalog.DivZero => ( + "System.DivideByZeroException", + "Attempted to divide by zero.", + 0x80020012, + new[] { "C.DivideByZero", "C.F3", "C.F2", "C.Main" }, + new[] + { + "DivZero.cs @ 15", + "DivZero.cs @ 24", + "DivZero.cs @ 36", + config.Flavor == Flavor.Framework ? "DivZero.cs @ 56" : "DivZero.cs @ 57", + }), + TargetCatalog.SimpleThrow => ( + "System.InvalidOperationException", + "Throwing an invalid operation....", + 0x80131509, + new[] { "UserObject.UseObject", "Simple.Main" }, + new[] { "UserObject.cs @ 19", "SimpleThrow.cs @ 12" }), + _ => throw new ArgumentOutOfRangeException(nameof(config), config.Target, "unexpected target"), + }; + + SosOutput pe = target.Sos("printexception"); + Assert.Equal(exceptionType, pe["Exception type"]); + Assert.Equal(message, pe["Message"]); + Assert.Equal("", pe["InnerException"]); + Assert.Equal(hresult, pe.Field("HResult").AsUInt32(Sos.Hex)); + AssertFrameSequence(pe, functions); + + SosOutput nested = target.Sos("printexception -nested"); + Assert.Equal(exceptionType, nested["Exception type"]); + Assert.Equal(message, nested["Message"]); + Assert.Equal("", nested["InnerException"]); + Assert.Equal(hresult, nested.Field("HResult").AsUInt32(Sos.Hex)); + AssertFrameSequence(nested, functions); + + SosOutput lines = target.Sos("printexception -lines"); + Assert.Equal(exceptionType, lines["Exception type"]); + Assert.Equal(message, lines["Message"]); + Assert.Equal("", lines["InnerException"]); + Assert.Equal(hresult, lines.Field("HResult").AsUInt32(Sos.Hex)); + AssertFrameSequence(lines, functions, sourceLines); + } + + [SosTheory] + [MemberData(nameof(ReflectionMatrix))] + public async Task PrintException_ReflectionInnerException(TestConfig config) + { + using Target target = await Targets.GetTargetAsync(config); + target.GoToFirstStop(); + + SosOutput pe = target.Sos("printexception"); + Assert.Equal("System.Reflection.TargetInvocationException", pe["Exception type"]); + Assert.Equal("Exception has been thrown by the target of an invocation.", pe["Message"]); + pe["InnerException"].AssertContains("System.Exception"); + Assert.Equal(0x80131604u, pe.Field("HResult").AsUInt32(Sos.Hex)); + pe.Table(("SP", Sos.Addr), ("IP", Sos.Addr), ("Function", Sos.ModuleFunctionWithOffset)) + .AssertContainsRow(row => row["Function"].Contains("RefLoader.Loader.Main"), "Function contains RefLoader.Loader.Main"); + + ulong innerExceptionAddr = pe["InnerException"].Extract(Sos.Addr); + SosOutput inner = target.Sos($"printexception {innerExceptionAddr:x}"); + Assert.Equal("System.Exception", inner["Exception type"]); + Assert.Equal("Exception from InvokedCode.Invoked.ExceptionNoHandler()", inner["Message"]); + Assert.Equal(0x80131500u, inner.Field("HResult").AsUInt32(Sos.Hex)); + + SosOutput nested = target.Sos("printexception -nested"); + nested.AssertContains("System.Reflection.TargetInvocationException"); + nested.AssertContains("System.Exception"); + + SosOutput lines = target.Sos("printexception -lines"); + AssertFrameSequence(lines, ["RefLoader.Loader.Main"], ["ReflectionTest.cs @ "]); + } + + private static void AssertFrameSequence( + SosOutput output, + IReadOnlyList expectedFunctions, + IReadOnlyList? expectedSourceLines = null) + { + if (expectedSourceLines is not null && expectedSourceLines.Count != expectedFunctions.Count) + { + throw new ArgumentException("Source-line expectations must match the function count.", nameof(expectedSourceLines)); + } + + SosTable table = output.Table("SP", "IP", "Function"); + int searchFrom = 0; + for (int expectedIndex = 0; expectedIndex < expectedFunctions.Count; expectedIndex++) + { + string expectedFunction = expectedFunctions[expectedIndex]; + int row = -1; + for (int i = searchFrom; i < table.Length; i++) + { + if (table.Row(i)["Function"].Value.Contains(expectedFunction, StringComparison.Ordinal)) + { + row = i; + break; + } + } + + if (row < 0) + { + throw output.Fail($"ordered stack frame '{expectedFunction}' at or after row {searchFrom}"); + } + + SosRow frame = table.Row(row); + if (!Sos.Addr.Matches(frame["SP"].Value) || !Sos.Addr.Matches(frame["IP"].Value)) + { + throw output.Fail($"frame '{expectedFunction}' to contain valid SP and IP addresses"); + } + + string functionCell = frame["Function"].Value; + int sourceStart = functionCell.LastIndexOf(" [", StringComparison.Ordinal); + string function = sourceStart >= 0 ? functionCell[..sourceStart] : functionCell; + if (!Sos.ModuleFunctionWithOffset.Matches(function)) + { + throw output.Fail($"frame '{expectedFunction}' to contain a module, function, and offset"); + } + if (expectedSourceLines is not null && + !functionCell.Contains(expectedSourceLines[expectedIndex], StringComparison.Ordinal)) + { + throw output.Fail( + $"frame '{expectedFunction}' to contain source annotation '{expectedSourceLines[expectedIndex]}'"); + } + + searchFrom = row + 1; + } } } diff --git a/src/tests/SOS.Tests/README.md b/src/tests/SOS.Tests/README.md index 555a5a8a4b..4f85e07919 100644 --- a/src/tests/SOS.Tests/README.md +++ b/src/tests/SOS.Tests/README.md @@ -72,7 +72,7 @@ Each theory receives one `TestConfig`, whose axes are: Notably, dotnet-dump is dump-only; cdb is Windows-only and LLDB is non-Windows; Framework is Windows-only; Server GC is Core/SingleFile dump-only; single-file Mini dumps and live LLDB navigation through stripped single-file images are -excluded; cDAC is Core-only and requires .NET 11 or later. +excluded; cDAC supports Core and SingleFile on .NET 11 or later. Tests opt into expensive axes. The default is dump, workstation GC, and Heap. Live, Server, Mini, and Full rows appear only where they exercise distinct @@ -164,6 +164,16 @@ A canonical test defines the smallest valid matrix, acquires and disposes a structure plus product data. Keep host conditionals in the matrix or host abstraction rather than duplicating the assertion body. +## Legacy coverage migration + +Legacy retirement requires assertion and matrix equivalence, not command-name +overlap. This layer retires `DivZero.script`, `NestedExceptionTest.script`, and +`SimpleThrow.script` after moving their exact exception, source-line, stack, +thread, live/dump, and CLRMA behavior into focused tests. `Reflection.script` +remains active because its reflected target-invocation boundary is still a +specialized legacy scenario. [COVERAGE.md](COVERAGE.md) records the evidence and +all remaining retained scenarios and gaps. + ## Controls Comma-separated matrix allow-lists are case-insensitive enum names: diff --git a/src/tests/SOS.Tests/RuntimeInfoTests.cs b/src/tests/SOS.Tests/RuntimeInfoTests.cs index 22987f34ba..5806b68959 100644 --- a/src/tests/SOS.Tests/RuntimeInfoTests.cs +++ b/src/tests/SOS.Tests/RuntimeInfoTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using SOS.TestHarness; using Xunit; @@ -53,7 +54,14 @@ public async Task Modules_Registers_Threads_DotnetDumpOnly(TestConfig config) target.GoToStopPoint(TargetCatalog.StopHeap); target.Sos("modules").AssertContains("SosHarnessScenarios"); - target.Sos("registers").AssertContains("rsp"); + string stackPointerRegister = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X86 => "esp", + Architecture.X64 => "rsp", + Architecture.Arm or Architecture.Arm64 => "sp", + _ => throw new PlatformNotSupportedException(), + }; + target.Sos("registers").AssertContains(stackPointerRegister); // The debuggee parks several worker threads, so the thread list has multiple entries. SosOutput threads = target.Sos("threads"); diff --git a/src/tests/SOS.Tests/SpecializedInspectionTests.cs b/src/tests/SOS.Tests/SpecializedInspectionTests.cs index a05982488f..2736b0f7a7 100644 --- a/src/tests/SOS.Tests/SpecializedInspectionTests.cs +++ b/src/tests/SOS.Tests/SpecializedInspectionTests.cs @@ -53,6 +53,22 @@ public async Task ThreadPool_ReportsWorkerStats(TestConfig config) [MemberData(nameof(Matrix))] public async Task SyncBlk_ReportsInflatedMonitor(TestConfig config) { + if (RepoLayout.Rid.StartsWith("linux-musl-", StringComparison.Ordinal)) + { + HarnessSkipException.Now( + "SyncBlock data is unavailable from the DAC on Linux musl, so syncblk cannot report inflated monitors."); + } + + if (OperatingSystem.IsMacOS() && + config.Host == Host.DotnetDump && + config.Dac == Dac.Legacy && + config.Flavor == Flavor.Core && + config.CoreVersion == CoreVersion.Net11) + { + HarnessSkipException.Now( + "https://github.com/dotnet/diagnostics/issues/5985: legacy DAC SyncBlock data is unavailable for macOS .NET 11 dumps."); + } + using Target target = await Targets.GetTargetAsync(config); target.GoToStopPoint(TargetCatalog.StopHeap); diff --git a/src/tests/SOS.Tests/StackInspectionTests.cs b/src/tests/SOS.Tests/StackInspectionTests.cs index 1144457add..8f254ac318 100644 --- a/src/tests/SOS.Tests/StackInspectionTests.cs +++ b/src/tests/SOS.Tests/StackInspectionTests.cs @@ -17,7 +17,8 @@ public sealed class StackInspectionTests { // Live opt-in: !dso (dumpstackobjects) scans a live thread's stack memory for object references, so // DumpStackObjects_ListsStackRoots runs dump AND live; the other stack commands here stay dump-only. - public static TheoryData Matrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], liveness: Liveness.AllValid); + public static TheoryData Matrix => + TestMatrices.CurrentThreadCommands([TargetCatalog.Scenarios], Liveness.AllValid); public static TheoryData CdbMatrix => TestMatrices.FullDumpOnCoreVersions([TargetCatalog.Scenarios], CoreVersion.Net8 | CoreVersion.Net9 | CoreVersion.Net10, Flavor.AllValid, Host.Cdb); public static TheoryData DotnetDumpMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.DotnetDump); diff --git a/src/tests/SOS.Tests/TestConfigValidityTests.cs b/src/tests/SOS.Tests/TestConfigValidityTests.cs index 1b550c33f9..14d32177df 100644 --- a/src/tests/SOS.Tests/TestConfigValidityTests.cs +++ b/src/tests/SOS.Tests/TestConfigValidityTests.cs @@ -34,15 +34,14 @@ public void SingleFileSnapshotExclusionRejectsInvalidValues(string value) } [Fact] - public void CDacRequiresSupportedCoreConfiguration() + public void CDacRequiresSupportedNet11Configuration() { TestConfig config = Config() with { Dac = Dac.CDac, CoreVersion = CoreVersion.Net11 }; Assert.True(TestConfig.IsDacSupported(config)); Assert.False(TestConfig.IsDacSupported(config with { CoreVersion = CoreVersion.Net10 })); Assert.False(TestConfig.IsDacSupported(config with { Flavor = Flavor.Framework })); - Assert.False(TestConfig.IsDacSupported(config with { Flavor = Flavor.SingleFile })); - Assert.False(TestConfig.IsValid(config with { Flavor = Flavor.SingleFile })); + Assert.True(TestConfig.IsDacSupported(config with { Flavor = Flavor.SingleFile })); } [Theory] diff --git a/src/tests/SOS.Tests/TestMatrices.cs b/src/tests/SOS.Tests/TestMatrices.cs index bd8600a3c5..6a6cb26fc3 100644 --- a/src/tests/SOS.Tests/TestMatrices.cs +++ b/src/tests/SOS.Tests/TestMatrices.cs @@ -8,11 +8,6 @@ namespace SOS.Tests; internal static class TestMatrices { - /// - /// Matrix for commands that use the managed stack walker. The universal cDAC can inspect a - /// single-file runtime, but it cannot currently start a stack walk for one (SOS reports - /// COR_E_INVALIDOPERATION), so that combination is not a supported stack-walk configuration. - /// public static TheoryData StackWalk( string[] targets, Flavor flavor = Flavor.AllValid, @@ -21,10 +16,11 @@ public static TheoryData StackWalk( GcType gcType = GcType.Workstation, DumpKind dumpKind = DumpKind.Heap, CoreVersion coreVersion = CoreVersion.All, - Dac dac = Dac.All) + Dac dac = Dac.All, + Func? filter = null) { TheoryData data = new(); - foreach (TestConfig config in StackWalkConfigs(targets, flavor, host, liveness, gcType, dumpKind, coreVersion, dac)) + foreach (TestConfig config in StackWalkConfigs(targets, flavor, host, liveness, gcType, dumpKind, coreVersion, dac, filter)) { data.Add(config); } @@ -40,12 +36,40 @@ public static IEnumerable StackWalkConfigs( GcType gcType = GcType.Workstation, DumpKind dumpKind = DumpKind.Heap, CoreVersion coreVersion = CoreVersion.All, - Dac dac = Dac.All) => + Dac dac = Dac.All, + Func? filter = null) => + // .NET 11 cDAC supports SingleFile stack walks; TestConfig rejects cDAC on earlier runtimes. TestConfig.Permutations(targets, flavor, host, liveness, gcType, dumpKind, coreVersion: coreVersion, dac: dac) - .Where(SupportsStackWalk); + .Where(SupportsCurrentThread) + .Where(config => filter is null || filter(config)); - public static bool SupportsStackWalk(TestConfig config) => - config.Flavor != Flavor.SingleFile || config.Dac != Dac.CDac; + public static TheoryData HeapEnumeration(string[] targets) + { + TheoryData data = new(); + foreach (TestConfig config in TestConfig.Permutations(targets).Where(SupportsHeapEnumeration)) + { + data.Add(config); + } + + return data; + } + + public static TheoryData CurrentThreadCommands( + string[] targets, + Liveness liveness = Liveness.Dump, + DumpKind dumpKind = DumpKind.Heap) + { + TheoryData data = new(); + foreach (TestConfig config in TestConfig.Permutations( + targets, + liveness: liveness, + dumpKind: dumpKind).Where(SupportsCurrentThread)) + { + data.Add(config); + } + + return data; + } /// /// Wraps for commands whose data is absent from a reduced Heap dump on @@ -97,10 +121,18 @@ public static TheoryData StackWalkFullDumpOnCoreVersions( Host host = Host.AllValid, Liveness liveness = Liveness.Dump, CoreVersion coreVersion = CoreVersion.All, - Dac dac = Dac.All) + Dac dac = Dac.All, + Func? filter = null) { TheoryData data = new(); - foreach (TestConfig config in StackWalkConfigs(targets, flavor, host, liveness, coreVersion: coreVersion, dac: dac)) + foreach (TestConfig config in StackWalkConfigs( + targets, + flavor, + host, + liveness, + coreVersion: coreVersion, + dac: dac, + filter: filter)) { data.Add(OperatingSystem.IsWindows() && (config.CoreVersion & fullDumpVersions) != 0 ? config with { DumpKind = DumpKind.Full } @@ -110,6 +142,36 @@ public static TheoryData StackWalkFullDumpOnCoreVersions( return data; } + internal static bool SupportsHeapEnumeration(TestConfig config) => + SupportsHeapEnumeration(config, OperatingSystem.IsWindows()); + + internal static bool SupportsHeapEnumeration(TestConfig config, bool isWindows) => + // https://github.com/dotnet/runtime/pull/132938: dbgeng /mw dumps omit the WKS card-table + // pointer slot, so cDAC cannot construct a heap until the runtime fix flows into this repo. + // Windows SingleFile crash dumps use dbgeng capture regardless of the later analysis host. + !isWindows + || config.Flavor != Flavor.SingleFile + || config.Dac != Dac.CDac; + + internal static bool SupportsGcRootEnumeration(TestConfig config) => + // Desktop SOS can fail GC-reference enumeration or corrupt the debugger host in clrstack -gc. + config.Flavor != Flavor.Framework; + + internal static bool SupportsCurrentThread(TestConfig config) => + // createdump ELF cores expose synthetic runtime thread IDs that LLDB cannot select, so + // current-thread commands report "The current thread is unmanaged" even on the crash thread. + config.Host != Host.Lldb || config.Liveness != Liveness.Dump; + + internal static bool SupportsICorDebugStackWalk(TestConfig config) => + SupportsICorDebugStackWalk(config, Environment.Is64BitProcess); + + internal static bool SupportsICorDebugStackWalk(TestConfig config, bool is64BitProcess) => + // Desktop x64 ICorDebug returns no frames for a dump captured at DivZero's second-chance crash. + !is64BitProcess + || config.Host != Host.Cdb + || config.Target != TargetCatalog.DivZero + || config.Flavor != Flavor.Framework; + public static TheoryData CoreFrameworkConditional(string[] targets) { TheoryData data = new(); diff --git a/src/tests/SOS.Tests/TestMatrixCapabilityTests.cs b/src/tests/SOS.Tests/TestMatrixCapabilityTests.cs new file mode 100644 index 0000000000..515d7655f4 --- /dev/null +++ b/src/tests/SOS.Tests/TestMatrixCapabilityTests.cs @@ -0,0 +1,84 @@ +// 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; + +public sealed class TestMatrixCapabilityTests +{ + [Fact] + public void HeapEnumerationExcludesWindowsSingleFileCDacDumps() + { + TestConfig config = Config(Host.Cdb, Dac.CDac) with + { + Flavor = Flavor.SingleFile, + CoreVersion = CoreVersion.Net11, + }; + + Assert.False(TestMatrices.SupportsHeapEnumeration(config, isWindows: true)); + Assert.True(TestMatrices.SupportsHeapEnumeration(config, isWindows: false)); + Assert.False(TestMatrices.SupportsHeapEnumeration(config with { Host = Host.DotnetDump }, isWindows: true)); + Assert.True(TestMatrices.SupportsHeapEnumeration(config with { Dac = Dac.Legacy }, isWindows: true)); + Assert.True(TestMatrices.SupportsHeapEnumeration(config with { Flavor = Flavor.Core }, isWindows: true)); + } + + [Fact] + public void GcRootEnumerationExcludesOnlyFramework() + { + TestConfig config = Config(Host.Cdb, Dac.Legacy); + + Assert.True(TestMatrices.SupportsGcRootEnumeration(config)); + Assert.False(TestMatrices.SupportsGcRootEnumeration(config with { Flavor = Flavor.Framework })); + } + + [Fact] + public void CurrentThreadCommandsExcludeOnlyLldbDumps() + { + TestConfig config = Config(Host.Lldb, Dac.Legacy); + + Assert.False(TestMatrices.SupportsCurrentThread(config)); + Assert.True(TestMatrices.SupportsCurrentThread(config with { Liveness = Liveness.Live })); + Assert.True(TestMatrices.SupportsCurrentThread(config with { Host = Host.DotnetDump })); + Assert.True(TestMatrices.SupportsCurrentThread(config with { Host = Host.Cdb })); + } + + [Fact] + public void ICorDebugStackWalkExcludesOnlyX64CdbFrameworkDivZeroDumps() + { + TestConfig config = Config(Host.Cdb, Dac.Legacy) with + { + Target = TargetCatalog.DivZero, + Flavor = Flavor.Framework, + }; + + Assert.False(TestMatrices.SupportsICorDebugStackWalk(config, is64BitProcess: true)); + Assert.True(TestMatrices.SupportsICorDebugStackWalk(config, is64BitProcess: false)); + Assert.True(TestMatrices.SupportsICorDebugStackWalk(config with { Host = Host.DotnetDump }, is64BitProcess: true)); + Assert.True(TestMatrices.SupportsICorDebugStackWalk(config with { Target = TargetCatalog.Scenarios }, is64BitProcess: true)); + Assert.True(TestMatrices.SupportsICorDebugStackWalk(config with { Flavor = Flavor.Core }, is64BitProcess: true)); + } + + [Theory] + [InlineData("Calculating live objects, this may take a while...", true)] + [InlineData("Calculating live objects complete: 42 objects from 3 roots", true)] + [InlineData("Caching GC roots, this may take a while.", true)] + [InlineData("Subsequent runs of this command will be faster.", true)] + [InlineData("0000000123456780", false)] + public void NotReachableInRangeIgnoresOnlyLivenessProgress(string line, bool expected) + { + Assert.Equal(expected, NativeAddressSpaceTests.IsLivenessProgress(line)); + } + + private static TestConfig Config(Host host, Dac dac) => + new( + TargetCatalog.Scenarios, + host, + Flavor.Core, + Liveness.Dump, + GcType.Workstation, + DumpKind.Heap, + CoreVersion.Net10, + dac); +} diff --git a/src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.cs b/src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.cs index d3a294b466..eea6557523 100644 --- a/src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.cs +++ b/src/tests/SOS.UnitTests/Debuggees/SosHarnessScenarios/SosHarnessScenarios.cs @@ -125,6 +125,7 @@ private static void Main() TextField = FieldMarkerText, Value = new ValueMarker { First = ValueMarkerFirst, Second = ValueMarkerSecond }, Numbers = known, + ObjectReferences = new[] { new ObjectReference { Value = live } }, MethodSignature = new byte[] { 0x00, 0x00, 0x01 }, // [DEFAULT] Void () SignatureElement = new byte[] { 0x08 }, // ELEMENT_TYPE_I4 }; @@ -373,6 +374,13 @@ public struct ValueMarker public long Second; } +// A value-type array element containing exactly one object-reference slot. !dumparray reports the element's +// address, giving notreachableinrange a real pointer range to scan without relying on object layout. +public struct ObjectReference +{ + public object? Value; +} + // A reference type with known instance fields of several shapes (primitive, wide primitive, reference, // and an embedded value type), so dumpobj prints a non-trivial Fields table to assert against. public sealed class FieldMarker @@ -382,6 +390,7 @@ public sealed class FieldMarker public string? TextField; public ValueMarker Value; public int[]? Numbers; + public ObjectReference[]? ObjectReferences; // Raw COR_SIGNATURE blobs so dumpsig/dumpsigelem can decode a real signature: a method signature // [DEFAULT] Void () = { CALLCONV_DEFAULT(0x00), argCount 0, ELEMENT_TYPE_VOID(0x01) }, and a single diff --git a/src/tests/SOS.UnitTests/SOS.cs b/src/tests/SOS.UnitTests/SOS.cs index 309a8f1b1e..6051bf33a6 100644 --- a/src/tests/SOS.UnitTests/SOS.cs +++ b/src/tests/SOS.UnitTests/SOS.cs @@ -365,34 +365,6 @@ public SOSExceptionTests(ITestOutputHelper output) private ITestOutputHelper Output { get; set; } - [SkippableTheory, MemberData(nameof(SOSTestHelpers.Configurations), MemberType = typeof(SOSTestHelpers))] - public async Task DivZero(TestConfiguration config) - { - await SOSTestHelpers.RunTest( - config, - debuggeeName: "DivZero", - scriptName: "DivZero.script", - Output, - testTriage: true); - } - - [SkippableTheory, MemberData(nameof(SOSTestHelpers.Configurations), MemberType = typeof(SOSTestHelpers))] - public async Task SimpleThrow(TestConfiguration config) - { - await SOSTestHelpers.RunTest(config, debuggeeName: "SimpleThrow", scriptName: "SimpleThrow.script", Output, testTriage: true); - } - - [SkippableTheory, MemberData(nameof(SOSTestHelpers.Configurations), MemberType = typeof(SOSTestHelpers))] - public async Task NestedExceptionTest(TestConfiguration config) - { - await SOSTestHelpers.RunTest( - config, - debuggeeName: "NestedExceptionTest", - scriptName: "NestedExceptionTest.script", - Output, - testTriage: true); - } - [SkippableTheory, MemberData(nameof(SOSTestHelpers.Configurations), MemberType = typeof(SOSTestHelpers))] public async Task TaskNestedException(TestConfiguration config) { diff --git a/src/tests/SOS.UnitTests/Scripts/DivZero.script b/src/tests/SOS.UnitTests/Scripts/DivZero.script deleted file mode 100644 index 1bf4e66633..0000000000 --- a/src/tests/SOS.UnitTests/Scripts/DivZero.script +++ /dev/null @@ -1,70 +0,0 @@ -# Divide By Zero debugging scenario -# 1) Load the executable -# 2) Run the executable and wait for it to crash -# 3) Take a dump of the executable. -# 4) Open the dump and compare the output - -CONTINUE - -LOADSOS - -# Verifying that PrintException gives us the right exception in the format above. -SOSCOMMAND:PrintException -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.DivideByZeroException\s+ -VERIFY:Message:\s+(|Attempted to divide by zero\.)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace \(generated\):\s+ -VERIFY:\s+SP\s+IP\s+\s+Function\s+ -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.DivideByZero(\(.*\))?\+0x\s+ -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.F3(\(.*\))?\+0x\s+ -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.F2(\(.*\))?\+0x\s+ -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.Main(\(.*\))?\+0x\s+ -VERIFY:(StackTraceString: \s+)?\s+ -VERIFY:HResult:\s+80020012\s+ - -SOSCOMMAND:PrintException -lines -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.DivideByZeroException\s+ -VERIFY:Message:\s+(|Attempted to divide by zero\.)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace \(generated\):\s+ -VERIFY:\s+SP\s+IP\s+\s+Function\s+ -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.DivideByZero(\(.*\))?\+0x\s+ -VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ 15\s*\]\s* -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.F3(\(.*\))?\+0x\s+ -VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ 24\s*\]\s* -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.F2(\(.*\))?\+0x\s+ -VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ 36\s*\]\s* -VERIFY:\s+\s+\s+[Dd]iv[Zz]ero.*!C\.Main(\(.*\))?\+0x\s+ -IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ 56\s*\]\s* -ENDIF:DESKTOP -!IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*DivZero[\\|/]DivZero\.cs @ 57\s*\]\s* -ENDIF:DESKTOP - -# Verify that Threads (clrthreads) works -SOSCOMMAND:clrthreads -VERIFY:\s*ThreadCount:\s+\s+ -VERIFY:\s+UnstartedThread:\s+\s+ -VERIFY:\s+BackgroundThread:\s+\s+ -VERIFY:\s+PendingThread:\s+\s+ -VERIFY:\s+DeadThread:\s+\s+ -VERIFY:\s+Hosted Runtime:\s+no\s+ -VERIFY:\s+ID\s+OSID\s+ThreadOBJ\s+State.*\s+ -VERIFY:\s+\s+\s+\s+.*\s+ - -# Verify that ClrStack with no options works -SOSCOMMAND:ClrStack -VERIFY:.*OS Thread Id:\s+0x\s+.* -VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:\s+\s+\s+(\*\*\* WARNING: Unable to verify checksum for DivZero.exe\s*)?C\.DivideByZero(\(.*\))?\s+\[(?i:.*[\\|/]DivZero\.cs) @ 15\s*\]\s+ -VERIFY:\s+\s+\s+C\.F3(\(.*\))?\s+\[(?i:.*[\\|/]DivZero\.cs) @ 24\s*\]\s+ -VERIFY:\s+\s+\s+C\.F2(\(.*\))?\s+\[(?i:.*[\\|/]DivZero\.cs) @ 36\s*\]\s+ -IFDEF:DESKTOP -VERIFY:\s+\s+\s+C\.Main(\(.*\))?\s+\[(?i:.*[\\|/]DivZero\.cs) @ 56\s*\]\s+ -ENDIF:DESKTOP -!IFDEF:DESKTOP -VERIFY:\s+\s+\s+C\.Main(\(.*\))?\s+\[(?i:.*[\\|/]DivZero\.cs) @ 57\s*\]\s+ -ENDIF:DESKTOP diff --git a/src/tests/SOS.UnitTests/Scripts/NestedExceptionTest.script b/src/tests/SOS.UnitTests/Scripts/NestedExceptionTest.script deleted file mode 100644 index 833ba12d24..0000000000 --- a/src/tests/SOS.UnitTests/Scripts/NestedExceptionTest.script +++ /dev/null @@ -1,144 +0,0 @@ -# Nested exception debugging scenario -# 1) load the executable -# 2) run to the exception -# 3) load sos -# We are only verifying the main PrintException fields and for the stacktrace, source lines in the program. - -LOADSOS - -# Verify that bpmd works -IFDEF:LIVE -# Issue: https://github.com/dotnet/diagnostics/issues/2459 -!IFDEF:ALPINE -# Issue: https://github.com/dotnet/diagnostics/issues/2673 -!IFDEF:UNIX_SINGLE_FILE_APP - -IFDEF:DESKTOP -SOSCOMMAND:bpmd NestedExceptionTest.exe NestedExceptionTest.Program.Main -ENDIF:DESKTOP -!IFDEF:DESKTOP -SOSCOMMAND:bpmd NestedExceptionTest.dll NestedExceptionTest.Program.Main -ENDIF:DESKTOP -CONTINUE - -# Unloads desktop sos and reloads new SOS -IFDEF:DESKTOP -LOADSOS -ENDIF:DESKTOP - -SOSCOMMAND:ClrStack -VERIFY:\s+\s+\s+NestedExceptionTest\.Program\.Main(\(.*\))?\s* -VERIFY:\[.*[\\|/]Debuggees[\\|/].*[Nn]ested[Ee]xception[Tt]est[\\|/][Nn]ested[Ee]xception[Tt]est\.cs @ (11|16)\s*\]\s* - -ENDIF:UNIX_SINGLE_FILE_APP -ENDIF:ALPINE -ENDIF:LIVE - -CONTINUE - -# 4) Verifying that !pe gives us the right exception in the format above. -SOSCOMMAND:PrintException -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Invalid operation exception, outer)\s+ -VERIFY:InnerException:\s+System\.FormatException, Use !?printexception to see more.\s+ -VERIFY:StackTrace \(generated\):\s+ -VERIFY:\s+SP\s+IP\s+\s+Function\s+ -VERIFY:\s+\s+\s+[Nn]ested[Ee]xception[Tt]est.*!NestedExceptionTest\.Program\.Main(\(.*\))?\+0x\s+ -VERIFY:(StackTraceString: \s+)?\s+ -VERIFY:HResult:\s+80131509\s+ -VERIFY:There are nested exceptions on this thread. Run with -nested for details\s+ - -# 5) Verifying that -nested gives us more info. -SOSCOMMAND:PrintException -nested -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Invalid operation exception, outer)\s+ -VERIFY:InnerException:\s+System\.FormatException, Use !?printexception to see more.\s+ -VERIFY:StackTrace \(generated\):\s+ -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Nn]ested[Ee]xception[Tt]est.*!NestedExceptionTest\.Program\.Main(\(.*\))?\+0x\s+ -VERIFY:(StackTraceString: \s+)?\s+ -VERIFY:HResult:\s+80131509\s+ -VERIFY:Nested exception\s+[\-]+\s+ -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.FormatException\s+ -VERIFY:Message:\s+(|Bad format exception, inner)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace \(generated\):\s+ -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Nn]ested[Ee]xception[Tt]est.*!NestedExceptionTest\.Program\.Main(\(.*\))?\+0x\s+ -VERIFY:(StackTraceString: \s+)? -VERIFY:HResult:\s+80131537 - -# 6) Verifying that the Exception Object/ID gives us the same output about the reflected exception. -SOSCOMMAND:PrintException InnerException:\s+System\.FormatException, Use !?printexception () to see more -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.FormatException\s+ -VERIFY:Message:\s+(|Bad format exception, inner)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace \(generated\): -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Nn]ested[Ee]xception[Tt]est.*!NestedExceptionTest\.Program\.Main(\(.*\))?\+0x\s+ -VERIFY:(StackTraceString: \s+)? -VERIFY:HResult:\s+80131537\s+ -VERIFY:There are nested exceptions on this thread. Run with -nested for details - -# 7) Verifying that there is source file and line number info -SOSCOMMAND:PrintException -lines -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Invalid operation exception, outer)\s+ -VERIFY:InnerException:\s+System\.FormatException, Use !?printexception to see more.\s+ -VERIFY:StackTrace \(generated\): -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Nn]ested[Ee]xception[Tt]est.*!NestedExceptionTest\.Program\.Main(\(.*\))?\+0x\s* -IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*[Nn]ested[Ee]xception[Tt]est[\\|/][Nn]ested[Ee]xception[Tt]est\.cs @ (11|20)\s*\]\s* -ENDIF:DESKTOP -!IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*[Nn]ested[Ee]xception[Tt]est[\\|/][Nn]ested[Ee]xception[Tt]est\.cs @ 20\s*\]\s* -ENDIF:DESKTOP -VERIFY:(StackTraceString: \s+)? -VERIFY:HResult:\s+80131509\s+ -VERIFY:There are nested exceptions on this thread. Run with -nested for details - -# Verify that Threads (clrthreads) works -SOSCOMMAND:clrthreads -VERIFY:\s*ThreadCount:\s+\s+ -VERIFY:\s+UnstartedThread:\s+\s+ -VERIFY:\s+BackgroundThread:\s+\s+ -VERIFY:\s+PendingThread:\s+\s+ -VERIFY:\s+DeadThread:\s+\s+ -VERIFY:\s+Hosted Runtime:\s+no\s+ -VERIFY:\s+ID\s+OSID\s+ThreadOBJ\s+State.*\s+ -VERIFY:\s+\s+\s+\s+.*\s+ - -# Verify that ClrStack with no options works -SOSCOMMAND:ClrStack -VERIFY:.*OS Thread Id:\s+0x\s+.* -VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:\s+\s+\s+NestedExceptionTest\.Program\.Main(\(.*\))?\s* -IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*[Nn]ested[Ee]xception[Tt]est[\\|/][Nn]ested[Ee]xception[Tt]est\.cs @ (11|20)\s*\]\s* -ENDIF:DESKTOP -!IFDEF:DESKTOP -VERIFY:\[.*[\\|/]Debuggees[\\|/].*[Nn]ested[Ee]xception[Tt]est[\\|/][Nn]ested[Ee]xception[Tt]est\.cs @ 20\s*\]\s* -ENDIF:DESKTOP - -# Verify the SOS clrma command (CLRMA managed analysis) under dotnet-dump. This exercises the -# CLRMACreateInstance/ICLRManagedAnalysis path used by Watson/!analyze, but in the dotnet-dump -# host so it can be validated locally without windbg/lldb. We only check the structural fields -# (provider, current exception type/HResult and the inner exception type/HResult) that !pe above -# already proved are available on these dumps. -IFDEF:DOTNETDUMP -SOSCOMMAND:clrma -VERIFY:Managed analysis provider:\s+SOSCLRMA\s+ -VERIFY:OSThreadId:\s+\s+ -VERIFY:Current exception:\s+ -VERIFY:\s+Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:\s+HResult:\s+80131509\s+ -VERIFY:\s+InnerException:\s+ -VERIFY:\s+Exception type:\s+System\.FormatException\s+ -VERIFY:\s+HResult:\s+80131537\s+ -ENDIF:DOTNETDUMP diff --git a/src/tests/SOS.UnitTests/Scripts/SimpleThrow.script b/src/tests/SOS.UnitTests/Scripts/SimpleThrow.script deleted file mode 100644 index 8ae602b04b..0000000000 --- a/src/tests/SOS.UnitTests/Scripts/SimpleThrow.script +++ /dev/null @@ -1,66 +0,0 @@ -# PrintException debugging scenario -# 1) Load the executable -# 2) Run the executable and wait for it to crash -# 3) Take a dump of the executable. (MUST BE AT SECOND CHANCE EXCEPTION!) -# 4) Open the dump and compare the output - -CONTINUE - -LOADSOS - -# B) Verifying that !pe gives us the right exception in the format above. -# For desktop, it has an SP, so we conditionally will match it. (ProjectN does not though.) -SOSCOMMAND:PrintException -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Throwing an invalid operation\.\.\.\.)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace\s+\(generated\):\s+ -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?UserObject\.UseObject.*\+0x\s+ -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?Simple\.Main.*\+0x\s+ -VERIFY:HResult:\s+80131509 - -# C) Verifying that -nested gives us the same output -SOSCOMMAND:PrintException -nested -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Throwing an invalid operation\.\.\.\.)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace\s+\(generated\):\s+ -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?UserObject\.UseObject.*\+0x\s+ -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?Simple\.Main.*\+0x\s+ -VERIFY:HResult:\s+80131509 - -SOSCOMMAND:PrintException -lines -VERIFY:Exception object:\s+\s+ -VERIFY:Exception type:\s+System\.InvalidOperationException\s+ -VERIFY:Message:\s+(|Throwing an invalid operation\.\.\.\.)\s+ -VERIFY:InnerException:\s+\s+ -VERIFY:StackTrace\s+\(generated\):\s+ -VERIFY:\s+SP\s+IP\s+Function\s+ -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?UserObject\.UseObject.*\+0x\s* -VERIFY:\[.*[\\|/]Debuggees[\\|/].*SimpleThrow[\\|/]UserObject\.cs @ 19\s*\]\s* -VERIFY:\s+\s+\s+[Ss]imple[Tt]hrow.*!(\$0_)?Simple\.Main.*\+0x\s+ -VERIFY:\[.*[\\|/]Debuggees[\\|/].*SimpleThrow[\\|/]SimpleThrow\.cs @ 12\s*\]\s* - -# Verify that Threads (clrthreads) works -SOSCOMMAND:clrthreads -VERIFY:\s*ThreadCount:\s+\s+ -VERIFY:\s+UnstartedThread:\s+\s+ -VERIFY:\s+BackgroundThread:\s+\s+ -VERIFY:\s+PendingThread:\s+\s+ -VERIFY:\s+DeadThread:\s+\s+ -VERIFY:\s+Hosted Runtime:\s+no\s+ -VERIFY:\s+ID\s+OSID\s+ThreadOBJ\s+State.*\s+ -VERIFY:\s+\s+\s+\s+.*\s+ - -# Verify that ClrStack with no options works -SOSCOMMAND:ClrStack -VERIFY:.*OS Thread Id:\s+0x\s+.* -VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:\s+\s+\s+(\*\*\* WARNING: Unable to verify checksum for SimpleThrow.exe\s*)?UserObject\.UseObject.*\s+ -VERIFY:[.*[\\|/]Debuggees[\\|/].*SimpleThrow[\\|/]UserObject\.cs @ 19\s*\] -VERIFY:\s+\s+\s+Simple\.Main\(\)\s+ -VERIFY:[.*[\\|/]Debuggees[\\|/].*SimpleThrow[\\|/]SimpleThrow\.cs @ 12\s*\]