Rasagar/Library/PackageCache/com.unity.package-validation-suite/Editor/ValidationSuite/ValidationTests/ApiValidation.cs

367 lines
18 KiB
C#
Raw Permalink Normal View History

2024-08-26 13:07:20 -07:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Semver;
using Unity.APIComparison.Framework.Changes;
using Unity.APIComparison.Framework.Collectors;
using UnityEditor.Compilation;
using UnityEditor.PackageManager.ValidationSuite.Utils;
using UnityEngine;
using AssemblyResolutionException = Mono.Cecil.AssemblyResolutionException;
namespace UnityEditor.PackageManager.ValidationSuite.ValidationTests
{
internal class ApiValidation : BaseAssemblyValidation
{
public ApiValidation()
{
TestName = "API Validation";
TestDescription = "Checks public API for style and changest that conflict with Semantic Versioning.";
TestCategory = TestCategory.ApiValidation;
SupportedValidations = new[] { ValidationType.CI, ValidationType.LocalDevelopmentInternal, ValidationType.Promotion };
SupportedPackageTypes = new[] { PackageType.Tooling };
}
public ApiValidation(ValidationAssemblyInformation validationAssemblyInformation)
: base(validationAssemblyInformation)
{
}
private AssemblyInfo[] GetAndCheckAsmdefs()
{
var relevantAssemblyInfo = GetRelevantAssemblyInfo();
if (Context.ProjectPackageInfo.LifecyclePhase != LifecyclePhase.Preview && Context.ProjectPackageInfo.LifecyclePhase != LifecyclePhase.Experimental)
{
var previousAssemblyDefinitions = GetAssemblyDefinitionDataInFolder(Context.PreviousPackageInfo.path);
var versionChangeType = Context.VersionChangeType;
foreach (var assemblyInfo in relevantAssemblyInfo)
{
//assembly is in the package
var previousAssemblyDefinition = previousAssemblyDefinitions.FirstOrDefault(ad => DoAssembliesMatch(ad, assemblyInfo.assemblyDefinition));
if (previousAssemblyDefinition == null)
continue; //new asmdefs are fine
var excludePlatformsDiff = string.Format("Was:\"{0}\" Now:\"{1}\"",
string.Join(", ", previousAssemblyDefinition.excludePlatforms),
string.Join(", ", assemblyInfo.assemblyDefinition.excludePlatforms));
if (previousAssemblyDefinition.excludePlatforms.Any(p => !assemblyInfo.assemblyDefinition.excludePlatforms.Contains(p)) &&
versionChangeType == VersionChangeType.Patch)
AddError("Removing from excludePlatfoms requires a new minor or major version. " + excludePlatformsDiff);
else if (assemblyInfo.assemblyDefinition.excludePlatforms.Any(p =>
!previousAssemblyDefinition.excludePlatforms.Contains(p)) &&
(versionChangeType == VersionChangeType.Patch || versionChangeType == VersionChangeType.Minor))
AddError("Adding to excludePlatforms requires a new major version. " + excludePlatformsDiff);
var includePlatformsDiff = string.Format("Was:\"{0}\" Now:\"{1}\"",
string.Join(", ", previousAssemblyDefinition.includePlatforms),
string.Join(", ", assemblyInfo.assemblyDefinition.includePlatforms));
if (previousAssemblyDefinition.includePlatforms.Any(p => !assemblyInfo.assemblyDefinition.includePlatforms.Contains(p)) &&
(versionChangeType == VersionChangeType.Patch || versionChangeType == VersionChangeType.Minor))
AddError("Removing from includePlatfoms requires a new major version. " + includePlatformsDiff);
else if (assemblyInfo.assemblyDefinition.includePlatforms.Any(p => !previousAssemblyDefinition.includePlatforms.Contains(p)))
{
if (previousAssemblyDefinition.includePlatforms.Length == 0 &&
(versionChangeType == VersionChangeType.Minor || versionChangeType == VersionChangeType.Patch))
AddError("Adding the first entry in includePlatforms requires a new major version. " + includePlatformsDiff);
else if (versionChangeType == VersionChangeType.Patch)
AddError("Adding to includePlatforms requires a new minor or major version. " + includePlatformsDiff);
}
}
}
return relevantAssemblyInfo;
}
private bool DoAssembliesMatch(AssemblyDefinition assemblyDefinition1, AssemblyDefinition assemblyDefinition2)
{
return validationAssemblyInformation.GetAssemblyName(assemblyDefinition1, true).Equals(validationAssemblyInformation.GetAssemblyName(assemblyDefinition2, false));
}
private AssemblyDefinition[] GetAssemblyDefinitionDataInFolder(string directory)
{
return Directory.GetFiles(directory, "*.asmdef", SearchOption.AllDirectories)
.Select(Utilities.GetDataFromJson<AssemblyDefinition>).ToArray();
}
protected override void Run(AssemblyInfo[] info)
{
TestState = TestState.Succeeded;
// SKIP_APIVALIDATION_PLATFORM_AND_VERSION_SANITY_CHECKS
// can be set to override the sanity checks and so ensure devs
// can run PVS tests on non Windows boxes.
#if !SKIP_APIVALIDATION_PLATFORM_AND_VERSION_SANITY_CHECKS
const string howToRunMessage =
"In order for API Validation to run the platform must be Windows and " +
"the package manifest must have a unity property that matches the major and minor version of " +
"the editor that Package Validation Suite is run with.";
// Skip unless platform is Windows
if (Application.platform != RuntimePlatform.WindowsEditor)
{
AddInformation(
"Skipping API Validation because platform is not Windows. " +
"API Validation is unreliable when comparing against assemblies built on a different platform.");
AddInformation(howToRunMessage);
TestState = TestState.NotRun;
return;
}
// Skip unless package manifest has unity property
if (string.IsNullOrEmpty(Context.ProjectPackageInfo.unity))
{
AddInformation("Skipping API Validation because package manifest has no unity property.");
AddInformation(howToRunMessage);
TestState = TestState.NotRun;
return;
}
// Skip unless package manifest unity property matches major and minor version of editor
var unityVersionMajorMinorMatch = Regex.Match(Application.unityVersion, @"^(\d+\.\d+)");
if (!unityVersionMajorMinorMatch.Success)
{
AddError($"Failed to extract major and minor version editor version string: {Application.unityVersion}");
return;
}
var unityVersionMajorMinor = unityVersionMajorMinorMatch.Groups[0].Value;
if (Context.ProjectPackageInfo.unity != unityVersionMajorMinor)
{
AddInformation("Skipping API Validation because package manifest unity property doesn't match current editor version.");
AddInformation(howToRunMessage);
TestState = TestState.NotRun;
return;
}
#endif
//does it compile?
if (EditorUtility.scriptCompilationFailed)
{
AddError("Compilation failed. Please fix any compilation errors.");
return;
}
if (EditorApplication.isCompiling)
{
AddError("Compilation in progress. Please wait for compilation to finish.");
return;
}
if (Context.PreviousPackageInfo == null)
{
AddInformation("No previous package version. Skipping Semantic Versioning checks.");
TestState = TestState.NotRun;
return;
}
//does it have asmdefs for all scripts?
var assemblies = GetAndCheckAsmdefs();
CheckApiDiff(assemblies);
//Run analyzers
}
[Serializable]
class AssemblyChange
{
public string assemblyName;
public List<string> additions = new List<string>();
public List<string> breakingChanges = new List<string>();
public AssemblyChange(string assemblyName)
{
this.assemblyName = assemblyName;
}
}
#if UNITY_2019_1_OR_NEWER
[Serializable]
class ApiDiff
{
public List<string> missingAssemblies = new List<string>();
public List<string> newAssemblies = new List<string>();
public List<AssemblyChange> assemblyChanges = new List<AssemblyChange>();
public int breakingChanges;
public int additions;
public int removedAssemblyCount;
}
#endif
private void CheckApiDiff(AssemblyInfo[] assemblyInfo)
{
#if UNITY_2018_1_OR_NEWER && !UNITY_2019_1_OR_NEWER
TestState = TestState.NotRun;
AddInformation("Api breaking changes validation only available on Unity 2019.1 or newer.");
return;
#else
var diff = new ApiDiff();
var assembliesForPackage = assemblyInfo.Where(a => !validationAssemblyInformation.IsTestAssembly(a)).ToArray();
if (Context.PreviousPackageBinaryDirectory == null)
{
TestState = TestState.NotRun;
AddInformation("Previous package binaries must be present on artifactory to do API diff.");
return;
}
var oldAssemblyPaths = Directory.GetFiles(Context.PreviousPackageBinaryDirectory, "*.dll");
//Build diff
foreach (var info in assembliesForPackage)
{
var assemblyDefinition = info.assemblyDefinition;
var oldAssemblyPath = oldAssemblyPaths.FirstOrDefault(p => Path.GetFileNameWithoutExtension(p) == assemblyDefinition.name);
if (info.assembly != null)
{
var extraSearchFolder = Path.GetDirectoryName(typeof(System.ObsoleteAttribute).Assembly.Location);
var assemblySearchFolder = new[]
{
extraSearchFolder, // System assemblies folder
Path.Combine(EditorApplication.applicationContentsPath, "Managed"), // Main Unity assemblies folder.
Path.Combine(EditorApplication.applicationContentsPath, "Managed/UnityEngine"), // Module assemblies folder.
Path.GetDirectoryName(info.assembly.outputPath), // TODO: This is not correct. We need to keep all dependencies for the previous binaries. For now, use the same folder as the current version when resolving dependencies.
Context.ProjectPackageInfo.path // make sure to add the package folder as well, because it may contain .dll files
};
const string logsDirectory = "Logs";
Utilities.EnsureDirectoryExists(logsDirectory);
File.WriteAllText($"{logsDirectory}/ApiValidationParameters.{DateTime.Now.Ticks}.txt", $"previous: {oldAssemblyPath}\ncurrent: {info.assembly.outputPath}\nsearch path: {string.Join("\n", assemblySearchFolder)}");
var apiChangesAssemblyInfo = new APIChangesCollector.AssemblyInfo()
{
BaseAssemblyPath = oldAssemblyPath,
BaseAssemblyExtraSearchFolders = assemblySearchFolder,
CurrentAssemblyPath = info.assembly.outputPath,
CurrentExtraSearchFolders = assemblySearchFolder
};
List<IAPIChange> entityChanges;
try
{
entityChanges = APIChangesCollector.Collect(apiChangesAssemblyInfo).SelectMany(c => c.Changes).Where(c => !IsOverride(c)).ToList();
}
catch (AssemblyResolutionException exception)
{
if (exception.AssemblyReference.Name == "UnityEditor.CoreModule" ||
exception.AssemblyReference.Name == "UnityEngine.CoreModule")
{
AddError(
"Failed comparing against assemblies of previously promoted version of package. \n" +
"This is most likely because the assemblies that were compared against were built with a different version of Unity. \n" +
"If you are certain that there are no API changes warranting bumping the package version then you can add an exception for this error:\n" +
ErrorDocumentation.GetLinkMessage("validation_exceptions.html", ""));
AddInformation($"APIChangesCollector.Collect threw exception:\n{exception}");
return;
}
throw;
}
var assemblyChange = new AssemblyChange(info.assembly.name)
{
additions = entityChanges.Where(c => c.IsAdd()).Select(c => c.ToString()).ToList(),
// Among all attribute changes, only the Obsolete attribute should be considered a breaking change
breakingChanges = entityChanges.Where(c => !c.IsAdd() && !((c.GetType()).Equals(typeof(AttributeChange)))).Select(c => c.ToString()).ToList()
};
if (entityChanges.Count > 0)
diff.assemblyChanges.Add(assemblyChange);
}
if (oldAssemblyPath == null)
diff.newAssemblies.Add(assemblyDefinition.name);
}
foreach (var oldAssemblyPath in oldAssemblyPaths)
{
var oldAssemblyName = Path.GetFileNameWithoutExtension(oldAssemblyPath);
if (assembliesForPackage.All(a => a.assemblyDefinition.name != oldAssemblyName))
diff.missingAssemblies.Add(oldAssemblyName);
}
//separate changes
diff.additions = diff.assemblyChanges.Sum(v => v.additions.Count);
diff.removedAssemblyCount = diff.missingAssemblies.Count;
diff.breakingChanges = diff.assemblyChanges.Sum(v => v.breakingChanges.Count);
AddInformation("Tested against version {0}", Context.PreviousPackageInfo.Id);
AddInformation("API Diff - Breaking changes: {0} Additions: {1} Missing Assemblies: {2}",
diff.breakingChanges,
diff.additions,
diff.removedAssemblyCount);
if (diff.breakingChanges > 0 || diff.additions > 0)
{
TestOutput.AddRange(diff.assemblyChanges.Select(c => new ValidationTestOutput() { Type = TestOutputType.Information, Output = JsonUtility.ToJson(c, true) }));
}
string json = JsonUtility.ToJson(diff, true);
// Ensure results directory exists before trying to write to it
Directory.CreateDirectory(ValidationSuiteReport.ResultsPath);
File.WriteAllText(Path.Combine(ValidationSuiteReport.ResultsPath, "ApiValidationReport.json"), json);
//Figure out type of version change (patch, minor, major)
//Error if changes are not allowed
var changeType = Context.VersionChangeType;
if (changeType == VersionChangeType.Unknown)
return;
if (Context.ProjectPackageInfo.LifecyclePhase == LifecyclePhase.Preview || Context.ProjectPackageInfo.LifecyclePhase == LifecyclePhase.Experimental)
ExperimentalPackageValidateApiDiffs(diff, changeType);
else
ReleasePackageValidateApiDiffs(diff, changeType);
#endif
}
private static bool IsOverride(IAPIChange change) => change is MemberAdded ma && ma.Overrides != null;
#if UNITY_2019_1_OR_NEWER
private void ExperimentalPackageValidateApiDiffs(ApiDiff diff, VersionChangeType changeType)
{
if (diff.breakingChanges > 0 && changeType == VersionChangeType.Patch)
AddError("For Experimental or Preview Packages, breaking changes require a new minor version.");
if (changeType == VersionChangeType.Patch)
{
foreach (var assembly in diff.missingAssemblies)
{
AddError("Assembly \"{0}\" no longer exists or is no longer included in build. For Experimental or Preview Packages, this change requires a new minor version.", assembly);
}
}
}
private void ReleasePackageValidateApiDiffs(ApiDiff diff, VersionChangeType changeType)
{
if (diff.breakingChanges > 0 && changeType != VersionChangeType.Major)
AddError("Breaking changes require a new major version.");
if (diff.additions > 0 && changeType == VersionChangeType.Patch)
AddError("Additions require a new minor or major version.");
if (changeType != VersionChangeType.Major)
{
foreach (var assembly in diff.missingAssemblies)
{
AddError("Assembly \"{0}\" no longer exists or is no longer included in build. This change requires a new major version.", assembly);
}
}
if (changeType == VersionChangeType.Patch)
{
foreach (var assembly in diff.newAssemblies)
{
AddError("New assembly \"{0}\" may only be added in a new minor or major version.", assembly);
}
}
}
#endif
}
}