public void TestFindChanges() { var directory = Path.GetDirectoryName(typeof(ApiDiffTest).Assembly.Location); var module1 = CecilUtility.ReadModule(Path.Combine(directory, "TestLibrary.V1.dll")); var module2 = CecilUtility.ReadModule(Path.Combine(directory, "TestLibrary.V2.dll")); FacadeModuleProcessor.MakePublicFacade(module1, false); FacadeModuleProcessor.MakePublicFacade(module2, false); var changes = ApiDiff.FindChanges(module1, module2); var diff = NormalizeDiff(changes.Select(change => $"{(change.IsBreaking ? "B" : "N")} {change.Message}")); var expectedDiff = NormalizeDiff(File.ReadAllLines(Path.Join(directory, "expected-diff.txt"))); var falseNegatives = diff.Except(expectedDiff).ToList(); if (falseNegatives.Count != 0) { Console.WriteLine("false positives:"); Console.Write(string.Join(Environment.NewLine, falseNegatives)); Console.WriteLine(); } var falsePositives = expectedDiff.Except(diff).ToList(); if (falsePositives.Count != 0) { Console.WriteLine("false negatives:"); Console.Write(string.Join(Environment.NewLine, falsePositives)); Console.WriteLine(); } CollectionAssert.AreEqual(expectedDiff, diff); }
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); } } }
static IReadOnlyList <TypeChanges> FindChanges(Stream stream1, Stream stream2) { var module1 = CecilUtility.ReadModule(stream1); var module2 = CecilUtility.ReadModule(stream2); FacadeModuleProcessor.MakePublicFacade(module1, keepInternalTypes: false); FacadeModuleProcessor.MakePublicFacade(module2, keepInternalTypes: false); return(ApiDiff.FindTypeChanges(module1, module2)); }
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); } } }
protected override bool OnInvoke(IEnumerable <string> extras) { if (Program.Verbose) { Console.WriteLine($"Comparing the following assemblies:"); foreach (var assembly in Assemblies) { Console.WriteLine($" - {assembly}"); } if (Directories.Count > 0) { Console.WriteLine($"Using the following search diectories:"); foreach (var directory in Directories) { Console.WriteLine($" - {directory}"); } } } var infoStreams = new List <Stream>(); { var config = new ApiInfoConfig { IgnoreResolutionErrors = !alwaysResolve, SearchDirectories = Directories, }; foreach (var assembly in Assemblies) { if (Program.Verbose) { Console.WriteLine($"Generating API info for {assembly}..."); } var xdoc = GetApiInfoDocument(assembly, config, out var wasXml); if (!wasXml && outputApi) { xdoc.Save(assembly + ".api.xml"); } // in order to compare assemblies, they have to be the same name var xass = xdoc.Element("assemblies").Element("assembly"); xass.SetAttributeValue("name", "Assembly"); var stream = new MemoryStream(); xdoc.Save(stream); stream.Position = 0; infoStreams.Add(stream); } } GenerateDiff(xmlDiff, writer => { if (Program.Verbose) { Console.WriteLine($"Generating XML diff at {xmlDiff}..."); } infoStreams[0].Position = 0; infoStreams[1].Position = 0; ApiDiff.Generate(infoStreams[0], infoStreams[1], writer); }); GenerateDiff(htmlDiff, writer => { if (Program.Verbose) { Console.WriteLine($"Generating HTML diff at {htmlDiff}..."); } infoStreams[0].Position = 0; infoStreams[1].Position = 0; var config = new ApiDiffFormattedConfig { Colorize = true, Formatter = ApiDiffFormatter.Html, IgnoreNonbreaking = excludeNonbreaking, IgnoreParameterNameChanges = excludeParameterNames, }; ApiDiffFormatted.Generate(infoStreams[0], infoStreams[1], writer, config); }); GenerateDiff(mdDiff, writer => { if (Program.Verbose) { Console.WriteLine($"Generating Markdown diff at {mdDiff}..."); } infoStreams[0].Position = 0; infoStreams[1].Position = 0; var config = new ApiDiffFormattedConfig { Formatter = ApiDiffFormatter.Markdown, IgnoreNonbreaking = excludeNonbreaking, IgnoreParameterNameChanges = excludeParameterNames, }; ApiDiffFormatted.Generate(infoStreams[0], infoStreams[1], writer, config); }); return(true); }
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"; if (!Directory.Exists(logsDirectory)) { Directory.CreateDirectory(logsDirectory); } File.WriteAllText($"{logsDirectory}/ApiValidationParameters.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).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); 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 }