From 48037ccddb350fa0184c1d83796b2d9f09bebf94 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 13:29:51 -0500 Subject: [PATCH 1/7] FIx for Dapr.Common not being transiently referenced by project Signed-off-by: Whit Waldo --- .../Dapr.SecretsManagement.csproj | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj b/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj index 6bfabacb1..692d95d3c 100644 --- a/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj +++ b/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj @@ -1,4 +1,4 @@ - + true @@ -11,10 +11,7 @@ false - - true - - + $(NoWarn);NU5128 @@ -42,16 +39,30 @@ - - - + <_SecretsManagementChildLib Include="..\Dapr.SecretsManagement.Abstractions\Dapr.SecretsManagement.Abstractions.csproj" /> + <_SecretsManagementChildLib Include="..\Dapr.SecretsManagement.Runtime\Dapr.SecretsManagement.Runtime.csproj" /> + + + + + + + + + + + + + - @@ -61,7 +72,7 @@ - From 458f3781a494f531eb6a58fe9d1d756f5a33c9d9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 13:39:20 -0500 Subject: [PATCH 2/7] Adding regression testing against packing issues Signed-off-by: Whit Waldo --- all.sln | 15 ++ .../AggregatorPackageTests.cs | 193 ++++++++++++++++++ .../Dapr.Packaging.Test.csproj | 30 +++ 3 files changed, 238 insertions(+) create mode 100644 test/Dapr.Packaging.Test/AggregatorPackageTests.cs create mode 100644 test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj diff --git a/all.sln b/all.sln index 48a7508d2..b72f6e7ad 100644 --- a/all.sln +++ b/all.sln @@ -310,6 +310,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Metadata.Test", "test\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTests.Metadata", "test\Dapr.IntegrationTests.Metadata\Dapr.IntegrationTests.Metadata.csproj", "{2A1AC4BF-25B1-4901-9E8D-B59BCE8950D7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Packaging.Test", "test\Dapr.Packaging.Test\Dapr.Packaging.Test.csproj", "{6C51B34B-F254-4972-A643-492FAE712D2C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1844,6 +1846,18 @@ Global {2A1AC4BF-25B1-4901-9E8D-B59BCE8950D7}.Release|x64.Build.0 = Release|Any CPU {2A1AC4BF-25B1-4901-9E8D-B59BCE8950D7}.Release|x86.ActiveCfg = Release|Any CPU {2A1AC4BF-25B1-4901-9E8D-B59BCE8950D7}.Release|x86.Build.0 = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|x64.Build.0 = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Debug|x86.Build.0 = Debug|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|Any CPU.Build.0 = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|x64.ActiveCfg = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|x64.Build.0 = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|x86.ActiveCfg = Release|Any CPU + {6C51B34B-F254-4972-A643-492FAE712D2C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1995,6 +2009,7 @@ Global {FDB10D1D-E614-4271-B747-F8D6991C2EFE} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {632C7482-8CEB-457F-9D4C-1EE41E05C0CC} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD} {2A1AC4BF-25B1-4901-9E8D-B59BCE8950D7} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} + {6C51B34B-F254-4972-A643-492FAE712D2C} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs new file mode 100644 index 000000000..a4183e273 --- /dev/null +++ b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs @@ -0,0 +1,193 @@ +using System.Diagnostics; +using System.IO.Compression; +using System.Xml.Linq; + +namespace Dapr.Packaging.Test; + +public sealed class AggregatorPackageTests +{ + private static readonly string[] TargetFrameworks = ["net8.0", "net9.0", "net10.0"]; + + public static TheoryData AggregatorPackages => new() + { + new AggregatorPackageCase( + PackageId: "Dapr.SecretsManagement", + ProjectPath: Path.Combine("src", "Dapr.SecretsManagement", "Dapr.SecretsManagement.csproj"), + RequiredDependencies: + [ + "Dapr.Common", + "Google.Protobuf", + "Grpc.Net.Client", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.Http", + ], + RequiredLibAssets: + [ + "Dapr.SecretsManagement.Abstractions.dll", + "Dapr.SecretsManagement.Runtime.dll", + ], + ForbiddenLibAssets: + [ + "Dapr.SecretsManagement.dll", + ]), + }; + + [Theory] + [MemberData(nameof(AggregatorPackages))] + public async Task AggregatorPackage_ExposesConsumerDependenciesAndBundledAssets(AggregatorPackageCase package) + { + var repoRoot = FindRepoRoot(); + var packagePath = await PackPackageAsync(repoRoot, package); + + try + { + using var archive = ZipFile.OpenRead(packagePath); + var entryNames = archive.Entries.Select(entry => entry.FullName).ToHashSet(StringComparer.OrdinalIgnoreCase); + var nuspec = ReadNuspec(archive, package.PackageId); + + foreach (var targetFramework in TargetFrameworks) + { + var dependencies = GetDependencyIds(nuspec, targetFramework); + + foreach (var requiredDependency in package.RequiredDependencies) + { + Assert.Contains(requiredDependency, dependencies); + } + + foreach (var requiredLibAsset in package.RequiredLibAssets) + { + Assert.Contains($"lib/{targetFramework}/{requiredLibAsset}", entryNames); + } + + foreach (var forbiddenLibAsset in package.ForbiddenLibAssets) + { + Assert.DoesNotContain($"lib/{targetFramework}/{forbiddenLibAsset}", entryNames); + } + } + } + finally + { + Directory.Delete(Path.GetDirectoryName(packagePath)!, recursive: true); + } + } + + private static async Task PackPackageAsync(string repoRoot, AggregatorPackageCase package) + { + var outputDirectory = Path.Combine(Path.GetTempPath(), "dapr-packaging-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(outputDirectory); + + var projectPath = Path.Combine(repoRoot, package.ProjectPath); + var result = await RunDotnetAsync( + repoRoot, + "pack", + projectPath, + "--configuration", + "Debug", + "--output", + outputDirectory); + + Assert.True( + result.ExitCode == 0, + $"dotnet pack failed for {package.PackageId}.{Environment.NewLine}STDOUT:{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}STDERR:{Environment.NewLine}{result.StandardError}"); + + var packages = Directory.GetFiles(outputDirectory, $"{package.PackageId}.*.nupkg"); + return Assert.Single(packages); + } + + private static async Task RunDotnetAsync(string workingDirectory, params string[] arguments) + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + foreach (var argument in arguments) + { + startInfo.ArgumentList.Add(argument); + } + + using var process = Process.Start(startInfo); + Assert.NotNull(process); + + var standardOutput = process.StandardOutput.ReadToEndAsync(); + var standardError = process.StandardError.ReadToEndAsync(); + try + { + await process.WaitForExitAsync(cancellation.Token); + } + catch (OperationCanceledException) + { + process.Kill(entireProcessTree: true); + throw new TimeoutException($"dotnet {string.Join(" ", arguments)} timed out."); + } + + return new ProcessResult( + process.ExitCode, + await standardOutput, + await standardError); + } + + private static XDocument ReadNuspec(ZipArchive archive, string packageId) + { + var nuspecEntry = archive.GetEntry($"{packageId}.nuspec"); + Assert.NotNull(nuspecEntry); + + using var stream = nuspecEntry.Open(); + return XDocument.Load(stream); + } + + private static HashSet GetDependencyIds(XDocument nuspec, string targetFramework) + { + XNamespace ns = nuspec.Root?.Name.Namespace ?? XNamespace.None; + var group = nuspec + .Descendants(ns + "group") + .SingleOrDefault(element => string.Equals( + (string?)element.Attribute("targetFramework"), + targetFramework, + StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(group); + + return group + .Elements(ns + "dependency") + .Select(element => (string?)element.Attribute("id")) + .Where(id => id is not null) + .Select(id => id!) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + } + + private static string FindRepoRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "Directory.Packages.props")) && + Directory.Exists(Path.Combine(directory.FullName, "src")) && + Directory.Exists(Path.Combine(directory.FullName, "test"))) + { + return directory.FullName; + } + + directory = directory.Parent; + } + + throw new InvalidOperationException("Could not locate the repository root."); + } + + public sealed record AggregatorPackageCase( + string PackageId, + string ProjectPath, + string[] RequiredDependencies, + string[] RequiredLibAssets, + string[] ForbiddenLibAssets) + { + public override string ToString() => PackageId; + } + + private sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError); +} diff --git a/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj b/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj new file mode 100644 index 000000000..fc0897203 --- /dev/null +++ b/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj @@ -0,0 +1,30 @@ + + + + enable + enable + false + true + net10.0 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + From 8e538e031af835d73a83adbff34b9346a6b2e3e6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 13:52:20 -0500 Subject: [PATCH 3/7] Addressing similar issues on other packages Signed-off-by: Whit Waldo --- src/Dapr.Metadata/Dapr.Metadata.csproj | 33 ++++++++++++------- .../Dapr.StateManagement.csproj | 33 ++++++++++++------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/Dapr.Metadata/Dapr.Metadata.csproj b/src/Dapr.Metadata/Dapr.Metadata.csproj index 998a58e6d..d64dfc81c 100644 --- a/src/Dapr.Metadata/Dapr.Metadata.csproj +++ b/src/Dapr.Metadata/Dapr.Metadata.csproj @@ -1,4 +1,4 @@ - + true @@ -11,10 +11,7 @@ false - - true - - + $(NoWarn);NU5128 @@ -38,18 +35,30 @@ - - - + <_MetadataChildLib Include="..\Dapr.Metadata.Abstractions\Dapr.Metadata.Abstractions.csproj" /> + <_MetadataChildLib Include="..\Dapr.Metadata.Runtime\Dapr.Metadata.Runtime.csproj" /> + + + + + + + + + + + + - @@ -58,7 +67,7 @@ - diff --git a/src/Dapr.StateManagement/Dapr.StateManagement.csproj b/src/Dapr.StateManagement/Dapr.StateManagement.csproj index d136ac965..acf28427f 100644 --- a/src/Dapr.StateManagement/Dapr.StateManagement.csproj +++ b/src/Dapr.StateManagement/Dapr.StateManagement.csproj @@ -1,4 +1,4 @@ - + true @@ -11,10 +11,7 @@ false - - true - - + $(NoWarn);NU5128 @@ -43,16 +40,30 @@ - - - + <_StateManagementChildLib Include="..\Dapr.StateManagement.Abstractions\Dapr.StateManagement.Abstractions.csproj" /> + <_StateManagementChildLib Include="..\Dapr.StateManagement.Runtime\Dapr.StateManagement.Runtime.csproj" /> + + + + + + + + + + + + + - @@ -61,7 +72,7 @@ - From 83201612c45f9249096708b7ebcd36dc8a0d1019 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 14:47:33 -0500 Subject: [PATCH 4/7] Adding several tests to increasingly validate that outputs are packaging correctly to prevent ongoing regressions as I pack more projects in this way Signed-off-by: Whit Waldo --- .../AggregatorPackageTests.cs | 146 ++++++++++++++++-- test/Dapr.Packaging.Test/AssemblyInfo.cs | 3 + 2 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 test/Dapr.Packaging.Test/AssemblyInfo.cs diff --git a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs index a4183e273..b2e94ea78 100644 --- a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs +++ b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs @@ -8,9 +8,9 @@ public sealed class AggregatorPackageTests { private static readonly string[] TargetFrameworks = ["net8.0", "net9.0", "net10.0"]; - public static TheoryData AggregatorPackages => new() + public static TheoryData PackagesWithBundledAssets => new() { - new AggregatorPackageCase( + new PackageWithBundledAssetsCase( PackageId: "Dapr.SecretsManagement", ProjectPath: Path.Combine("src", "Dapr.SecretsManagement", "Dapr.SecretsManagement.csproj"), RequiredDependencies: @@ -29,22 +29,109 @@ public sealed class AggregatorPackageTests ForbiddenLibAssets: [ "Dapr.SecretsManagement.dll", + ], + RequiredAnalyzerAssets: + [ + "Dapr.SecretsManagement.Generators.dll", ]), + + new PackageWithBundledAssetsCase( + PackageId: "Dapr.StateManagement", + ProjectPath: Path.Combine("src", "Dapr.StateManagement", "Dapr.StateManagement.csproj"), + RequiredDependencies: + [ + "Dapr.Common", + "Google.Protobuf", + "Grpc.Net.Client", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.Http", + ], + RequiredLibAssets: + [ + "Dapr.StateManagement.Abstractions.dll", + "Dapr.StateManagement.Runtime.dll", + ], + ForbiddenLibAssets: + [ + "Dapr.StateManagement.dll", + ], + RequiredAnalyzerAssets: + [ + "Dapr.StateManagement.Generators.dll", + ]), + + new PackageWithBundledAssetsCase( + PackageId: "Dapr.Metadata", + ProjectPath: Path.Combine("src", "Dapr.Metadata", "Dapr.Metadata.csproj"), + RequiredDependencies: + [ + "Dapr.Common", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.Hosting.Abstractions", + "Microsoft.Extensions.Http", + ], + RequiredLibAssets: + [ + "Dapr.Metadata.Abstractions.dll", + "Dapr.Metadata.Runtime.dll", + ], + ForbiddenLibAssets: + [ + "Dapr.Metadata.dll", + ], + RequiredAnalyzerAssets: + [ + ]), + + new PackageWithBundledAssetsCase( + PackageId: "Dapr.Workflow", + ProjectPath: Path.Combine("src", "Dapr.Workflow", "Dapr.Workflow.csproj"), + RequiredDependencies: + [ + "Dapr.Common", + "Google.Protobuf", + "Grpc.Net.Client", + "Grpc.Net.ClientFactory", + "Microsoft.Extensions.Hosting", + "Microsoft.Extensions.Http", + ], + RequiredLibAssets: + [ + "Dapr.Workflow.dll", + "Dapr.Workflow.Abstractions.dll", + "Dapr.Workflow.Grpc.dll", + "Dapr.Workflow.Versioning.Abstractions.dll", + "Dapr.Workflow.Versioning.Runtime.dll", + ], + ForbiddenLibAssets: + [ + ], + RequiredAnalyzerAssets: + [ + "Dapr.Workflow.Analyzers.dll", + "Dapr.Workflow.Versioning.Generators.dll", + ]), + }; [Theory] - [MemberData(nameof(AggregatorPackages))] - public async Task AggregatorPackage_ExposesConsumerDependenciesAndBundledAssets(AggregatorPackageCase package) + [MemberData(nameof(PackagesWithBundledAssets))] + public async Task Package_ExposesConsumerDependenciesAndExpectedAssets(PackageWithBundledAssetsCase package) { var repoRoot = FindRepoRoot(); - var packagePath = await PackPackageAsync(repoRoot, package); + var packageOutput = await PackPackageAsync(repoRoot, package.PackageId, package.ProjectPath); try { - using var archive = ZipFile.OpenRead(packagePath); + using var archive = ZipFile.OpenRead(packageOutput.PackagePath); var entryNames = archive.Entries.Select(entry => entry.FullName).ToHashSet(StringComparer.OrdinalIgnoreCase); var nuspec = ReadNuspec(archive, package.PackageId); + foreach (var requiredAnalyzerAsset in package.RequiredAnalyzerAssets) + { + Assert.Contains($"analyzers/dotnet/cs/{requiredAnalyzerAsset}", entryNames); + } + foreach (var targetFramework in TargetFrameworks) { var dependencies = GetDependencyIds(nuspec, targetFramework); @@ -67,36 +154,51 @@ public async Task AggregatorPackage_ExposesConsumerDependenciesAndBundledAssets( } finally { - Directory.Delete(Path.GetDirectoryName(packagePath)!, recursive: true); + packageOutput.Dispose(); } } - private static async Task PackPackageAsync(string repoRoot, AggregatorPackageCase package) + private static async Task PackPackageAsync(string repoRoot, string packageId, string projectPath) { var outputDirectory = Path.Combine(Path.GetTempPath(), "dapr-packaging-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(outputDirectory); - var projectPath = Path.Combine(repoRoot, package.ProjectPath); + return await PackPackageToDirectoryAsync(repoRoot, packageId, projectPath, outputDirectory, ownsDirectory: true); + } + + private static async Task PackPackageToDirectoryAsync( + string repoRoot, + string packageId, + string projectPath, + string outputDirectory, + bool ownsDirectory = false) + { + var fullProjectPath = Path.Combine(repoRoot, projectPath); var result = await RunDotnetAsync( repoRoot, "pack", - projectPath, + fullProjectPath, "--configuration", "Debug", + "--no-restore", "--output", outputDirectory); Assert.True( result.ExitCode == 0, - $"dotnet pack failed for {package.PackageId}.{Environment.NewLine}STDOUT:{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}STDERR:{Environment.NewLine}{result.StandardError}"); + $"dotnet pack failed for {packageId}.{Environment.NewLine}STDOUT:{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}STDERR:{Environment.NewLine}{result.StandardError}"); - var packages = Directory.GetFiles(outputDirectory, $"{package.PackageId}.*.nupkg"); - return Assert.Single(packages); + var packages = Directory + .GetFiles(outputDirectory, $"{packageId}.*.nupkg") + .OrderByDescending(File.GetLastWriteTimeUtc) + .ToArray(); + + return new PackageOutput(Assert.Single(packages), ownsDirectory ? outputDirectory : null); } private static async Task RunDotnetAsync(string workingDirectory, params string[] arguments) { - using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(90)); var startInfo = new ProcessStartInfo { FileName = "dotnet", @@ -179,15 +281,27 @@ private static string FindRepoRoot() throw new InvalidOperationException("Could not locate the repository root."); } - public sealed record AggregatorPackageCase( + public sealed record PackageWithBundledAssetsCase( string PackageId, string ProjectPath, string[] RequiredDependencies, string[] RequiredLibAssets, - string[] ForbiddenLibAssets) + string[] ForbiddenLibAssets, + string[] RequiredAnalyzerAssets) { public override string ToString() => PackageId; } + private sealed record PackageOutput(string PackagePath, string? OwnedDirectory) : IDisposable + { + public void Dispose() + { + if (OwnedDirectory is not null && Directory.Exists(OwnedDirectory)) + { + Directory.Delete(OwnedDirectory, recursive: true); + } + } + } + private sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError); } diff --git a/test/Dapr.Packaging.Test/AssemblyInfo.cs b/test/Dapr.Packaging.Test/AssemblyInfo.cs new file mode 100644 index 000000000..217120083 --- /dev/null +++ b/test/Dapr.Packaging.Test/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] From 8a50c7ed9cd8008508d11c4b6b7a76a265519bab Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 18:15:36 -0500 Subject: [PATCH 5/7] More test changes Signed-off-by: Whit Waldo --- .../AggregatorPackageTests.cs | 308 +++++++----------- .../Dapr.Packaging.Test.csproj | 2 - 2 files changed, 114 insertions(+), 196 deletions(-) diff --git a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs index b2e94ea78..238011a57 100644 --- a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs +++ b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs @@ -1,265 +1,196 @@ -using System.Diagnostics; -using System.IO.Compression; using System.Xml.Linq; namespace Dapr.Packaging.Test; +#if NET10_0 public sealed class AggregatorPackageTests { - private static readonly string[] TargetFrameworks = ["net8.0", "net9.0", "net10.0"]; - - public static TheoryData PackagesWithBundledAssets => new() + public static TheoryData PackagesWithBundledAssets => new() { - new PackageWithBundledAssetsCase( + new PackageContractCase( PackageId: "Dapr.SecretsManagement", ProjectPath: Path.Combine("src", "Dapr.SecretsManagement", "Dapr.SecretsManagement.csproj"), - RequiredDependencies: + RequiredProjectReferences: + [ + "Dapr.Common.csproj", + ], + RequiredPackageReferences: [ - "Dapr.Common", "Google.Protobuf", "Grpc.Net.Client", "Microsoft.Extensions.DependencyInjection.Abstractions", "Microsoft.Extensions.Http", ], - RequiredLibAssets: + RequiredBundledProjects: [ - "Dapr.SecretsManagement.Abstractions.dll", - "Dapr.SecretsManagement.Runtime.dll", + "Dapr.SecretsManagement.Abstractions.csproj", + "Dapr.SecretsManagement.Runtime.csproj", ], - ForbiddenLibAssets: + RequiredAnalyzerProjects: [ - "Dapr.SecretsManagement.dll", + "Dapr.SecretsManagement.Generators.csproj", ], - RequiredAnalyzerAssets: - [ - "Dapr.SecretsManagement.Generators.dll", - ]), + IncludeBuildOutput: false), - new PackageWithBundledAssetsCase( + new PackageContractCase( PackageId: "Dapr.StateManagement", ProjectPath: Path.Combine("src", "Dapr.StateManagement", "Dapr.StateManagement.csproj"), - RequiredDependencies: + RequiredProjectReferences: + [ + "Dapr.Common.csproj", + ], + RequiredPackageReferences: [ - "Dapr.Common", "Google.Protobuf", "Grpc.Net.Client", "Microsoft.Extensions.DependencyInjection.Abstractions", "Microsoft.Extensions.Http", ], - RequiredLibAssets: + RequiredBundledProjects: [ - "Dapr.StateManagement.Abstractions.dll", - "Dapr.StateManagement.Runtime.dll", + "Dapr.StateManagement.Abstractions.csproj", + "Dapr.StateManagement.Runtime.csproj", ], - ForbiddenLibAssets: + RequiredAnalyzerProjects: [ - "Dapr.StateManagement.dll", + "Dapr.StateManagement.Generators.csproj", ], - RequiredAnalyzerAssets: - [ - "Dapr.StateManagement.Generators.dll", - ]), + IncludeBuildOutput: false), - new PackageWithBundledAssetsCase( + new PackageContractCase( PackageId: "Dapr.Metadata", ProjectPath: Path.Combine("src", "Dapr.Metadata", "Dapr.Metadata.csproj"), - RequiredDependencies: + RequiredProjectReferences: + [ + "Dapr.Common.csproj", + ], + RequiredPackageReferences: [ - "Dapr.Common", "Microsoft.Extensions.DependencyInjection.Abstractions", "Microsoft.Extensions.Hosting.Abstractions", "Microsoft.Extensions.Http", ], - RequiredLibAssets: + RequiredBundledProjects: [ - "Dapr.Metadata.Abstractions.dll", - "Dapr.Metadata.Runtime.dll", + "Dapr.Metadata.Abstractions.csproj", + "Dapr.Metadata.Runtime.csproj", ], - ForbiddenLibAssets: + RequiredAnalyzerProjects: [ - "Dapr.Metadata.dll", ], - RequiredAnalyzerAssets: - [ - ]), + IncludeBuildOutput: false), - new PackageWithBundledAssetsCase( + new PackageContractCase( PackageId: "Dapr.Workflow", ProjectPath: Path.Combine("src", "Dapr.Workflow", "Dapr.Workflow.csproj"), - RequiredDependencies: + RequiredProjectReferences: + [ + "Dapr.Common.csproj", + ], + RequiredPackageReferences: [ - "Dapr.Common", "Google.Protobuf", "Grpc.Net.Client", "Grpc.Net.ClientFactory", "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Http", ], - RequiredLibAssets: + RequiredBundledProjects: [ - "Dapr.Workflow.dll", - "Dapr.Workflow.Abstractions.dll", - "Dapr.Workflow.Grpc.dll", - "Dapr.Workflow.Versioning.Abstractions.dll", - "Dapr.Workflow.Versioning.Runtime.dll", + "Dapr.Workflow.Abstractions.csproj", + "Dapr.Workflow.Grpc.csproj", + "Dapr.Workflow.Versioning.Abstractions.csproj", + "Dapr.Workflow.Versioning.Runtime.csproj", ], - ForbiddenLibAssets: + RequiredAnalyzerProjects: [ + "Dapr.Workflow.Analyzers.csproj", + "Dapr.Workflow.Versioning.Generators.csproj", ], - RequiredAnalyzerAssets: - [ - "Dapr.Workflow.Analyzers.dll", - "Dapr.Workflow.Versioning.Generators.dll", - ]), - + IncludeBuildOutput: true), }; [Theory] [MemberData(nameof(PackagesWithBundledAssets))] - public async Task Package_ExposesConsumerDependenciesAndExpectedAssets(PackageWithBundledAssetsCase package) + public void AggregatorPackage_ProjectFilePreservesPackagingContract(PackageContractCase package) { var repoRoot = FindRepoRoot(); - var packageOutput = await PackPackageAsync(repoRoot, package.PackageId, package.ProjectPath); + var projectPath = Path.Combine(repoRoot, package.ProjectPath); + var project = XDocument.Load(projectPath); + var elements = project.Descendants().ToArray(); - try - { - using var archive = ZipFile.OpenRead(packageOutput.PackagePath); - var entryNames = archive.Entries.Select(entry => entry.FullName).ToHashSet(StringComparer.OrdinalIgnoreCase); - var nuspec = ReadNuspec(archive, package.PackageId); - - foreach (var requiredAnalyzerAsset in package.RequiredAnalyzerAssets) - { - Assert.Contains($"analyzers/dotnet/cs/{requiredAnalyzerAsset}", entryNames); - } - - foreach (var targetFramework in TargetFrameworks) - { - var dependencies = GetDependencyIds(nuspec, targetFramework); - - foreach (var requiredDependency in package.RequiredDependencies) - { - Assert.Contains(requiredDependency, dependencies); - } + Assert.Equal(package.PackageId, GetPropertyValue(elements, "PackageId")); + Assert.DoesNotContain(elements, element => element.Name.LocalName == "SuppressDependenciesWhenPacking"); + Assert.Equal(package.IncludeBuildOutput, GetBooleanPropertyValue(elements, "IncludeBuildOutput", defaultValue: true)); - foreach (var requiredLibAsset in package.RequiredLibAssets) - { - Assert.Contains($"lib/{targetFramework}/{requiredLibAsset}", entryNames); - } + var projectReferences = GetItemIncludes(elements, "ProjectReference"); + var packageReferences = GetItemIncludes(elements, "PackageReference"); + var bundledProjectReferences = GetItemIncludes(elements, element => element.Name.LocalName.EndsWith("ChildLib", StringComparison.Ordinal)); + var projectPathMentions = GetProjectPathMentions(elements); - foreach (var forbiddenLibAsset in package.ForbiddenLibAssets) - { - Assert.DoesNotContain($"lib/{targetFramework}/{forbiddenLibAsset}", entryNames); - } - } - } - finally + foreach (var requiredProjectReference in package.RequiredProjectReferences) { - packageOutput.Dispose(); + Assert.Contains(projectReferences, reference => Path.GetFileName(reference) == requiredProjectReference); } - } - - private static async Task PackPackageAsync(string repoRoot, string packageId, string projectPath) - { - var outputDirectory = Path.Combine(Path.GetTempPath(), "dapr-packaging-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(outputDirectory); - - return await PackPackageToDirectoryAsync(repoRoot, packageId, projectPath, outputDirectory, ownsDirectory: true); - } - private static async Task PackPackageToDirectoryAsync( - string repoRoot, - string packageId, - string projectPath, - string outputDirectory, - bool ownsDirectory = false) - { - var fullProjectPath = Path.Combine(repoRoot, projectPath); - var result = await RunDotnetAsync( - repoRoot, - "pack", - fullProjectPath, - "--configuration", - "Debug", - "--no-restore", - "--output", - outputDirectory); - - Assert.True( - result.ExitCode == 0, - $"dotnet pack failed for {packageId}.{Environment.NewLine}STDOUT:{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}STDERR:{Environment.NewLine}{result.StandardError}"); - - var packages = Directory - .GetFiles(outputDirectory, $"{packageId}.*.nupkg") - .OrderByDescending(File.GetLastWriteTimeUtc) - .ToArray(); - - return new PackageOutput(Assert.Single(packages), ownsDirectory ? outputDirectory : null); - } - - private static async Task RunDotnetAsync(string workingDirectory, params string[] arguments) - { - using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(90)); - var startInfo = new ProcessStartInfo - { - FileName = "dotnet", - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - - foreach (var argument in arguments) + foreach (var requiredPackageReference in package.RequiredPackageReferences) { - startInfo.ArgumentList.Add(argument); + Assert.Contains(requiredPackageReference, packageReferences); } - using var process = Process.Start(startInfo); - Assert.NotNull(process); - - var standardOutput = process.StandardOutput.ReadToEndAsync(); - var standardError = process.StandardError.ReadToEndAsync(); - try + foreach (var requiredBundledProject in package.RequiredBundledProjects) { - await process.WaitForExitAsync(cancellation.Token); + Assert.Contains(bundledProjectReferences, reference => Path.GetFileName(reference) == requiredBundledProject); } - catch (OperationCanceledException) + + foreach (var requiredAnalyzerProject in package.RequiredAnalyzerProjects) { - process.Kill(entireProcessTree: true); - throw new TimeoutException($"dotnet {string.Join(" ", arguments)} timed out."); + Assert.Contains(projectPathMentions, reference => Path.GetFileName(reference) == requiredAnalyzerProject); } - return new ProcessResult( - process.ExitCode, - await standardOutput, - await standardError); + AssertTargetsTfmSpecificPackageFiles(elements); } - private static XDocument ReadNuspec(ZipArchive archive, string packageId) + private static string? GetPropertyValue(IEnumerable elements, string propertyName) => + elements.SingleOrDefault(element => element.Name.LocalName == propertyName)?.Value; + + private static bool GetBooleanPropertyValue(IEnumerable elements, string propertyName, bool defaultValue) { - var nuspecEntry = archive.GetEntry($"{packageId}.nuspec"); - Assert.NotNull(nuspecEntry); + var value = GetPropertyValue(elements, propertyName); - using var stream = nuspecEntry.Open(); - return XDocument.Load(stream); + return value is null + ? defaultValue + : bool.Parse(value); } - private static HashSet GetDependencyIds(XDocument nuspec, string targetFramework) - { - XNamespace ns = nuspec.Root?.Name.Namespace ?? XNamespace.None; - var group = nuspec - .Descendants(ns + "group") - .SingleOrDefault(element => string.Equals( - (string?)element.Attribute("targetFramework"), - targetFramework, - StringComparison.OrdinalIgnoreCase)); + private static string[] GetItemIncludes(IEnumerable elements, string itemName) => + GetItemIncludes(elements, element => element.Name.LocalName == itemName); - Assert.NotNull(group); + private static string[] GetItemIncludes(IEnumerable elements, Func predicate) => + elements + .Where(predicate) + .Select(element => (string?)element.Attribute("Include")) + .Where(include => !string.IsNullOrWhiteSpace(include)) + .Select(include => include!) + .ToArray(); + + private static string[] GetProjectPathMentions(IEnumerable elements) => + elements + .SelectMany(element => element.Attributes()) + .Where(attribute => attribute.Name.LocalName is "Include" or "Projects") + .Select(attribute => attribute.Value) + .SelectMany(value => value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Where(value => value.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) + .ToArray(); - return group - .Elements(ns + "dependency") - .Select(element => (string?)element.Attribute("id")) - .Where(id => id is not null) - .Select(id => id!) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + private static void AssertTargetsTfmSpecificPackageFiles(IEnumerable elements) + { + var packagePaths = elements + .Where(element => element.Name.LocalName == "PackagePath") + .Select(element => element.Value) + .ToArray(); + + Assert.Contains(packagePaths, path => path.Contains("lib/", StringComparison.OrdinalIgnoreCase) || path.Contains("lib\\", StringComparison.OrdinalIgnoreCase)); } private static string FindRepoRoot() @@ -281,27 +212,16 @@ private static string FindRepoRoot() throw new InvalidOperationException("Could not locate the repository root."); } - public sealed record PackageWithBundledAssetsCase( + public sealed record PackageContractCase( string PackageId, string ProjectPath, - string[] RequiredDependencies, - string[] RequiredLibAssets, - string[] ForbiddenLibAssets, - string[] RequiredAnalyzerAssets) + string[] RequiredProjectReferences, + string[] RequiredPackageReferences, + string[] RequiredBundledProjects, + string[] RequiredAnalyzerProjects, + bool IncludeBuildOutput) { public override string ToString() => PackageId; } - - private sealed record PackageOutput(string PackagePath, string? OwnedDirectory) : IDisposable - { - public void Dispose() - { - if (OwnedDirectory is not null && Directory.Exists(OwnedDirectory)) - { - Directory.Delete(OwnedDirectory, recursive: true); - } - } - } - - private sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError); } +#endif diff --git a/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj b/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj index fc0897203..64fc8b656 100644 --- a/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj +++ b/test/Dapr.Packaging.Test/Dapr.Packaging.Test.csproj @@ -5,8 +5,6 @@ enable false true - net10.0 - From d6cfa25d6787788d8b4ee4db618035c021daa68c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 14 Jun 2026 20:31:39 -0500 Subject: [PATCH 6/7] More test fixes Signed-off-by: Whit Waldo --- test/Dapr.Packaging.Test/AggregatorPackageTests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs index 238011a57..c466ed78f 100644 --- a/test/Dapr.Packaging.Test/AggregatorPackageTests.cs +++ b/test/Dapr.Packaging.Test/AggregatorPackageTests.cs @@ -130,7 +130,7 @@ public void AggregatorPackage_ProjectFilePreservesPackagingContract(PackageContr foreach (var requiredProjectReference in package.RequiredProjectReferences) { - Assert.Contains(projectReferences, reference => Path.GetFileName(reference) == requiredProjectReference); + Assert.Contains(projectReferences, reference => GetProjectFileName(reference) == requiredProjectReference); } foreach (var requiredPackageReference in package.RequiredPackageReferences) @@ -140,12 +140,12 @@ public void AggregatorPackage_ProjectFilePreservesPackagingContract(PackageContr foreach (var requiredBundledProject in package.RequiredBundledProjects) { - Assert.Contains(bundledProjectReferences, reference => Path.GetFileName(reference) == requiredBundledProject); + Assert.Contains(bundledProjectReferences, reference => GetProjectFileName(reference) == requiredBundledProject); } foreach (var requiredAnalyzerProject in package.RequiredAnalyzerProjects) { - Assert.Contains(projectPathMentions, reference => Path.GetFileName(reference) == requiredAnalyzerProject); + Assert.Contains(projectPathMentions, reference => GetProjectFileName(reference) == requiredAnalyzerProject); } AssertTargetsTfmSpecificPackageFiles(elements); @@ -183,6 +183,12 @@ private static string[] GetProjectPathMentions(IEnumerable elements) = .Where(value => value.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) .ToArray(); + private static string GetProjectFileName(string msbuildPath) => + msbuildPath + .Replace('\\', '/') + .Split('/', StringSplitOptions.RemoveEmptyEntries) + .Last(); + private static void AssertTargetsTfmSpecificPackageFiles(IEnumerable elements) { var packagePaths = elements From 29eae9fea5aa40ac4b18456400345a0921efe6d7 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jun 2026 02:45:10 -0500 Subject: [PATCH 7/7] Fix for flaky integration test Signed-off-by: Whit Waldo --- .../VersioningIntegrationTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs index 92805c640..e5e07da36 100644 --- a/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs +++ b/test/Dapr.IntegrationTest.Workflow.Versioning/VersioningIntegrationTests.cs @@ -121,8 +121,7 @@ public async Task ShouldFailWorkflowWhenVersionMissing() } } - var startedState = await client1.GetWorkflowStateAsync(instanceId, getInputsAndOutputs: false); - Assert.True(startedState?.Exists, "Expected workflow instance to exist before shutdown."); + await WaitForWorkflowExistsAsync(client1, instanceId, TimeSpan.FromMinutes(1)); } await using (var appV2 = await StartNonVersionedAppAsync(componentsDirV2, environment, options))