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..83d5b83e335a1c 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 # Temporarily due to CI capacity constraints. Will re-enable once osx queues are more available. resources: repositories: @@ -134,3 +141,88 @@ 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) msbuild + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj + /t:GenerateAllDumps + /p:CIDumpVersionsOnly=true + /p:SetDisableAuxProviderSignatureCheck=true + /p:TargetArchitecture=$(archType) + -bl:$(Build.SourcesDirectory)/artifacts/log/DumpGeneration.binlog + displayName: 'Generate cDAC 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: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() + # Fail the job if any test or publish step above reported issues. + - script: echo "One or more dump test steps failed." && exit 1 + displayName: 'Fail if tests failed' + condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues') 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..5b948f4973afde --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs @@ -0,0 +1,103 @@ +// 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[(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(); + } +} 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..58e85258f42834 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.props @@ -0,0 +1,17 @@ + + + + + Exe + $(NetCoreAppToolCurrent);net10.0 + true + enable + $(ArtifactsBinDir)DumpTests\$(MSBuildProjectName)\$(Configuration)\ + true + + $(NoWarn);CA1852 + + Heap + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets new file mode 100644 index 00000000000000..8c6ba474d40e6c --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + + + + <_DumpTypeOutput Include="$(MSBuildProjectName)" DumpTypes="$(DumpTypes)" /> + + + 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..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/MultiModule.csproj @@ -0,0 +1,5 @@ + + + Full + + 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..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/StackWalk.csproj @@ -0,0 +1,5 @@ + + + Full + + 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..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/TypeHierarchy/TypeHierarchy.csproj @@ -0,0 +1,5 @@ + + + Full + + 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..ec01919743f604 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Base class for dump-based cDAC integration tests. +/// Each test is a [ConditionalTheory] parameterized by . +/// Call at the start of every test method to load +/// the dump and evaluate skip attributes such as . +/// +public abstract class DumpTestBase : IDisposable +{ + private ClrMdDumpHost? _host; + private ContractDescriptorTarget? _target; + + /// + /// The set of runtime versions to test against. + /// Each entry produces a separate test invocation via [MemberData]. + /// + public static IEnumerable TestConfigurations + { + get + { + if (!IsVersionSkipped("local")) + yield return [new TestConfiguration("local")]; + + if (!IsVersionSkipped("net10.0")) + yield return [new TestConfiguration("net10.0")]; + } + } + + /// + /// The name of the debuggee that produced the dump (e.g., "BasicThreads"). + /// + protected abstract string DebuggeeName { get; } + + /// + /// The dump type to load (e.g., "heap", "full"). Determines the subdirectory + /// under the dump root where the dump file is located. Override in test classes + /// to select a different dump type. + /// + protected virtual string DumpType => "heap"; + + /// + /// The cDAC Target created from the dump. + /// + protected ContractDescriptorTarget Target => _target ?? throw new InvalidOperationException("Dump not loaded."); + + /// + /// Loads the dump for the given and evaluates skip + /// attributes on the calling test method. Call this as the first line of every test. + /// + protected void InitializeDumpTest(TestConfiguration config, [CallerMemberName] string callerName = "") + { + EvaluateVersionSkipAttributes(config, callerName); + + string dumpPath = GetDumpPath(config.RuntimeVersion); + + Assert.True(File.Exists(dumpPath), $"Dump file not found: {dumpPath}"); + + + _host = ClrMdDumpHost.Open(dumpPath); + ulong contractDescriptor = _host.FindContractDescriptorAddress(); + + bool created = ContractDescriptorTarget.TryCreate( + contractDescriptor, + _host.ReadFromTarget, + writeToTarget: static (_, _) => -1, + _host.GetThreadContext, + additionalFactories: [], + out _target); + + Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}"); + + } + + public void Dispose() + { + _host?.Dispose(); + System.GC.SuppressFinalize(this); + } + + /// + /// Checks the calling test method for and + /// throws if the current configuration matches. + /// + private void EvaluateVersionSkipAttributes(TestConfiguration config, string callerName) + { + if (config.RuntimeVersion is "net10.0" && DumpType == "heap") + { + // Skip heap dumps on net10.0 for now, as they are currently generated with an older cDAC version that doesn't populate all fields + throw new SkipTestException($"[net10.0] Skipping heap dump tests due to outdated dump generation."); + } + + MethodInfo? method = GetType().GetMethod(callerName, BindingFlags.Public | BindingFlags.Instance); + if (method is null) + return; + + foreach (SkipOnVersionAttribute attr in method.GetCustomAttributes()) + { + if (string.Equals(attr.Version, config.RuntimeVersion, StringComparison.OrdinalIgnoreCase)) + throw new SkipTestException($"[{config.RuntimeVersion}] {attr.Reason}"); + } + } + + private string GetDumpPath(string runtimeVersion) + { + 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, DumpType, 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/DumpTestHelpers.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs new file mode 100644 index 00000000000000..939755d426fe72 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestHelpers.cs @@ -0,0 +1,88 @@ +// 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 System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Helpers for resolving method names and other metadata from cDAC contracts. +/// +internal static class DumpTestHelpers +{ + /// + /// Resolves the method name for a using the + /// RuntimeTypeSystem, Loader, and EcmaMetadata contracts. Returns null + /// if the name cannot be resolved (e.g., missing metadata). + /// + public static string? GetMethodName(ContractDescriptorTarget target, MethodDescHandle mdHandle) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + + if (rts.IsNoMetadataMethod(mdHandle, out string dynamicName)) + return dynamicName; + + uint token = rts.GetMethodToken(mdHandle); + TargetPointer mt = rts.GetMethodTable(mdHandle); + TargetPointer modulePtr = rts.GetModule(rts.GetTypeHandle(mt)); + + ILoader loader = target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = target.Contracts.EcmaMetadata; + MetadataReader? reader = ecmaMetadata.GetMetadata(moduleHandle); + if (reader is null) + return null; + + MethodDefinitionHandle methodDef = MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF)); + + return reader.GetString(reader.GetMethodDefinition(methodDef).Name); + } + + /// + /// Resolves the method name for a stack frame's MethodDesc pointer. + /// Returns null if the frame has no MethodDesc or the name cannot be resolved. + /// + public static string? GetMethodName(ContractDescriptorTarget target, TargetPointer methodDescPtr) + { + if (methodDescPtr == TargetPointer.Null) + return null; + + MethodDescHandle mdHandle = target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(methodDescPtr); + + return GetMethodName(target, mdHandle); + } + + /// + /// Finds the thread that called FailFast by walking each thread's stack and looking + /// for a frame whose method name contains "FailFast". Asserts if no such thread is found. + /// + public static ThreadData FindFailFastThread(ContractDescriptorTarget target) + { + IThread threadContract = target.Contracts.Thread; + IStackWalk stackWalk = target.Contracts.StackWalk; + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + TargetPointer currentThreadPtr = storeData.FirstThread; + while (currentThreadPtr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(currentThreadPtr); + + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(threadData)) + { + TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame); + string? name = GetMethodName(target, methodDescPtr); + if (name is not null && name.Contains("FailFast")) + return threadData; + } + + currentThreadPtr = threadData.NextThread; + } + + Assert.Fail("Could not find a thread with FailFast on the stack"); + return default; + } +} 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..1c36b19d833b9f --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -0,0 +1,193 @@ + + + + + $([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)')) + + + + + + + + + + local + local;net10.0 + + + + + + + + + + + + + + + + + + + + + + + + <_DumpTypeItem Remove="@(_DumpTypeItem)" /> + <_DumpTypeItem Include="$(_DebuggeeDumpTypes)" /> + + + + + + + + + <_MiniDumpType Condition="'$(_DumpTypeName)' == 'Heap'">2 + <_MiniDumpType Condition="'$(_DumpTypeName)' == 'Full'">4 + <_DumpTypeDirName Condition="'$(_DumpTypeName)' == 'Heap'">heap + <_DumpTypeDirName Condition="'$(_DumpTypeName)' == 'Full'">full + + + + + + + + <_DebuggeeDir>$([MSBuild]::NormalizeDirectory('$(DebuggeesDir)', '$(DebuggeeName)')) + <_DumpDir>$([MSBuild]::NormalizeDirectory('$(DumpOutputDir)', '$(DumpRuntimeVersion)', '$(_DumpTypeDirName)', '$(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..97560b619c838f --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/EcmaMetadataDumpTests.cs @@ -0,0 +1,75 @@ +// 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 class EcmaMetadataDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "MultiModule"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void EcmaMetadata_RootModuleHasMetadataAddress(TestConfiguration config) + { + InitializeDumpTest(config); + 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"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void EcmaMetadata_CanGetMetadataReader(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void EcmaMetadata_MetadataReaderHasTypeDefs(TestConfiguration config) + { + InitializeDumpTest(config); + 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"); + } +} 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..fc1d2b04c86be1 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs @@ -0,0 +1,138 @@ +// 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 class LoaderDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "MultiModule"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void Loader_CanGetRootAssembly(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + Assert.NotNull(loader); + + TargetPointer rootAssembly = loader.GetRootAssembly(); + Assert.NotEqual(TargetPointer.Null, rootAssembly); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_RootAssemblyHasModule(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + TargetPointer rootAssembly = loader.GetRootAssembly(); + + ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly); + TargetPointer modulePtr = loader.GetModule(moduleHandle); + Assert.NotEqual(TargetPointer.Null, modulePtr); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_CanGetModulePath(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void Loader_AppDomainHasFriendlyName(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + string name = loader.GetAppDomainFriendlyName(); + Assert.NotNull(name); + Assert.NotEmpty(name); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void Loader_GlobalLoaderAllocatorIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + TargetPointer globalLA = loader.GetGlobalLoaderAllocator(); + Assert.NotEqual(TargetPointer.Null, globalLA); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_RootModuleHasFileName(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_RootModuleIsNotDynamic(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + TargetPointer rootAssembly = loader.GetRootAssembly(); + ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly); + + Assert.False(loader.IsDynamic(moduleHandle)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_RootModuleHasLoaderAllocator(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + TargetPointer rootAssembly = loader.GetRootAssembly(); + ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly); + + TargetPointer la = loader.GetLoaderAllocator(moduleHandle); + Assert.NotEqual(TargetPointer.Null, la); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void Loader_RootModuleHasILBase(TestConfiguration config) + { + InitializeDumpTest(config); + ILoader loader = Target.Contracts.Loader; + TargetPointer rootAssembly = loader.GetRootAssembly(); + ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(rootAssembly); + + TargetPointer ilBase = loader.GetILBase(moduleHandle); + Assert.NotEqual(TargetPointer.Null, ilBase); + } +} 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..40c7ed5bb6debe --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj @@ -0,0 +1,38 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md new file mode 100644 index 00000000000000..d1dfeb3093a5cb --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -0,0 +1,246 @@ +# cDAC Dump Tests + +Integration tests that validate the cDAC (contract-based Data Access Component) reader +against real crash dumps. Each test loads a dump produced by a purpose-built debuggee +app, creates a `ContractDescriptorTarget` via ClrMD, and exercises one or more cDAC +contracts against real runtime data structures. + +## Overview + +### Architecture + +``` +Debuggees/ Purpose-built console apps that crash via FailFast +DumpTests.targets MSBuild logic to build debuggees, run them, and collect dumps +RunDumpTests.ps1 Windows helper script to orchestrate dump generation + test runs +DumpTestBase.cs Base class: loads a dump, creates a cDAC Target, handles skipping +ClrMdDumpHost.cs Wraps ClrMD DataTarget for memory reads and symbol lookup +TestConfiguration Runtime version descriptor, implements IXunitSerializable +*DumpTests.cs Test classes organized by cDAC contract +``` + +### Debuggees + +Each debuggee is a small console app under `Debuggees/` that exercises specific runtime +features and then calls `Environment.FailFast()` to produce a crash dump. + +| Debuggee | Purpose | Dump Type | +|----------|---------|-----------| +| BasicThreads | Thread management, thread store | Heap | +| GCRoots | GC object graphs, pinned handles | Heap | +| ServerGC | Server GC mode heap structures | Heap | +| StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Full | +| MultiModule | Multi-assembly metadata resolution | Full | +| TypeHierarchy | Type inheritance, method tables | Full | + +The dump type is configured per-debuggee via the `DumpTypes` property in each debuggee's +`.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Debuggees that +need full memory content (e.g., metadata-heavy scenarios) override this to `Full`. + +### Test Classes + +Each test class targets a specific cDAC contract and specifies which debuggee dump to +use. Tests are `[ConditionalTheory]` methods parameterized by `TestConfiguration`. + +| Test Class | Contract | Debuggee | +|------------|----------|----------| +| ThreadDumpTests | Thread | BasicThreads | +| RuntimeInfoDumpTests | RuntimeInfo | BasicThreads | +| WorkstationGCDumpTests | GC (Workstation) | GCRoots | +| ServerGCDumpTests | GC (Server) | ServerGC | +| StackWalkDumpTests | StackWalk | StackWalk | +| RuntimeTypeSystemDumpTests | RuntimeTypeSystem | TypeHierarchy | +| LoaderDumpTests | Loader | MultiModule | +| EcmaMetadataDumpTests | EcmaMetadata | MultiModule | + +### Runtime Versions + +Tests run against multiple runtime versions via `TestConfiguration`: + +- **`local`** — Framework-dependent build run with the repo's testhost (the runtime + you just built). Used for validating current development work. +- **`net10.0`** — Self-contained publish against the released .NET 10 runtime. Used + for cross-version compatibility testing. + +Each version produces a separate test invocation via `[MemberData(nameof(TestConfigurations))]`. + +> **Note:** .NET 10 did not support cDAC in heap dumps. Heap dump tests for net10.0 +> are automatically skipped. Only full dump tests (RuntimeTypeSystem, Loader, +> EcmaMetadata) run against net10.0. + +### Test Skipping + +Tests can be conditionally skipped using attributes: + +- **`[SkipOnVersion("net10.0", "reason")]`** — Skip when running against a specific + runtime version. Evaluated before the dump is loaded. + +Multiple skip attributes can be stacked on a single method. + +### Dump Directory Layout + +Dumps are written to: + +``` +artifacts/dumps/cdac/{version}/{dumptype}/{debuggee}/{debuggee}.dmp +``` + +For example: + +``` +artifacts/dumps/cdac/ + local/ + heap/ + BasicThreads/BasicThreads.dmp + full/ + StackWalk/StackWalk.dmp + net10.0/ + full/ + TypeHierarchy/TypeHierarchy.dmp +``` + +## Running Locally (Windows) + +### Prerequisites + +1. Build the runtime and test host: + + ```cmd + build.cmd clr+libs+tools.cdac -rc release + ``` + +2. Ensure the repo dotnet (`.dotnet/dotnet.exe`) is available. + +### Using RunDumpTests.ps1 + +The `RunDumpTests.ps1` script is the recommended way to run locally on Windows. +Run it from the `DumpTests/` directory. + +```powershell +# Generate dumps and run all tests +.\RunDumpTests.ps1 + +# Generate dumps only (no tests) +.\RunDumpTests.ps1 -Action dumps + +# Run tests only (dumps must already exist) +.\RunDumpTests.ps1 -Action test + +# Target a specific runtime version +.\RunDumpTests.ps1 -Versions local + +# Force regeneration of existing dumps +.\RunDumpTests.ps1 -Force + +# Filter tests by name +.\RunDumpTests.ps1 -Filter "*StackWalk*" +``` + +> **Admin Note (Windows):** Heap dumps require the DAC (`mscordaccore.dll`), which is +> unsigned in local builds. Pass `-SetSignatureCheck` on first run to set the +> `DisableAuxProviderSignatureCheck` registry key under +> `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\MiniDumpSettings`. +> This requires running as Administrator. + +### Using MSBuild Directly + +You can also invoke dump generation directly via MSBuild: + +```cmd +.dotnet\dotnet msbuild src\native\managed\cdac\tests\DumpTests\Microsoft.Diagnostics.DataContractReader.DumpTests.csproj /t:GenerateAllDumps /v:minimal +``` + +And run the tests: + +```cmd +.dotnet\dotnet test src\native\managed\cdac\tests\DumpTests\Microsoft.Diagnostics.DataContractReader.DumpTests.csproj +``` + +### Using a CI Dump Archive + +If you have a `.tar.gz` archive of dumps from CI, you can run tests against them +without generating dumps locally: + +```powershell +.\RunDumpTests.ps1 -DumpArchive C:\Downloads\CdacDumps_linux_x64.tar.gz +``` + +This extracts the archive, auto-detects which runtime versions are present, and +runs the tests with `CDAC_DUMP_ROOT` pointing to the extracted dumps. + +## Code Coverage + +To collect code coverage for the cDAC reader, pass `-Coverage` to `RunDumpTests.ps1`: + +```powershell +.\RunDumpTests.ps1 -Coverage +``` + +This uses [coverlet](https://github.com/coverlet-coverage/coverlet) to instrument the +test run and [ReportGenerator](https://github.com/danielpalme/ReportGenerator) to +produce an HTML report. + +### Prerequisites + +Restore the required dotnet tools (one-time setup from the DumpTests directory): + +```powershell +dotnet tool restore +``` + +This installs `coverlet` and `reportgenerator` as local tools. + +### Output + +- **Raw coverage data:** `artifacts/TestResults/cdac-dump-tests/` +- **HTML report:** `artifacts/coverage/cdac-dump-tests/index.html` +- **Text summary:** printed to the console after the test run + +Coverage is collected for the `Microsoft.Diagnostics.DataContractReader.*` assemblies +exercised during the test run. The report shows which cDAC contract implementations +are covered by the dump tests. + +## CI Pipeline + +The CI pipeline is defined in `eng/pipelines/runtime-diagnostics.yml` and runs on +a schedule (nightly) and on PRs that touch `src/native/managed/cdac/**` or +`eng/pipelines/**`. + +### Current CI Behavior + +CI currently generates **local dumps only** (`CIDumpVersionsOnly=true`). The net10.0 +self-contained publish requires a separate .NET 10 SDK which is not available on CI +agents. + +The pipeline has three stages: + +1. **Build** — Builds the runtime (`clr+libs+tools.cdac+host+packs`) and runs the + standard diagnostics test suite (with and without cDAC). + +2. **DumpCreation** — Builds the runtime, invokes `GenerateAllDumps` to create crash + dumps for each debuggee, and publishes the dumps as pipeline artifacts. Runs on + each platform in `cdacDumpPlatforms` (currently `windows_x64` and `linux_x64`). + +3. **DumpTest** — Downloads dump artifacts from all platforms and runs the test suite + against each set of dumps. This enables cross-platform validation (e.g., running + Linux dump analysis on a Windows host). Tests for net10.0 are skipped via + `SkipDumpVersions=net10.0`. + +## Adding a New Debuggee + +1. Create a new directory under `Debuggees/` with a `.csproj` and `Program.cs`. +2. The app should exercise the runtime feature you want to test, then call + `Environment.FailFast("message")` to trigger a crash dump. +3. The `.csproj` inherits defaults from `Debuggees/Directory.Build.props` + (output path, target frameworks, `DumpTypes=Heap`). +4. Override `Full` if your test needs full memory dumps. +5. The debuggee is auto-discovered by `DumpTests.targets` — no list to update. + +## Adding a New Test + +1. Create a new test class inheriting from `DumpTestBase`. +2. Override `DebuggeeName` to match the debuggee directory name. +3. Override `DumpType` if the debuggee uses full dumps (`"full"`). +4. Write tests as `[ConditionalTheory]` with `[MemberData(nameof(TestConfigurations))]`. +5. Call `InitializeDumpTest(config)` as the first line of every test method. +6. Use `[SkipOnVersion("net10.0", "reason")]` for conditional test skipping. 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..8740c6807e040a --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/RunDumpTests.ps1 @@ -0,0 +1,373 @@ +<# +.SYNOPSIS + Generates crash dumps and/or runs cDAC dump-based integration tests. + +.DESCRIPTION + This script orchestrates the cDAC dump test workflow on Windows: + 1. Invoke MSBuild GenerateAllDumps target to build debuggees and produce crash dumps + 2. 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. + + Dump types are controlled per-debuggee via the DumpTypes property in each + debuggee's csproj (default: Heap, set in Debuggees/Directory.Build.props). + + Dumps are written to: artifacts\dumps\cdac\{version}\{dumptype}\{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}/{dumptype}/{debuggee}/{debuggee}.dmp + +.PARAMETER SetSignatureCheck + When set, configures the DisableAuxProviderSignatureCheck registry key + (Windows 11+ / Server 2022+) to allow heap dumps with an unsigned DAC. + Requires running as Administrator. Used by CI and for first-time local setup. + +.PARAMETER Coverage + When set, collects code coverage via coverlet and generates an HTML report + using ReportGenerator. Requires the dotnet tools 'coverlet' and + 'reportgenerator' to be restored (dotnet tool restore). + The report is written to artifacts\coverage\cdac-dump-tests\. + +.PARAMETER VerboseOutput + When set, increases MSBuild and dotnet verbosity to normal (default: minimal/quiet). + +.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 + +.EXAMPLE + .\RunDumpTests.ps1 -SetSignatureCheck + +.EXAMPLE + .\RunDumpTests.ps1 -Coverage +#> + +[CmdletBinding()] +param( + [ValidateSet("dumps", "test", "all")] + [string]$Action = "all", + + [string]$Versions = "all", + + [switch]$Force, + + [string]$TestHostConfiguration = "Release", + + [string]$Filter = "", + + [string]$DumpArchive = "", + + [switch]$SetSignatureCheck, + + [switch]$Coverage, + + [switch]$VerboseOutput +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# --- Verbosity levels --- +$msbuildVerbosity = if ($VerboseOutput) { "normal" } else { "minimal" } +$dotnetVerbosity = if ($VerboseOutput) { "n" } else { "q" } + +# --- 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" + +# --- Coverage paths --- +$coverageResultsDir = Join-Path $repoRoot "artifacts\TestResults\cdac-dump-tests" +$coverageReportDir = Join-Path $repoRoot "artifacts\coverage\cdac-dump-tests" + +function Build-CoverageArgs { + if (-not $Coverage) { return @() } + return @( + "--collect:XPlat Code Coverage", + "--results-directory", $coverageResultsDir + ) +} + +function New-CoverageReport { + if (-not $Coverage) { return } + + $coverageFiles = @(Get-ChildItem -Path $coverageResultsDir -Recurse -Filter "coverage.cobertura.xml" -ErrorAction SilentlyContinue) + if ($coverageFiles.Count -eq 0) { + Write-Host " No coverage files found. Skipping report generation." -ForegroundColor Yellow + return + } + + $inputFiles = ($coverageFiles | ForEach-Object { $_.FullName }) -join ";" + Write-Host "" + Write-Host "--- Generating coverage report ---" -ForegroundColor Cyan + + & $dotnet tool run reportgenerator ` + "-reports:$inputFiles" ` + "-targetdir:$coverageReportDir" ` + "-reporttypes:Html;TextSummary" ` + "-verbosity:Warning" 2>&1 | ForEach-Object { Write-Host " $_" } + + if ($LASTEXITCODE -eq 0) { + $summaryFile = Join-Path $coverageReportDir "Summary.txt" + if (Test-Path $summaryFile) { + Write-Host "" + Write-Host "--- Coverage Summary ---" -ForegroundColor Cyan + Get-Content $summaryFile | ForEach-Object { Write-Host " $_" } + } + Write-Host "" + Write-Host " Full report: $coverageReportDir\index.html" -ForegroundColor Green + } + else { + Write-Host " Report generation failed." -ForegroundColor Yellow + } +} + +if (-not (Test-Path $dotnet)) { + Write-Error "Repo dotnet not found at $dotnet. Run build.cmd first." + exit 1 +} + +# --- Debuggees and versions --- +$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.Replace("$extractDir\", "") + $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", $dotnetVerbosity) + 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" + $coverageArgs = Build-CoverageArgs + $testArgs = @($dumpTestsProj, "--no-build", "--logger", "console;verbosity=detailed") + $coverageArgs + if ($filterExpr) { + $testArgs += @("--filter", $filterExpr) + } + & $dotnet test @testArgs 2>&1 | ForEach-Object { Write-Host " $_" } + $testExitCode = $LASTEXITCODE + $ErrorActionPreference = $saved + Remove-Item Env:\CDAC_DUMP_ROOT -ErrorAction SilentlyContinue + + New-CoverageReport + + 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 " Force: $Force" +if ($Filter) { Write-Host " Filter: $Filter" } +Write-Host "" + +# --- Force: delete existing dumps --- +if ($Force -and $Action -in @("dumps", "all")) { + $dumpOutputDir = Join-Path $repoRoot "artifacts\dumps\cdac" + if (Test-Path $dumpOutputDir) { + Write-Host "Deleting existing dumps: $dumpOutputDir" -ForegroundColor Yellow + Remove-Item $dumpOutputDir -Recurse -Force + } +} + +# --- Generate dumps via MSBuild target --- +# Dump generation (including registry setup for heap dumps on Windows) is handled +# entirely by DumpTests.targets. Each debuggee's DumpTypes property controls what +# types are generated (see Debuggees/Directory.Build.props for the default). +if ($Action -in @("dumps", "all")) { + Write-Host "--- Generating dumps ---" -ForegroundColor Cyan + + $msbuildArgs = @( + "msbuild", $dumpTestsProj, + "/t:GenerateAllDumps", + "/p:TestHostConfiguration=$TestHostConfiguration", + "/p:DumpVersions=`"$($selectedVersions -join ';')`"", + "/v:$msbuildVerbosity" + ) + + if ($SetSignatureCheck) { + $msbuildArgs += "/p:SetDisableAuxProviderSignatureCheck=true" + } + + & $dotnet @msbuildArgs 2>&1 | ForEach-Object { Write-Host " $_" } + if ($LASTEXITCODE -ne 0) { + Write-Host "" + Write-Host "DUMP GENERATION FAILED" -ForegroundColor Red + exit 1 + } +} + +# --- Run tests --- +if ($Action -in @("test", "all")) { + Write-Host "" + Write-Host "--- Building test project ---" -ForegroundColor Cyan + & $dotnet build $dumpTestsProj --nologo -v $dotnetVerbosity 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 + + $coverageArgs = Build-CoverageArgs + $testArgs = @($dumpTestsProj, "--no-build", "--logger", "console;verbosity=detailed") + $coverageArgs + + # Apply user-supplied name filter (glob-style: * maps to dotnet test's ~ operator) + if ($Filter) { + $namePattern = $Filter.Replace("*", "") + $testArgs += @("--filter", "FullyQualifiedName~$namePattern") + } + + # dotnet test writes failure details to stderr; suppress termination so we see full results. + $saved = $ErrorActionPreference + $ErrorActionPreference = "Continue" + & $dotnet test @testArgs 2>&1 | ForEach-Object { Write-Host " $_" } + $testExitCode = $LASTEXITCODE + $ErrorActionPreference = $saved + + New-CoverageReport + + 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..2a0d78ffe45fa2 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs @@ -0,0 +1,41 @@ +// 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 class RuntimeInfoDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "BasicThreads"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeInfo_ArchitectureIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeInfo runtimeInfo = Target.Contracts.RuntimeInfo; + RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); + + Assert.True(Enum.IsDefined(arch), + $"Expected a valid RuntimeInfoArchitecture enum value, got {arch}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeInfo_OperatingSystemIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeInfo runtimeInfo = Target.Contracts.RuntimeInfo; + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + + Assert.True(Enum.IsDefined(os), + $"Expected a valid RuntimeInfoOperatingSystem enum value, got {os}"); + } +} 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..b1914e48f3a6f7 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/RuntimeTypeSystemDumpTests.cs @@ -0,0 +1,257 @@ +// 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 class RuntimeTypeSystemDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "TypeHierarchy"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public void RuntimeTypeSystem_CanGetMethodTableFromModule(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_FreeObjectMethodTableIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_StringMethodTableIsString(TestConfiguration config) + { + InitializeDumpTest(config); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableHasParent(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_StringHasObjectParent(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableHasReasonableBaseSize(TestConfiguration config) + { + InitializeDumpTest(config); + 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}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_StringHasNonZeroComponentSize(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableContainsNoGCPointers(TestConfiguration config) + { + InitializeDumpTest(config); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableHasValidToken(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableHasMethods(TestConfiguration config) + { + InitializeDumpTest(config); + 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}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_StringIsNotGenericTypeDefinition(TestConfiguration config) + { + InitializeDumpTest(config); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_StringCorElementTypeIsClass(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void RuntimeTypeSystem_ObjectMethodTableHasIntroducedMethods(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + } +} 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..7d68338f89d9ae --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs @@ -0,0 +1,119 @@ +// 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 class ServerGCDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "ServerGC"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_IsServerGC(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + string[] gcIdentifiers = gcContract.GetGCIdentifiers(); + Assert.Contains(GCIdentifiers.Server, gcIdentifiers); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_MaxGenerationIsReasonable(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + uint maxGen = gcContract.GetMaxGeneration(); + Assert.True(maxGen >= 1 && maxGen <= 4, + $"Expected max generation between 1 and 4, got {maxGen}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_StructuresAreValid(TestConfiguration config) + { + InitializeDumpTest(config); + 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"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_CanEnumerateHeaps(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_CanGetHeapData(TestConfiguration config) + { + InitializeDumpTest(config); + 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"); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_BoundsAreReasonable(TestConfiguration config) + { + InitializeDumpTest(config); + 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})"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_EachHeapHasGenerationData(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + 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"); + } + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/SkipOnVersionAttribute.cs b/src/native/managed/cdac/tests/DumpTests/SkipOnVersionAttribute.cs new file mode 100644 index 00000000000000..f81a74d01ffa96 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/SkipOnVersionAttribute.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// When applied to a test method, causes the test to be skipped for +/// the specified runtime version. Checked by +/// before each test runs. +/// Multiple attributes can be stacked on a single method. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class SkipOnVersionAttribute : Attribute +{ + public string Version { get; } + public string Reason { get; } + + public SkipOnVersionAttribute(string version, string reason) + { + Version = version; + Reason = reason; + } +} 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..bb45d3b6452588 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -0,0 +1,102 @@ +// 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 class StackWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackWalk_CanWalkCrashingThread(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + IEnumerable frames = stackWalk.CreateStackWalk(crashingThread); + List frameList = frames.ToList(); + + Assert.True(frameList.Count > 0, "Expected at least one stack frame on the crashing thread"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackWalk_HasMultipleFrames(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + 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 >= 5, + $"Expected multiple stack frames from the crashing thread, got {frameList.Count}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackWalk_ManagedFramesHaveValidMethodDescs(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + 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); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackWalk_FramesHaveRawContext(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + 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"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/TestConfiguration.cs b/src/native/managed/cdac/tests/DumpTests/TestConfiguration.cs new file mode 100644 index 00000000000000..7c5ca0e5a667d3 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/TestConfiguration.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Describes which dump to load for a parameterized dump test. +/// Implements so xUnit can serialize it +/// across test method invocations and display it in test explorer. +/// +public sealed class TestConfiguration : IXunitSerializable +{ + /// + /// The runtime version identifier (e.g., "local", "net10.0"). + /// Maps to a subdirectory under the dump root. + /// + public string RuntimeVersion { get; set; } = string.Empty; + + public TestConfiguration() { } + + public TestConfiguration(string runtimeVersion) + { + RuntimeVersion = runtimeVersion; + } + + public override string ToString() => RuntimeVersion; + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(RuntimeVersion), RuntimeVersion); + } + + public void Deserialize(IXunitSerializationInfo info) + { + RuntimeVersion = info.GetValue(nameof(RuntimeVersion)); + } +} 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..d46a4dbc47fc8c --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs @@ -0,0 +1,114 @@ +// 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; + +/// +/// Dump-based integration tests for the Thread contract. +/// Uses the BasicThreads debuggee dump, which spawns 5 named threads then crashes. +/// +public class ThreadDumpTests : DumpTestBase +{ + private const int SpawnedThreadCount = 5; + + protected override string DebuggeeName => "BasicThreads"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void ThreadStoreData_HasExpectedThreadCount(TestConfiguration config) + { + InitializeDumpTest(config); + 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}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void EnumerateThreads_CanWalkThreadList(TestConfiguration config) + { + InitializeDumpTest(config); + 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); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void ThreadStoreData_HasFinalizerThread(TestConfiguration config) + { + InitializeDumpTest(config); + IThread threadContract = Target.Contracts.Thread; + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + Assert.NotEqual(TargetPointer.Null, storeData.FinalizerThread); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void ThreadStoreData_HasGCThread(TestConfiguration config) + { + InitializeDumpTest(config); + 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; + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void Threads_HaveValidIds(TestConfiguration config) + { + InitializeDumpTest(config); + 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; + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void ThreadCounts_AreNonNegative(TestConfiguration config) + { + InitializeDumpTest(config); + 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}"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs new file mode 100644 index 00000000000000..4bf67825f5cc8c --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs @@ -0,0 +1,90 @@ +// 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 GC contract in workstation GC mode. +/// Uses the GCRoots debuggee dump, which pins objects and creates GC handles +/// under the default workstation GC. +/// +public class WorkstationGCDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "GCRoots"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_IsWorkstationGC(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + string[] gcIdentifiers = gcContract.GetGCIdentifiers(); + Assert.Contains(GCIdentifiers.Workstation, gcIdentifiers); + + uint heapCount = gcContract.GetGCHeapCount(); + Assert.Equal(1u, heapCount); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_MaxGenerationIsReasonable(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + uint maxGen = gcContract.GetMaxGeneration(); + Assert.True(maxGen >= 1 && maxGen <= 4, + $"Expected max generation between 1 and 4, got {maxGen}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_StructuresAreValid(TestConfiguration config) + { + InitializeDumpTest(config); + 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"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_CanEnumerateHeaps(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + uint heapCount = gcContract.GetGCHeapCount(); + Assert.Equal(1u, heapCount); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_CanGetHeapData(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + GCHeapData heapData = gcContract.GetHeapData(); + Assert.NotNull(heapData.GenerationTable); + Assert.True(heapData.GenerationTable.Count > 0, "Expected at least one generation"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_BoundsAreReasonable(TestConfiguration config) + { + InitializeDumpTest(config); + 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})"); + } +} 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 + + + +