diff --git a/eng/Subsets.props b/eng/Subsets.props
index 2b9028d5df1d61..ccc00152a2bb07 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -165,7 +165,7 @@
$(AllSubsetsExpansion)+clr.paltests+clr.paltestlist+clr.hosts+clr.jit+clr.alljits+clr.alljitscommunity+clr.spmi+clr.corelib+clr.nativecorelib+clr.tools+clr.toolstests+clr.packages
$(AllSubsetsExpansion)+linuxdac+alpinedac
$(AllSubsetsExpansion)+mono.runtime+provision.emsdk+mono.aotcross+mono.corelib+mono.manifests+mono.packages+mono.tools+mono.wasmruntime+mono.wasiruntime+mono.wasmworkload+mono.mscordbi+mono.workloads
- $(AllSubsetsExpansion)+tools.illink+tools.cdac+tools.illinktests+tools.cdactests
+ $(AllSubsetsExpansion)+tools.illink+tools.cdac+tools.illinktests+tools.cdactests+tools.cdacdumptests
$(AllSubsetsExpansion)+host.native+host.pkg+host.tools+host.pretest+host.tests
$(AllSubsetsExpansion)+libs.native+libs.sfx+libs.oob+libs.pretest+libs.tests
$(AllSubsetsExpansion)+packs.product+packs.installers+packs.tests
@@ -253,6 +253,7 @@
+
@@ -523,6 +524,10 @@
+
+
+
+
diff --git a/eng/Versions.props b/eng/Versions.props
index bfa35f15609e3b..d1955761a0f492 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -155,6 +155,7 @@
13.0.3
1.0.2
4.18.4
+ 3.1.512801
8.0.2
2.14.3
2.9.1
diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml
index 3fde402abd2009..a1e704baf30402 100644
--- a/eng/pipelines/runtime-diagnostics.yml
+++ b/eng/pipelines/runtime-diagnostics.yml
@@ -5,6 +5,13 @@ parameters:
displayName: Diagnostics Branch
type: string
default: main
+- name: cdacDumpPlatforms
+ displayName: cDAC Dump Platforms
+ type: object
+ default:
+ - windows_x64
+ - linux_x64
+ - osx_x64
resources:
repositories:
@@ -134,3 +141,82 @@ extends:
buildConfiguration: $(_BuildConfig)
continueOnError: true
condition: always()
+
+ #
+ # cDAC Dump Creation — Build runtime, create crash dumps, publish dump artifacts
+ #
+ - stage: DumpCreation
+ dependsOn: []
+ jobs:
+ - template: /eng/pipelines/common/platform-matrix.yml
+ parameters:
+ jobTemplate: /eng/pipelines/common/global-build-job.yml
+ buildConfig: release
+ platforms: ${{ parameters.cdacDumpPlatforms }}
+ jobParameters:
+ buildArgs: -s clr+libs+tools.cdac -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig)
+ nameSuffix: CdacDumpGeneration
+ timeoutInMinutes: 120
+ postBuildSteps:
+ - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) build
+ $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
+ /p:CIDumpVersionsOnly=true
+ /p:TargetArchitecture=$(archType)
+ -bl:$(Build.SourcesDirectory)/artifacts/log/DumpGeneration.binlog
+ displayName: 'Build Dump Tests and Generate Dumps'
+ - template: /eng/pipelines/common/upload-artifact-step.yml
+ parameters:
+ rootFolder: $(Build.SourcesDirectory)/artifacts/dumps/cdac
+ includeRootFolder: false
+ archiveType: tar
+ archiveExtension: .tar.gz
+ tarCompression: gz
+ artifactName: CdacDumps_$(osGroup)_$(archType)
+ displayName: cDAC Dump Artifacts
+
+ #
+ # cDAC Dump Tests — Download dumps from all platforms, run tests cross-platform
+ #
+ - stage: DumpTest
+ dependsOn:
+ - DumpCreation
+ jobs:
+ - template: /eng/pipelines/common/platform-matrix.yml
+ parameters:
+ jobTemplate: /eng/pipelines/common/global-build-job.yml
+ buildConfig: release
+ platforms: ${{ parameters.cdacDumpPlatforms }}
+ jobParameters:
+ buildArgs: -s tools.cdacdumptests /p:SkipDumpGeneration=true /p:SkipDumpVersions=net10.0
+ nameSuffix: CdacDumpTests
+ timeoutInMinutes: 60
+ postBuildSteps:
+ # Download and test against dumps from each platform
+ - ${{ each dumpPlatform in parameters.cdacDumpPlatforms }}:
+ - template: /eng/pipelines/common/download-artifact-step.yml
+ parameters:
+ artifactName: CdacDumps_${{ dumpPlatform }}
+ artifactFileName: CdacDumps_${{ dumpPlatform }}.tar.gz
+ unpackFolder: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }}
+ displayName: '${{ dumpPlatform }} Dumps'
+ - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) test
+ $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
+ --no-build
+ --logger "trx;LogFileName=CdacDumpTests_${{ dumpPlatform }}.trx"
+ --results-directory $(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}
+ displayName: 'Run cDAC Dump Tests (${{ dumpPlatform }} dumps)'
+ continueOnError: true
+ env:
+ CDAC_DUMP_ROOT: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }}
+ - task: PublishTestResults@2
+ displayName: 'Publish Results ($(osGroup)-$(archType) → ${{ dumpPlatform }})'
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: '**/*.trx'
+ searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}'
+ testRunTitle: 'cDAC Dump Tests $(osGroup)-$(archType) → ${{ dumpPlatform }}'
+ failTaskOnFailedTests: true
+ publishRunAttachments: true
+ buildConfiguration: $(_BuildConfig)
+ continueOnError: true
+ condition: always()
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs
index 05fba0fc545ff8..ccdc5017b5d5b8 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs
@@ -33,7 +33,10 @@ public Thread(Target target, TargetPointer address)
// Address of the exception tracker
ExceptionTracker = address + (ulong)type.Fields[nameof(ExceptionTracker)].Offset;
- UEWatsonBucketTrackerBuckets = target.ReadPointer(address + (ulong)type.Fields[nameof(UEWatsonBucketTrackerBuckets)].Offset);
+ // UEWatsonBucketTrackerBuckets does not exist on certain platforms
+ UEWatsonBucketTrackerBuckets = type.Fields.TryGetValue(nameof(UEWatsonBucketTrackerBuckets), out Target.FieldInfo watsonFieldInfo)
+ ? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset)
+ : TargetPointer.Null;
ThreadLocalDataPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(ThreadLocalDataPtr)].Offset);
}
diff --git a/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs
new file mode 100644
index 00000000000000..ead5a6ad29e4ac
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Diagnostics.Runtime;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Wraps a ClrMD DataTarget to provide the memory read callback and symbol lookup
+/// needed to create a from a crash dump.
+///
+internal sealed class ClrMdDumpHost : IDisposable
+{
+ private static readonly string[] s_runtimeModuleNames =
+ {
+ "coreclr.dll",
+ "libcoreclr.so",
+ "libcoreclr.dylib",
+ };
+
+ private readonly DataTarget _dataTarget;
+
+ public string DumpPath { get; }
+
+ private ClrMdDumpHost(string dumpPath, DataTarget dataTarget)
+ {
+ DumpPath = dumpPath;
+ _dataTarget = dataTarget;
+ }
+
+ ///
+ /// Open a crash dump and prepare it for cDAC analysis.
+ ///
+ public static ClrMdDumpHost Open(string dumpPath)
+ {
+ DataTarget dataTarget = DataTarget.LoadDump(dumpPath);
+ return new ClrMdDumpHost(dumpPath, dataTarget);
+ }
+
+ ///
+ /// Read memory from the dump at the specified address.
+ /// Returns 0 on success, non-zero on failure.
+ ///
+ public int ReadFromTarget(ulong address, Span buffer)
+ {
+ int bytesRead = _dataTarget.DataReader.Read(address, buffer);
+ return bytesRead == buffer.Length ? 0 : -1;
+ }
+
+ ///
+ /// Get a thread's register context from the dump.
+ /// Returns 0 on success, non-zero on failure.
+ ///
+ public int GetThreadContext(uint threadId, uint contextFlags, Span buffer)
+ {
+ return _dataTarget.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1;
+ }
+
+ ///
+ /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump.
+ /// Uses ClrMD's built-in export resolution which handles PE, ELF, and Mach-O formats.
+ ///
+ public ulong FindContractDescriptorAddress()
+ {
+ foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules())
+ {
+ string? fileName = module.FileName;
+ if (fileName is null)
+ continue;
+
+ // Path.GetFileName doesn't handle Windows paths on a Linux/macOS host,
+ // so split on both separators to extract the file name correctly when
+ // analyzing cross-platform dumps.
+ int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\'));
+ string name = lastSep >= 0 ? fileName.Substring(lastSep + 1) : fileName;
+ if (!IsRuntimeModule(name))
+ continue;
+
+ ulong address = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor");
+ if (address != 0)
+ return address;
+ }
+
+ throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export in any runtime module in the dump.");
+ }
+
+ private static bool IsRuntimeModule(string fileName)
+ {
+ foreach (string name in s_runtimeModuleNames)
+ {
+ if (fileName.Equals(name, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Dispose()
+ {
+ _dataTarget.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/Program.cs
new file mode 100644
index 00000000000000..4571e8566a1c29
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/Program.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading;
+
+///
+/// Debuggee app for cDAC dump tests.
+/// Spawns threads with known names, ensures they are all alive, then crashes
+/// so a dump is produced for analysis.
+///
+internal static class Program
+{
+ // These constants are referenced by ThreadDumpTests to assert expected values.
+ public const int SpawnedThreadCount = 5;
+ public static readonly string[] ThreadNames = new[]
+ {
+ "cdac-test-thread-0",
+ "cdac-test-thread-1",
+ "cdac-test-thread-2",
+ "cdac-test-thread-3",
+ "cdac-test-thread-4",
+ };
+
+ private static void Main()
+ {
+ // Barrier ensures all threads are alive and named before we crash.
+ // participantCount = SpawnedThreadCount + 1 (main thread)
+ using Barrier barrier = new(SpawnedThreadCount + 1);
+
+ Thread[] threads = new Thread[SpawnedThreadCount];
+ for (int i = 0; i < SpawnedThreadCount; i++)
+ {
+ int index = i;
+ threads[i] = new Thread(() =>
+ {
+ // Signal that this thread is alive and wait for all others.
+ barrier.SignalAndWait();
+
+ // Keep the thread alive until the process crashes.
+ Thread.Sleep(Timeout.Infinite);
+ })
+ {
+ Name = ThreadNames[index],
+ IsBackground = true,
+ };
+ threads[i].Start();
+ }
+
+ // Wait until all spawned threads have reached the barrier.
+ barrier.SignalAndWait();
+
+ // All threads are alive and named. Crash to produce a dump.
+ Environment.FailFast("cDAC dump test: BasicThreads debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props
new file mode 100644
index 00000000000000..3bb2049cfe904d
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+
+ Exe
+ $(NetCoreAppToolCurrent);net10.0
+ true
+ enable
+ $(ArtifactsBinDir)DumpTests\$(MSBuildProjectName)\$(Configuration)\
+ true
+
+ $(NoWarn);CA1852
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/ExceptionState.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/ExceptionState.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/ExceptionState.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/Program.cs
new file mode 100644
index 00000000000000..e24cee8ce4829a
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionState/Program.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+///
+/// Debuggee for cDAC dump tests — exercises the Exception contract.
+/// Creates a nested exception chain then crashes with FailFast.
+///
+internal static class Program
+{
+ public class DebuggeeException : Exception
+ {
+ public DebuggeeException(string message) : base(message) { }
+ public DebuggeeException(string message, Exception inner) : base(message, inner) { }
+ }
+
+ private static void Main()
+ {
+ Exception? caughtException;
+
+ // Build a nested exception chain
+ try
+ {
+ try
+ {
+ try
+ {
+ throw new InvalidOperationException("innermost exception");
+ }
+ catch (Exception ex)
+ {
+ throw new DebuggeeException("middle exception", ex);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new DebuggeeException("outermost exception", ex);
+ }
+ }
+ catch (Exception ex)
+ {
+ caughtException = ex;
+ }
+
+ // Keep the exception chain alive
+ GC.KeepAlive(caughtException);
+
+ Environment.FailFast("cDAC dump test: ExceptionState debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/Program.cs
new file mode 100644
index 00000000000000..fabc70ce665962
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/Program.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+///
+/// Debuggee for cDAC dump tests — exercises the Object and GC contracts.
+/// Pins objects, creates GC handles, then crashes.
+///
+internal static class Program
+{
+ public const int PinnedObjectCount = 5;
+ public const string TestStringValue = "cDAC-GCRoots-test-string";
+
+ private static void Main()
+ {
+ // Allocate objects of various types
+ string testString = TestStringValue;
+ byte[] byteArray = new byte[1024];
+ object boxedInt = 42;
+
+ // Create pinned handles
+ GCHandle[] pinnedHandles = new GCHandle[PinnedObjectCount];
+ byte[][] pinnedArrays = new byte[PinnedObjectCount][];
+
+ for (int i = 0; i < PinnedObjectCount; i++)
+ {
+ pinnedArrays[i] = new byte[64];
+ pinnedHandles[i] = GCHandle.Alloc(pinnedArrays[i], GCHandleType.Pinned);
+ }
+
+ // Create weak and strong handles
+ var weakRef = new WeakReference(new object());
+ var strongHandle = GCHandle.Alloc(testString, GCHandleType.Normal);
+
+ // Keep references alive
+ GC.KeepAlive(testString);
+ GC.KeepAlive(byteArray);
+ GC.KeepAlive(boxedInt);
+ GC.KeepAlive(pinnedHandles);
+ GC.KeepAlive(pinnedArrays);
+ GC.KeepAlive(weakRef);
+ GC.KeepAlive(strongHandle);
+
+ Environment.FailFast("cDAC dump test: GCRoots debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/MultiModule.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/MultiModule.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/MultiModule.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs
new file mode 100644
index 00000000000000..d803f40e8d39ea
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Reflection;
+using System.Runtime.Loader;
+
+///
+/// Debuggee for cDAC dump tests — exercises the Loader contract.
+/// Loads assemblies from multiple AssemblyLoadContexts then crashes.
+///
+internal static class Program
+{
+ public const int CustomAlcCount = 3;
+
+ private static void Main()
+ {
+ // Create multiple AssemblyLoadContexts and load the runtime assembly in each
+ // to exercise the Loader contract's module enumeration.
+ AssemblyLoadContext[] contexts = new AssemblyLoadContext[CustomAlcCount];
+ Assembly[] loadedAssemblies = new Assembly[CustomAlcCount];
+
+ for (int i = 0; i < CustomAlcCount; i++)
+ {
+ contexts[i] = new AssemblyLoadContext($"cdac-test-alc-{i}", isCollectible: true);
+
+ // Each ALC will have the core assembly visible.
+ // We can also load the current assembly's location in a new context,
+ // but for simplicity, just exercise the ALC infrastructure.
+ loadedAssemblies[i] = contexts[i].LoadFromAssemblyName(typeof(object).Assembly.GetName());
+ }
+
+ // Also load System.Xml to have another module present
+ var xmlAssembly = Assembly.Load("System.Private.Xml");
+
+ // Keep references alive
+ GC.KeepAlive(contexts);
+ GC.KeepAlive(loadedAssemblies);
+ GC.KeepAlive(xmlAssembly);
+
+ Environment.FailFast("cDAC dump test: MultiModule debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/Program.cs
new file mode 100644
index 00000000000000..ea8cd960dc8d88
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/Program.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime;
+using System.Runtime.InteropServices;
+
+///
+/// Debuggee for cDAC dump tests — exercises the GC contract in server GC mode.
+/// Allocates objects across heaps, pins some, then crashes.
+///
+internal static class Program
+{
+ private static void Main()
+ {
+ // Verify server GC is enabled
+ if (!GCSettings.IsServerGC)
+ {
+ Console.Error.WriteLine("ERROR: Server GC is not enabled.");
+ Environment.Exit(1);
+ }
+
+ // Allocate objects to populate multiple heaps
+ object[] roots = new object[100];
+ for (int i = 0; i < roots.Length; i++)
+ {
+ roots[i] = new byte[1024 * (i + 1)];
+ }
+
+ // Create pinned handles
+ GCHandle[] pinnedHandles = new GCHandle[10];
+ for (int i = 0; i < pinnedHandles.Length; i++)
+ {
+ pinnedHandles[i] = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
+ }
+
+ GC.KeepAlive(roots);
+ GC.KeepAlive(pinnedHandles);
+
+ Environment.FailFast("cDAC dump test: ServerGC debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/ServerGC.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/ServerGC.csproj
new file mode 100644
index 00000000000000..e634aa8ad21491
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/ServerGC/ServerGC.csproj
@@ -0,0 +1,5 @@
+
+
+ true
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/Program.cs
new file mode 100644
index 00000000000000..7fed5894a501b7
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/Program.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.CompilerServices;
+
+///
+/// Debuggee for cDAC dump tests — exercises the StackWalk contract.
+/// Establishes a deep, deterministic call stack then crashes.
+/// Each method uses NoInlining to ensure distinct stack frames.
+///
+internal static class Program
+{
+ public const int ExpectedManagedFrameCount = 4;
+
+ private static void Main()
+ {
+ MethodA();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void MethodA()
+ {
+ MethodB();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void MethodB()
+ {
+ MethodC();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void MethodC()
+ {
+ Environment.FailFast("cDAC dump test: StackWalk debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/StackWalk.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/StackWalk.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/StackWalk.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/Program.cs
new file mode 100644
index 00000000000000..158b43dccc55c9
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/Program.cs
@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+
+///
+/// Debuggee for cDAC dump tests — exercises the RuntimeTypeSystem and Loader contracts.
+/// Loads types with inheritance, generics, and arrays, then crashes.
+///
+internal static class Program
+{
+ // Base class hierarchy
+ public class Animal
+ {
+ public virtual string Name => "Animal";
+ }
+
+ public class Dog : Animal
+ {
+ public override string Name => "Dog";
+ public string Breed { get; set; } = "Unknown";
+ }
+
+ public class GuideDog : Dog
+ {
+ public string Handler { get; set; } = "None";
+ }
+
+ // Generic types
+ public class Container
+ {
+ public T? Value { get; set; }
+ }
+
+ public class Pair
+ {
+ public TKey? Key { get; set; }
+ public TValue? Value { get; set; }
+ }
+
+ // Interface hierarchy
+ public interface IIdentifiable
+ {
+ int Id { get; }
+ }
+
+ public class IdentifiableAnimal : Animal, IIdentifiable
+ {
+ public int Id { get; set; }
+ }
+
+ private static void Main()
+ {
+ // Create instances so the runtime loads and lays out these types
+ var dog = new Dog { Breed = "Labrador" };
+ var guideDog = new GuideDog { Handler = "John", Breed = "Shepherd" };
+ var container = new Container { Value = 42 };
+ var pair = new Pair { Key = "Rex", Value = dog };
+ var idAnimal = new IdentifiableAnimal { Id = 1 };
+
+ // Arrays of various types
+ int[] intArray = new[] { 1, 2, 3, 4, 5 };
+ string[] stringArray = new[] { "hello", "world" };
+ Dog[] dogArray = new[] { dog, guideDog };
+
+ // Multi-dimensional array
+ int[,] matrix = new int[3, 3];
+
+ // Generic collections
+ var list = new List { dog, guideDog, idAnimal };
+ var dict = new Dictionary
+ {
+ ["dog"] = dog,
+ ["guide"] = guideDog,
+ };
+
+ // Keep references alive
+ GC.KeepAlive(dog);
+ GC.KeepAlive(guideDog);
+ GC.KeepAlive(container);
+ GC.KeepAlive(pair);
+ GC.KeepAlive(idAnimal);
+ GC.KeepAlive(intArray);
+ GC.KeepAlive(stringArray);
+ GC.KeepAlive(dogArray);
+ GC.KeepAlive(matrix);
+ GC.KeepAlive(list);
+ GC.KeepAlive(dict);
+
+ Environment.FailFast("cDAC dump test: TypeHierarchy debuggee intentional crash");
+ }
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj
new file mode 100644
index 00000000000000..35e3d8428b7cfc
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj
@@ -0,0 +1,2 @@
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs
new file mode 100644
index 00000000000000..fc02de1e1f14ab
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Microsoft.DotNet.XUnitExtensions;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Base class for dump-based cDAC integration tests.
+/// Loads a crash dump via , creates a
+/// , and provides helpers for
+/// version-aware and OS-aware test skipping.
+///
+///
+/// Tests that need conditional skipping should:
+///
+/// - Use [ConditionalFact] instead of [Fact]
+/// - Call or
+/// at the start of the test method
+///
+///
+public abstract class DumpTestBase : IAsyncLifetime
+{
+ private ClrMdDumpHost? _host;
+ private ContractDescriptorTarget? _target;
+ private string? _targetOS;
+
+ ///
+ /// The name of the debuggee that produced the dump (e.g., "BasicThreads").
+ ///
+ protected abstract string DebuggeeName { get; }
+
+ ///
+ /// The runtime version identifier (e.g., "local", "net10.0").
+ ///
+ protected abstract string RuntimeVersion { get; }
+
+ ///
+ /// The cDAC Target created from the dump.
+ ///
+ protected ContractDescriptorTarget Target => _target ?? throw new InvalidOperationException("Dump not loaded.");
+
+ ///
+ /// The target operating system of the dump, resolved from the RuntimeInfo contract.
+ /// May be null if the contract is unavailable.
+ ///
+ protected string TargetOS => _targetOS ?? string.Empty;
+
+ ///
+ /// Loads the dump and creates the cDAC Target before each test.
+ /// Also resolves the target OS from the RuntimeInfo contract.
+ ///
+ public Task InitializeAsync()
+ {
+ if (IsVersionSkipped(RuntimeVersion))
+ throw new SkipTestException($"RuntimeVersion '{RuntimeVersion}' is in SkipDumpVersions list.");
+
+ string dumpPath = GetDumpPath();
+ _host = ClrMdDumpHost.Open(dumpPath);
+ ulong contractDescriptor = _host.FindContractDescriptorAddress();
+
+ bool created = ContractDescriptorTarget.TryCreate(
+ contractDescriptor,
+ _host.ReadFromTarget,
+ writeToTarget: null!,
+ _host.GetThreadContext,
+ additionalFactories: [],
+ out _target);
+
+ Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}");
+
+ try
+ {
+ _targetOS = _target!.Contracts.RuntimeInfo.GetTargetOperatingSystem().ToString();
+ }
+ catch
+ {
+ // Resolving the target OS is best-effort. The RuntimeInfo contract may be
+ // unavailable in older dumps or throw for unexpected reasons. Treat the OS
+ // as unknown and allow tests to continue — they can handle a missing TargetOS.
+ _targetOS = null;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task DisposeAsync()
+ {
+ _host?.Dispose();
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Skips the current test if matches .
+ /// Must be used with [ConditionalFact] for xunit to report the test as skipped.
+ ///
+ protected void SkipIfVersion(string version, string reason)
+ {
+ if (string.Equals(RuntimeVersion, version, StringComparison.OrdinalIgnoreCase))
+ throw new SkipTestException($"[{version}] {reason}");
+ }
+
+ ///
+ /// Skips the current test if the dump's target OS matches .
+ /// Must be used with [ConditionalFact] for xunit to report the test as skipped.
+ ///
+ protected void SkipIfTargetOS(string operatingSystem, string reason)
+ {
+ if (string.Equals(TargetOS, operatingSystem, StringComparison.OrdinalIgnoreCase))
+ throw new SkipTestException($"[TargetOS={TargetOS}] {reason}");
+ }
+
+ private string GetDumpPath()
+ {
+ string? dumpRoot = Environment.GetEnvironmentVariable("CDAC_DUMP_ROOT");
+ if (string.IsNullOrEmpty(dumpRoot))
+ {
+ string? repoRoot = FindRepoRoot();
+ if (repoRoot is null)
+ throw new InvalidOperationException("Could not locate the repository root.");
+
+ dumpRoot = Path.Combine(repoRoot, "artifacts", "dumps", "cdac");
+ }
+
+ return Path.Combine(dumpRoot, RuntimeVersion, DebuggeeName, $"{DebuggeeName}.dmp");
+ }
+
+ ///
+ /// Checks if the given version is in the SkipDumpVersions MSBuild property
+ /// (baked into runtimeconfig.json as CDAC_SKIP_VERSIONS).
+ ///
+ private static bool IsVersionSkipped(string version)
+ {
+ if (AppContext.GetData("CDAC_SKIP_VERSIONS") is not string skipVersions)
+ return false;
+
+ foreach (string entry in skipVersions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ if (string.Equals(entry, version, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static string? FindRepoRoot()
+ {
+ string? dir = AppContext.BaseDirectory;
+ while (dir is not null)
+ {
+ if (File.Exists(Path.Combine(dir, "global.json")))
+ return dir;
+ dir = Path.GetDirectoryName(dir);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets
new file mode 100644
index 00000000000000..665aa8c504c72c
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets
@@ -0,0 +1,150 @@
+
+
+
+
+ $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'dumps', 'cdac'))
+ Release
+ $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'Debuggees'))
+
+ <_DotNetExe>$([MSBuild]::NormalizePath('$(RepoRoot)', '.dotnet', 'dotnet$(ExeSuffix)'))
+
+
+
+
+ <_TestHostConfig Condition="'$(TestHostConfiguration)' == ''">Release
+ <_TestHostConfig Condition="'$(TestHostConfiguration)' != ''">$(TestHostConfiguration)
+ <_HostArch>$(TargetArchitecture)
+ <_HostArch Condition="'$(_HostArch)' == ''">x64
+ $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'testhost', '$(NetCoreAppCurrent)-$(HostOS)-$(_TestHostConfig)-$(_HostArch)'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_DebuggeeDir>$([MSBuild]::NormalizeDirectory('$(DebuggeesDir)', '$(DebuggeeName)'))
+ <_DumpDir>$([MSBuild]::NormalizeDirectory('$(DumpOutputDir)', '$(DumpRuntimeVersion)', '$(DebuggeeName)'))
+ <_DumpFile>$([MSBuild]::NormalizePath('$(_DumpDir)', '$(DebuggeeName).dmp'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_DebuggeeBinDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'DumpTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)', '$(NetCoreAppCurrent)'))
+ <_TestHostDotNet>$([MSBuild]::NormalizePath('$(TestHostDir)', 'dotnet$(ExeSuffix)'))
+ <_DebuggeeDll>$([MSBuild]::NormalizePath('$(_DebuggeeBinDir)', '$(DebuggeeName).dll'))
+ <_DebuggeeCsproj>$([MSBuild]::NormalizePath('$(_DebuggeeDir)', '$(DebuggeeName).csproj'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PublishDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'DumpTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)', '$(DumpRuntimeVersion)-publish'))
+ <_DebuggeeCsproj>$([MSBuild]::NormalizePath('$(_DebuggeeDir)', '$(DebuggeeName).csproj'))
+ <_PublishedExe>$([MSBuild]::NormalizePath('$(_PublishDir)', '$(DebuggeeName)$(ExeSuffix)'))
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/EcmaMetadataDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/EcmaMetadataDumpTests.cs
new file mode 100644
index 00000000000000..a461bb01c83d17
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/EcmaMetadataDumpTests.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection.Metadata;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the EcmaMetadata contract.
+/// Uses the MultiModule debuggee dump, which loads multiple assemblies.
+///
+public abstract class EcmaMetadataDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "MultiModule";
+
+ [ConditionalFact]
+ public void EcmaMetadata_ContractIsAvailable()
+ {
+ IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata;
+ Assert.NotNull(ecmaMetadata);
+ }
+
+ [ConditionalFact]
+ public void EcmaMetadata_RootModuleHasMetadataAddress()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata;
+
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ TargetSpan metadataSpan = ecmaMetadata.GetReadOnlyMetadataAddress(moduleHandle);
+ Assert.NotEqual(TargetPointer.Null, metadataSpan.Address);
+ Assert.True(metadataSpan.Size > 0, "Expected metadata size > 0");
+ }
+
+ [ConditionalFact]
+ public void EcmaMetadata_CanGetMetadataReader()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata;
+
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle);
+ Assert.NotNull(reader);
+ }
+
+ [ConditionalFact]
+ public void EcmaMetadata_MetadataReaderHasTypeDefs()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata;
+
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle);
+ Assert.NotNull(reader);
+
+ // The MultiModule debuggee defines at least the Program class
+ int typeDefCount = 0;
+ foreach (TypeDefinitionHandle tdh in reader.TypeDefinitions)
+ {
+ typeDefCount++;
+ }
+ Assert.True(typeDefCount > 0, "Expected at least one TypeDef in module metadata");
+ }
+}
+
+public class EcmaMetadataDumpTests_Local : EcmaMetadataDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class EcmaMetadataDumpTests_Net10 : EcmaMetadataDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/ExceptionDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ExceptionDumpTests.cs
new file mode 100644
index 00000000000000..8110928266283c
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/ExceptionDumpTests.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the Exception contract.
+/// Uses the ExceptionState debuggee dump, which crashes with a nested exception chain.
+///
+public abstract class ExceptionDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "ExceptionState";
+
+ [ConditionalFact]
+ public void Exception_ThreadHasCurrentException()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ Assert.NotNull(threadContract);
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+ Assert.True(storeData.ThreadCount > 0);
+ }
+
+ [ConditionalFact]
+ public void Exception_ContractIsAvailable()
+ {
+ IException exceptionContract = Target.Contracts.Exception;
+ Assert.NotNull(exceptionContract);
+ }
+
+ [ConditionalFact]
+ public void Exception_CrashingThreadHasLastThrownObject()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ // Walk threads to find one with a non-null LastThrownObjectHandle
+ bool foundExceptionThread = false;
+ TargetPointer currentThread = storeData.FirstThread;
+ while (currentThread != TargetPointer.Null)
+ {
+ ThreadData threadData = threadContract.GetThreadData(currentThread);
+ if (threadData.LastThrownObjectHandle != TargetPointer.Null)
+ {
+ foundExceptionThread = true;
+ break;
+ }
+ currentThread = threadData.NextThread;
+ }
+
+ // FailFast with a message should leave an exception on the crashing thread
+ Assert.True(foundExceptionThread, "Expected at least one thread with a LastThrownObjectHandle");
+ }
+
+ [ConditionalFact]
+ public void Exception_CanGetExceptionDataFromFirstNestedException()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ IException exceptionContract = Target.Contracts.Exception;
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ // Find a thread with a first nested exception
+ TargetPointer currentThread = storeData.FirstThread;
+ while (currentThread != TargetPointer.Null)
+ {
+ ThreadData threadData = threadContract.GetThreadData(currentThread);
+ if (threadData.FirstNestedException != TargetPointer.Null)
+ {
+ // Walk the nested exception chain
+ TargetPointer nestedEx = threadData.FirstNestedException;
+ TargetPointer managedException = exceptionContract.GetNestedExceptionInfo(nestedEx, out TargetPointer nextNested);
+ Assert.NotEqual(TargetPointer.Null, managedException);
+
+ ExceptionData exData = exceptionContract.GetExceptionData(managedException);
+ Assert.NotEqual(TargetPointer.Null, exData.Message);
+ return;
+ }
+ currentThread = threadData.NextThread;
+ }
+
+ // If no nested exceptions found, that's still valid — FailFast may not create nested chain
+ }
+}
+
+public class ExceptionDumpTests_Local : ExceptionDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class ExceptionDumpTests_Net10 : ExceptionDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs
new file mode 100644
index 00000000000000..37ca410fd93061
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the Loader contract.
+/// Uses the MultiModule debuggee dump, which loads assemblies from multiple ALCs.
+///
+public abstract class LoaderDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "MultiModule";
+
+ [ConditionalFact]
+ public void Loader_CanGetRootAssembly()
+ {
+ ILoader loader = Target.Contracts.Loader;
+ Assert.NotNull(loader);
+
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ Assert.NotEqual(TargetPointer.Null, rootAssembly);
+ }
+
+ [ConditionalFact]
+ public void Loader_RootAssemblyHasModule()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+ TargetPointer modulePtr = loader.GetModule(moduleHandle);
+ Assert.NotEqual(TargetPointer.Null, modulePtr);
+ }
+
+ [ConditionalFact]
+ public void Loader_CanGetModulePath()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+ string path = loader.GetPath(moduleHandle);
+ Assert.NotNull(path);
+ Assert.NotEmpty(path);
+ }
+
+ [ConditionalFact]
+ public void Loader_AppDomainHasFriendlyName()
+ {
+ ILoader loader = Target.Contracts.Loader;
+ string name = loader.GetAppDomainFriendlyName();
+ Assert.NotNull(name);
+ Assert.NotEmpty(name);
+ }
+
+ [ConditionalFact]
+ public void Loader_GlobalLoaderAllocatorIsValid()
+ {
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer globalLA = loader.GetGlobalLoaderAllocator();
+ Assert.NotEqual(TargetPointer.Null, globalLA);
+ }
+
+ [ConditionalFact]
+ public void Loader_RootModuleHasFileName()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ string fileName = loader.GetFileName(moduleHandle);
+ Assert.NotNull(fileName);
+ Assert.NotEmpty(fileName);
+ Assert.Contains("MultiModule", fileName);
+ }
+
+ [ConditionalFact]
+ public void Loader_RootModuleIsNotDynamic()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ Assert.False(loader.IsDynamic(moduleHandle));
+ }
+
+ [ConditionalFact]
+ public void Loader_RootModuleHasLoaderAllocator()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ TargetPointer la = loader.GetLoaderAllocator(moduleHandle);
+ Assert.NotEqual(TargetPointer.Null, la);
+ }
+
+ [ConditionalFact]
+ public void Loader_RootModuleHasILBase()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+
+ TargetPointer ilBase = loader.GetILBase(moduleHandle);
+ Assert.NotEqual(TargetPointer.Null, ilBase);
+ }
+}
+
+public class LoaderDumpTests_Local : LoaderDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class LoaderDumpTests_Net10 : LoaderDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
new file mode 100644
index 00000000000000..beceb6f6495c61
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
@@ -0,0 +1,37 @@
+
+
+ true
+ $(NetCoreAppToolCurrent)
+ enable
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/native/managed/cdac/tests/DumpTests/ObjectDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ObjectDumpTests.cs
new file mode 100644
index 00000000000000..07a17565a1ac8c
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/ObjectDumpTests.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Linq;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the Object and GC contracts in workstation GC mode.
+/// Uses the GCRoots debuggee dump, which pins objects and creates GC handles.
+///
+public abstract class ObjectDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "GCRoots";
+
+ [ConditionalFact]
+ public void Object_ContractIsAvailable()
+ {
+ IObject objectContract = Target.Contracts.Object;
+ Assert.NotNull(objectContract);
+ }
+
+ [ConditionalFact]
+ public void GC_ContractIsAvailable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ Assert.NotNull(gcContract);
+ }
+
+ [ConditionalFact]
+ public void GC_IsWorkstationGC()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint heapCount = gcContract.GetGCHeapCount();
+ Assert.Equal(1u, heapCount);
+ }
+
+ [ConditionalFact]
+ public void GC_HeapCountIsNonZero()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint heapCount = gcContract.GetGCHeapCount();
+ Assert.True(heapCount > 0, "Expected at least one GC heap");
+ }
+
+ [ConditionalFact]
+ public void GC_MaxGenerationIsReasonable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint maxGen = gcContract.GetMaxGeneration();
+ Assert.True(maxGen >= 1 && maxGen <= 4,
+ $"Expected max generation between 1 and 4, got {maxGen}");
+ }
+
+ [ConditionalFact]
+ public void GC_CanGetHeapData()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ GCHeapData heapData = gcContract.GetHeapData();
+ Assert.NotNull(heapData.GenerationTable);
+ Assert.True(heapData.GenerationTable.Count > 0, "Expected at least one generation");
+ }
+
+ [ConditionalFact]
+ public void GC_StructuresAreValid()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ bool valid = gcContract.GetGCStructuresValid();
+ Assert.True(valid, "Expected GC structures to be valid in a dump taken outside of GC");
+ }
+
+ [ConditionalFact]
+ public void GC_BoundsAreReasonable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ gcContract.GetGCBounds(out TargetPointer minAddr, out TargetPointer maxAddr);
+ Assert.True(minAddr < maxAddr,
+ $"Expected GC min address (0x{minAddr:X}) < max address (0x{maxAddr:X})");
+ }
+
+ [ConditionalFact]
+ public void Object_StringMethodTableHasCorrectComponentSize()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(stringMT);
+
+ uint componentSize = rts.GetComponentSize(handle);
+ Assert.Equal(2u, componentSize);
+ }
+}
+
+public class ObjectDumpTests_Local : ObjectDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class ObjectDumpTests_Net10 : ObjectDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1
new file mode 100644
index 00000000000000..9b4850f7f2aa9b
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1
@@ -0,0 +1,393 @@
+<#
+.SYNOPSIS
+ Generates crash dumps and/or runs cDAC dump-based integration tests.
+
+.DESCRIPTION
+ This script orchestrates the cDAC dump test workflow on Windows:
+ 1. Build debuggee apps for the selected runtime version(s)
+ 2. Run them to produce crash dumps
+ 3. Build and run the dump analysis tests
+
+ Alternatively, use -DumpArchive to import a tar.gz archive of dumps
+ downloaded from CI and run the tests against those dumps.
+
+ NOTE: This script is Windows-only. For cross-platform CI builds,
+ the MSBuild-based DumpTests.targets handles platform detection
+ automatically via $(HostOS), $(ExeSuffix), and $(PortableTargetRid).
+
+ Dumps are written to: artifacts\dumps\cdac\{version}\{debuggee}\
+ The script must be run from the DumpTests directory.
+
+.PARAMETER Action
+ What to do: "dumps" (generate only), "test" (run tests only), or "all" (both).
+ Default: "all"
+
+.PARAMETER Versions
+ Comma-separated list of runtime versions to target.
+ Supported values: "local", "net10.0", or "all" for both.
+ Default: "all"
+
+.PARAMETER Force
+ When set, deletes existing dumps before regenerating.
+
+.PARAMETER TestHostConfiguration
+ Configuration of the testhost used for the "local" runtime version.
+ Default: "Release"
+
+.PARAMETER Filter
+ Glob-style filter for test names. Uses substring matching.
+ Examples: "*StackWalk*", "*Thread*", "*GC_Heap*"
+ Default: "" (run all tests)
+
+.PARAMETER DumpArchive
+ Path to a tar.gz archive of dumps downloaded from CI.
+ When specified, the archive is extracted and tests are run against
+ the extracted dumps. Skips dump generation entirely.
+ The archive should contain: {version}/{debuggee}/{debuggee}.dmp
+
+.EXAMPLE
+ .\RunDumpTests.ps1
+
+.EXAMPLE
+ .\RunDumpTests.ps1 -Action dumps -Versions net10.0
+
+.EXAMPLE
+ .\RunDumpTests.ps1 -Force
+
+.EXAMPLE
+ .\RunDumpTests.ps1 -Action test -Versions local
+
+.EXAMPLE
+ .\RunDumpTests.ps1 -Filter "*StackWalk*"
+
+.EXAMPLE
+ .\RunDumpTests.ps1 -DumpArchive C:\Downloads\CdacDumps_linux_x64.tar.gz
+#>
+
+[CmdletBinding()]
+param(
+ [ValidateSet("dumps", "test", "all")]
+ [string]$Action = "all",
+
+ [string]$Versions = "all",
+
+ [switch]$Force,
+
+ [string]$TestHostConfiguration = "Release",
+
+ [string]$Filter = "",
+
+ [string]$DumpArchive = ""
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+# --- Resolve paths ---
+function Find-RepoRoot([string]$startDir) {
+ $dir = $startDir
+ while ($dir) {
+ if (Test-Path (Join-Path $dir "global.json")) {
+ return $dir
+ }
+ $parent = Split-Path -Parent $dir
+ if ($parent -eq $dir) { break }
+ $dir = $parent
+ }
+ Write-Error "Could not find repository root (no global.json found above $startDir)"
+ exit 1
+}
+$repoRoot = Find-RepoRoot $PSScriptRoot
+$dotnet = Join-Path $repoRoot ".dotnet\dotnet.exe"
+$dumpTestsProj = Join-Path $PSScriptRoot "Microsoft.Diagnostics.DataContractReader.DumpTests.csproj"
+$dumpOutputDir = Join-Path $repoRoot "artifacts\dumps\cdac"
+$debuggeesDir = Join-Path $PSScriptRoot "Debuggees"
+
+if (-not (Test-Path $dotnet)) {
+ Write-Error "Repo dotnet not found at $dotnet. Run build.cmd first."
+ exit 1
+}
+
+# --- Debuggees and versions ---
+$allDebugees = @("BasicThreads", "TypeHierarchy", "ExceptionState", "MultiModule", "GCRoots", "ServerGC", "StackWalk")
+$allVersions = @("local", "net10.0")
+
+# --- DumpArchive mode: extract and test CI dumps ---
+if ($DumpArchive) {
+ if (-not (Test-Path $DumpArchive)) {
+ Write-Error "Dump archive not found: $DumpArchive"
+ exit 1
+ }
+
+ $extractDir = Join-Path $repoRoot "artifacts\dumps\ci-imported"
+ if (Test-Path $extractDir) {
+ Write-Host "Cleaning previous import: $extractDir" -ForegroundColor Yellow
+ Remove-Item $extractDir -Recurse -Force
+ }
+ New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
+
+ Write-Host ""
+ Write-Host "=== cDAC Dump Tests (CI Archive) ===" -ForegroundColor Cyan
+ Write-Host " Archive: $DumpArchive"
+ Write-Host " Extract: $extractDir"
+ if ($Filter) { Write-Host " Filter: $Filter" }
+ Write-Host ""
+
+ Write-Host "--- Extracting archive ---" -ForegroundColor Cyan
+ tar -xzf $DumpArchive -C $extractDir
+ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to extract archive."; exit 1 }
+
+ # List extracted dumps
+ $dumpFiles = Get-ChildItem -Path $extractDir -Recurse -Filter "*.dmp"
+ if ($dumpFiles.Count -eq 0) {
+ Write-Error "No .dmp files found in archive."
+ exit 1
+ }
+ foreach ($f in $dumpFiles) {
+ $rel = $f.FullName.Substring($extractDir.Length + 1)
+ $size = [math]::Round($f.Length / 1MB, 1)
+ Write-Host " Found: $rel (${size} MB)" -ForegroundColor Green
+ }
+
+ # Detect which versions are present in the archive and skip the rest
+ $presentVersions = Get-ChildItem -Path $extractDir -Directory | Select-Object -ExpandProperty Name
+ $skipVersions = $allVersions | Where-Object { $_ -notin $presentVersions }
+ if ($skipVersions) {
+ $skipVersionsStr = $skipVersions -join ";"
+ Write-Host " Versions in archive: $($presentVersions -join ', ')" -ForegroundColor Green
+ Write-Host " Skipping versions: $($skipVersions -join ', ')" -ForegroundColor Yellow
+ }
+ else {
+ $skipVersionsStr = ""
+ Write-Host " Versions in archive: $($presentVersions -join ', ')" -ForegroundColor Green
+ }
+
+ Write-Host ""
+ Write-Host "--- Building test project ---" -ForegroundColor Cyan
+ $buildArgs = @($dumpTestsProj, "--nologo", "-v", "q")
+ if ($skipVersionsStr) {
+ $buildArgs += "/p:SkipDumpVersions=$skipVersionsStr"
+ }
+ & $dotnet build @buildArgs 2>&1 | ForEach-Object { Write-Host " $_" }
+ if ($LASTEXITCODE -ne 0) { Write-Error "Test project build failed."; exit 1 }
+
+ Write-Host ""
+ Write-Host "--- Running tests against CI dumps ---" -ForegroundColor Cyan
+
+ $filterExpr = ""
+ if ($Filter) {
+ $namePattern = $Filter.Replace("*", "")
+ $filterExpr = "FullyQualifiedName~$namePattern"
+ }
+
+ $env:CDAC_DUMP_ROOT = $extractDir
+ $saved = $ErrorActionPreference
+ $ErrorActionPreference = "Continue"
+ if ($filterExpr) {
+ & $dotnet test $dumpTestsProj --no-build --filter $filterExpr --logger "console;verbosity=detailed" 2>&1 | ForEach-Object { Write-Host " $_" }
+ }
+ else {
+ & $dotnet test $dumpTestsProj --no-build --logger "console;verbosity=detailed" 2>&1 | ForEach-Object { Write-Host " $_" }
+ }
+ $testExitCode = $LASTEXITCODE
+ $ErrorActionPreference = $saved
+ Remove-Item Env:\CDAC_DUMP_ROOT -ErrorAction SilentlyContinue
+
+ if ($testExitCode -ne 0) {
+ Write-Host ""
+ Write-Host "TESTS FAILED" -ForegroundColor Red
+ exit 1
+ }
+
+ Write-Host ""
+ Write-Host "ALL TESTS PASSED" -ForegroundColor Green
+ exit 0
+}
+
+if ($Versions -eq "all") {
+ $selectedVersions = $allVersions
+}
+else {
+ $selectedVersions = $Versions -split "," | ForEach-Object { $_.Trim() }
+ foreach ($v in $selectedVersions) {
+ if ($v -notin $allVersions) {
+ Write-Error "Unknown version '$v'. Supported: $($allVersions -join ', '), all"
+ exit 1
+ }
+ }
+}
+
+Write-Host ""
+Write-Host "=== cDAC Dump Tests ===" -ForegroundColor Cyan
+Write-Host " Action: $Action"
+Write-Host " Versions: $($selectedVersions -join ', ')"
+Write-Host " Debuggees: $($allDebugees -join ', ')"
+Write-Host " Force: $Force"
+if ($Filter) { Write-Host " Filter: $Filter" }
+Write-Host ""
+
+# --- Force: delete existing dumps ---
+if ($Force -and $Action -in @("dumps", "all")) {
+ foreach ($version in $selectedVersions) {
+ $versionDumpDir = Join-Path $dumpOutputDir $version
+ if (Test-Path $versionDumpDir) {
+ Write-Host "Deleting existing dumps: $versionDumpDir" -ForegroundColor Yellow
+ Remove-Item $versionDumpDir -Recurse -Force
+ }
+ }
+}
+
+# --- Windows: allow unsigned DAC for heap dumps ---
+# Heap dumps (type 2) require the DAC which is unsigned in local builds.
+# Set the DisableAuxProviderSignatureCheck registry value so that
+# MiniDumpWriteDump accepts the unsigned DAC (Windows 11+ only).
+# This mirrors the approach used by dotnet/diagnostics DumpGenerationFixture.
+if ($Action -in @("dumps", "all")) {
+ $regPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\MiniDumpSettings"
+ try {
+ if (-not (Test-Path $regPath)) {
+ New-Item -Path $regPath -Force | Out-Null
+ }
+ Set-ItemProperty -Path $regPath -Name "DisableAuxProviderSignatureCheck" -Value 1 -Type DWord
+ Write-Host " Set DisableAuxProviderSignatureCheck=1 for unsigned DAC support" -ForegroundColor Green
+ }
+ catch [System.UnauthorizedAccessException] {
+ Write-Host " Warning: Could not set DisableAuxProviderSignatureCheck (run as admin for heap dump support)" -ForegroundColor Yellow
+ }
+}
+
+# --- Helper: resolve TFM for local builds ---
+$localTfm = $null
+function Get-LocalTfm {
+ if ($null -eq $script:localTfm) {
+ $script:localTfm = & $dotnet msbuild $dumpTestsProj /nologo /v:m "/getProperty:NetCoreAppCurrent" 2>$null
+ if ([string]::IsNullOrWhiteSpace($script:localTfm)) { $script:localTfm = "net11.0" }
+ }
+ return $script:localTfm
+}
+
+# --- Helper: set dump env vars ---
+function Set-DumpEnvVars($dumpFilePath) {
+ $env:DOTNET_DbgEnableMiniDump = "1"
+ $env:DOTNET_DbgMiniDumpType = "2"
+ $env:DOTNET_DbgMiniDumpName = $dumpFilePath
+}
+
+function Clear-DumpEnvVars {
+ Remove-Item Env:\DOTNET_DbgEnableMiniDump -ErrorAction SilentlyContinue
+ Remove-Item Env:\DOTNET_DbgMiniDumpType -ErrorAction SilentlyContinue
+ Remove-Item Env:\DOTNET_DbgMiniDumpName -ErrorAction SilentlyContinue
+}
+
+# --- Generate dumps ---
+if ($Action -in @("dumps", "all")) {
+ Write-Host "--- Generating dumps ---" -ForegroundColor Cyan
+
+ foreach ($version in $selectedVersions) {
+ foreach ($debuggee in $allDebugees) {
+ $dumpFile = Join-Path $dumpOutputDir "$version\$debuggee\$debuggee.dmp"
+
+ if (Test-Path $dumpFile) {
+ $size = [math]::Round((Get-Item $dumpFile).Length / 1MB, 1)
+ Write-Host " [$version/$debuggee] Already exists (${size} MB). Use -Force to regenerate." -ForegroundColor DarkGray
+ continue
+ }
+
+ Write-Host " [$version/$debuggee] Generating dump..." -ForegroundColor Green
+ $debuggeeCsproj = Join-Path $debuggeesDir "$debuggee\$debuggee.csproj"
+ $dumpDir = Join-Path $dumpOutputDir "$version\$debuggee"
+ New-Item -ItemType Directory -Path $dumpDir -Force | Out-Null
+
+ if ($version -eq "local") {
+ $tfm = Get-LocalTfm
+ $binDir = Join-Path $repoRoot "artifacts\bin\DumpTests\$debuggee\Release\$tfm"
+ $testHostDir = Join-Path $repoRoot "artifacts\bin\testhost\$tfm-windows-$TestHostConfiguration-x64"
+
+ if (-not (Test-Path "$testHostDir\dotnet.exe")) {
+ Write-Error " [$version/$debuggee] Testhost not found at $testHostDir. Run 'build.cmd clr+libs -rc release' first."
+ exit 1
+ }
+
+ & $dotnet build $debuggeeCsproj -c Release -f $tfm --nologo -v q 2>&1 | Out-Null
+ if ($LASTEXITCODE -ne 0) { Write-Error " [$version/$debuggee] Build failed."; exit 1 }
+
+ Set-DumpEnvVars (Join-Path $dumpDir "$debuggee.dmp")
+ # The debuggee crashes on purpose (FailFast), so suppress stderr errors.
+ $saved = $ErrorActionPreference
+ $ErrorActionPreference = "Continue"
+ & "$testHostDir\dotnet.exe" exec "$binDir\$debuggee.dll" 2>&1 | ForEach-Object { Write-Host " $_" }
+ $ErrorActionPreference = $saved
+ Clear-DumpEnvVars
+ }
+ else {
+ $publishDir = Join-Path $repoRoot "artifacts\bin\DumpTests\$debuggee\Release\$version-publish"
+
+ & $dotnet publish $debuggeeCsproj -c Release -f $version -r win-x64 --self-contained -o $publishDir --nologo -v q 2>&1 | Out-Null
+ if ($LASTEXITCODE -ne 0) { Write-Error " [$version/$debuggee] Publish failed."; exit 1 }
+
+ Set-DumpEnvVars (Join-Path $dumpDir "$debuggee.dmp")
+ # The debuggee crashes on purpose (FailFast), so suppress stderr errors.
+ $saved = $ErrorActionPreference
+ $ErrorActionPreference = "Continue"
+ & "$publishDir\$debuggee.exe" 2>&1 | ForEach-Object { Write-Host " $_" }
+ $ErrorActionPreference = $saved
+ Clear-DumpEnvVars
+ }
+
+ $dumpFile = Join-Path $dumpDir "$debuggee.dmp"
+ if (Test-Path $dumpFile) {
+ $size = [math]::Round((Get-Item $dumpFile).Length / 1MB, 1)
+ Write-Host " [$version/$debuggee] Dump created (${size} MB)" -ForegroundColor Green
+ }
+ else {
+ Write-Error " [$version/$debuggee] Dump was not created!"
+ exit 1
+ }
+ }
+ }
+}
+
+# --- Run tests ---
+if ($Action -in @("test", "all")) {
+ Write-Host ""
+ Write-Host "--- Building test project ---" -ForegroundColor Cyan
+ & $dotnet build $dumpTestsProj --nologo -v q 2>&1 | ForEach-Object { Write-Host " $_" }
+ if ($LASTEXITCODE -ne 0) { Write-Error "Test project build failed."; exit 1 }
+
+ Write-Host ""
+ Write-Host "--- Running tests ---" -ForegroundColor Cyan
+
+ # Build a filter for the selected versions
+ $filters = @()
+ foreach ($version in $selectedVersions) {
+ $suffix = switch ($version) {
+ "local" { "_Local" }
+ "net10.0" { "_Net10" }
+ }
+ $filters += "FullyQualifiedName~$suffix"
+ }
+ $filterExpr = $filters -join " | "
+
+ # Apply user-supplied name filter (glob-style: * maps to dotnet test's ~ operator)
+ if ($Filter) {
+ # Convert glob wildcards to dotnet test FullyQualifiedName contains filter
+ $namePattern = $Filter.Replace("*", "")
+ $filterExpr = "($filterExpr) & FullyQualifiedName~$namePattern"
+ }
+
+ # dotnet test writes failure details to stderr; suppress termination so we see full results.
+ $saved = $ErrorActionPreference
+ $ErrorActionPreference = "Continue"
+ & $dotnet test $dumpTestsProj --no-build --filter $filterExpr --logger "console;verbosity=detailed" 2>&1 | ForEach-Object { Write-Host " $_" }
+ $testExitCode = $LASTEXITCODE
+ $ErrorActionPreference = $saved
+
+ if ($testExitCode -ne 0) {
+ Write-Host ""
+ Write-Host "TESTS FAILED" -ForegroundColor Red
+ exit 1
+ }
+
+ Write-Host ""
+ Write-Host "ALL TESTS PASSED" -ForegroundColor Green
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs
new file mode 100644
index 00000000000000..b0002843659a43
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the RuntimeInfo contract.
+/// Uses the BasicThreads debuggee dump (any dump works for these tests).
+///
+public abstract class RuntimeInfoDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "BasicThreads";
+
+ [ConditionalFact]
+ public void RuntimeInfo_ContractIsAvailable()
+ {
+ IRuntimeInfo runtimeInfo = Target.Contracts.RuntimeInfo;
+ Assert.NotNull(runtimeInfo);
+ }
+
+ [ConditionalFact]
+ public void RuntimeInfo_ArchitectureIsValid()
+ {
+ IRuntimeInfo runtimeInfo = Target.Contracts.RuntimeInfo;
+ RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture();
+
+ Assert.True(Enum.IsDefined(arch),
+ $"Expected a valid RuntimeInfoArchitecture enum value, got {arch}");
+ }
+
+ [ConditionalFact]
+ public void RuntimeInfo_OperatingSystemIsValid()
+ {
+ IRuntimeInfo runtimeInfo = Target.Contracts.RuntimeInfo;
+ RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem();
+
+ Assert.True(Enum.IsDefined(os),
+ $"Expected a valid RuntimeInfoOperatingSystem enum value, got {os}");
+ }
+}
+
+public class RuntimeInfoDumpTests_Local : RuntimeInfoDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class RuntimeInfoDumpTests_Net10 : RuntimeInfoDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs
new file mode 100644
index 00000000000000..ee3153edd0432f
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs
@@ -0,0 +1,238 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.Metadata;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the RuntimeTypeSystem contract.
+/// Uses the TypeHierarchy debuggee dump, which loads types with inheritance,
+/// generics, and arrays.
+///
+public abstract class RuntimeTypeSystemDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "TypeHierarchy";
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_CanGetMethodTableFromModule()
+ {
+ SkipIfVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10");
+ ILoader loader = Target.Contracts.Loader;
+ Assert.NotNull(loader);
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+ Assert.NotNull(rts);
+
+ TargetPointer rootAssembly = loader.GetRootAssembly();
+ ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly);
+ TargetPointer modulePtr = loader.GetModule(moduleHandle);
+ Assert.NotEqual(TargetPointer.Null, modulePtr);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableIsValid()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+ Assert.NotNull(rts);
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ Assert.NotEqual(TargetPointer.Null, objectMT);
+
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+ Assert.False(rts.IsFreeObjectMethodTable(handle));
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_FreeObjectMethodTableIsValid()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+ Assert.NotNull(rts);
+
+ TargetPointer freeObjMTGlobal = Target.ReadGlobalPointer("FreeObjectMethodTable");
+ TargetPointer freeObjMT = Target.ReadPointer(freeObjMTGlobal);
+ Assert.NotEqual(TargetPointer.Null, freeObjMT);
+
+ TypeHandle handle = rts.GetTypeHandle(freeObjMT);
+ Assert.True(rts.IsFreeObjectMethodTable(handle));
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_StringMethodTableIsString()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+ Assert.NotNull(rts);
+
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ Assert.NotEqual(TargetPointer.Null, stringMT);
+
+ TypeHandle handle = rts.GetTypeHandle(stringMT);
+ Assert.True(rts.IsString(handle));
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableHasParent()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle objectHandle = rts.GetTypeHandle(objectMT);
+
+ // System.Object has no parent
+ TargetPointer parent = rts.GetParentMethodTable(objectHandle);
+ Assert.Equal(TargetPointer.Null, parent);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_StringHasObjectParent()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ TypeHandle stringHandle = rts.GetTypeHandle(stringMT);
+
+ // System.String's parent should be System.Object
+ TargetPointer parent = rts.GetParentMethodTable(stringHandle);
+ Assert.Equal(objectMT, parent);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableHasReasonableBaseSize()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+
+ uint baseSize = rts.GetBaseSize(handle);
+ Assert.True(baseSize > 0 && baseSize < 1024,
+ $"Expected System.Object base size between 1 and 1024, got {baseSize}");
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_StringHasNonZeroComponentSize()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(stringMT);
+
+ // String has a component size (char size = 2)
+ uint componentSize = rts.GetComponentSize(handle);
+ Assert.Equal(2u, componentSize);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableContainsNoGCPointers()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+
+ // System.Object has no GC-tracked fields
+ Assert.False(rts.ContainsGCPointers(handle));
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableHasValidToken()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+
+ uint token = rts.GetTypeDefToken(handle);
+ // TypeDef tokens have the form 0x02xxxxxx
+ Assert.Equal(0x02000000u, token & 0xFF000000u);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableHasMethods()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+
+ ushort numMethods = rts.GetNumMethods(handle);
+ // System.Object has ToString, Equals, GetHashCode, Finalize, etc.
+ Assert.True(numMethods >= 4, $"Expected System.Object to have at least 4 methods, got {numMethods}");
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_StringIsNotGenericTypeDefinition()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(stringMT);
+
+ Assert.False(rts.IsGenericTypeDefinition(handle));
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_StringCorElementTypeIsClass()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer stringMTGlobal = Target.ReadGlobalPointer("StringMethodTable");
+ TargetPointer stringMT = Target.ReadPointer(stringMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(stringMT);
+
+ // GetSignatureCorElementType returns the MethodTable's stored CorElementType,
+ // which is Class for System.String (not CorElementType.String)
+ CorElementType corType = rts.GetSignatureCorElementType(handle);
+ Assert.Equal(CorElementType.Class, corType);
+ }
+
+ [ConditionalFact]
+ public void RuntimeTypeSystem_ObjectMethodTableHasIntroducedMethods()
+ {
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable");
+ TargetPointer objectMT = Target.ReadPointer(objectMTGlobal);
+ TypeHandle handle = rts.GetTypeHandle(objectMT);
+
+ IEnumerable methodDescs = rts.GetIntroducedMethodDescs(handle);
+ List methods = methodDescs.ToList();
+
+ Assert.True(methods.Count >= 4, $"Expected System.Object to have at least 4 introduced methods, got {methods.Count}");
+
+ // Each method desc should have a valid token
+ foreach (TargetPointer mdPtr in methods)
+ {
+ MethodDescHandle mdHandle = rts.GetMethodDescHandle(mdPtr);
+ uint token = rts.GetMethodToken(mdHandle);
+ // MethodDef tokens have the form 0x06xxxxxx
+ Assert.Equal(0x06000000u, token & 0xFF000000u);
+ }
+ }
+}
+
+public class RuntimeTypeSystemDumpTests_Local : RuntimeTypeSystemDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class RuntimeTypeSystemDumpTests_Net10 : RuntimeTypeSystemDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs
new file mode 100644
index 00000000000000..1c0a83ad62f5ba
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the GC contract in server GC mode.
+/// Uses the ServerGC debuggee dump, which enables server GC and allocates
+/// objects across multiple heaps.
+///
+public abstract class ServerGCDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "ServerGC";
+
+ [ConditionalFact]
+ public void ServerGC_ContractIsAvailable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ Assert.NotNull(gcContract);
+ }
+
+ [ConditionalFact]
+ public void ServerGC_IsServerGC()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ // Server GC rejects the parameterless GetHeapData() — verify it uses
+ // the per-heap overload instead, confirming this is a server GC dump.
+ Assert.Throws(() => gcContract.GetHeapData());
+ }
+
+ [ConditionalFact]
+ public void ServerGC_MaxGenerationIsReasonable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint maxGen = gcContract.GetMaxGeneration();
+ Assert.True(maxGen >= 1 && maxGen <= 4,
+ $"Expected max generation between 1 and 4, got {maxGen}");
+ }
+
+ [ConditionalFact]
+ public void ServerGC_StructuresAreValid()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ bool valid = gcContract.GetGCStructuresValid();
+ Assert.True(valid, "Expected GC structures to be valid in a dump taken outside of GC");
+ }
+
+ [ConditionalFact]
+ public void ServerGC_CanEnumerateHeaps()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint heapCount = gcContract.GetGCHeapCount();
+
+ List heaps = gcContract.GetGCHeaps().ToList();
+ Assert.Equal((int)heapCount, heaps.Count);
+ foreach (TargetPointer heap in heaps)
+ {
+ Assert.NotEqual(TargetPointer.Null, heap);
+ }
+ }
+
+ [ConditionalFact]
+ public void ServerGC_CanGetHeapData()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ List heaps = gcContract.GetGCHeaps().ToList();
+ Assert.True(heaps.Count > 0, "Expected at least one GC heap");
+
+ foreach (TargetPointer heap in heaps)
+ {
+ GCHeapData heapData = gcContract.GetHeapData(heap);
+ Assert.NotNull(heapData.GenerationTable);
+ Assert.True(heapData.GenerationTable.Count > 0, "Expected at least one generation");
+ }
+ }
+
+ [ConditionalFact]
+ public void ServerGC_BoundsAreReasonable()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ gcContract.GetGCBounds(out TargetPointer minAddr, out TargetPointer maxAddr);
+ Assert.True(minAddr < maxAddr,
+ $"Expected GC min address (0x{minAddr:X}) < max address (0x{maxAddr:X})");
+ }
+
+ [ConditionalFact]
+ public void ServerGC_EachHeapHasGenerationData()
+ {
+ SkipIfVersion("net10.0", "GC contract is not available in .NET 10 dumps");
+ IGC gcContract = Target.Contracts.GC;
+ uint maxGen = gcContract.GetMaxGeneration();
+ List heaps = gcContract.GetGCHeaps().ToList();
+
+ foreach (TargetPointer heap in heaps)
+ {
+ GCHeapData heapData = gcContract.GetHeapData(heap);
+ Assert.NotNull(heapData.GenerationTable);
+ Assert.True(heapData.GenerationTable.Count > 0,
+ $"Expected generation table for heap 0x{heap:X} to be non-empty");
+ }
+ }
+}
+
+public class ServerGCDumpTests_Local : ServerGCDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class ServerGCDumpTests_Net10 : ServerGCDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs
new file mode 100644
index 00000000000000..64b17efdeeebee
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Dump-based integration tests for the StackWalk contract.
+/// Uses the StackWalk debuggee dump, which has a deterministic call stack:
+/// Main → MethodA → MethodB → MethodC → FailFast.
+///
+public abstract class StackWalkDumpTestsBase : DumpTestBase
+{
+ protected override string DebuggeeName => "StackWalk";
+
+ [ConditionalFact]
+ public void StackWalk_ContractIsAvailable()
+ {
+ IStackWalk stackWalk = Target.Contracts.StackWalk;
+ Assert.NotNull(stackWalk);
+ }
+
+ [ConditionalFact]
+ public void StackWalk_CanWalkCrashingThread()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ IStackWalk stackWalk = Target.Contracts.StackWalk;
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+ ThreadData crashingThread = FindCrashingThread(threadContract, storeData);
+
+ IEnumerable frames = stackWalk.CreateStackWalk(crashingThread);
+ List frameList = frames.ToList();
+
+ Assert.True(frameList.Count > 0, "Expected at least one stack frame on the crashing thread");
+ }
+
+ [ConditionalFact]
+ public void StackWalk_HasMultipleFrames()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ IStackWalk stackWalk = Target.Contracts.StackWalk;
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+ ThreadData crashingThread = FindCrashingThread(threadContract, storeData);
+
+ IEnumerable frames = stackWalk.CreateStackWalk(crashingThread);
+ List frameList = frames.ToList();
+
+ // The debuggee has Main → MethodA → MethodB → MethodC → FailFast,
+ // but the stack walk may include runtime helper frames and native transitions.
+ // We just assert there are multiple frames visible.
+ Assert.True(frameList.Count >= 1,
+ $"Expected at least 1 stack frame from the crashing thread, got {frameList.Count}");
+ }
+
+ [ConditionalFact]
+ public void StackWalk_ManagedFramesHaveValidMethodDescs()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ IStackWalk stackWalk = Target.Contracts.StackWalk;
+ IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem;
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+ ThreadData crashingThread = FindCrashingThread(threadContract, storeData);
+
+ IEnumerable frames = stackWalk.CreateStackWalk(crashingThread);
+
+ foreach (IStackDataFrameHandle frame in frames)
+ {
+ TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame);
+ if (methodDescPtr == TargetPointer.Null)
+ continue;
+
+ // Each managed frame's MethodDesc should resolve to a valid MethodDescHandle
+ MethodDescHandle mdHandle = rts.GetMethodDescHandle(methodDescPtr);
+ uint token = rts.GetMethodToken(mdHandle);
+ // MethodDef tokens have the form 0x06xxxxxx
+ Assert.Equal(0x06000000u, token & 0xFF000000u);
+ }
+ }
+
+ [ConditionalFact]
+ public void StackWalk_FramesHaveRawContext()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ IStackWalk stackWalk = Target.Contracts.StackWalk;
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+ ThreadData crashingThread = FindCrashingThread(threadContract, storeData);
+
+ IEnumerable frames = stackWalk.CreateStackWalk(crashingThread);
+ IStackDataFrameHandle? firstFrame = frames.FirstOrDefault();
+ Assert.NotNull(firstFrame);
+
+ byte[] context = stackWalk.GetRawContext(firstFrame);
+ Assert.NotNull(context);
+ Assert.True(context.Length > 0, "Expected non-empty raw context for stack frame");
+ }
+
+ ///
+ /// Finds the first thread in the thread list — in a FailFast dump, this is typically
+ /// the main thread that initiated the crash.
+ ///
+ private static ThreadData FindCrashingThread(IThread threadContract, ThreadStoreData storeData)
+ {
+ TargetPointer currentThread = storeData.FirstThread;
+ Assert.NotEqual(TargetPointer.Null, currentThread);
+ return threadContract.GetThreadData(currentThread);
+ }
+}
+
+public class StackWalkDumpTests_Local : StackWalkDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+public class StackWalkDumpTests_Net10 : StackWalkDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs
new file mode 100644
index 00000000000000..d9266ccbd50345
--- /dev/null
+++ b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs
@@ -0,0 +1,120 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Microsoft.Diagnostics.DataContractReader.Contracts;
+using Xunit;
+
+namespace Microsoft.Diagnostics.DataContractReader.DumpTests;
+
+///
+/// Shared dump-based integration tests for the Thread contract.
+/// Uses the BasicThreads debuggee dump, which spawns 5 named threads then crashes.
+/// Subclasses specify which runtime version's dump to test against.
+///
+public abstract class ThreadDumpTestsBase : DumpTestBase
+{
+ private const int SpawnedThreadCount = 5;
+
+ protected override string DebuggeeName => "BasicThreads";
+
+ [ConditionalFact]
+ public void ThreadStoreData_HasExpectedThreadCount()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ Assert.NotNull(threadContract);
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ Assert.True(storeData.ThreadCount >= SpawnedThreadCount + 1,
+ $"Expected at least {SpawnedThreadCount + 1} threads, got {storeData.ThreadCount}");
+ }
+
+ [ConditionalFact]
+ public void EnumerateThreads_CanWalkThreadList()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ Assert.NotNull(threadContract);
+
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ int count = 0;
+ TargetPointer currentThread = storeData.FirstThread;
+ while (currentThread != TargetPointer.Null)
+ {
+ ThreadData threadData = threadContract.GetThreadData(currentThread);
+ count++;
+ currentThread = threadData.NextThread;
+
+ Assert.NotEqual(new TargetNUInt(0), threadData.OSId);
+ }
+
+ Assert.Equal(storeData.ThreadCount, count);
+ }
+
+ [ConditionalFact]
+ public void ThreadStoreData_HasFinalizerThread()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ Assert.NotEqual(TargetPointer.Null, storeData.FinalizerThread);
+ }
+
+ [ConditionalFact]
+ public void ThreadStoreData_HasGCThread()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ // GC thread may or may not be set depending on runtime state at crash time,
+ // but the field should be readable without throwing.
+ _ = storeData.GCThread;
+ }
+
+ [ConditionalFact]
+ public void Threads_HaveValidIds()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ ThreadStoreData storeData = threadContract.GetThreadStoreData();
+
+ TargetPointer currentThread = storeData.FirstThread;
+ HashSet seenIds = new();
+
+ while (currentThread != TargetPointer.Null)
+ {
+ ThreadData threadData = threadContract.GetThreadData(currentThread);
+ Assert.True(seenIds.Add(threadData.Id), $"Duplicate thread ID: {threadData.Id}");
+ currentThread = threadData.NextThread;
+ }
+ }
+
+ [ConditionalFact]
+ public void ThreadCounts_AreNonNegative()
+ {
+ IThread threadContract = Target.Contracts.Thread;
+ ThreadStoreCounts counts = threadContract.GetThreadCounts();
+
+ Assert.True(counts.UnstartedThreadCount >= 0, $"UnstartedThreadCount should be non-negative, got {counts.UnstartedThreadCount}");
+ Assert.True(counts.BackgroundThreadCount >= 0, $"BackgroundThreadCount should be non-negative, got {counts.BackgroundThreadCount}");
+ Assert.True(counts.PendingThreadCount >= 0, $"PendingThreadCount should be non-negative, got {counts.PendingThreadCount}");
+ Assert.True(counts.DeadThreadCount >= 0, $"DeadThreadCount should be non-negative, got {counts.DeadThreadCount}");
+ }
+
+}
+
+///
+/// Thread contract tests against a dump from the local (in-repo) runtime build.
+///
+public class ThreadDumpTests_Local : ThreadDumpTestsBase
+{
+ protected override string RuntimeVersion => "local";
+}
+
+///
+/// Thread contract tests against a dump from the .NET 10 release runtime.
+///
+public class ThreadDumpTests_Net10 : ThreadDumpTestsBase
+{
+ protected override string RuntimeVersion => "net10.0";
+}
diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj
index deab69ce582e52..c9de2a1bac2da7 100644
--- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj
+++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj
@@ -5,6 +5,10 @@
false
+
+
+
+