Skip to content

Commit d22bb74

Browse files
AnipikViktorHofer
andauthored
Add initial commit for package validation (#16992)
* Add intitial version of package Validation * Add some basic tests * addressing feedback * fix and some more tests * logging the errors on the fly , add GetLastStableVersion, removing duplicate tuples. * fix test builds * add some feedback * adding a new test logger to test the output for tests * addressing some mroe feedback around running targets * Restore fixes, baseline conditional validation * Use full namespace in task invocations * React to ericstj's feedback * address feedback * merge checker with diagnostic bag * use hashset for diaglist and add comments in the package * fix failing test on ci and resx files * add left and right to resx Co-authored-by: Viktor Hofer <viktor.hofer@microsoft.com>
1 parent ea2bf36 commit d22bb74

34 files changed

+2300
-16
lines changed

sdk.sln

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compatibility", "Compatibil
345345
EndProject
346346
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ApiCompatibility", "src\Compatibility\Microsoft.DotNet.ApiCompatibility\Microsoft.DotNet.ApiCompatibility.csproj", "{3F5A028C-C51B-434A-8C10-37680CD2635C}"
347347
EndProject
348-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.ApiCompatibility.Tests", "src\Tests\Microsoft.DotNet.ApiCompatibility.Tests\Microsoft.DotNet.ApiCompatibility.Tests.csproj", "{24F084ED-35BB-401E-89F5-63E5E22C3B3B}"
348+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ApiCompatibility.Tests", "src\Tests\Microsoft.DotNet.ApiCompatibility.Tests\Microsoft.DotNet.ApiCompatibility.Tests.csproj", "{24F084ED-35BB-401E-89F5-63E5E22C3B3B}"
349+
EndProject
349350
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Msi", "src\Microsoft.Win32.Msi\Microsoft.Win32.Msi.csproj", "{3D002392-6308-41DF-8BD5-224CCC5B049F}"
350351
EndProject
351352
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Msi.Tests", "src\Tests\Microsoft.Win32.Msi.Tests\Microsoft.Win32.Msi.Tests.csproj", "{80932949-B8B2-4163-B325-76F8FDBE3897}"
352353
EndProject
353354
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Msi.Manual.Tests", "src\Tests\Microsoft.Win32.Msi.Manual.Tests\Microsoft.Win32.Msi.Manual.Tests.csproj", "{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8}"
354355
EndProject
356+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.PackageValidation", "src\Compatibility\Microsoft.DotNet.PackageValidation\Microsoft.DotNet.PackageValidation.csproj", "{E56BEA9A-B52A-4781-9FF4-217439923319}"
357+
EndProject
358+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.PackageValidation.Tests", "src\Tests\Microsoft.DotNet.PackageValidation.Tests\Microsoft.DotNet.PackageValidation.Tests.csproj", "{69C03400-12AC-4E4D-B970-6A880616BF68}"
359+
EndProject
355360
Global
356361
GlobalSection(SolutionConfigurationPlatforms) = preSolution
357362
Debug|Any CPU = Debug|Any CPU
@@ -646,6 +651,14 @@ Global
646651
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8}.Debug|Any CPU.Build.0 = Debug|Any CPU
647652
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8}.Release|Any CPU.ActiveCfg = Release|Any CPU
648653
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8}.Release|Any CPU.Build.0 = Release|Any CPU
654+
{E56BEA9A-B52A-4781-9FF4-217439923319}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
655+
{E56BEA9A-B52A-4781-9FF4-217439923319}.Debug|Any CPU.Build.0 = Debug|Any CPU
656+
{E56BEA9A-B52A-4781-9FF4-217439923319}.Release|Any CPU.ActiveCfg = Release|Any CPU
657+
{E56BEA9A-B52A-4781-9FF4-217439923319}.Release|Any CPU.Build.0 = Release|Any CPU
658+
{69C03400-12AC-4E4D-B970-6A880616BF68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
659+
{69C03400-12AC-4E4D-B970-6A880616BF68}.Debug|Any CPU.Build.0 = Debug|Any CPU
660+
{69C03400-12AC-4E4D-B970-6A880616BF68}.Release|Any CPU.ActiveCfg = Release|Any CPU
661+
{69C03400-12AC-4E4D-B970-6A880616BF68}.Release|Any CPU.Build.0 = Release|Any CPU
649662
EndGlobalSection
650663
GlobalSection(SolutionProperties) = preSolution
651664
HideSolutionNode = FALSE
@@ -765,6 +778,8 @@ Global
765778
{3D002392-6308-41DF-8BD5-224CCC5B049F} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E}
766779
{80932949-B8B2-4163-B325-76F8FDBE3897} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
767780
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
781+
{E56BEA9A-B52A-4781-9FF4-217439923319} = {AF683E5C-421E-4DE0-ADD7-9841E5D12BFA}
782+
{69C03400-12AC-4E4D-B970-6A880616BF68} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
768783
EndGlobalSection
769784
GlobalSection(ExtensibilityGlobals) = postSolution
770785
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}

src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticBag.cs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,34 @@ public class DiagnosticBag<T> where T : IDiagnostic
1515
{
1616
private readonly Dictionary<string, HashSet<string>> _ignore;
1717
private readonly HashSet<string> _noWarn;
18-
1918
private readonly List<T> _differences = new();
2019

2120
/// <summary>
2221
/// Instantiate an diagnostic bag with the provided settings to ignore diagnostics.
2322
/// </summary>
2423
/// <param name="noWarn">Comma separated list of diagnostic IDs to ignore.</param>
2524
/// <param name="ignoredDifferences">An array of differences to ignore based on diagnostic ID and reference ID.</param>
26-
public DiagnosticBag(string noWarn, (string diagnosticId, string referenceId)[] ignoredDifferences)
25+
public DiagnosticBag(string noWarn, (string diagnosticId, string referenceId)[] ignoredDifferences) : this (noWarn?.Split(';'), ignoredDifferences)
26+
{
27+
}
28+
29+
public DiagnosticBag(IEnumerable<string> noWarn, (string diagnosticId, string referenceId)[] ignoredDifferences)
2730
{
28-
_noWarn = new HashSet<string>(noWarn?.Split(';'));
31+
_noWarn = new HashSet<string>(noWarn);
2932
_ignore = new Dictionary<string, HashSet<string>>();
3033

31-
foreach ((string diagnosticId, string referenceId) in ignoredDifferences)
34+
if (ignoredDifferences != null)
3235
{
33-
if (!_ignore.TryGetValue(diagnosticId, out HashSet<string> members))
36+
foreach ((string diagnosticId, string referenceId) in ignoredDifferences)
3437
{
35-
members = new HashSet<string>();
36-
_ignore.Add(diagnosticId, members);
37-
}
38+
if (!_ignore.TryGetValue(diagnosticId, out HashSet<string> members))
39+
{
40+
members = new HashSet<string>();
41+
_ignore.Add(diagnosticId, members);
42+
}
3843

39-
members.Add(referenceId);
44+
members.Add(referenceId);
45+
}
4046
}
4147
}
4248

@@ -50,24 +56,37 @@ public void AddRange(IEnumerable<T> differences)
5056
Add(difference);
5157
}
5258

59+
public void Add(DiagnosticBag<T> bag)
60+
{
61+
AddRange(bag.Differences);
62+
}
63+
5364
/// <summary>
5465
/// Adds a difference to the diagnostic bag if they are not found in the exclusion settings.
5566
/// </summary>
5667
/// <param name="difference">The difference to add.</param>
5768
public void Add(T difference)
5869
{
59-
if (_noWarn.Contains(difference.DiagnosticId))
60-
return;
70+
if (!Filter(difference.DiagnosticId, difference.ReferenceId))
71+
{
72+
_differences.Add(difference);
73+
}
74+
}
75+
76+
public bool Filter(string diagnosticId, string referenceId)
77+
{
78+
if (_noWarn.Contains(diagnosticId))
79+
return true;
6180

62-
if (_ignore.TryGetValue(difference.DiagnosticId, out HashSet<string> members))
81+
if (_ignore.TryGetValue(diagnosticId, out HashSet<string> members))
6382
{
64-
if (members.Contains(difference.ReferenceId))
83+
if (members.Contains(referenceId))
6584
{
66-
return;
85+
return true;
6786
}
6887
}
6988

70-
_differences.Add(difference);
89+
return false;
7190
}
7291

7392
/// <summary>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.IO.Compression;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.DotNet.ApiCompatibility;
10+
using Microsoft.DotNet.ApiCompatibility.Abstractions;
11+
using NuGet.Common;
12+
13+
namespace Microsoft.DotNet.PackageValidation
14+
{
15+
/// <summary>
16+
/// Runs ApiCompat over different assembly tuples.
17+
/// </summary>
18+
public class ApiCompatRunner
19+
{
20+
private List<(string leftAssemblyPackagePath, string leftAssemblyRelativePath, string rightAssemblyPackagePath, string rightAssemblyRelativePath, string assemblyName, string compatibilityReason, string header)> _queue = new();
21+
private readonly ILogger _log;
22+
private readonly ApiComparer _differ = new();
23+
24+
public ApiCompatRunner(string noWarn, (string, string)[] ignoredDifferences, ILogger log)
25+
{
26+
_log = log;
27+
_differ.NoWarn = noWarn;
28+
_differ.IgnoredDifferences = ignoredDifferences;
29+
}
30+
31+
/// <summary>
32+
/// Runs the api compat for the tuples in the queue.
33+
/// </summary>
34+
public void RunApiCompat()
35+
{
36+
foreach (var apicompatTuples in _queue.Distinct())
37+
{
38+
// TODO: Add Assembly version check.
39+
// TODO: Add optimisations tuples.
40+
// TODO: Run it Asynchronously.
41+
using (Stream leftAssemblyStream = GetFileStreamFromPackage(apicompatTuples.leftAssemblyPackagePath, apicompatTuples.leftAssemblyRelativePath))
42+
using (Stream rightAssemblyStream = GetFileStreamFromPackage(apicompatTuples.rightAssemblyPackagePath, apicompatTuples.rightAssemblyRelativePath))
43+
{
44+
IAssemblySymbol leftSymbols = new AssemblySymbolLoader().LoadAssembly(apicompatTuples.assemblyName, leftAssemblyStream);
45+
IAssemblySymbol rightSymbols = new AssemblySymbolLoader().LoadAssembly(apicompatTuples.assemblyName, rightAssemblyStream);
46+
47+
IEnumerable<CompatDifference> differences = _differ.GetDifferences(leftSymbols, rightSymbols);
48+
49+
if (differences.Any())
50+
{
51+
_log.LogError(apicompatTuples.compatibilityReason);
52+
_log.LogError(apicompatTuples.header);
53+
}
54+
55+
foreach (CompatDifference difference in differences)
56+
{
57+
_log.LogError(difference.ToString());
58+
}
59+
}
60+
}
61+
_queue.Clear();
62+
}
63+
64+
/// <summary>
65+
/// Queues the api compat for 2 assemblies.
66+
/// </summary>
67+
/// <param name="leftPackagePath">Path to package containing left assembly.</param>
68+
/// <param name="leftRelativePath">Relative left assembly path in package.</param>
69+
/// <param name="rightPackagePath">Path to package containing right assembly.</param>
70+
/// <param name="rightRelativePath">Relative right assembly path in package.</param>
71+
/// <param name="assemblyName">The name of the assembly.</param>
72+
/// <param name="compatibiltyReason">The reason for assembly compatibilty.</param>
73+
/// <param name="header">The header for the api compat diagnostics.</param>
74+
public void QueueApiCompat(string leftPackagePath, string leftRelativePath, string rightPackagePath, string rightRelativePath, string assemblyName, string compatibiltyReason, string header)
75+
{
76+
_queue.Add((leftPackagePath, leftRelativePath, rightPackagePath, rightRelativePath, assemblyName, compatibiltyReason, header));
77+
}
78+
79+
private static Stream GetFileStreamFromPackage(string packagePath, string entry)
80+
{
81+
MemoryStream ms = new MemoryStream();
82+
using (FileStream stream = File.OpenRead(packagePath))
83+
{
84+
var zipFile = new ZipArchive(stream);
85+
zipFile.GetEntry(entry).Open().CopyTo(ms);
86+
ms.Seek(0, SeekOrigin.Begin);
87+
}
88+
return ms;
89+
}
90+
}
91+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using Microsoft.DotNet.ApiCompatibility;
8+
using Microsoft.DotNet.ApiCompatibility.Abstractions;
9+
using NuGet.Common;
10+
using NuGet.ContentModel;
11+
using NuGet.Frameworks;
12+
13+
namespace Microsoft.DotNet.PackageValidation
14+
{
15+
/// <summary>
16+
/// Validates that no target framework / rid support is dropped in the latest package.
17+
/// Reports all the breaking changes in the latest package.
18+
/// </summary>
19+
public class BaselinePackageValidator
20+
{
21+
private static HashSet<string> s_diagList = new HashSet<string>{ DiagnosticIds.TargetFrameworkDropped, DiagnosticIds.TargetFrameworkAndRidPairDropped };
22+
23+
private readonly Package _baselinePackage;
24+
private readonly bool _runApiCompat;
25+
private readonly ApiCompatRunner _apiCompatRunner;
26+
private readonly ILogger _log;
27+
private readonly DiagnosticBag<IDiagnostic> _diagnosticBag;
28+
29+
public BaselinePackageValidator(Package baselinePackage, string noWarn, (string, string)[] ignoredDifferences, bool runApiCompat, ILogger log)
30+
{
31+
_baselinePackage = baselinePackage;
32+
_runApiCompat = runApiCompat;
33+
_log = log;
34+
_apiCompatRunner = new(noWarn, ignoredDifferences, _log);
35+
_diagnosticBag = new(noWarn?.Split(';')?.Where(t => s_diagList.Contains(t)), ignoredDifferences);
36+
}
37+
38+
/// <summary>
39+
/// Validates the latest nuget package doesnot drop any target framework/rid and does not introduce any breaking changes.
40+
/// </summary>
41+
/// <param name="package">Nuget Package that needs to be validated.</param>
42+
public void Validate(Package package)
43+
{
44+
foreach (ContentItem baselineCompileTimeAsset in _baselinePackage.CompileAssets)
45+
{
46+
NuGetFramework baselineTargetFramework = (NuGetFramework)baselineCompileTimeAsset.Properties["tfm"];
47+
ContentItem latestCompileTimeAsset = package.FindBestCompileAssetForFramework(baselineTargetFramework);
48+
if (latestCompileTimeAsset == null)
49+
{
50+
if (!_diagnosticBag.Filter(DiagnosticIds.TargetFrameworkDropped, baselineTargetFramework.ToString()))
51+
{
52+
string message = string.Format(Resources.MissingTargetFramework, baselineTargetFramework.ToString());
53+
_log.LogError(DiagnosticIds.TargetFrameworkDropped + " " + message);
54+
}
55+
}
56+
else if (_runApiCompat)
57+
{
58+
_apiCompatRunner.QueueApiCompat(_baselinePackage.PackagePath,
59+
baselineCompileTimeAsset.Path,
60+
package.PackagePath,
61+
latestCompileTimeAsset.Path,
62+
Path.GetFileName(package.PackagePath),
63+
Resources.BaselineVersionValidatorHeader,
64+
string.Format(Resources.ApiCompatibilityHeader, baselineCompileTimeAsset.Path, latestCompileTimeAsset.Path));
65+
}
66+
}
67+
68+
foreach (ContentItem baselineRuntimeAsset in _baselinePackage.RuntimeAssets)
69+
{
70+
NuGetFramework baselineTargetFramework = (NuGetFramework)baselineRuntimeAsset.Properties["tfm"];
71+
ContentItem latestRuntimeAsset = package.FindBestRuntimeAssetForFramework(baselineTargetFramework);
72+
if (latestRuntimeAsset == null)
73+
{
74+
if (!_diagnosticBag.Filter(DiagnosticIds.TargetFrameworkDropped, baselineTargetFramework.ToString()))
75+
{
76+
string message = string.Format(Resources.MissingTargetFramework, baselineTargetFramework.ToString());
77+
_log.LogError(DiagnosticIds.TargetFrameworkDropped + " " + message);
78+
}
79+
}
80+
else
81+
{
82+
if (_runApiCompat)
83+
{
84+
_apiCompatRunner.QueueApiCompat(_baselinePackage.PackagePath,
85+
baselineRuntimeAsset.Path,
86+
package.PackagePath,
87+
latestRuntimeAsset.Path,
88+
Path.GetFileName(package.PackagePath),
89+
Resources.BaselineVersionValidatorHeader,
90+
string.Format(Resources.ApiCompatibilityHeader, baselineRuntimeAsset.Path, latestRuntimeAsset.Path));
91+
}
92+
}
93+
}
94+
95+
foreach (ContentItem baselineRuntimeSpecificAsset in _baselinePackage.RuntimeSpecificAssets)
96+
{
97+
NuGetFramework baselineTargetFramework = (NuGetFramework)baselineRuntimeSpecificAsset.Properties["tfm"];
98+
string baselineRid = (string)baselineRuntimeSpecificAsset.Properties["rid"];
99+
ContentItem latestRuntimeSpecificAsset = package.FindBestRuntimeAssetForFrameworkAndRuntime(baselineTargetFramework, baselineRid);
100+
if (latestRuntimeSpecificAsset == null)
101+
{
102+
if (!_diagnosticBag.Filter(DiagnosticIds.TargetFrameworkDropped, baselineTargetFramework.ToString() + "-" + baselineRid))
103+
{
104+
string message = string.Format(Resources.MissingTargetFrameworkAndRid, baselineTargetFramework.ToString(), baselineRid);
105+
_log.LogError(DiagnosticIds.TargetFrameworkAndRidPairDropped + " " + message);
106+
}
107+
}
108+
else
109+
{
110+
if (_runApiCompat)
111+
{
112+
_apiCompatRunner.QueueApiCompat(_baselinePackage.PackagePath,
113+
baselineRuntimeSpecificAsset.Path,
114+
package.PackagePath,
115+
latestRuntimeSpecificAsset.Path,
116+
Path.GetFileName(package.PackagePath),
117+
Resources.BaselineVersionValidatorHeader,
118+
string.Format(Resources.ApiCompatibilityHeader, baselineRuntimeSpecificAsset.Path, latestRuntimeSpecificAsset.Path));
119+
}
120+
}
121+
}
122+
123+
_apiCompatRunner.RunApiCompat();
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)