From ece4345e822abe872117d30305fd23c308ae300c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 29 Jan 2026 12:19:31 +0100 Subject: [PATCH 1/2] Cache dotnet and unpacking local packages in Integratin Tests --- .../Build.cs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs index 836ecd25e9..c7ce259a88 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs @@ -16,6 +16,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using System.Diagnostics; +using System.Globalization; namespace Microsoft.TestPlatform.Acceptance.IntegrationTests; @@ -29,10 +31,18 @@ public class Build : IntegrationTestBase [AssemblyInitialize] public static void AssemblyInitialize(TestContext _) { + var sw = Stopwatch.StartNew(); SetDotnetEnvironment(); + Debug.WriteLine($"Setting dotnet environment took: {sw.ElapsedMilliseconds} ms"); + sw.Restart(); BuildTestAssetsAndUnzipPackages(); + Debug.WriteLine($"Building test assets and unzipping packages took: {sw.ElapsedMilliseconds} ms"); + sw.Restart(); BuildTestAssetsCompatibility(); + Debug.WriteLine($"Building test assets compatibility matrix took: {sw.ElapsedMilliseconds} ms"); + sw.Restart(); CopyAndPatchDotnet(); + Debug.WriteLine($"Copying and patching dotnet took: {sw.ElapsedMilliseconds} ms"); } private static void SetDotnetEnvironment() @@ -50,13 +60,22 @@ private static void CopyAndPatchDotnet() { var patchedDotnetDir = Path.GetFullPath(Path.Combine(Root, "artifacts", "tmp", ".dotnet")); - // Copy dotnet. - DirectoryUtils.CopyDirectory(new DirectoryInfo(DotnetDir), new DirectoryInfo(patchedDotnetDir)); + var dotnetExe = OSUtils.IsWindows ? "dotnet.exe" : "dotnet"; + var originalDotnetExePath = Path.Combine(DotnetDir, dotnetExe); + var patchedDotnetExePath = Path.Combine(patchedDotnetDir, dotnetExe); - // Copy target file and build task dll into it. - var netTestSdkVersion = IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion; - var packageName = $"Microsoft.TestPlatform.Build.{netTestSdkVersion}.nupkg"; - var packagePath = Path.GetFullPath(Path.Combine(IntegrationTestEnvironment.PublishDirectory, packageName)); + // It is not necessary to copy whole dotnet folder before each test run + // we just need to make sure the build files are updated automatically, + // so dotnet test tests reflect what is in our local build targets. + bool skipCopy = File.Exists(originalDotnetExePath) + && File.Exists(patchedDotnetExePath) + && File.GetLastWriteTime(originalDotnetExePath) == File.GetLastWriteTime(patchedDotnetExePath); + + if (!skipCopy) + { + // Copy .dotnet + DirectoryUtils.CopyDirectory(new DirectoryInfo(DotnetDir), new DirectoryInfo(patchedDotnetDir)); + } // e.g. artifacts\tmp\.dotnet\sdk\ var sdkDirectory = Path.Combine(patchedDotnetDir, "sdk"); @@ -72,6 +91,13 @@ private static void CopyAndPatchDotnet() } var dotnetSdkDirectory = dotnetSdkDirectories.Single(); + + // Copy target file and build task dll into it. + // This updates the definition for running dotnet test from what we have built locally. + var netTestSdkVersion = IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion; + var packageName = $"Microsoft.TestPlatform.Build.{netTestSdkVersion}.nupkg"; + var packagePath = Path.GetFullPath(Path.Combine(IntegrationTestEnvironment.PublishDirectory, packageName)); + DirectoryUtils.CopyDirectory(Path.Combine(packagePath, "lib", "netstandard2.0"), dotnetSdkDirectory); DirectoryUtils.CopyDirectory(Path.Combine(packagePath, "runtimes", "any", "native"), dotnetSdkDirectory); } @@ -367,12 +393,24 @@ private static void BuildTestAssetsAndUnzipPackages() { var packagePath = Path.Combine(IntegrationTestEnvironment.LocalPackageSource, packageName); var unzipPath = Path.Combine(IntegrationTestEnvironment.PublishDirectory, packageName); + + var cacheMarkerPath = Path.Combine(unzipPath, packageName + ".cache"); + if (File.Exists(cacheMarkerPath)) + { + if (File.ReadAllText(cacheMarkerPath) == File.GetLastWriteTimeUtc(packagePath).ToString(CultureInfo.InvariantCulture)) + { + // Already extracted and using the latest built packages. + continue; + } + } + if (Directory.Exists(unzipPath)) { Directory.Delete(unzipPath, recursive: true); } ZipFile.ExtractToDirectory(packagePath, unzipPath); + File.WriteAllText(cacheMarkerPath, File.GetLastWriteTimeUtc(packagePath).ToString(CultureInfo.InvariantCulture)); } } } From d6b0def4166c3196c16139fa76dc0f7da20b7a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 30 Jan 2026 13:00:01 +0100 Subject: [PATCH 2/2] Clean obj when packages update to force rebuild --- .../Build.cs | 94 +++++++++++++------ 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs index c7ce259a88..836b187c16 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; -using System.Text.RegularExpressions; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.TestPlatform.TestUtilities; @@ -16,8 +18,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; -using System.Diagnostics; -using System.Globalization; namespace Microsoft.TestPlatform.Acceptance.IntegrationTests; @@ -35,16 +35,44 @@ public static void AssemblyInitialize(TestContext _) SetDotnetEnvironment(); Debug.WriteLine($"Setting dotnet environment took: {sw.ElapsedMilliseconds} ms"); sw.Restart(); - BuildTestAssetsAndUnzipPackages(); + + var nugetCache = Path.GetFullPath(Path.Combine(Root, ".packages")); + var packagesAreNew = UnzipExecutablePackages(); + if (packagesAreNew) + { + CleanNugetCacheAndProjects(nugetCache); + } Debug.WriteLine($"Building test assets and unzipping packages took: {sw.ElapsedMilliseconds} ms"); sw.Restart(); - BuildTestAssetsCompatibility(); + BuildTestAssets(nugetCache); + BuildTestAssetsCompatibility(nugetCache); Debug.WriteLine($"Building test assets compatibility matrix took: {sw.ElapsedMilliseconds} ms"); sw.Restart(); CopyAndPatchDotnet(); Debug.WriteLine($"Copying and patching dotnet took: {sw.ElapsedMilliseconds} ms"); } + private static void BuildTestAssets(string nugetCache) + { + var testAssets = Path.GetFullPath(Path.Combine(Root, "test", "TestAssets", "TestAssets.sln")); + var nugetFeeds = GetNugetSourceParameters(Root); + + var netTestSdkVersion = IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion; + + ExecuteApplication2(Dotnet, $"""restore --packages {nugetCache} {nugetFeeds} --source "{IntegrationTestEnvironment.LocalPackageSource}" -p:PackageVersion={netTestSdkVersion} "{testAssets}" """); + ExecuteApplication2(Dotnet, $"""build "{testAssets}" --configuration {IntegrationTestEnvironment.BuildConfiguration} --no-restore"""); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Build special project written in IL. + // This project is used on Windows only Tests. On non-Windows the build fails with: "IlasmToolPath must be set in order to build ilproj's outside of Windows.". + var cilProject = Path.Combine(Root, "test", "TestAssets", "CILProject", "CILProject.proj"); + var binPath = Path.Combine(Root, "artifacts", "bin", "TestAssets", "CILProject", IntegrationTestEnvironment.BuildConfiguration, "net462"); + ExecuteApplication2(Dotnet, $"""restore --packages {nugetCache} {nugetFeeds} --source "{IntegrationTestEnvironment.LocalPackageSource}" "{cilProject}" """); + ExecuteApplication2(Dotnet, $"""build "{cilProject}" --configuration {IntegrationTestEnvironment.BuildConfiguration} --no-restore --output {binPath}"""); + } + } + private static void SetDotnetEnvironment() { // We need to set this to point to our dotnet, because we cannot guarantee what is installed on the machine in Program Files, @@ -102,10 +130,9 @@ private static void CopyAndPatchDotnet() DirectoryUtils.CopyDirectory(Path.Combine(packagePath, "runtimes", "any", "native"), dotnetSdkDirectory); } - private static void BuildTestAssetsCompatibility() + private static void BuildTestAssetsCompatibility(string nugetCache) { var testAssetsDir = Path.GetFullPath(Path.Combine(Root, "test", "TestAssets")); - var nugetCache = Path.GetFullPath(Path.Combine(Root, ".packages")); var generated = Path.GetFullPath(Path.Combine(Root, "artifacts", "tmp", "GeneratedTestAssets")); var generatedSln = Path.Combine(generated, "CompatibilityTestAssets.slnx"); @@ -195,7 +222,7 @@ private static void BuildTestAssetsCompatibility() var rebuild = true; if (cacheIdText == currentCacheId) { - // Cache is up-to-date, just rebuilding solution. + // Project cache is up-to-date, just rebuilding solution. ExecuteApplication2(Dotnet, $"""restore --packages {nugetCache} {nugetFeeds} --source "{IntegrationTestEnvironment.LocalPackageSource}" "{generatedSln}" """); ExecuteApplication2(Dotnet, $"build {generatedSln} --no-restore --configuration {IntegrationTestEnvironment.BuildConfiguration} -v:minimal"); rebuild = false; @@ -359,29 +386,14 @@ protected static void ExecuteApplication2(string path, string? args, } } - private static void BuildTestAssetsAndUnzipPackages() + private static bool UnzipExecutablePackages() { - var testAssets = Path.GetFullPath(Path.Combine(Root, "test", "TestAssets", "TestAssets.sln")); - - var nugetCache = Path.GetFullPath(Path.Combine(Root, ".packages")); - var nugetFeeds = GetNugetSourceParameters(Root); var netTestSdkVersion = IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion; - ExecuteApplication2(Dotnet, $"""restore --packages {nugetCache} {nugetFeeds} --source "{IntegrationTestEnvironment.LocalPackageSource}" -p:PackageVersion={netTestSdkVersion} "{testAssets}" """); - ExecuteApplication2(Dotnet, $"""build "{testAssets}" --configuration {IntegrationTestEnvironment.BuildConfiguration} --no-restore"""); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Build special project written in IL. - // This project is used on Windows only Tests. On non-Windows the build fails with: "IlasmToolPath must be set in order to build ilproj's outside of Windows.". - var cilProject = Path.Combine(Root, "test", "TestAssets", "CILProject", "CILProject.proj"); - var binPath = Path.Combine(Root, "artifacts", "bin", "TestAssets", "CILProject", IntegrationTestEnvironment.BuildConfiguration, "net462"); - ExecuteApplication2(Dotnet, $"""restore --packages {nugetCache} {nugetFeeds} --source "{IntegrationTestEnvironment.LocalPackageSource}" "{cilProject}" """); - ExecuteApplication2(Dotnet, $"""build "{cilProject}" --configuration {IntegrationTestEnvironment.BuildConfiguration} --no-restore --output {binPath}"""); - } - + // Extract locally built packages that have our tools (like vstest.console.exe) into tmp directory, + // so we can use them to run tests. var packagesToExtract = new[] - { +{ $"Microsoft.TestPlatform.{netTestSdkVersion}.nupkg", $"Microsoft.TestPlatform.CLI.{netTestSdkVersion}.nupkg", $"Microsoft.TestPlatform.Build.{netTestSdkVersion}.nupkg", @@ -389,6 +401,7 @@ private static void BuildTestAssetsAndUnzipPackages() $"Microsoft.TestPlatform.Portable.{netTestSdkVersion}.nupkg", }; + var packagesAreNew = false; foreach (var packageName in packagesToExtract) { var packagePath = Path.Combine(IntegrationTestEnvironment.LocalPackageSource, packageName); @@ -404,6 +417,9 @@ private static void BuildTestAssetsAndUnzipPackages() } } + // I any package is new we will clean the package cache before restore and build. + packagesAreNew |= true; + if (Directory.Exists(unzipPath)) { Directory.Delete(unzipPath, recursive: true); @@ -412,5 +428,29 @@ private static void BuildTestAssetsAndUnzipPackages() ZipFile.ExtractToDirectory(packagePath, unzipPath); File.WriteAllText(cacheMarkerPath, File.GetLastWriteTimeUtc(packagePath).ToString(CultureInfo.InvariantCulture)); } + + return packagesAreNew; + } + + private static void CleanNugetCacheAndProjects(string nugetCache) + { + // dotnet clean needs the packages in place, but here we don't yet know what projects we will build + // luckily they are all built into artifacts/bin/TestAssets and artifacts/obj/TestAssets so we just need to delete + // the obj to force re-build in the next steps. + + var objPath = Path.Combine(Root, "artifacts", "obj", "TestAssets"); + Directory.Delete(objPath, recursive: true); + + // Then clean all -dev and -ci packages from the cache to force updating from local source. + foreach (var packageDir in Directory.GetDirectories(nugetCache)) + { + foreach (var versionDir in Directory.GetDirectories(packageDir)) + { + if (versionDir.EndsWith("-dev") || versionDir.EndsWith("-ci")) + { + Directory.Delete(versionDir, recursive: true); + } + } + } } }