diff --git a/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.targets b/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.targets index 114513e916..7525d0e6a2 100644 --- a/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.targets +++ b/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.targets @@ -14,39 +14,38 @@ Copyright (C) Microsoft Corporation. All rights reserved. --> - + + PreserveNewest Microsoft.Azure.Cosmos.ServiceInterop.dll False - - - PreserveNewest Cosmos.CRTCompat.dll False - - - PreserveNewest msvcp140.dll False - - - PreserveNewest vcruntime140.dll False - - - PreserveNewest vcruntime140_1.dll diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/MSBuild/CosmosTargetsInteropPublishTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/MSBuild/CosmosTargetsInteropPublishTests.cs new file mode 100644 index 0000000000..ffbdc4ea11 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/MSBuild/CosmosTargetsInteropPublishTests.cs @@ -0,0 +1,268 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Tests.MSBuild +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Integration tests that verify Windows native DLLs are only copied when publishing + /// for Windows RuntimeIdentifiers, and not for Linux/macOS targets. + /// These tests pack the SDK into a local NuGet package to properly test the .targets file behavior. + /// + [TestClass] + [TestCategory("LongRunning")] + public class CosmosTargetsInteropPublishTests + { + private static string testProjectsRoot; + private static string localNugetPackagePath; + private static string packageVersion; + private static readonly string[] WindowsNativeDlls = new[] + { + "Microsoft.Azure.Cosmos.ServiceInterop.dll", + "Cosmos.CRTCompat.dll", + "msvcp140.dll", + "vcruntime140.dll", + "vcruntime140_1.dll" + }; + + [ClassInitialize] + public static async Task ClassInitialize(TestContext _) + { + testProjectsRoot = Path.Combine(Path.GetTempPath(), "CosmosTargetsTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(testProjectsRoot); + + // Create local NuGet package from the SDK + await CreateLocalNuGetPackageAsync(); + } + + [ClassCleanup] + public static void ClassCleanup() + { + if (Directory.Exists(testProjectsRoot)) + { + try + { + Directory.Delete(testProjectsRoot, recursive: true); + } + catch + { + // Ignore cleanup errors + } + } + } + + /// + /// Tests that Windows native DLLs are not copied when publishing for non-Windows platforms. + /// + /// The runtime identifier to test (e.g., linux-x64, osx-x64). + [TestMethod] + [DataRow("linux-x64")] + [DataRow("linux-arm64")] + [DataRow("osx-x64")] + [DataRow("osx-arm64")] + public async Task Publish_WithNonWindowsRuntimeIdentifier_DoesNotCopyWindowsDlls(string runtimeIdentifier) + { + string publishPath = await this.CreateAndPublishTestProjectAsync($"NonWinTest_{runtimeIdentifier}", runtimeIdentifier); + + this.AssertWindowsDllsNotPresent(publishPath, runtimeIdentifier); + } + + /// + /// Tests that Windows native DLLs are copied when publishing for Windows platforms. + /// + /// The runtime identifier to test (e.g., win-x64, win-x86). + [TestMethod] + [DataRow("win-x64")] + [DataRow("win-x86")] + [DataRow("win-arm64")] + public async Task Publish_WithWindowsRuntimeIdentifier_CopiesWindowsDlls(string runtimeIdentifier) + { + string publishPath = await this.CreateAndPublishTestProjectAsync($"WinTest_{runtimeIdentifier}", runtimeIdentifier); + + this.AssertWindowsDllsPresent(publishPath, runtimeIdentifier); + } + + /// + /// Tests that Windows native DLLs are copied when publishing without a RuntimeIdentifier, + /// which is the most common developer scenario (regular 'dotnet publish' without -r). + /// + [TestMethod] + public async Task Publish_WithoutRuntimeIdentifier_CopiesWindowsDlls() + { + string publishPath = await this.CreateAndPublishTestProjectAsync("NoRidTest", runtimeIdentifier: null); + + this.AssertWindowsDllsPresent(publishPath, "no RuntimeIdentifier"); + } + + private static async Task CreateLocalNuGetPackageAsync() + { + string repoRoot = GetRepositoryRoot(); + string cosmosProjectPath = Path.Combine(repoRoot, "Microsoft.Azure.Cosmos", "src", "Microsoft.Azure.Cosmos.csproj"); + string packOutputDir = Path.Combine(testProjectsRoot, "nuget-packages"); + Directory.CreateDirectory(packOutputDir); + + // Use a unique version to avoid cache conflicts + packageVersion = $"99.0.0-test.{DateTime.UtcNow:yyyyMMddHHmmss}"; + + await RunDotnetCommandAsync( + $"pack \"{cosmosProjectPath}\" -c Release -o \"{packOutputDir}\" /p:Version={packageVersion} /p:PackageVersion={packageVersion}", + timeoutMinutes: 10); + + localNugetPackagePath = packOutputDir; + } + + private async Task CreateAndPublishTestProjectAsync(string projectName, string runtimeIdentifier) + { + string projectDir = Path.Combine(testProjectsRoot, projectName); + Directory.CreateDirectory(projectDir); + + string projectFile = Path.Combine(projectDir, $"{projectName}.csproj"); + string programFile = Path.Combine(projectDir, "Program.cs"); + string nugetConfigFile = Path.Combine(projectDir, "nuget.config"); + + // Create nuget.config to use local package source + File.WriteAllText(nugetConfigFile, $@" + + + + + + +"); + + // Create a simple console app project that references the local NuGet package + File.WriteAllText(projectFile, $@" + + Exe + net8.0 + enable + + + + + + +"); + + // Create a minimal Program.cs + File.WriteAllText(programFile, @"System.Console.WriteLine(""Test app for verifying Cosmos SDK package behavior"");"); + + // Publish the project + string publishDir = Path.Combine(projectDir, "bin", "publish", runtimeIdentifier ?? "no-rid"); + string ridArgument = runtimeIdentifier != null ? $"-r {runtimeIdentifier} --self-contained false" : string.Empty; + await RunDotnetCommandAsync( + $"publish \"{projectFile}\" -c Release -o \"{publishDir}\" {ridArgument}".Trim(), + timeoutMinutes: 5); + + return publishDir; + } + + private void AssertWindowsDllsNotPresent(string publishPath, string runtimeIdentifier) + { + Assert.IsTrue(Directory.Exists(publishPath), $"Publish directory does not exist: {publishPath}"); + this.AssertPublishOutputIsValid(publishPath, runtimeIdentifier); + + foreach (string dll in WindowsNativeDlls) + { + string dllPath = Path.Combine(publishPath, dll); + Assert.IsFalse(File.Exists(dllPath), + $"Windows native DLL '{dll}' should NOT be present when publishing for {runtimeIdentifier}, but was found at: {dllPath}"); + } + } + + private void AssertWindowsDllsPresent(string publishPath, string runtimeIdentifier) + { + Assert.IsTrue(Directory.Exists(publishPath), $"Publish directory does not exist: {publishPath}"); + this.AssertPublishOutputIsValid(publishPath, runtimeIdentifier); + + foreach (string dll in WindowsNativeDlls) + { + string dllPath = Path.Combine(publishPath, dll); + Assert.IsTrue(File.Exists(dllPath), + $"Windows native DLL '{dll}' SHOULD be present when publishing for {runtimeIdentifier}, but was NOT found at: {dllPath}"); + } + } + + private void AssertPublishOutputIsValid(string publishPath, string runtimeIdentifier) + { + string[] publishedFiles = Directory.GetFiles(publishPath); + Assert.IsTrue(publishedFiles.Length > 0, + $"Publish directory is empty for {runtimeIdentifier}. Publish may have silently failed: {publishPath}"); + + string sdkDllPath = Path.Combine(publishPath, "Microsoft.Azure.Cosmos.Client.dll"); + Assert.IsTrue(File.Exists(sdkDllPath), + $"Microsoft.Azure.Cosmos.Client.dll not found in publish output for {runtimeIdentifier}. " + + $"Publish may not have included the SDK package correctly: {publishPath}"); + } + + private static string GetRepositoryRoot() + { + string currentDir = AppDomain.CurrentDomain.BaseDirectory; + + while (currentDir != null && !File.Exists(Path.Combine(currentDir, "Microsoft.Azure.Cosmos.sln"))) + { + DirectoryInfo parent = Directory.GetParent(currentDir); + if (parent == null) + { + break; + } + currentDir = parent.FullName; + } + + Assert.IsNotNull(currentDir, "Could not find repository root"); + return currentDir; + } + + private static async Task RunDotnetCommandAsync(string arguments, int timeoutMinutes = 5) + { + ProcessStartInfo processInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + string commandLine = $"dotnet {arguments}"; + Console.WriteLine($"Executing: {commandLine}"); + + Process process = Process.Start(processInfo); + if (process == null) + { + Assert.Fail($"Failed to start process: {commandLine}"); + } + + using (process) + { + Task outputTask = process.StandardOutput.ReadToEndAsync(); + Task errorTask = process.StandardError.ReadToEndAsync(); + + bool exited = process.WaitForExit((int)TimeSpan.FromMinutes(timeoutMinutes).TotalMilliseconds); + if (!exited) + { + process.Kill(); + Assert.Fail($"Command timed out after {timeoutMinutes} minutes.\nCommand: {commandLine}"); + } + + string output = await outputTask; + string error = await errorTask; + + if (process.ExitCode != 0) + { + Assert.Fail($"Command failed with exit code {process.ExitCode}.\nCommand: {commandLine}\nOutput: {output}\nError: {error}"); + } + + Assert.IsTrue(string.IsNullOrEmpty(error), $"Command had unexpected error output.\nCommand: {commandLine}\nError: {error}"); + Console.WriteLine($"Command succeeded: {commandLine}"); + } + } + } +}