diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs index a8955d1f2f96..0a8f5cbf47e7 100644 --- a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs @@ -18,66 +18,66 @@ public static class Program public static async Task Main(string[] args) { - RootCommand rootCommand = new("genapidiff"); + RootCommand rootCommand = new("ApiDiff - Tool for generating a markdown diff of two different versions of the same assembly."); - Option optionBeforeAssembliesFolderPath = new(name: "", aliases: ["--before", "-b"]) + Option optionBeforeAssembliesFolderPath = new("--before", "-b") { Description = "The path to the folder containing the old (before) assemblies to be included in the diff.", Arity = ArgumentArity.ExactlyOne, Required = true }; - Option optionBeforeRefAssembliesFolderPath = new(name: "", aliases: ["--refbefore", "-rb"]) + Option optionBeforeRefAssembliesFolderPath = new("--refbefore", "-rb") { Description = "The path to the folder containing the references required by old (before) assemblies, not to be included in the diff.", Arity = ArgumentArity.ExactlyOne, Required = false }; - Option optionAfterAssembliesFolderPath = new(name: "", aliases: ["--after", "-a"]) + Option optionAfterAssembliesFolderPath = new("--after", "-a") { Description = "The path to the folder containing the new (after) assemblies to be included in the diff.", Arity = ArgumentArity.ExactlyOne, Required = true }; - Option optionAfterRefAssembliesFolderPath = new(name: "", aliases: ["--refafter", "-ra"]) + Option optionAfterRefAssembliesFolderPath = new("--refafter", "-ra") { Description = "The path to the folder containing references required by the new (after) reference assemblies, not to be included in the diff.", Arity = ArgumentArity.ExactlyOne, Required = false }; - Option optionOutputFolderPath = new(name: "", aliases: ["--output", "-o"]) + Option optionOutputFolderPath = new("--output", "-o") { Description = "The path to the output folder.", Arity = ArgumentArity.ExactlyOne, Required = true }; - Option optionBeforeFriendlyName = new(name: "", aliases: ["--beforeFriendlyName", "-bfn"]) + Option optionBeforeFriendlyName = new("--beforeFriendlyName", "-bfn") { Description = "The friendly name to describe the 'before' assembly.", Arity = ArgumentArity.ExactlyOne, Required = true }; - Option optionAfterFriendlyName = new(name: "", aliases: ["--afterFriendlyName", "-afn"]) + Option optionAfterFriendlyName = new("--afterFriendlyName", "-afn") { Description = "The friendly name to describe the 'after' assembly.", Arity = ArgumentArity.ExactlyOne, Required = true }; - Option optionTableOfContentsTitle = new(name: "", aliases: ["--tableOfContentsTitle", "-tc"]) + Option optionTableOfContentsTitle = new("--tableOfContentsTitle", "-tc") { Description = $"The optional title of the markdown table of contents file that is placed in the output folder.", - Arity = ArgumentArity.ZeroOrMore, + Arity = ArgumentArity.ExactlyOne, Required = false, DefaultValueFactory = _ => "api_diff" }; - Option optionFilesWithAssembliesToExclude = new(name: "", aliases: ["--assembliesToExclude", "-eas"]) + Option optionFilesWithAssembliesToExclude = new("--assembliesToExclude", "-eas") { Description = "An optional array of filepaths, each containing a list of assemblies that should be excluded from the diff. Each file should contain one assembly name per line, with no extensions.", Arity = ArgumentArity.ZeroOrMore, @@ -85,7 +85,7 @@ public static async Task Main(string[] args) DefaultValueFactory = _ => null }; - Option optionFilesWithAttributesToExclude = new(name: "", aliases: ["--attributesToExclude", "-eattrs"]) + Option optionFilesWithAttributesToExclude = new("--attributesToExclude", "-eattrs") { Description = $"An optional array of filepaths, each containing a list of attributes to exclude from the diff. Each file should contain one API full name per line. You can either modify the default file '{AttributesToExcludeDefaultFileName}' to add your own attributes, or include additional files using this command line option.", Arity = ArgumentArity.ZeroOrMore, @@ -93,7 +93,7 @@ public static async Task Main(string[] args) DefaultValueFactory = _ => [new FileInfo(AttributesToExcludeDefaultFileName)] }; - Option optionFilesWithApisToExclude = new(name: "", aliases: ["--apisToExclude", "-eapis"]) + Option optionFilesWithApisToExclude = new("--apisToExclude", "-eapis") { Description = "An optional array of filepaths, each containing a list of APIs to exclude from the diff. Each file should contain one API full name per line.", Arity = ArgumentArity.ZeroOrMore, @@ -101,13 +101,13 @@ public static async Task Main(string[] args) DefaultValueFactory = _ => null }; - Option optionAddPartialModifier = new(name: "", aliases: ["--addPartialModifier", "-apm"]) + Option optionAddPartialModifier = new("--addPartialModifier", "-apm") { Description = "Add the 'partial' modifier to types.", DefaultValueFactory = _ => false }; - Option optionAttachDebugger = new(name: "", aliases: ["--attachDebugger", "-d"]) + Option optionAttachDebugger = new("--attachDebugger", "-d") { Description = "Stops the tool at startup, prints the process ID and waits for a debugger to attach.", DefaultValueFactory = _ => false @@ -128,9 +128,9 @@ public static async Task Main(string[] args) rootCommand.Options.Add(optionAddPartialModifier); rootCommand.Options.Add(optionAttachDebugger); - rootCommand.SetAction(async (ParseResult result) => + rootCommand.SetAction(async (ParseResult result, CancellationToken cancellationToken) => { - DiffConfiguration c = new( + DiffConfiguration diffConfig = new( BeforeAssembliesFolderPath: result.GetValue(optionBeforeAssembliesFolderPath) ?? throw new NullReferenceException("Null before assemblies directory"), BeforeAssemblyReferencesFolderPath: result.GetValue(optionBeforeRefAssembliesFolderPath), AfterAssembliesFolderPath: result.GetValue(optionAfterAssembliesFolderPath) ?? throw new NullReferenceException("Null after assemblies directory"), @@ -145,13 +145,15 @@ public static async Task Main(string[] args) AddPartialModifier: result.GetValue(optionAddPartialModifier), AttachDebugger: result.GetValue(optionAttachDebugger) ); - await HandleCommandAsync(c).ConfigureAwait(false); + await HandleCommandAsync(diffConfig, cancellationToken).ConfigureAwait(false); }); await rootCommand.Parse(args).InvokeAsync(); } - private static Task HandleCommandAsync(DiffConfiguration diffConfig) + private static Task HandleCommandAsync(DiffConfiguration diffConfig, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + var log = new ConsoleLog(MessageImportance.Normal); string assembliesToExclude = string.Join(", ", diffConfig.FilesWithAssembliesToExclude?.Select(a => a.FullName) ?? []); @@ -197,7 +199,7 @@ private static Task HandleCommandAsync(DiffConfiguration diffConfig) diagnosticOptions: null // TODO: If needed, add CLI option to pass specific diagnostic options ); - return diffGenerator.RunAsync(); + return diffGenerator.RunAsync(cancellationToken); } private static void WaitForDebugger() diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs index b86a24d77cd7..ae5ef0367769 100644 --- a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs @@ -87,11 +87,13 @@ internal FileOutputDiffGenerator(ILog log, public IReadOnlyDictionary Results => _results.AsReadOnly(); /// - public async Task RunAsync() + public async Task RunAsync(CancellationToken cancellationToken) { Debug.Assert(_beforeAssembliesFolderPaths.Length == 1); Debug.Assert(_afterAssembliesFolderPaths.Length == 1); + cancellationToken.ThrowIfCancellationRequested(); + (IAssemblySymbolLoader beforeLoader, Dictionary beforeAssemblySymbols) = AssemblySymbolLoader.CreateFromFiles( _log, @@ -118,7 +120,7 @@ public async Task RunAsync() _addPartialModifier, _diagnosticOptions); - await generator.RunAsync().ConfigureAwait(false); + await generator.RunAsync(cancellationToken).ConfigureAwait(false); // If true, output is disk. Otherwise, it's the Results dictionary. if (_writeToDisk) @@ -135,6 +137,8 @@ public async Task RunAsync() foreach ((string assemblyName, string text) in generator.Results.OrderBy(r => r.Key)) { + cancellationToken.ThrowIfCancellationRequested(); + string fileName = $"{_tableOfContentsTitle}_{assemblyName}.md"; tableOfContents.AppendLine($"* [{assemblyName}]({fileName})"); diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs index 32c7f2ceb478..7a27f4c5bb9b 100644 --- a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs @@ -13,5 +13,5 @@ public interface IDiffGenerator /// /// Asynchronously runs the diff generator and may populate the dictionary depending on the use case. /// - Task RunAsync(); + Task RunAsync(CancellationToken cancellationToken); } diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs index a5cb01b16ef6..a481547425f5 100644 --- a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs @@ -85,12 +85,14 @@ internal MemoryOutputDiffGenerator( public IReadOnlyDictionary Results => _results.AsReadOnly(); /// - public async Task RunAsync() + public async Task RunAsync(CancellationToken cancellationToken) { Stopwatch swRun = Stopwatch.StartNew(); foreach ((string beforeAssemblyName, IAssemblySymbol beforeAssemblySymbol) in _beforeAssemblySymbols) { + cancellationToken.ThrowIfCancellationRequested(); + // Needs to block so the _afterAssemblySymbols dictionary gets updated. await ProcessBeforeAndAfterAssemblyAsync(beforeAssemblyName, beforeAssemblySymbol).ConfigureAwait(false); } @@ -98,6 +100,8 @@ public async Task RunAsync() // Needs to happen after processing the before and after assemblies and filtering out the existing ones. foreach ((string afterAssemblyName, IAssemblySymbol afterAssemblySymbol) in _afterAssemblySymbols) { + cancellationToken.ThrowIfCancellationRequested(); + await ProcessNewAssemblyAsync(afterAssemblyName, afterAssemblySymbol).ConfigureAwait(false); } diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs index d4d9f893b80b..55d470427905 100644 --- a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs @@ -61,7 +61,7 @@ protected async Task RunTestAsync( addPartialModifier, DiffGeneratorFactory.DefaultDiagnosticOptions); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); foreach ((string expectedAssemblyName, string expectedCode) in expected) { diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs index cb699c336267..24e96ea62a24 100644 --- a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs @@ -96,7 +96,7 @@ public async Task DiskRead_DiskWrite() outputFolderPath.DirPath, writeToDisk: true); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, ExpectedTableOfContents, DefaultExpectedAssemblyMarkdowns); } @@ -255,7 +255,7 @@ Lines preceded by a '+' are additions and a '-' indicates removal. outputFolderPath.DirPath, writeToDisk: true); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, expectedTableOfContents, expectedAssemblyMarkdowns); } @@ -286,7 +286,7 @@ public async Task DiskRead_DiskWrite_ExcludeAssembly() writeToDisk: true, filesWithAssembliesToExclude: await GetFileWithListsAsync(root, [DefaultAssemblyName])); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); VerifyDiskWrite(outputFolderPath.FullName, DefaultTableOfContentsTitle, ExpectedEmptyTableOfContents, []); } @@ -376,7 +376,7 @@ public class MySubClass outputFolderPath.DirPath, writeToDisk: true); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, ExpectedTableOfContents, expectedAssemblyMarkdowns); } @@ -401,7 +401,7 @@ public async Task DiskRead_MemoryWrite() outputFolderPath.DirPath, writeToDisk: false); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); string tableOfContentsMarkdownFilePath = Path.Join(outputFolderPath.DirPath, $"{DefaultTableOfContentsTitle}.md"); Assert.Contains(tableOfContentsMarkdownFilePath, generator.Results.Keys); @@ -440,7 +440,7 @@ public async Task DiskRead_MemoryWrite_ExcludeAssembly() writeToDisk: false, filesWithAssembliesToExclude: await GetFileWithListsAsync(root, [DefaultAssemblyName])); - await generator.RunAsync(); + await generator.RunAsync(CancellationToken.None); string tableOfContentsMarkdownFilePath = Path.Join(outputFolderPath.FullName, $"{DefaultTableOfContentsTitle}.md"); Assert.Contains(tableOfContentsMarkdownFilePath, generator.Results.Keys);