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 + + + +