diff --git a/docs/mdsource/doc-index.include.md b/docs/mdsource/doc-index.include.md index 34428ced8..6a7580623 100644 --- a/docs/mdsource/doc-index.include.md +++ b/docs/mdsource/doc-index.include.md @@ -11,6 +11,7 @@ * [Guids](/docs/guids.md) * [Dates](/docs/dates.md) * [Scrubbing](/docs/scrubbers.md) + * [Solution Discovery](/docs/solution-discovery.md) * [Counter](/docs/counter.md) * [Members that throw](/docs/members-throw.md) * [Ordering](/docs/ordering.md) diff --git a/docs/mdsource/scrubbers.source.md b/docs/mdsource/scrubbers.source.md index ed8734807..342c5f493 100644 --- a/docs/mdsource/scrubbers.source.md +++ b/docs/mdsource/scrubbers.source.md @@ -191,3 +191,4 @@ snippet: Verify.Xunit.Tests/Scrubbers/ScrubberLevelsSample.Usage.verified.txt * [Guid behavior](guids.md) * [Date behavior](dates.md) * [Numeric Ids](numeric-ids.md) + * [Solution Discovery](solution-discovery.md) diff --git a/docs/mdsource/solution-discovery.source.md b/docs/mdsource/solution-discovery.source.md new file mode 100644 index 000000000..262fe7b10 --- /dev/null +++ b/docs/mdsource/solution-discovery.source.md @@ -0,0 +1,84 @@ +# Solution Discovery + + +Verify automatically discovers solution information at build time and embeds it into the test assembly as metadata. This metadata is used to determine where to store snapshot files. + + +## How It Works + +During the build process, Verify searches for solution files (`.slnx` or `.sln`) in the following locations (relative to the project directory): + +1. Project directory +2. Parent directory +3. Parent's parent directory + +If a single solution file is found, Verify extracts and stores: + +- `SolutionDir` - The directory containing the solution file +- `SolutionName` - The solution file name without extension + +This information is embedded in the assembly as `AssemblyMetadataAttribute` values with keys: + +- `Verify.SolutionDirectory` +- `Verify.SolutionName` + + +## Preference Rules + +- If both `.slnx` and `.sln` files exist in the same directory, `.slnx` is preferred +- If multiple `.slnx` files exist, Verify cannot auto-discover and will show a warning +- If no `.slnx` files exist but multiple `.sln` files exist, Verify cannot auto-discover and will show a warning + + +## Explicit Override + +Explicitly set solution information via MSBuild command-line properties: + +```bash +dotnet build /p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + +When `SolutionDir` is explicitly provided but `SolutionName` is not, Verify will attempt to derive `SolutionName` from the solution file found in the specified `SolutionDir`. + + +## Warning Messages + +If Verify cannot discover solution information, it will emit build warnings with guidance: + + +### Multiple Solution Files Found + +``` +Multiple solution files found. Unable to auto-discover SolutionDir and SolutionName. +Found: C:\Path\Solution1.slnx;C:\Path\Solution2.slnx;C:\Path\Solution.sln. +Verify searches for .slnx and .sln files in the project directory, parent directory, and parent's parent directory. +To resolve this, either ensure only one solution file exists in these locations, or explicitly set SolutionDir and SolutionName via command line: +/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + + +### No Solution Files Found + +``` +No solution files found. Unable to auto-discover SolutionDir and SolutionName. +Verify searches for .slnx and .sln files in the project directory '(C:\Path\To\Project)', parent directory, and parent's parent directory. +To resolve this, either add a solution file to one of these locations, or explicitly set SolutionDir and SolutionName via command line: +/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + + +### Multiple Solution Files in Explicit SolutionDir + +``` +Multiple solution files found in SolutionDir 'C:\Path\To\Solution\'. Unable to auto-discover SolutionName. +Found: C:\Path\To\Solution\Solution1.slnx;C:\Path\To\Solution\Solution2.sln. +To resolve this, explicitly set SolutionName via command line: +/p:SolutionName="MySolution" +``` + + +## Build Integration + +Solution discovery happens automatically as part of the build via the `Verify.props` MSBuild targets file, which is automatically imported when referencing the Verify NuGet package. + +The discovery runs in the `DiscoverSolutionInfo` target, which executes before compilation, and the discovered values are written to generated source files by the `WriteVerifyAttributes` target. diff --git a/docs/readme.md b/docs/readme.md index dbf211f4c..f3f6426d6 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -20,6 +20,7 @@ To change this file edit the source file and then run MarkdownSnippets. * [Guids](/docs/guids.md) * [Dates](/docs/dates.md) * [Scrubbing](/docs/scrubbers.md) + * [Solution Discovery](/docs/solution-discovery.md) * [Counter](/docs/counter.md) * [Members that throw](/docs/members-throw.md) * [Ordering](/docs/ordering.md) diff --git a/docs/scrubbers.md b/docs/scrubbers.md index 54d7cd36f..97087b4c5 100644 --- a/docs/scrubbers.md +++ b/docs/scrubbers.md @@ -930,3 +930,4 @@ A B C * [Guid behavior](guids.md) * [Date behavior](dates.md) * [Numeric Ids](numeric-ids.md) + * [Solution Discovery](solution-discovery.md) diff --git a/docs/solution-discovery.md b/docs/solution-discovery.md new file mode 100644 index 000000000..0025a3672 --- /dev/null +++ b/docs/solution-discovery.md @@ -0,0 +1,91 @@ + + +# Solution Discovery + + +Verify automatically discovers solution information at build time and embeds it into the test assembly as metadata. This metadata is used to determine where to store snapshot files. + + +## How It Works + +During the build process, Verify searches for solution files (`.slnx` or `.sln`) in the following locations (relative to the project directory): + +1. Project directory +2. Parent directory +3. Parent's parent directory + +If a single solution file is found, Verify extracts and stores: + +- `SolutionDir` - The directory containing the solution file +- `SolutionName` - The solution file name without extension + +This information is embedded in the assembly as `AssemblyMetadataAttribute` values with keys: + +- `Verify.SolutionDirectory` +- `Verify.SolutionName` + + +## Preference Rules + +- If both `.slnx` and `.sln` files exist in the same directory, `.slnx` is preferred +- If multiple `.slnx` files exist, Verify cannot auto-discover and will show a warning +- If no `.slnx` files exist but multiple `.sln` files exist, Verify cannot auto-discover and will show a warning + + +## Explicit Override + +Explicitly set solution information via MSBuild command-line properties: + +```bash +dotnet build /p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + +When `SolutionDir` is explicitly provided but `SolutionName` is not, Verify will attempt to derive `SolutionName` from the solution file found in the specified `SolutionDir`. + + +## Warning Messages + +If Verify cannot discover solution information, it will emit build warnings with guidance: + + +### Multiple Solution Files Found + +``` +Multiple solution files found. Unable to auto-discover SolutionDir and SolutionName. +Found: C:\Path\Solution1.slnx;C:\Path\Solution2.slnx;C:\Path\Solution.sln. +Verify searches for .slnx and .sln files in the project directory, parent directory, and parent's parent directory. +To resolve this, either ensure only one solution file exists in these locations, or explicitly set SolutionDir and SolutionName via command line: +/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + + +### No Solution Files Found + +``` +No solution files found. Unable to auto-discover SolutionDir and SolutionName. +Verify searches for .slnx and .sln files in the project directory '(C:\Path\To\Project)', parent directory, and parent's parent directory. +To resolve this, either add a solution file to one of these locations, or explicitly set SolutionDir and SolutionName via command line: +/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution" +``` + + +### Multiple Solution Files in Explicit SolutionDir + +``` +Multiple solution files found in SolutionDir 'C:\Path\To\Solution\'. Unable to auto-discover SolutionName. +Found: C:\Path\To\Solution\Solution1.slnx;C:\Path\To\Solution\Solution2.sln. +To resolve this, explicitly set SolutionName via command line: +/p:SolutionName="MySolution" +``` + + +## Build Integration + +Solution discovery happens automatically as part of the build via the `Verify.props` MSBuild targets file, which is automatically imported when referencing the Verify NuGet package. + +The discovery runs in the `DiscoverSolutionInfo` target, which executes before compilation, and the discovered values are written to generated source files by the `WriteVerifyAttributes` target. diff --git a/readme.md b/readme.md index 90e516425..c0c75fe70 100644 --- a/readme.md +++ b/readme.md @@ -1083,6 +1083,7 @@ To opt out of this feature, include the following in the project file: * [Guids](/docs/guids.md) * [Dates](/docs/dates.md) * [Scrubbing](/docs/scrubbers.md) + * [Solution Discovery](/docs/solution-discovery.md) * [Counter](/docs/counter.md) * [Members that throw](/docs/members-throw.md) * [Ordering](/docs/ordering.md) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 733bda9c2..7bc0f262a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/src/Verify.NUnit/Extensions.cs b/src/Verify.NUnit/Extensions.cs index c9840d6ab..0a45ca1b6 100644 --- a/src/Verify.NUnit/Extensions.cs +++ b/src/Verify.NUnit/Extensions.cs @@ -37,7 +37,7 @@ public static bool TryGetParent(this TestAdapter adapter, [NotNullWhen(true)] ou return methodParameterNames; } - var names = GetConstructorParameterNames(method.TypeInfo.Type, parent.Arguments.Length); + var names = method.TypeInfo.Type.GetConstructorParameterNames(parent.Arguments.Length); if (methodParameterNames == null) { return names.ToList(); @@ -72,4 +72,4 @@ public static IEnumerable GetConstructorParameterNames(this Type type, i return names; } -} \ No newline at end of file +} diff --git a/src/Verify.Tests/GlobalUsings.cs b/src/Verify.Tests/GlobalUsings.cs index 43e2f3dca..43922434b 100644 --- a/src/Verify.Tests/GlobalUsings.cs +++ b/src/Verify.Tests/GlobalUsings.cs @@ -7,4 +7,6 @@ global using EmptyFiles; global using Polyfills; global using System.Collections.ObjectModel; +global using System.Reflection.Metadata; +global using System.Reflection.PortableExecutable; global using System.Security.Claims; \ No newline at end of file diff --git a/src/Verify.Tests/SolutionDiscoveryTests.cs b/src/Verify.Tests/SolutionDiscoveryTests.cs new file mode 100644 index 000000000..6d4a59e9e --- /dev/null +++ b/src/Verify.Tests/SolutionDiscoveryTests.cs @@ -0,0 +1,466 @@ +#if NET10_0 +public class SolutionDiscoveryTests +{ + [Fact] + public async Task SingleSlnxFile() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .slnx file in same directory as project + var slnxPath = Path.Combine(tempDir, "TestSolution.slnx"); + await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly and verify metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir); + Assert.Equal("TestSolution", solutionName); + } + + [Fact] + public async Task SingleSlnFile() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .sln file in same directory as project + var slnPath = Path.Combine(tempDir, "TestSolution.sln"); + await File.WriteAllTextAsync(slnPath, CreateMinimalSlnContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly and verify metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir); + Assert.Equal("TestSolution", solutionName); + } + + [Fact] + public async Task BothSlnxAndSln_PreferSlnx() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create both .slnx and .sln files + var slnxPath = Path.Combine(tempDir, "PreferredSolution.slnx"); + await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent()); + + var slnPath = Path.Combine(tempDir, "OtherSolution.sln"); + await File.WriteAllTextAsync(slnPath, CreateMinimalSlnContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly and verify metadata - should use .slnx + var assemblyPath = GetAssemblyPath(projectDir); + var (_, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + // Should prefer .slnx file + Assert.Equal("PreferredSolution", solutionName); + } + + [Fact] + public async Task MultipleSlnxFiles_ShowsWarning() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create multiple .slnx files + await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution1.slnx"), CreateMinimalSlnxContent()); + await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution2.slnx"), CreateMinimalSlnxContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Should contain warning about multiple solution files with helpful guidance + Assert.Contains("Multiple solution files found", output); + Assert.Contains("Verify searches for .slnx and .sln files", output); + Assert.Contains("/p:SolutionDir=", output); + Assert.Contains("/p:SolutionName=", output); + + // Load assembly - should NOT have solution metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Null(solutionDir); + Assert.Null(solutionName); + } + + [Fact] + public async Task MultipleSlnFiles_ShowsWarning() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create multiple .sln files + await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution1.sln"), CreateMinimalSlnContent()); + await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution2.sln"), CreateMinimalSlnContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Should contain warning about multiple solution files with helpful guidance + Assert.Contains("Multiple solution files found", output); + Assert.Contains("Verify searches for .slnx and .sln files", output); + Assert.Contains("/p:SolutionDir=", output); + Assert.Contains("/p:SolutionName=", output); + + // Load assembly - should NOT have solution metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Null(solutionDir); + Assert.Null(solutionName); + } + + [Fact] + public async Task SolutionInParentDirectory() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure: tempDir/src/TestProject + var srcDir = Path.Combine(tempDir, "src"); + var projectDir = Path.Combine(srcDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .slnx file in parent directory + var slnxPath = Path.Combine(srcDir, "TestSolution.slnx"); + await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly and verify metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(srcDir + Path.DirectorySeparatorChar, solutionDir); + Assert.Equal("TestSolution", solutionName); + } + + [Fact] + public async Task SolutionInParentsParentDirectory() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure: tempDir/src/SubDir/TestProject + var srcDir = Path.Combine(tempDir, "src"); + var subDir = Path.Combine(srcDir, "SubDir"); + var projectDir = Path.Combine(subDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .slnx file in parent's parent directory + var slnxPath = Path.Combine(srcDir, "TestSolution.slnx"); + await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly and verify metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(srcDir + Path.DirectorySeparatorChar, solutionDir); + Assert.Equal("TestSolution", solutionName); + } + + [Fact] + public async Task NoSolutionFile_NoMetadata() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .csproj file (no solution file) + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project + var (success, output) = await BuildProject(csprojPath); + Assert.True(success, $"Build failed: {output}"); + + // Should contain warning about no solution files found + Assert.Contains("No solution files found", output); + Assert.Contains("Verify searches for .slnx and .sln files", output); + Assert.Contains("/p:SolutionDir=", output); + Assert.Contains("/p:SolutionName=", output); + + // Load assembly - should NOT have solution metadata + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Null(solutionDir); + Assert.Null(solutionName); + } + + [Fact] + public async Task ExplicitSolutionName_OverridesDiscovery() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Create .slnx file (would normally be discovered) + var slnxPath = Path.Combine(tempDir, "DiscoveredSolution.slnx"); + await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent()); + + // Create .csproj file + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent()); + + // Build project with explicit SolutionName (and let SolutionDir be discovered normally) + var explicitSolutionName = "ExplicitSolution"; + var (success, output) = await BuildProject(csprojPath, solutionName: explicitSolutionName); + Assert.True(success, $"Build failed: {output}"); + + // Load assembly - should use explicit SolutionName, but discovered SolutionDir + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + // SolutionDir should still be discovered + Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir); + // But SolutionName should be the explicitly provided value + Assert.Equal(explicitSolutionName, solutionName); + } + + static string CreateMinimalCsprojContent() + { + // Get the path to Verify.csproj and Verify.props relative to test project + var verifyProjectPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "Verify.csproj"); + + var verifyPropsPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "buildTransitive", "Verify.props"); + + return $""" + + + net10.0 + Library + TestProject + + + + + + + """; + } + + static string CreateMinimalSlnxContent() => + """ + { + "solution": { + "path": "TestSolution.slnx", + "version": "1.0" + } + } + """; + + static string CreateMinimalSlnContent() => + """ + Microsoft Visual Studio Solution File, Format Version 12.00 + VisualStudioVersion = 17.0.31903.59 + MinimumVisualStudioVersion = 10.0.40219.1 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + """; + + static async Task<(bool success, string output)> BuildProject(string csprojPath, string? solutionDir = null, string? solutionName = null) + { + var args = $"build \"{csprojPath}\" --configuration Release --verbosity normal"; + + if (solutionDir != null) + { + args += $" \"/p:SolutionDir={solutionDir}\""; + } + + if (solutionName != null) + { + args += $" \"/p:SolutionName={solutionName}\""; + } + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + var output = await outputTask; + var error = await errorTask; + var fullOutput = output + Environment.NewLine + error; + + return (process.ExitCode == 0, fullOutput); + } + + static string GetAssemblyPath(string projectDir) + { + var binPath = Path.Combine(projectDir, "bin", "Release", "net10.0"); + var dllPath = Path.Combine(binPath, "TestProject.dll"); + + if (File.Exists(dllPath)) + { + return dllPath; + } + + throw new FileNotFoundException($"Assembly not found at {dllPath}"); + } + + static (string? solutionDir, string? solutionName) LoadAssemblyAndGetMetadata(string assemblyPath) + { + // Use MetadataReader to read assembly attributes without loading the assembly + using var fileStream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(fileStream); + var metadataReader = peReader.GetMetadataReader(); + + string? solutionDir = null; + string? solutionName = null; + + foreach (var attributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes()) + { + var attribute = metadataReader.GetCustomAttribute(attributeHandle); + + // Check if this is AssemblyMetadataAttribute + if (attribute.Constructor.Kind != HandleKind.MemberReference) + { + continue; + } + + var constructor = metadataReader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var attributeType = constructor.Parent; + + if (attributeType.Kind != HandleKind.TypeReference) + { + continue; + } + + var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)attributeType); + var typeName = metadataReader.GetString(typeRef.Name); + var typeNamespace = metadataReader.GetString(typeRef.Namespace); + + if (typeName != "AssemblyMetadataAttribute" || + typeNamespace != "System.Reflection") + { + continue; + } + + var value = attribute.DecodeValue(new CustomAttributeTypeProvider()); + if (value.FixedArguments.Length != 2) + { + continue; + } + + var key = value.FixedArguments[0].Value as string; + var val = value.FixedArguments[1].Value as string; + + if (key == "Verify.SolutionDirectory") + { + solutionDir = val; + } + else if (key == "Verify.SolutionName") + { + solutionName = val; + } + } + + return (solutionDir, solutionName); + } + + class CustomAttributeTypeProvider : ICustomAttributeTypeProvider + { + public object GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode; + public object GetSystemType() => typeof(Type); + public object GetSZArrayType(object elementType) => elementType; + public object GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => reader.GetTypeDefinition(handle); + public object GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => reader.GetTypeReference(handle); + public object GetTypeFromSerializedName(string name) => name; + public PrimitiveTypeCode GetUnderlyingEnumType(object type) => PrimitiveTypeCode.Int32; + public bool IsSystemType(object type) => type is Type; + } +} + +#endif diff --git a/src/Verify.Tests/TempDirectoryTests.cs b/src/Verify.Tests/TempDirectoryTests.cs index 172ef087b..bc226d0d8 100644 --- a/src/Verify.Tests/TempDirectoryTests.cs +++ b/src/Verify.Tests/TempDirectoryTests.cs @@ -342,4 +342,4 @@ await Verify(new // using var temp = new TempDirectory(); // File.Create(Path.Combine(temp, "test.txt")); // } -} \ No newline at end of file +} diff --git a/src/Verify.Tests/Verify.Tests.csproj b/src/Verify.Tests/Verify.Tests.csproj index 2d638418a..bd801ce8f 100644 --- a/src/Verify.Tests/Verify.Tests.csproj +++ b/src/Verify.Tests/Verify.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Verify/buildTransitive/Verify.props b/src/Verify/buildTransitive/Verify.props index 553e4d4b2..2137668b3 100644 --- a/src/Verify/buildTransitive/Verify.props +++ b/src/Verify/buildTransitive/Verify.props @@ -8,8 +8,91 @@ $(NoWarn);CA2255 $(AfterMicrosoftNetSdkProps);$(MSBuildThisFileDirectory)Verify.AfterMicrosoftNetSdk.props + + + + false + true + + + false + true + + + + + + + + + + + + + + + + + + + + @(VerifyFoundSlnx->Count()) + @(VerifyFoundSln->Count()) + $([MSBuild]::Add($(VerifySlnxCount), $(VerifySlnCount))) + + + + + @(VerifyFoundSlnx) + + + + + @(VerifyFoundSlnx) + + + + + @(VerifyFoundSln) + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFullPath($(VerifySolutionPath))) + $([System.IO.Path]::GetDirectoryName($(VerifySolutionPathNormalized)))\ + + + + + $([System.IO.Path]::GetFullPath($(VerifySolutionPath))) + + + + + $([System.IO.Path]::GetFileNameWithoutExtension($(VerifySolutionPathNormalized))) + + +