Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ updates:
patterns:
- "NUnit*"
- "coverlet*"
- "Verify*"

- package-ecosystem: github-actions
directory: /
Expand Down
3 changes: 0 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<!-- Source Generator Testing -->
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0" />
<PackageVersion Include="Verify.NUnit" Version="31.28.0" />
<!-- Analyzer Testing (pinned to match Microsoft.CodeAnalysis.CSharp 5.0.0) -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.4" />
Expand Down
25 changes: 6 additions & 19 deletions tests/SharpDicom.Codecs.Tests/NativeCodecsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />

<!-- Source Generator Testing -->
<PackageReference Include="Verify.SourceGenerators" />
<PackageReference Include="Verify.NUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>
Expand All @@ -30,8 +28,13 @@
<ProjectReference Include="..\..\src\SharpDicom.Generators\SharpDicom.Generators.csproj" />
</ItemGroup>

<!-- Share test source files from main test project, excluding obj/bin and NET5+ only tests -->
<!-- Share test source files from main test project, excluding obj/bin, generator snapshots and NET5+ only tests -->
<ItemGroup>
<Compile Include="..\SharpDicom.Tests\**\*.cs" Exclude="..\SharpDicom.Tests\obj\**;..\SharpDicom.Tests\bin\**;..\SharpDicom.Tests\Cli\**;..\SharpDicom.Tests\Benchmarks\**;..\SharpDicom.Tests\Deidentification\DeidentificationConfigLoaderTests.cs;..\SharpDicom.Tests\Deidentification\DeidentificationIntegrationTests.cs;..\SharpDicom.Tests\Deidentification\BatchDeidentifierTests.cs;..\SharpDicom.Tests\Deidentification\DeidentificationApiSmokeTests.cs;..\SharpDicom.Tests\Network\Tls\TlsServerOptionsTests.cs;..\SharpDicom.Tests\Network\Tls\TlsIntegrationTests.cs" LinkBase="Tests" />
<Compile Include="..\SharpDicom.Tests\**\*.cs" Exclude="..\SharpDicom.Tests\obj\**;..\SharpDicom.Tests\bin\**;..\SharpDicom.Tests\Cli\**;..\SharpDicom.Tests\Benchmarks\**;..\SharpDicom.Tests\Deidentification\DeidentificationConfigLoaderTests.cs;..\SharpDicom.Tests\Deidentification\DeidentificationIntegrationTests.cs;..\SharpDicom.Tests\Deidentification\BatchDeidentifierTests.cs;..\SharpDicom.Tests\Deidentification\DeidentificationApiSmokeTests.cs;..\SharpDicom.Tests\Network\Tls\TlsServerOptionsTests.cs;..\SharpDicom.Tests\Network\Tls\TlsIntegrationTests.cs;..\SharpDicom.Tests\**\*.verified.cs;..\SharpDicom.Tests\**\*.received.cs" LinkBase="Tests" />
</ItemGroup>

<!-- Repository root for resolving generator snapshot paths when CI maps source paths to /_/ -->
<ItemGroup>
<AssemblyMetadata Include="RepositoryRoot" Value="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../'))" />
</ItemGroup>
</Project>
40 changes: 19 additions & 21 deletions tests/SharpDicom.Tests/Generators/DicomDictionaryGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Snapshot tests for the DicomDictionaryGenerator using Verify.SourceGenerators.
/// Snapshot tests for the DicomDictionaryGenerator.
/// </summary>
[TestFixture]
public class DicomDictionaryGeneratorTests
{
[Test]
public Task GeneratesTagsFromMinimalXml()
public void GeneratesTagsFromMinimalXml()
{
// Minimal Part 6-like XML with one tag
var xml = """
Expand All @@ -39,11 +37,11 @@ public Task GeneratesTagsFromMinimalXml()
</book>
""";

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 = """
Expand All @@ -67,11 +65,11 @@ public Task GeneratesMultiVRTag()
</book>
""";

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 = """
Expand All @@ -95,11 +93,11 @@ public Task GeneratesRetiredTag()
</book>
""";

return VerifyGenerator(xml, "part06.xml");
VerifyGenerator(xml, "part06.xml");
}

[Test]
public Task GeneratesUIDs()
public void GeneratesUIDs()
{
// UID registry table
var xml = """
Expand Down Expand Up @@ -129,11 +127,11 @@ public Task GeneratesUIDs()
</book>
""";

return VerifyGenerator(xml, "part06.xml");
VerifyGenerator(xml, "part06.xml");
}

[Test]
public Task HandlesMaskedTag()
public void HandlesMaskedTag()
{
// Masked tag (50xx,0010)
var xml = """
Expand All @@ -157,31 +155,31 @@ public Task HandlesMaskedTag()
</book>
""";

return VerifyGenerator(xml, "part06.xml");
VerifyGenerator(xml, "part06.xml");
}

[Test]
public Task HandlesEmptyXml()
public void HandlesEmptyXml()
{
var xml = """
<?xml version="1.0" encoding="UTF-8"?>
<book xmlns="http://docbook.org/ns/docbook">
</book>
""";

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 = """
Expand Down Expand Up @@ -217,10 +215,10 @@ public Task GeneratesTagsAndUIDsCombined()
</book>
""";

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
Expand All @@ -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);
}
}
107 changes: 107 additions & 0 deletions tests/SharpDicom.Tests/Generators/GeneratorSnapshot.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Snapshot assertions for source generator output.
/// </summary>
/// <remarks>
/// Each generated source is compared against
/// <c>{TestClass}.{TestMethod}#{HintName}.verified.cs</c> next to the calling test file.
/// On mismatch a <c>.received.cs</c> file is written alongside for review; accept a change
/// by renaming it to <c>.verified.cs</c>.
/// </remarks>
internal static class GeneratorSnapshot
{
private static readonly UTF8Encoding Utf8WithBom = new(encoderShouldEmitUTF8Identifier: true);

/// <summary>
/// Asserts that the generator run produced no diagnostics or exceptions and that
/// its output matches the verified snapshots for the current test.
/// </summary>
/// <param name="driver">The generator driver after <c>RunGenerators</c>.</param>
/// <param name="callerFilePath">Path of the calling test file (supplied by the compiler).</param>
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<string>();
var expectedFiles = new HashSet<string>(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");
}

/// <summary>
/// Maps a compiler-recorded source path back to the local file system. Deterministic
/// CI builds record source paths under <c>/_/</c> rather than the real repository root.
/// </summary>
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<AssemblyMetadataAttribute>()
.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();
}
Loading
Loading