Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move unified build sdk validation to a test project #19090

Merged
merged 55 commits into from
Apr 2, 2024

Conversation

jtschuster
Copy link
Member

@jtschuster jtschuster commented Mar 18, 2024

Instead of the msbuild tasks, use a test project similar to the source-build sdk smoke tests to test the files present in the archive and their versions. This began with a copy of the source build smoke tests that were adapted to unified build. The diff for the shared files is below.

I'm investigating why the baselines aren't working in CI, but this is ready to review otherwise.

Diff from SourceBuild smoke tests
--- "a/source-build-smoke-tests/BaselineHelper.cs"
+++ "b/unified-build/BaselineHelper.cs"
@@ -44,6 +44,8 @@ public static void CompareEntries(string baselineFileName, IOrderedEnumerable<st
         public static void CompareBaselineContents(string baselineFileName, string actualContents, ITestOutputHelper outputHelper, bool warnOnDiffs = false, string baselineSubDir = "")
         {
             string actualFilePath = Path.Combine(TestBase.LogsDirectory, $"Updated{baselineFileName}");
+            if (!actualContents.EndsWith(Environment.NewLine))
+                actualContents += Environment.NewLine;
             File.WriteAllText(actualFilePath, actualContents);

             CompareFiles(GetBaselineFilePath(baselineFileName, baselineSubDir), actualFilePath, outputHelper, warnOnDiffs);
@@ -73,7 +75,10 @@ public static void CompareFiles(string expectedFilePath, string actualFilePath,

             if (!warnOnDiffs)
             {
-                Assert.Null(message);
+                if (message is not null)
+                {
+                    Assert.Fail(message);
+                }
             }
         }

@@ -90,31 +95,31 @@ public static string DiffFiles(string file1Path, string file2Path, ITestOutputHe
         public static string GetBaselineFilePath(string baselineFileName, string baselineSubDir = "") =>
             Path.Combine(GetAssetsDirectory(), "baselines", baselineSubDir, baselineFileName);

-        public static string RemoveRids(string diff, bool isPortable = false) =>
-            isPortable ? diff.Replace(Config.PortableRid, "portable-rid") : diff.Replace(Config.TargetRid, "banana-rid");
+        public static string RemoveRids(string diff, string portableRid, string targetRid, bool isPortable = false) =>
+            isPortable ? diff.Replace(portableRid, "portable-rid") : diff.Replace(targetRid, "banana-rid");

         public static string RemoveVersions(string source)
         {
             // Remove version numbers for examples like "roslyn4.1", "net8.0", and "netstandard2.1".
-            string pathSeparator = Regex.Escape(Path.DirectorySeparatorChar.ToString());
+            string pathSeparator = $"[{Regex.Escape(@"\")}|{Regex.Escape(@"/")}]";
             string result = Regex.Replace(source, $@"{pathSeparator}(net|roslyn)[1-9]+\.[0-9]+{pathSeparator}", match =>
             {
                 string wordPart = match.Groups[1].Value;
-                return $"{Path.DirectorySeparatorChar}{wordPart}{NonSemanticVersionPlaceholder}{Path.DirectorySeparatorChar}";
+                return $"{'/'}{wordPart}{NonSemanticVersionPlaceholder}{'/'}";
             });
-
+
             // Remove semantic versions
             // Regex source: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
             // The regex from https://semver.org has been modified to account for the following:
-                // - The version should be preceded by a path separator, '.', '-', or '/'
-                // - The version should match a release identifier that begins with '.' or '-'
-                // - The version may have one or more release identifiers that begin with '.' or '-'
-                // - The version should end before a path separator, '.', '-', or '/'
+            // - The version should be preceded by a path separator, '.', '-', or '/'
+            // - The version should match a release identifier that begins with '.' or '-'
+            // - The version may have one or more release identifiers that begin with '.' or '-'
+            // - The version should end before a path separator, '.', '-', or '/'
             Regex semanticVersionRegex = new(
-                @"(?<=[./-])(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))+"
+                @"(?<=[./\\-_])(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))+"
                 + @"(((?:[-.]((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)))+"
                 + @"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
-                + @"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?(?=[/.-])");
+                + @"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?(?=[/\\.-_])");
             return semanticVersionRegex.Replace(result, SemanticVersionPlaceholder);
         }

diff --git "a/source-build-smoke-tests/Config.cs" "b/unified-build/Config.cs"
index 6bc2393e8..0d0228eb4 100644
--- "a/source-build-smoke-tests/Config.cs"
+++ "b/unified-build/Config.cs"
@@ -4,48 +4,83 @@

 using System;
 using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit.Abstractions;

 namespace Microsoft.DotNet.SourceBuild.SmokeTests;

-internal static class Config
+public class Config : IDisposable
 {
-    public const string DotNetDirectoryEnv = "SMOKE_TESTS_DOTNET_DIR";
-    public const string ExcludeOmniSharpEnv = "SMOKE_TESTS_EXCLUDE_OMNISHARP";
-    public const string IncludeArtifactsSizeEnv = "SMOKE_TESTS_INCLUDE_ARTIFACTSSIZE";
-    public const string MsftSdkTarballPathEnv = "SMOKE_TESTS_MSFT_SDK_TARBALL_PATH";
-    public const string PoisonReportPathEnv = "SMOKE_TESTS_POISON_REPORT_PATH";
-    public const string PortableRidEnv = "SMOKE_TESTS_PORTABLE_RID";
-    public const string PrereqsPathEnv = "SMOKE_TESTS_PREREQS_PATH";
-    public const string CustomPackagesPathEnv = "SMOKE_TESTS_CUSTOM_PACKAGES_PATH";
-    public const string SdkTarballPathEnv = "SMOKE_TESTS_SDK_TARBALL_PATH";
-    public const string SourceBuiltArtifactsPathEnv = "SMOKE_TESTS_SOURCEBUILT_ARTIFACTS_PATH";
-    public const string TargetRidEnv = "SMOKE_TESTS_TARGET_RID";
-    public const string WarnSdkContentDiffsEnv = "SMOKE_TESTS_WARN_SDK_CONTENT_DIFFS";
-    public const string WarnLicenseScanDiffsEnv = "SMOKE_TESTS_WARN_LICENSE_SCAN_DIFFS";
-    public const string RunningInCIEnv = "SMOKE_TESTS_RUNNING_IN_CI";
-    public const string LicenseScanPathEnv = "SMOKE_TESTS_LICENSE_SCAN_PATH";
+    IMessageSink _sink;
+    public Config(IMessageSink sink)
+    {
+        BuildVersion = Environment.GetEnvironmentVariable(BuildVersionEnv) ?? throw new InvalidOperationException($"'{BuildVersionEnv}' must be specified");
+        PortableRid = Environment.GetEnvironmentVariable(PortableRidEnv) ?? throw new InvalidOperationException($"'{PortableRidEnv}' must be specified");
+        UbSdkArchivePath = Environment.GetEnvironmentVariable(UbSdkTarballPathEnv) ?? throw new InvalidOperationException($"'{UbSdkTarballPathEnv}' must be specified");
+        TargetRid = Environment.GetEnvironmentVariable(TargetRidEnv) ?? throw new InvalidOperationException($"'{TargetRidEnv}' must be specified");
+        TargetArchitecture = TargetRid.Split('-')[1];
+        WarnOnSdkContentDiffs = bool.TryParse(Environment.GetEnvironmentVariable(WarnSdkContentDiffsEnv), out bool warnOnSdkContentDiffs) && warnOnSdkContentDiffs;
+        MsftSdkArchivePath = Environment.GetEnvironmentVariable(MsftSdkTarballPathEnv) ?? DownloadMsftSdkArchive().Result;
+    }

-    public static string DotNetDirectory { get; } =
-        Environment.GetEnvironmentVariable(DotNetDirectoryEnv) ?? Path.Combine(Directory.GetCurrentDirectory(), ".dotnet");
-    public static string? MsftSdkTarballPath { get; } = Environment.GetEnvironmentVariable(MsftSdkTarballPathEnv);
-    public static string? PoisonReportPath { get; } = Environment.GetEnvironmentVariable(PoisonReportPathEnv);
-    public static string PortableRid { get; } = Environment.GetEnvironmentVariable(PortableRidEnv) ??
-        throw new InvalidOperationException($"'{Config.PortableRidEnv}' must be specified");
-    public static string? PrereqsPath { get; } = Environment.GetEnvironmentVariable(PrereqsPathEnv);
-    public static string? CustomPackagesPath { get; } = Environment.GetEnvironmentVariable(CustomPackagesPathEnv);
-    public static string? SdkTarballPath { get; } = Environment.GetEnvironmentVariable(SdkTarballPathEnv);
-    public static string? SourceBuiltArtifactsPath { get; } = Environment.GetEnvironmentVariable(SourceBuiltArtifactsPathEnv);
-    public static string TargetRid { get; } = Environment.GetEnvironmentVariable(TargetRidEnv) ??
-        throw new InvalidOperationException($"'{Config.TargetRidEnv}' must be specified");
-    public static string TargetArchitecture { get; } = TargetRid.Split('-')[1];
-    public static bool WarnOnSdkContentDiffs { get; } =
-        bool.TryParse(Environment.GetEnvironmentVariable(WarnSdkContentDiffsEnv), out bool warnOnSdkContentDiffs) && warnOnSdkContentDiffs;
-    public static bool WarnOnLicenseScanDiffs { get; } =
-        bool.TryParse(Environment.GetEnvironmentVariable(WarnLicenseScanDiffsEnv), out bool warnOnLicenseScanDiffs) && warnOnLicenseScanDiffs;
+    public const string BuildVersionEnv = "UNIFIED_BUILD_VALIDATION_BUILD_VERSION";
+    public const string MsftSdkTarballPathEnv = "UNIFIED_BUILD_VALIDATION_MSFT_SDK_TARBALL_PATH";
+    public const string PortableRidEnv = "UNIFIED_BUILD_VALIDATION_PORTABLE_RID";
+    public const string PrereqsPathEnv = "UNIFIED_BUILD_VALIDATION_PREREQS_PATH";
+    public const string UbSdkTarballPathEnv = "UNIFIED_BUILD_VALIDATION_SDK_TARBALL_PATH";
+    public const string SourceBuiltArtifactsPathEnv = "UNIFIED_BUILD_VALIDATION_SOURCEBUILT_ARTIFACTS_PATH";
+    public const string TargetRidEnv = "UNIFIED_BUILD_VALIDATION_TARGET_RID";
+    public const string WarnSdkContentDiffsEnv = "UNIFIED_BUILD_VALIDATION_WARN_SDK_CONTENT_DIFFS";

-    // Indicates whether the tests are being run in the context of a CI pipeline
-    public static bool RunningInCI { get; } =
-        bool.TryParse(Environment.GetEnvironmentVariable(RunningInCIEnv), out bool runningInCI) && runningInCI;
-
-    public static string? LicenseScanPath { get; } = Environment.GetEnvironmentVariable(LicenseScanPathEnv);
+    public string? MsftSdkArchivePath { get; }
+    public string BuildVersion { get; }
+    public string PortableRid { get; }
+    public string UbSdkArchivePath { get; }
+    public string TargetRid { get; }
+    public string TargetArchitecture { get; }
+    public bool WarnOnSdkContentDiffs { get; }
+    string? _downloadedMsftSdkPath = null;
+
+    static string GetArchiveExtension(string path)
+    {
+        if (path.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
+            return ".zip";
+        if (path.EndsWith(".tar.gz", StringComparison.InvariantCultureIgnoreCase))
+            return ".tar.gz";
+        throw new InvalidOperationException($"Path does not have a valid archive extenions: '{path}'");
+    }
+
+    public async Task<string> DownloadMsftSdkArchive()
+    {
+        var client = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = false });
+        var channel = BuildVersion[..5] + "xx";
+        var akaMsUrl = $"https://aka.ms/dotnet/{channel}/daily/dotnet-sdk-{TargetRid}{GetArchiveExtension(UbSdkArchivePath)}";
+        Console.WriteLine($"Downloading latest sdk from '{akaMsUrl}'");
+        var redirectResponse = await client.GetAsync(akaMsUrl);
+        // aka.ms returns a 301 for valid redirects and a 302 to Bing for invalid URLs
+        if (redirectResponse.StatusCode != HttpStatusCode.Moved)
+        {
+            throw new InvalidOperationException($"Could not find download link for Microsoft built sdk at '{akaMsUrl}'");
+        }
+        var closestUrl = redirectResponse.Headers.Location!.ToString();
+        Console.WriteLine($"Redirected to '{closestUrl}'");
+        HttpResponseMessage packageResponse = await client.GetAsync(closestUrl);
+        var packageUriPath = packageResponse.RequestMessage!.RequestUri!.LocalPath;
+        _downloadedMsftSdkPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + "." + Path.GetFileName(packageUriPath));
+        Console.WriteLine($"Downloading to '{_downloadedMsftSdkPath}'");
+        using (var file = File.Create(_downloadedMsftSdkPath))
+        {
+            await packageResponse.Content.CopyToAsync(file);
+        }
+        return _downloadedMsftSdkPath;
+    }
+    public void Dispose()
+    {
+        if (_downloadedMsftSdkPath != null)
+        {
+            File.Delete(_downloadedMsftSdkPath);
+        }
+    }
 }
diff --git "a/source-build-smoke-tests/Exclusions.cs" "b/unified-build/Exclusions.cs"
new file mode 100644
index 000000000..f5be124c6
--- /dev/null
+++ "b/unified-build/Exclusions.cs"
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Enumeration;
+using System.Linq;
+using Microsoft.DotNet.SourceBuild.SmokeTests;
+
+public class Exclusions
+{
+    public const string UbPrefix = "ub";
+    public const string MsftPrefix = "msft";
+
+    public Exclusions(string rid)
+    {
+        _rid = rid;
+    }
+
+    string _rid;
+
+    string[] GetRidSpecificExclusionFileNames(string path)
+    {
+        string filename = Path.GetFileNameWithoutExtension(path);
+        string extension = Path.GetExtension(path);
+        Debug.Assert(path == $"{filename}{extension}", $"{path} != {filename}{extension}");
+        string[] parts = _rid.Split('-');
+        string[] fileNames = new string[parts.Length+1];
+        fileNames[0] = $"{filename}{extension}";
+        for (int i = 1; i < parts.Length; i++)
+        {
+            fileNames[i] = $"{filename}-{string.Join('-', parts[..i])}-any{extension}";
+        }
+        fileNames[parts.Length] = $"{filename}-{_rid}{extension}";
+        return fileNames;
+    }
+
+    public IEnumerable<string> RemoveContentDiffFileExclusions(IEnumerable<string> files, string? prefix = null)
+    {
+        var exclusions = GetFileExclusions(prefix);
+        Func<string, bool> condition = f => !IsFileExcluded(f, exclusions, prefix);
+        return files.Where(condition);
+    }
+
+    public IEnumerable<string> RemoveAssemblyVersionFileExclusions(IEnumerable<string> files, string? prefix = null)
+    {
+        var exclusions = GetFileExclusions(prefix).Concat(GetNativeDllExclusions(prefix)).Concat(GetAssemblyVersionExclusions(prefix));
+        Func<string, bool> condition = f => !IsFileExcluded(f, exclusions, prefix);
+        return files.Where(condition);
+    }
+
+    //public List<string> UbFileExclusions => _ubFileExclusions ??= GetFileExclusions(UbPrefix);
+    //List<string>? _ubFileExclusions = null;
+
+    //public List<string> UbAssemblyVersionExclusions => _ubAssemblyVersionExclusions ??= UbFileExclusions.Concat(GetAssemblyVersionExclusions(UbPrefix)).Concat(GetNativeDllExclusions(UbPrefix)).ToList();
+    //List<string>? _ubAssemblyVersionExclusions = null;
+
+    //public List<string> MsftFileExclusions => _msftFileExclusions ??= GetFileExclusions(MsftPrefix);
+    //List<string>? _msftFileExclusions = null;
+
+    //public List<string> MsftAssemblyVersionExclusions => _msftAssemblyVersionExclusions ??= MsftFileExclusions.Concat(GetAssemblyVersionExclusions(MsftPrefix)).Concat(GetNativeDllExclusions(MsftPrefix)).ToList();
+    //List<string>? _msftAssemblyVersionExclusions = null;
+
+    public List<string> GetFileExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("SdkFileDiffExclusions.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+    public List<string> GetAssemblyVersionExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("SdkAssemblyVersionDiffExclusions.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+    public List<string> GetNativeDllExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("NativeDlls.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+    public string GetBaselineFileDiffFileName() => GetRidSpecificExclusionFileNames("MsftToUbSdkFiles.diff").Last();
+
+    static string NormalizePath(string path)
+    {
+        return path.Replace('\\', '/');
+    }
+
+    public bool IsFileExcluded(string file, IEnumerable<string> exclusions, string? prefix = null)
+        => exclusions.Any(exclusion => FileSystemName.MatchesSimpleExpression(exclusion, NormalizePath(file)));
+}
diff --git "a/source-build-smoke-tests/SdkContentTests.cs" "b/unified-build/SdkContentTests.cs"
index 29bb7dbc5..6c6b57bd8 100644
--- "a/source-build-smoke-tests/SdkContentTests.cs"
+++ "b/unified-build/SdkContentTests.cs"
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information.

 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Data;
 using System.IO;
@@ -10,6 +11,7 @@
 using System.Linq;
 using System.Reflection;
 using System.Text.RegularExpressions;
+using System.Threading.Tasks;
 using Microsoft.Extensions.FileSystemGlobbing;
 using Xunit;
 using Xunit.Abstractions;
@@ -17,62 +19,66 @@
 namespace Microsoft.DotNet.SourceBuild.SmokeTests;

 [Trait("Category", "SdkContent")]
-public class SdkContentTests : SdkTests
+public class SdkContentTests : TestBase
 {
-    private const string MsftSdkType = "msft";
-    private const string SourceBuildSdkType = "sb";
-
-    public SdkContentTests(ITestOutputHelper outputHelper) : base(outputHelper) { }
+    Exclusions Exclusions;
+    public SdkContentTests(ITestOutputHelper outputHelper, Config config) : base(outputHelper, config)
+    {
+        Exclusions = new(Config.TargetRid);
+    }

     /// <Summary>
     /// Verifies the file layout of the source built sdk tarball to the Microsoft build.
-    /// The differences are captured in baselines/MsftToSbSdkDiff.txt.
+    /// The differences are captured in baselines/MsftToUbSdkDiff.txt.
     /// Version numbers that appear in paths are compared but are stripped from the baseline.
     /// This makes the baseline durable between releases.  This does mean however, entries
     /// in the baseline may appear identical if the diff is version specific.
     /// </Summary>
-    [SkippableFact(new[] { Config.MsftSdkTarballPathEnv, Config.SdkTarballPathEnv }, skipOnNullOrWhiteSpaceEnv: true)]
-    public void CompareMsftToSbFileList()
+    [Fact]
+    public void CompareMsftToUbFileList()
     {
         const string msftFileListingFileName = "msftSdkFiles.txt";
-        const string sbFileListingFileName = "sbSdkFiles.txt";
-        WriteTarballFileList(Config.MsftSdkTarballPath, msftFileListingFileName, isPortable: true, MsftSdkType);
-        WriteTarballFileList(Config.SdkTarballPath, sbFileListingFileName, isPortable: false, SourceBuildSdkType);
+        const string ubFileListingFileName = "ubSdkFiles.txt";
+        WriteTarballFileList(Config.MsftSdkArchivePath, msftFileListingFileName, isPortable: true, Exclusions.MsftPrefix);
+        WriteTarballFileList(Config.UbSdkArchivePath, ubFileListingFileName, isPortable: true, Exclusions.UbPrefix);

-        string diff = BaselineHelper.DiffFiles(msftFileListingFileName, sbFileListingFileName, OutputHelper);
+        string diff = BaselineHelper.DiffFiles(msftFileListingFileName, ubFileListingFileName, OutputHelper);
         diff = RemoveDiffMarkers(diff);
-        BaselineHelper.CompareBaselineContents("MsftToSbSdkFiles.diff", diff, OutputHelper, Config.WarnOnSdkContentDiffs);
+        BaselineHelper.CompareBaselineContents(Exclusions.GetBaselineFileDiffFileName(), diff, OutputHelper, Config.WarnOnSdkContentDiffs);
     }

-    [SkippableFact(new[] { Config.MsftSdkTarballPathEnv, Config.SdkTarballPathEnv }, skipOnNullOrWhiteSpaceEnv: true)]
-    public void CompareMsftToSbAssemblyVersions()
+    [Fact]
+    public async Task CompareMsftToUbAssemblyVersions()
     {
-        Assert.NotNull(Config.MsftSdkTarballPath);
-        Assert.NotNull(Config.SdkTarballPath);
+        Assert.NotNull(Config.MsftSdkArchivePath);
+        Assert.NotNull(Config.UbSdkArchivePath);

         DirectoryInfo tempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
         try
         {
-            DirectoryInfo sbSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, SourceBuildSdkType));
-            Utilities.ExtractTarball(Config.SdkTarballPath, sbSdkDir.FullName, OutputHelper);
+            DirectoryInfo ubSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, Exclusions.UbPrefix));
+            Utilities.ExtractTarball(Config.UbSdkArchivePath, ubSdkDir.FullName, OutputHelper);

-            DirectoryInfo msftSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, MsftSdkType));
-            Utilities.ExtractTarball(Config.MsftSdkTarballPath, msftSdkDir.FullName, OutputHelper);
+            DirectoryInfo msftSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, Exclusions.MsftPrefix));
+            Utilities.ExtractTarball(Config.MsftSdkArchivePath, msftSdkDir.FullName, OutputHelper);

-            Dictionary<string, Version?> sbSdkAssemblyVersions = GetSbSdkAssemblyVersions(sbSdkDir.FullName);
-            Dictionary<string, Version?> msftSdkAssemblyVersions = GetMsftSdkAssemblyVersions(msftSdkDir.FullName, sbSdkAssemblyVersions);
+            var t1 = Task.Run(() => GetSdkAssemblyVersions(ubSdkDir.FullName));
+            var t2 = Task.Run(() => GetSdkAssemblyVersions(msftSdkDir.FullName));
+            var results = await Task.WhenAll(t1, t2);
+            Dictionary<string, Version?> ubSdkAssemblyVersions = results[0];
+            Dictionary<string, Version?> msftSdkAssemblyVersions = results[1];

-            RemoveExcludedAssemblyVersionPaths(sbSdkAssemblyVersions, msftSdkAssemblyVersions);
+            RemoveExcludedAssemblyVersionPaths(ubSdkAssemblyVersions, msftSdkAssemblyVersions);

-            const string SbVersionsFileName = "sb_assemblyversions.txt";
-            WriteAssemblyVersionsToFile(sbSdkAssemblyVersions, SbVersionsFileName);
+            const string UbVersionsFileName = "ub_assemblyversions.txt";
+            WriteAssemblyVersionsToFile(ubSdkAssemblyVersions, UbVersionsFileName);

             const string MsftVersionsFileName = "msft_assemblyversions.txt";
             WriteAssemblyVersionsToFile(msftSdkAssemblyVersions, MsftVersionsFileName);

-            string diff = BaselineHelper.DiffFiles(MsftVersionsFileName, SbVersionsFileName, OutputHelper);
+            string diff = BaselineHelper.DiffFiles(MsftVersionsFileName, UbVersionsFileName, OutputHelper);
             diff = RemoveDiffMarkers(diff);
-            BaselineHelper.CompareBaselineContents("MsftToSbSdkAssemblyVersions.diff", diff, OutputHelper, Config.WarnOnSdkContentDiffs);
+            BaselineHelper.CompareBaselineContents($"MsftToUbSdkAssemblyVersions-{Config.TargetRid}.diff", diff, OutputHelper, Config.WarnOnSdkContentDiffs);
         }
         finally
         {
@@ -80,27 +86,40 @@ public void CompareMsftToSbAssemblyVersions()
         }
     }

-    private static void RemoveExcludedAssemblyVersionPaths(Dictionary<string, Version?> sbSdkAssemblyVersions, Dictionary<string, Version?> msftSdkAssemblyVersions)
+    private void RemoveExcludedAssemblyVersionPaths(Dictionary<string, Version?> ubSdkAssemblyVersions, Dictionary<string, Version?> msftSdkAssemblyVersions)
     {
-        IEnumerable<string> assemblyVersionDiffFilters = GetSdkAssemblyVersionDiffExclusionFilters()
+        IEnumerable<string> assemblyVersionDiffFilters = Exclusions.GetAssemblyVersionExclusions()
             .Select(filter => filter.TrimStart("./".ToCharArray()));
+        // Remove entries that are not in both. If they should be in both, the mismatch will be caught in another test
+        foreach (var kvp in ubSdkAssemblyVersions)
+        {
+            if (!msftSdkAssemblyVersions.ContainsKey(kvp.Key))
+            {
+                ubSdkAssemblyVersions.Remove(kvp.Key);
+            }
+        }

-        // Remove any excluded files as long as SB SDK's file has the same or greater assembly version compared to the corresponding
+        foreach (var kvp in msftSdkAssemblyVersions)
+        {
+            if (!ubSdkAssemblyVersions.ContainsKey(kvp.Key))
+            {
+                msftSdkAssemblyVersions.Remove(kvp.Key);
+            }
+        }
+
+        // Remove any excluded files as long as UB SDK's file has the same or greater assembly version compared to the corresponding
         // file in the MSFT SDK. If the version is less, the file will show up in the results as this is not a scenario
         // that is valid for shipping.
-        string[] sbSdkFileArray = sbSdkAssemblyVersions.Keys.ToArray();
-        for (int i = sbSdkFileArray.Length - 1; i >= 0; i--)
+        string[] ubSdkFileArray = ubSdkAssemblyVersions.Keys.ToArray();
+        for (int i = ubSdkFileArray.Length - 1; i >= 0; i--)
         {
-            string assemblyPath = sbSdkFileArray[i];
-            Version? sbVersion = sbSdkAssemblyVersions[assemblyPath];
-            Version? msftVersion = msftSdkAssemblyVersions[assemblyPath];
-
-            if (sbVersion is not null &&
-                msftVersion is not null &&
-                sbVersion >= msftVersion &&
+            string assemblyPath = ubSdkFileArray[i];
+            if (ubSdkAssemblyVersions.TryGetValue(assemblyPath, out Version? ubVersion) &&
+                msftSdkAssemblyVersions.TryGetValue(assemblyPath, out Version? msftVersion) &&
+                ubVersion >= msftVersion &&
                 Utilities.IsFileExcluded(assemblyPath, assemblyVersionDiffFilters))
             {
-                sbSdkAssemblyVersions.Remove(assemblyPath);
+                ubSdkAssemblyVersions.Remove(assemblyPath);
                 msftSdkAssemblyVersions.Remove(assemblyPath);
             }
         }
@@ -115,32 +134,6 @@ private static void WriteAssemblyVersionsToFile(Dictionary<string, Version?> ass
         File.WriteAllLines(outputPath, lines);
     }

-    private Dictionary<string, Version?> GetMsftSdkAssemblyVersions(
-        string msftSdkPath, Dictionary<string, Version?> sbSdkAssemblyVersions)
-    {
-        Dictionary<string, Version?> msftSdkAssemblyVersions = new();
-        foreach ((string relativePath, _) in sbSdkAssemblyVersions)
-        {
-            // Now we want to find the corresponding file that exists in the MSFT SDK.
-            // We've already replaced version numbers with placeholders in the path.
-            // So we can't directly use the relative path to find the corresponding file. Instead,
-            // we need to replace the version placeholders with wildcards and find the path through path matching.
-            string file = Path.Combine(msftSdkPath, relativePath);
-            Matcher matcher = BaselineHelper.GetFileMatcherFromPath(relativePath);
-
-            file = FindMatchingFilePath(msftSdkPath, matcher, relativePath);
-
-            if (!File.Exists(file))
-            {
-                continue;
-            }
-
-            AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
-            msftSdkAssemblyVersions.Add(BaselineHelper.RemoveVersions(relativePath), GetVersion(assemblyName));
-        }
-        return msftSdkAssemblyVersions;
-    }
-
     // It's known that assembly versions can be different between builds in their revision field. Disregard that difference
     // by excluding that field in the output.
     private static Version? GetVersion(AssemblyName assemblyName)
@@ -153,42 +146,46 @@ private static void WriteAssemblyVersionsToFile(Dictionary<string, Version?> ass
         return null;
     }

-    private string FindMatchingFilePath(string rootDir, Matcher matcher, string representativeFile)
+    private Dictionary<string, Version?> GetSdkAssemblyVersions(string ubSdkPath, string? prefix = null)
     {
-        foreach (string file in Directory.EnumerateFiles(rootDir, "*", SearchOption.AllDirectories))
-        {
-            if (matcher.Match(rootDir, file).HasMatches)
-            {
-                return file;
-            }
-        }
-
-        Assert.Fail($"Unable to find matching file for '{representativeFile}' in '{rootDir}'.");
-        return string.Empty;
-    }
-
-    private Dictionary<string, Version?> GetSbSdkAssemblyVersions(string sbSdkPath)
-    {
-        IEnumerable<string> exclusionFilters = GetSdkDiffExclusionFilters(SourceBuildSdkType)
+        Exclusions ex = Exclusions;
+        IEnumerable<string> exclusionFilters = ex.GetFileExclusions(prefix)
+            .Concat(ex.GetNativeDllExclusions(prefix))
+            .Concat(ex.GetAssemblyVersionExclusions(prefix))
             .Select(filter => filter.TrimStart("./".ToCharArray()));
-        Dictionary<string, Version?> sbSdkAssemblyVersions = new();
-        foreach (string file in Directory.EnumerateFiles(sbSdkPath, "*", SearchOption.AllDirectories))
+        ConcurrentDictionary<string, Version?> ubSdkAssemblyVersions = new();
+        List<Task> tasks = new List<Task>();
+        foreach (string dir in Directory.EnumerateDirectories(ubSdkPath, "*", SearchOption.AllDirectories).Append(ubSdkPath))
         {
-            string fileExt = Path.GetExtension(file);
-            if (fileExt.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
-                fileExt.Equals(".exe", StringComparison.OrdinalIgnoreCase))
+            var t = Task.Run(() =>
             {
-                AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
-                string relativePath = Path.GetRelativePath(sbSdkPath, file);
-                string normalizedPath = BaselineHelper.RemoveVersions(relativePath);
-
-                if (!Utilities.IsFileExcluded(normalizedPath, exclusionFilters))
+                foreach (string file in Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly))
                 {
-                    sbSdkAssemblyVersions.Add(normalizedPath, GetVersion(assemblyName));
+                    string fileExt = Path.GetExtension(file);
+                    if (fileExt.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
+                        fileExt.Equals(".exe", StringComparison.OrdinalIgnoreCase))
+                    {
+                        string relativePath = Path.GetRelativePath(ubSdkPath, file);
+                        string normalizedPath = BaselineHelper.RemoveVersions(relativePath);
+                        if (!ex.IsFileExcluded(normalizedPath, exclusionFilters))
+                        {
+                            try
+                            {
+                                AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
+                                Assert.True(ubSdkAssemblyVersions.TryAdd(normalizedPath, GetVersion(assemblyName)));
+                            }
+                            catch (BadImageFormatException)
+                            {
+                                Console.WriteLine($"BadImageFormatException: {file}");
+                            }
+                        }
+                    }
                 }
-            }
+            });
+            tasks.Add(t);
         }
-        return sbSdkAssemblyVersions;
+        Task.WaitAll(tasks.ToArray());
+        return ubSdkAssemblyVersions.ToDictionary();
     }

     private void WriteTarballFileList(string? tarballPath, string outputFileName, bool isPortable, string sdkType)
@@ -199,29 +196,20 @@ private void WriteTarballFileList(string? tarballPath, string outputFileName, bo
         }

         string fileListing = Utilities.GetTarballContentNames(tarballPath).Aggregate((a, b) => $"{a}{Environment.NewLine}{b}");
-        fileListing = BaselineHelper.RemoveRids(fileListing, isPortable);
+        fileListing = BaselineHelper.RemoveRids(fileListing, Config.PortableRidEnv, Config.TargetRid, isPortable);
         fileListing = BaselineHelper.RemoveVersions(fileListing);
         IEnumerable<string> files = fileListing.Split(Environment.NewLine).OrderBy(path => path);
-        files = RemoveExclusions(files, GetSdkDiffExclusionFilters(sdkType));
+        files = Exclusions.RemoveContentDiffFileExclusions(files, sdkType);

         File.WriteAllLines(outputFileName, files);
     }

-    private static IEnumerable<string> RemoveExclusions(IEnumerable<string> files, IEnumerable<string> exclusions) =>
-        files.Where(item => !Utilities.IsFileExcluded(item, exclusions));
-
-    private static IEnumerable<string> GetSdkDiffExclusionFilters(string sdkType) =>
-        Utilities.ParseExclusionsFile("SdkFileDiffExclusions.txt", sdkType);
-
-    private static IEnumerable<string> GetSdkAssemblyVersionDiffExclusionFilters() =>
-        Utilities.ParseExclusionsFile("SdkAssemblyVersionDiffExclusions.txt");
-
     private static string RemoveDiffMarkers(string source)
     {
         Regex indexRegex = new("^index .*", RegexOptions.Multiline);
         string result = indexRegex.Replace(source, "index ------------");

         Regex diffSegmentRegex = new("^@@ .* @@", RegexOptions.Multiline);
-        return diffSegmentRegex.Replace(result, "@@ ------------ @@");
+        return diffSegmentRegex.Replace(result, "@@ ------------ @@").ReplaceLineEndings();
     }
 }
diff --git "a/source-build-smoke-tests/SdkTests.cs" "b/source-build-smoke-tests/SdkTests.cs"
deleted file mode 100644
index ba3381360..000000000
--- "a/source-build-smoke-tests/SdkTests.cs"
+++ /dev/null
@@ -1,20 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using Xunit.Abstractions;
-
-namespace Microsoft.DotNet.SourceBuild.SmokeTests;
-
-/// <summary>
-/// Shared base class for all SDK-based smoke tests.
-/// </summary>
-public abstract class SdkTests : TestBase
-{
-    internal DotNetHelper DotNetHelper { get; }
-
-    protected SdkTests(ITestOutputHelper outputHelper) : base(outputHelper)
-    {
-        DotNetHelper = new DotNetHelper(outputHelper);
-    }
-}
diff --git "a/source-build-smoke-tests/TestBase.cs" "b/unified-build/TestBase.cs"
index 963f07109..95043eb4d 100644
--- "a/source-build-smoke-tests/TestBase.cs"
+++ "b/unified-build/TestBase.cs"
@@ -3,20 +3,22 @@
 // See the LICENSE file in the project root for more information.

 using System.IO;
+using Xunit;
 using Xunit.Abstractions;

 namespace Microsoft.DotNet.SourceBuild.SmokeTests;

-public abstract class TestBase
+public abstract class TestBase : IClassFixture<Config>
 {
+    protected Config Config;
     public static string LogsDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), "logs");

     public ITestOutputHelper OutputHelper { get; }

-    public TestBase(ITestOutputHelper outputHelper)
+    public TestBase(ITestOutputHelper outputHelper, Config config)
     {
         OutputHelper = outputHelper;
-
+        Config = config;
         if (!Directory.Exists(LogsDirectory))
         {
             Directory.CreateDirectory(LogsDirectory);
diff --git "a/source-build-smoke-tests/Utilities.cs" "b/unified-build/Utilities.cs"
index 228a65a61..a56b4f025 100644
--- "a/source-build-smoke-tests/Utilities.cs"
+++ "b/unified-build/Utilities.cs"
@@ -23,7 +23,7 @@ public static class Utilities
     /// Returns whether the given file path is excluded by the given exclusions using glob file matching.
     /// </summary>
     public static bool IsFileExcluded(string filePath, IEnumerable<string> exclusions) =>
-        GetMatchingFileExclusions(filePath, exclusions, exclusion => exclusion).Any();
+        GetMatchingFileExclusions(filePath.Replace('\\', '/'), exclusions, exclusion => exclusion).Any();

     public static IEnumerable<T> GetMatchingFileExclusions<T>(string filePath, IEnumerable<T> exclusions, Func<T, string> getExclusionExpression) =>
         exclusions.Where(exclusion => FileSystemName.MatchesSimpleExpression(getExclusionExpression(exclusion), filePath));
@@ -49,12 +49,44 @@ public static IEnumerable<string> ParseExclusionsFile(string exclusionsFileName,
             .Where(line => !string.IsNullOrEmpty(line))
             .ToList();
     }
-
+
+    public static IEnumerable<string> TryParseExclusionsFile(string exclusionsFileName, string? prefix = null)
+    {
+        string exclusionsFilePath = Path.Combine(BaselineHelper.GetAssetsDirectory(), exclusionsFileName);
+        int prefixSkip = prefix?.Length + 1 ?? 0;
+        if (!File.Exists(exclusionsFilePath))
+        {
+            return [];
+        }
+        return File.ReadAllLines(exclusionsFilePath)
+            // process only specific exclusions if a prefix is provided
+            .Where(line => prefix is null || line.StartsWith(prefix + ","))
+            .Select(line =>
+            {
+                // Ignore comments
+                var index = line.IndexOf('#');
+                return index >= 0 ? line[prefixSkip..index].TrimEnd() : line[prefixSkip..];
+            })
+            .Where(line => !string.IsNullOrEmpty(line))
+            .ToList();
+    }
+
     public static void ExtractTarball(string tarballPath, string outputDir, ITestOutputHelper outputHelper)
     {
         // TarFile doesn't properly handle hard links (https://github.com/dotnet/runtime/pull/85378#discussion_r1221817490),
         // use 'tar' instead.
-        ExecuteHelper.ExecuteProcessValidateExitCode("tar", $"xzf {tarballPath} -C {outputDir}", outputHelper);
+        if (tarballPath.EndsWith(".tar.gz", StringComparison.InvariantCultureIgnoreCase) || tarballPath.EndsWith(".tgz", StringComparison.InvariantCultureIgnoreCase))
+        {
+            ExecuteHelper.ExecuteProcessValidateExitCode("tar", $"xzf {tarballPath} -C {outputDir}", outputHelper);
+        }
+        else if (tarballPath.EndsWith(".zip"))
+        {
+            ZipFile.ExtractToDirectory(tarballPath, outputDir);
+        }
+        else
+        {
+            throw new InvalidOperationException($"Unsupported archive format: {tarballPath}");
+        }
     }

     public static void ExtractTarball(string tarballPath, string outputDir, string targetFilePath)
@@ -66,16 +98,16 @@ public static void ExtractTarball(string tarballPath, string outputDir, string t
         using GZipStream decompressorStream = new(fileStream, CompressionMode.Decompress);
         using TarReader reader = new(decompressorStream);

-        TarEntry entry;
+        TarEntry? entry;
         while ((entry = reader.GetNextEntry()) is not null)
         {
             if (matcher.Match(entry.Name).HasMatches)
             {
                 string outputPath = Path.Join(outputDir, entry.Name);
-                Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+                Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);

                 using FileStream outputFileStream = File.Create(outputPath);
-                entry.DataStream.CopyTo(outputFileStream);
+                entry.DataStream!.CopyTo(outputFileStream);
                 break;
             }
         }
@@ -83,14 +115,30 @@ public static void ExtractTarball(string tarballPath, string outputDir, string t

     public static IEnumerable<string> GetTarballContentNames(string tarballPath)
     {
-        using FileStream fileStream = File.OpenRead(tarballPath);
-        using GZipStream decompressorStream = new(fileStream, CompressionMode.Decompress);
-        using TarReader reader = new(decompressorStream);
-
-        TarEntry entry;
-        while ((entry = reader.GetNextEntry()) is not null)
+        if (tarballPath.EndsWith(".zip"))
         {
-            yield return entry.Name;
+            using ZipArchive zip = ZipFile.OpenRead(tarballPath);
+            foreach (ZipArchiveEntry entry in zip.Entries)
+            {
+                yield return entry.FullName;
+            }
+            yield break;
+        }
+        else if (tarballPath.EndsWith(".tar.gz") || tarballPath.EndsWith(".tgz"))
+        {
+            using FileStream fileStream = File.OpenRead(tarballPath);
+            using GZipStream decompressorStream = new(fileStream, CompressionMode.Decompress);
+            using TarReader reader = new(decompressorStream);
+
+            TarEntry? entry;
+            while ((entry = reader.GetNextEntry()) is not null)
+            {
+                yield return entry.Name;
+            }
+        }
+        else
+        {
+            throw new InvalidOperationException($"Unsupported archive format: {tarballPath}");
         }
     }

@@ -102,7 +150,7 @@ public static void ExtractNupkg(string package, string outputDir)
         foreach (ZipArchiveEntry entry in zip.Entries)
         {
             string outputPath = Path.Combine(outputDir, entry.FullName);
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
             entry.ExtractToFile(outputPath);
         }
     }

@jtschuster jtschuster marked this pull request as ready for review March 21, 2024 14:08
@jtschuster jtschuster requested review from a team as code owners March 21, 2024 14:08
- Move Directory.Build.props to build UB tests with arcade
- Use msbuild task to run tests rather than exec
- Update old condition for building UB tests
@@ -15,7 +15,7 @@
UnpackTarballs;
BuildUnifiedBuildTasks;
BuildMSBuildSdkResolver;
BuildSdkArchiveDiff;
BuildUnifiedBuildValidationTests;
Copy link
Member

@ViktorHofer ViktorHofer Mar 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this is test project it doesn't belong here anymore. That can be updated in a follow-up though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it automatically get restored by arcade as a test project? Or can we assume it will never be run in an offline build?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will get restored same as the build.proj and the repo-projects/ .proj files. But as mentioned, it might be better to do that in a follow-up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like building this before the sdk version is determined, and the runtime config values aren't being set, so it'll need to be done here.

- Remove reference to Logging package
- Update FileGlobbing Version to 8.0
- Move Validation target to installer repo-projects project
- Set VSTestUseMSBuildOutput=true in test project file
@jtschuster
Copy link
Member Author

It looks like we don't set DOTNET_ROOT(though we do set DOTNET_INSTALL_DIR, DOTNET_PATH, and a few others) in the build, so the testhost on Windows looks for the 9.0 runtime in C:\Program Files\dotnet and can't find it. Should we be setting that somewhere in the build? Otherwise, I can make the test project self-contained to work around the issue.

- Remove setting VSTestUseMSBuildOutput
- Set DotnetHostPath in runsettings
- Silence output if using msbuild logger
- Remove nuget config
- Don't publish self-contained
@jtschuster jtschuster merged commit 1821497 into dotnet:main Apr 2, 2024
22 checks passed
@ellahathaway
Copy link
Member

ellahathaway commented Apr 3, 2024

@jtschuster - Friendly reminder to do a squash and merge when merging PRs :).

Edit: Looking through this commit history, I thought it might've just been merged, but I could totally be wrong!

@jtschuster
Copy link
Member Author

@ellahathaway Oops, looks like it was merged without a squash. Thanks for the notice, squash and merge is the only option in runtime so I've gotten used to doing the default.

using Xunit;
using Xunit.Abstractions;

namespace Microsoft.DotNet.SourceBuild.SmokeTests;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these namespaces should be updated to match the project name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants