diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a87cf069..76c75882 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,7 +15,6 @@ updates: patterns: - "NUnit*" - "coverlet*" - - "Verify*" - package-ecosystem: github-actions directory: / diff --git a/Directory.Packages.props b/Directory.Packages.props index ef3978fa..1fb2c7ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,9 +33,6 @@ - - - diff --git a/tests/SharpDicom.Codecs.Tests/NativeCodecsTests.cs b/tests/SharpDicom.Codecs.Tests/NativeCodecsTests.cs index e93b447b..5f89dec5 100644 --- a/tests/SharpDicom.Codecs.Tests/NativeCodecsTests.cs +++ b/tests/SharpDicom.Codecs.Tests/NativeCodecsTests.cs @@ -428,8 +428,8 @@ public void NativeCodecOptions_CanSetAllProperties() [Test] public void Initialize_WithEnableOptions_SetsProperties() { - // Note: Enable options are only applied during successful initialization - // When native library is unavailable, options aren't applied after DllNotFoundException + // Enable options are applied before the native library is probed, + // so they take effect whether or not the library loads NativeCodecs.Initialize(new NativeCodecOptions { SuppressInitializationErrors = true, @@ -439,23 +439,10 @@ public void Initialize_WithEnableOptions_SetsProperties() EnableVideo = false }); - // If native library was loaded successfully, enable options should be set - if (NativeCodecs.IsAvailable) - { - Assert.That(NativeCodecs.EnableJpeg, Is.False); - Assert.That(NativeCodecs.EnableJpeg2000, Is.False); - Assert.That(NativeCodecs.EnableJpegLs, Is.False); - Assert.That(NativeCodecs.EnableVideo, Is.False); - } - else - { - // When library unavailable, options weren't applied - properties remain default - // This is a design limitation when native libraries are missing - Assert.That(NativeCodecs.EnableJpeg, Is.True, "Default value when library unavailable"); - Assert.That(NativeCodecs.EnableJpeg2000, Is.True, "Default value when library unavailable"); - Assert.That(NativeCodecs.EnableJpegLs, Is.True, "Default value when library unavailable"); - Assert.That(NativeCodecs.EnableVideo, Is.True, "Default value when library unavailable"); - } + Assert.That(NativeCodecs.EnableJpeg, Is.False); + Assert.That(NativeCodecs.EnableJpeg2000, Is.False); + Assert.That(NativeCodecs.EnableJpegLs, Is.False); + Assert.That(NativeCodecs.EnableVideo, Is.False); } #endregion diff --git a/tests/SharpDicom.Tests.Polyfills/SharpDicom.Tests.Polyfills.csproj b/tests/SharpDicom.Tests.Polyfills/SharpDicom.Tests.Polyfills.csproj index d2255072..eb595df0 100644 --- a/tests/SharpDicom.Tests.Polyfills/SharpDicom.Tests.Polyfills.csproj +++ b/tests/SharpDicom.Tests.Polyfills/SharpDicom.Tests.Polyfills.csproj @@ -18,8 +18,6 @@ - - @@ -30,8 +28,13 @@ - + - + + + + + + diff --git a/tests/SharpDicom.Tests/Generators/DicomDictionaryGeneratorTests.cs b/tests/SharpDicom.Tests/Generators/DicomDictionaryGeneratorTests.cs index 9a6dbe2b..284ae4e7 100644 --- a/tests/SharpDicom.Tests/Generators/DicomDictionaryGeneratorTests.cs +++ b/tests/SharpDicom.Tests/Generators/DicomDictionaryGeneratorTests.cs @@ -1,21 +1,19 @@ using System.Collections.Immutable; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using NUnit.Framework; using SharpDicom.Generators; -using VerifyNUnit; namespace SharpDicom.Tests.Generators; /// -/// Snapshot tests for the DicomDictionaryGenerator using Verify.SourceGenerators. +/// Snapshot tests for the DicomDictionaryGenerator. /// [TestFixture] public class DicomDictionaryGeneratorTests { [Test] - public Task GeneratesTagsFromMinimalXml() + public void GeneratesTagsFromMinimalXml() { // Minimal Part 6-like XML with one tag var xml = """ @@ -39,11 +37,11 @@ public Task GeneratesTagsFromMinimalXml() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task GeneratesMultiVRTag() + public void GeneratesMultiVRTag() { // Tag with multiple VRs (US or SS) var xml = """ @@ -67,11 +65,11 @@ public Task GeneratesMultiVRTag() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task GeneratesRetiredTag() + public void GeneratesRetiredTag() { // Retired tag (should be marked with IsRetired=true) var xml = """ @@ -95,11 +93,11 @@ public Task GeneratesRetiredTag() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task GeneratesUIDs() + public void GeneratesUIDs() { // UID registry table var xml = """ @@ -129,11 +127,11 @@ public Task GeneratesUIDs() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task HandlesMaskedTag() + public void HandlesMaskedTag() { // Masked tag (50xx,0010) var xml = """ @@ -157,11 +155,11 @@ public Task HandlesMaskedTag() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task HandlesEmptyXml() + public void HandlesEmptyXml() { var xml = """ @@ -169,19 +167,19 @@ public Task HandlesEmptyXml() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task ReportsDiagnosticForInvalidXml() + public void ReportsDiagnosticForInvalidXml() { var xml = "not valid xml"; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } [Test] - public Task GeneratesTagsAndUIDsCombined() + public void GeneratesTagsAndUIDsCombined() { // Combined tags and UIDs in same XML var xml = """ @@ -217,10 +215,10 @@ public Task GeneratesTagsAndUIDsCombined() """; - return VerifyGenerator(xml, "part06.xml"); + VerifyGenerator(xml, "part06.xml"); } - private static Task VerifyGenerator(string xmlContent, string fileName) + private static void VerifyGenerator(string xmlContent, string fileName) { // Create compilation // IL3000: Assembly.Location is required for source generator tests to get metadata references @@ -246,6 +244,6 @@ private static Task VerifyGenerator(string xmlContent, string fileName) driver = driver.RunGenerators(compilation); // Verify snapshot - return Verifier.Verify(driver); + GeneratorSnapshot.Verify(driver); } } diff --git a/tests/SharpDicom.Tests/Generators/GeneratorSnapshot.cs b/tests/SharpDicom.Tests/Generators/GeneratorSnapshot.cs new file mode 100644 index 00000000..3fe2c461 --- /dev/null +++ b/tests/SharpDicom.Tests/Generators/GeneratorSnapshot.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.CodeAnalysis; +using NUnit.Framework; + +namespace SharpDicom.Tests.Generators; + +/// +/// Snapshot assertions for source generator output. +/// +/// +/// Each generated source is compared against +/// {TestClass}.{TestMethod}#{HintName}.verified.cs next to the calling test file. +/// On mismatch a .received.cs file is written alongside for review; accept a change +/// by renaming it to .verified.cs. +/// +internal static class GeneratorSnapshot +{ + private static readonly UTF8Encoding Utf8WithBom = new(encoderShouldEmitUTF8Identifier: true); + + /// + /// Asserts that the generator run produced no diagnostics or exceptions and that + /// its output matches the verified snapshots for the current test. + /// + /// The generator driver after RunGenerators. + /// Path of the calling test file (supplied by the compiler). + public static void Verify(GeneratorDriver driver, [CallerFilePath] string callerFilePath = "") + { + var runResult = driver.GetRunResult(); + var exceptions = runResult.Results.Where(r => r.Exception is not null).Select(r => r.Exception!.ToString()); + Assert.That(exceptions, Is.Empty, "Generator threw an exception"); + Assert.That(runResult.Diagnostics.Select(d => d.ToString()), Is.Empty, "Generator reported diagnostics"); + + var directory = Path.GetDirectoryName(ResolveSourcePath(callerFilePath))!; + var test = TestContext.CurrentContext.Test; + var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1); + var prefix = $"{className}.{test.MethodName}#"; + + var failures = new List(); + var expectedFiles = new HashSet(StringComparer.Ordinal); + + foreach (var generated in runResult.Results.SelectMany(r => r.GeneratedSources)) + { + var baseName = prefix + Path.GetFileNameWithoutExtension(generated.HintName); + var verifiedPath = Path.Combine(directory, baseName + ".verified.cs"); + var receivedPath = Path.Combine(directory, baseName + ".received.cs"); + expectedFiles.Add(Path.GetFileName(verifiedPath)); + + var actual = Normalize($"//HintName: {generated.HintName}\n{generated.SourceText}"); + var expected = File.Exists(verifiedPath) ? Normalize(File.ReadAllText(verifiedPath)) : null; + + if (actual == expected) + { + if (File.Exists(receivedPath)) + { + File.Delete(receivedPath); + } + + continue; + } + + File.WriteAllText(receivedPath, actual + "\n", Utf8WithBom); + failures.Add(expected is null + ? $"Missing snapshot {Path.GetFileName(verifiedPath)}; wrote {Path.GetFileName(receivedPath)}" + : $"Snapshot mismatch for {Path.GetFileName(verifiedPath)}; wrote {Path.GetFileName(receivedPath)}"); + } + + foreach (var stale in Directory.GetFiles(directory, prefix + "*.verified.cs")) + { + if (!expectedFiles.Contains(Path.GetFileName(stale))) + { + failures.Add($"Snapshot {Path.GetFileName(stale)} has no corresponding generated source"); + } + } + + Assert.That(failures, Is.Empty, "Generator output does not match snapshots"); + } + + /// + /// Maps a compiler-recorded source path back to the local file system. Deterministic + /// CI builds record source paths under /_/ rather than the real repository root. + /// + private static string ResolveSourcePath(string callerFilePath) + { + const string MappedRoot = "/_/"; + + if (File.Exists(callerFilePath) || !callerFilePath.StartsWith(MappedRoot, StringComparison.Ordinal)) + { + return callerFilePath; + } + + var repositoryRoot = typeof(GeneratorSnapshot).Assembly + .GetCustomAttributes() + .Single(a => a.Key == "RepositoryRoot") + .Value!; + + return Path.Combine(repositoryRoot, callerFilePath.Substring(MappedRoot.Length)); + } + + private static string Normalize(string text) => + text.TrimStart('').Replace("\r\n", "\n").TrimEnd(); +} diff --git a/tests/SharpDicom.Tests/Generators/Part15ParserTests.cs b/tests/SharpDicom.Tests/Generators/Part15ParserTests.cs index b6766bae..9c0e47ad 100644 --- a/tests/SharpDicom.Tests/Generators/Part15ParserTests.cs +++ b/tests/SharpDicom.Tests/Generators/Part15ParserTests.cs @@ -1,10 +1,8 @@ using System.Collections.Immutable; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using NUnit.Framework; using SharpDicom.Generators; -using VerifyNUnit; namespace SharpDicom.Tests.Generators; @@ -15,7 +13,7 @@ namespace SharpDicom.Tests.Generators; public class Part15ParserTests { [Test] - public Task ParsesBasicProfileActions() + public void ParsesBasicProfileActions() { // Table E.1-1 structure with common attributes var xml = """ @@ -67,11 +65,11 @@ public Task ParsesBasicProfileActions() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } [Test] - public Task ParsesPatientName() + public void ParsesPatientName() { // Patient Name with Z action var xml = """ @@ -104,11 +102,11 @@ public Task ParsesPatientName() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } [Test] - public Task ParsesSOPInstanceUID() + public void ParsesSOPInstanceUID() { // SOP Instance UID with U action and RetainUIDs option var xml = """ @@ -141,11 +139,11 @@ public Task ParsesSOPInstanceUID() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } [Test] - public Task ParsesRetiredAttribute() + public void ParsesRetiredAttribute() { // Retired attribute (Acquisition Comments) var xml = """ @@ -178,11 +176,11 @@ public Task ParsesRetiredAttribute() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } [Test] - public Task ParsesCompoundActions() + public void ParsesCompoundActions() { // Compound action X/Z and X/Z/D var xml = """ @@ -232,11 +230,11 @@ public Task ParsesCompoundActions() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } [Test] - public Task ParsesAllProfileOptions() + public void ParsesAllProfileOptions() { // Institution Name with multiple profile options var xml = """ @@ -269,10 +267,10 @@ public Task ParsesAllProfileOptions() """; - return VerifyGenerator(xml); + VerifyGenerator(xml); } - private static Task VerifyGenerator(string xmlContent) + private static void VerifyGenerator(string xmlContent) { // Create compilation #pragma warning disable IL3000 // Required for source generator tests @@ -297,6 +295,6 @@ private static Task VerifyGenerator(string xmlContent) driver = driver.RunGenerators(compilation); // Verify snapshot - return Verifier.Verify(driver); + GeneratorSnapshot.Verify(driver); } } diff --git a/tests/SharpDicom.Tests/ModuleInitializer.cs b/tests/SharpDicom.Tests/ModuleInitializer.cs deleted file mode 100644 index e0404289..00000000 --- a/tests/SharpDicom.Tests/ModuleInitializer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.CompilerServices; -using VerifyTests; - -namespace SharpDicom.Tests; - -/// -/// Module initializer for Verify.SourceGenerators setup. -/// -public static class ModuleInitializer -{ - /// - /// Initializes Verify.SourceGenerators settings. - /// - [ModuleInitializer] - public static void Initialize() - { - VerifySourceGenerators.Initialize(); - } -} diff --git a/tests/SharpDicom.Tests/SharpDicom.Tests.csproj b/tests/SharpDicom.Tests/SharpDicom.Tests.csproj index 877f654d..708f117c 100644 --- a/tests/SharpDicom.Tests/SharpDicom.Tests.csproj +++ b/tests/SharpDicom.Tests/SharpDicom.Tests.csproj @@ -17,8 +17,6 @@ - - @@ -28,4 +26,14 @@ + + + + + + + + + +