diff --git a/.vscode/settings.json b/.vscode/settings.json index 89a3c7cca0c7..3a67def99f21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "files.trimTrailingWhitespace": true, - "files.associations": { - "*.*proj": "xml", - "*.props": "xml", - "*.targets": "xml", - "*.tasks": "xml" - } + "files.trimTrailingWhitespace": true, + "files.associations": { + "*.*proj": "xml", + "*.props": "xml", + "*.targets": "xml", + "*.tasks": "xml" + } } diff --git a/eng/Build.props b/eng/Build.props index e47402faf882..fa5e1d21d634 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -28,6 +28,7 @@ + + @@ -172,6 +174,7 @@ and are generated based on the last package release. + diff --git a/eng/Signing.props b/eng/Signing.props index 0dc432069dfa..f56fab785ab9 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -97,6 +97,12 @@ <_DotNetFilesToExclude Include="$(RedistNetCorePath)dotnet.exe" CertificateName="None" /> + + + 2.3.2 10.0.1 + 15.8.166 15.8.166 + 1.2.6 15.8.166 3.0.0 3.0.0 @@ -232,6 +234,7 @@ 0.10.1 1.0.2 12.0.1 + 13.0.4 3.12.1 17.17134.0 2.43.0 diff --git a/src/Shared/Process/ProcessExtensions.cs b/src/Shared/Process/ProcessExtensions.cs index c6cbd1f9700b..5fbefcdb241e 100644 --- a/src/Shared/Process/ProcessExtensions.cs +++ b/src/Shared/Process/ProcessExtensions.cs @@ -1,8 +1,9 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -41,42 +42,56 @@ public static void KillTree(this Process process, TimeSpan timeout) private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) { - RunProcessAndWaitForExit( - "pgrep", - $"-P {parentId}", - timeout, - out var stdout); - - if (!string.IsNullOrEmpty(stdout)) + try { - using (var reader = new StringReader(stdout)) + RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out var stdout); + + if (!string.IsNullOrEmpty(stdout)) { - while (true) + using (var reader = new StringReader(stdout)) { - var text = reader.ReadLine(); - if (text == null) + while (true) { - return; - } + var text = reader.ReadLine(); + if (text == null) + { + return; + } - if (int.TryParse(text, out var id)) - { - children.Add(id); - // Recursively get the children - GetAllChildIdsUnix(id, children, timeout); + if (int.TryParse(text, out var id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } } } } } + catch (Win32Exception ex) when (ex.Message.Contains("No such file or directory")) + { + // This probably means that pgrep isn't installed. Nothing to be done? + } } private static void KillProcessUnix(int processId, TimeSpan timeout) { - RunProcessAndWaitForExit( - "kill", - $"-TERM {processId}", - timeout, - out var stdout); + try + { + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out var stdout); + } + catch (Win32Exception ex) when (ex.Message.Contains("No such file or directory")) + { + // This probably means that the process is already dead + } } private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) diff --git a/src/Tools/Microsoft.dotnet-openapi/README.md b/src/Tools/Microsoft.dotnet-openapi/README.md new file mode 100644 index 000000000000..9ad333bddcde --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/README.md @@ -0,0 +1,84 @@ +# Microsoft.dotnet-openapi + +`Microsoft.dotnet-openapi` is a tool for managing OpenAPI references within your project. + +## Commands + +### Add Commands + + + +#### Add File + +##### Options + +| Short option| Long option| Description | Example | +|-------|------|-------|---------| +| -v|--verbose | Show verbose output. |dotnet openapi add file *-v* .\OpenAPI.json | +| -p|--updateProject | The project to operate on. |dotnet openapi add file *--updateProject .\Ref.csproj* .\OpenAPI.json | + +##### Arguments + +| Argument | Description | Example | +|-------------|-------------|---------| +| source-file | The source to create a reference from. Must be an OpenAPI file. |dotnet openapi add file *.\OpenAPI.json* | + +#### Add URL + +##### Options + +| Short option| Long option| Description | Example | +|-------|------|-------------|---------| +| -v|--verbose | Show verbose output. |dotnet openapi add url *-v* | +| -p|--updateProject | The project to operate on. |dotnet openapi add url *--updateProject .\Ref.csproj* | +| -o|--output-file | Where to place the local copy of the OpenAPI file. |dotnet openapi add url *--output-file myclient.json* | + +##### Arguments + +| Argument | Description | Example | +|-------------|-------------|---------| +| source-file | The source to create a reference from. Must be a URL. |dotnet openapi add url | + +### Remove + +##### Options + +| Short option| Long option| Description| Example | +|-------|------|------------|---------| +| -v|--verbose | Show verbose output. |dotnet openapi remove *-v*| +| -p|--updateProject | The project to operate on. |dotnet openapi remove *--updateProject .\Ref.csproj* .\OpenAPI.json | + +#### Arguments + +| Argument | Description| Example | +| ------------|------------|---------| +| source-file | The source to remove the reference to. |dotnet openapi remove *.\OpenAPI.json* | + +### Refresh + +#### Options + +| Short option| Long option| Description | Example | +|-------|------|-------------|---------| +| -v|--verbose | Show verbose output. | dotnet openapi refresh *-v* | +| -p|--updateProject | The project to operate on. | dotnet openapi refresh *--updateProject .\Ref.csproj* | + +#### Arguments + +| Argument | Description | Example | +| ------------|-------------|---------| +| source-file | The URL to refresh the reference from. | dotnet openapi refresh ** | diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Application.cs b/src/Tools/Microsoft.dotnet-openapi/src/Application.cs new file mode 100644 index 000000000000..52e25445b1fc --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Application.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.Build.Locator; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.DotNet.OpenApi.Commands; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.DotNet.OpenApi +{ + internal class Application : CommandLineApplication + { + static Application() + { + MSBuildLocator.RegisterDefaults(); + } + + public Application( + string workingDirectory, + IHttpClientWrapper httpClient, + TextWriter output = null, + TextWriter error = null) + { + Out = output ?? Out; + Error = error ?? Error; + + WorkingDirectory = workingDirectory; + + Name = "openapi"; + FullName = "OpenApi reference management tool"; + Description = "OpenApi reference management operations."; + ShortVersionGetter = GetInformationalVersion; + + Help = HelpOption("-?|-h|--help"); + Help.Inherited = true; + + Invoke = () => + { + ShowHelp(); + return 0; + }; + + Commands.Add(new AddCommand(this, httpClient)); + Commands.Add(new RemoveCommand(this, httpClient)); + Commands.Add(new RefreshCommand(this, httpClient)); + } + + public string WorkingDirectory { get; } + + public CommandOption Help { get; } + + public new int Execute(params string[] args) + { + try + { + return base.Execute(args); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + foreach (var innerException in ex.InnerExceptions) + { + Error.WriteLine(ex.InnerException.Message); + } + return 1; + } + + catch (ArgumentException ex) + { + // Don't show a call stack when we have unneeded arguments, just print the error message. + // The code that throws this exception will print help, so no need to do it here. + Error.WriteLine(ex.Message); + return 1; + } + catch (CommandParsingException ex) + { + // Don't show a call stack when we have unneeded arguments, just print the error message. + // The code that throws this exception will print help, so no need to do it here. + Error.WriteLine(ex.Message); + return 1; + } + catch (OperationCanceledException) + { + // This is a cancellation, not a failure. + Error.WriteLine("Cancelled"); + return 1; + } + catch (Exception ex) + { + Error.WriteLine(ex); + return 1; + } + } + + private string GetInformationalVersion() + { + var assembly = typeof(Application).GetTypeInfo().Assembly; + var attribute = assembly.GetCustomAttribute(); + return attribute.InformationalVersion; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/CodeGenerator.cs b/src/Tools/Microsoft.dotnet-openapi/src/CodeGenerator.cs new file mode 100644 index 000000000000..f885993c2964 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/CodeGenerator.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.DotNet.OpenApi +{ + public enum CodeGenerator + { + NSwagCSharp, + NSwagTypeScript + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddCommand.cs new file mode 100644 index 000000000000..bf879c8b2033 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddCommand.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.DotNet.Openapi.Tools; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class AddCommand : BaseCommand + { + private const string CommandName = "add"; + + public AddCommand(Application parent, IHttpClientWrapper httpClient) + : base(parent, CommandName, httpClient) + { + Commands.Add(new AddFileCommand(this, httpClient)); + //TODO: Add AddprojectComand here: https://github.com/aspnet/AspNetCore/issues/12738 + Commands.Add(new AddURLCommand(this, httpClient)); + } + + internal new Application Parent => (Application)base.Parent; + + protected override Task ExecuteCoreAsync() + { + ShowHelp(); + return Task.FromResult(0); + } + + protected override bool ValidateArguments() + { + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddFileCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddFileCommand.cs new file mode 100644 index 000000000000..b7f6cc9f0022 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddFileCommand.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class AddFileCommand : BaseCommand + { + private const string CommandName = "file"; + + private const string SourceFileArgName = "source-file"; + + public AddFileCommand(AddCommand parent, IHttpClientWrapper httpClient) + : base(parent, CommandName, httpClient) + { + _codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue); + _sourceFileArg = Argument(SourceFileArgName, $"The OpenAPI file to add. This must be a path to local OpenAPI file(s)", multipleValues: true); + } + + internal readonly CommandArgument _sourceFileArg; + internal readonly CommandOption _codeGeneratorOption; + + private readonly string[] ApprovedExtensions = new[] { ".json", ".yaml", ".yml" }; + + protected override async Task ExecuteCoreAsync() + { + var projectFilePath = ResolveProjectFile(ProjectFileOption); + + Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceFileArgName); + var codeGenerator = GetCodeGenerator(_codeGeneratorOption); + + foreach (var sourceFile in _sourceFileArg.Values) + { + if (!ApprovedExtensions.Any(e => sourceFile.EndsWith(e))) + { + await Warning.WriteLineAsync($"The extension for the given file '{sourceFile}' should have been one of: {string.Join(",", ApprovedExtensions)}."); + await Warning.WriteLineAsync($"The reference has been added, but may fail at build-time if the format is not correct."); + } + await AddOpenAPIReference(OpenApiReference, projectFilePath, sourceFile, codeGenerator); + } + + return 0; + } + + private bool IsLocalFile(string file) + { + return File.Exists(GetFullPath(file)); + } + + protected override bool ValidateArguments() + { + ValidateCodeGenerator(_codeGeneratorOption); + + try + { + Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceFileArgName); + } + catch(ArgumentException ex) + { + Error.Write(ex.Message); + return false; + } + + foreach (var sourceFile in _sourceFileArg.Values) + { + if (!IsLocalFile(sourceFile)) + { + Error.Write($"{SourceFileArgName} of '{sourceFile}' could not be found."); + } + } + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddProjectCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddProjectCommand.cs new file mode 100644 index 000000000000..a2a73794f8f1 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddProjectCommand.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class AddProjectCommand : BaseCommand + { + private const string CommandName = "project"; + + private const string SourceProjectArgName = "source-project"; + + public AddProjectCommand(BaseCommand parent, IHttpClientWrapper httpClient) + : base(parent, CommandName, httpClient) + { + _codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue); + _sourceProjectArg = Argument(SourceProjectArgName, $"The OpenAPI project to add. This must be the path to project file(s) containing OpenAPI endpoints", multipleValues: true); + } + + internal readonly CommandArgument _sourceProjectArg; + internal readonly CommandOption _codeGeneratorOption; + + protected override async Task ExecuteCoreAsync() + { + var projectFilePath = ResolveProjectFile(ProjectFileOption); + + var codeGenerator = GetCodeGenerator(_codeGeneratorOption); + + foreach (var sourceFile in _sourceProjectArg.Values) + { + await AddOpenAPIReference(OpenApiProjectReference, projectFilePath, sourceFile, codeGenerator); + } + + return 0; + } + + protected override bool ValidateArguments() + { + ValidateCodeGenerator(_codeGeneratorOption); + foreach (var sourceFile in _sourceProjectArg.Values) + { + if (!IsProjectFile(sourceFile)) + { + throw new ArgumentException($"{SourceProjectArgName} of '{sourceFile}' was not valid. Valid values must be project file(s)"); + } + } + + Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceProjectArgName); + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddURLCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddURLCommand.cs new file mode 100644 index 000000000000..600584c0fba5 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/AddURLCommand.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class AddURLCommand : BaseCommand + { + private const string CommandName = "url"; + + private const string OutputFileName = "--output-file"; + private const string SourceUrlArgName = "source-URL"; + + public AddURLCommand(AddCommand parent, IHttpClientWrapper httpClient) + : base(parent, CommandName, httpClient) + { + _codeGeneratorOption = Option("-c|--code-generator", "The code generator to use. Defaults to 'NSwagCSharp'.", CommandOptionType.SingleValue); + _outputFileOption = Option(OutputFileName, "The destination to download the remote OpenAPI file to.", CommandOptionType.SingleValue); + _sourceFileArg = Argument(SourceUrlArgName, $"The OpenAPI file to add. This must be a URL to a remote OpenAPI file.", multipleValues: true); + } + + internal readonly CommandOption _outputFileOption; + + internal readonly CommandArgument _sourceFileArg; + internal readonly CommandOption _codeGeneratorOption; + + protected override async Task ExecuteCoreAsync() + { + var projectFilePath = ResolveProjectFile(ProjectFileOption); + + var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceUrlArgName); + var codeGenerator = GetCodeGenerator(_codeGeneratorOption); + + // We have to download the file from that URL, save it to a local file, then create a OpenApiReference + var outputFile = await DownloadGivenOption(sourceFile, _outputFileOption); + + await AddOpenAPIReference(OpenApiReference, projectFilePath, outputFile, codeGenerator, sourceFile); + + return 0; + } + + protected override bool ValidateArguments() + { + ValidateCodeGenerator(_codeGeneratorOption); + var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceUrlArgName); + if (!IsUrl(sourceFile)) + { + Error.Write($"{SourceUrlArgName} was not valid. Valid values are URLs"); + return false; + } + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs new file mode 100644 index 000000000000..7123f0d04e1e --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs @@ -0,0 +1,538 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.DotNet.Openapi.Tools.Internal; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal abstract class BaseCommand : CommandLineApplication + { + protected string WorkingDirectory; + + protected readonly IHttpClientWrapper _httpClient; + + public const string OpenApiReference = "OpenApiReference"; + public const string OpenApiProjectReference = "OpenApiProjectReference"; + protected const string SourceUrlAttrName = "SourceUrl"; + + public const string ContentDispositionHeaderName = "Content-Disposition"; + private const string CodeGeneratorAttrName = "CodeGenerator"; + private const string DefaultExtension = ".json"; + + internal const string PackageVersionUrl = "https://go.microsoft.com/fwlink/?linkid=2099561"; + + public BaseCommand(CommandLineApplication parent, string name, IHttpClientWrapper httpClient) + { + Parent = parent; + Name = name; + Out = parent.Out ?? Out; + Error = parent.Error ?? Error; + _httpClient = httpClient; + + ProjectFileOption = Option("-p|--updateProject", "The project file update.", CommandOptionType.SingleValue); + + if (Parent is Application) + { + WorkingDirectory = ((Application)Parent).WorkingDirectory; + } + else + { + WorkingDirectory = ((Application)Parent.Parent).WorkingDirectory; + } + + OnExecute(ExecuteAsync); + } + + public CommandOption ProjectFileOption { get; } + + public TextWriter Warning + { + get { return Out; } + } + + protected abstract Task ExecuteCoreAsync(); + + protected abstract bool ValidateArguments(); + + private async Task ExecuteAsync() + { + if (GetApplication().Help.HasValue()) + { + ShowHelp(); + return 0; + } + + if (!ValidateArguments()) + { + ShowHelp(); + return 1; + } + + return await ExecuteCoreAsync(); + } + + private Application GetApplication() + { + var parent = Parent; + while(!(parent is Application)) + { + parent = parent.Parent; + } + return (Application)parent; + } + + internal FileInfo ResolveProjectFile(CommandOption projectOption) + { + string project; + if (projectOption.HasValue()) + { + project = projectOption.Value(); + project = GetFullPath(project); + if (!File.Exists(project)) + { + throw new ArgumentException($"The project '{project}' does not exist."); + } + } + else + { + var projects = Directory.GetFiles(WorkingDirectory, "*.csproj", SearchOption.TopDirectoryOnly); + if (projects.Length == 0) + { + throw new ArgumentException("No project files were found in the current directory. Either move to a new directory or provide the project explicitly"); + } + if (projects.Length > 1) + { + throw new ArgumentException("More than one project was found in this directory, either remove a duplicate or explicitly provide the project."); + } + + project = projects[0]; + } + + return new FileInfo(project); + } + + protected Project LoadProject(FileInfo projectFile) + { + var project = ProjectCollection.GlobalProjectCollection.LoadProject( + projectFile.FullName, + globalProperties: null, + toolsVersion: null); + project.ReevaluateIfNecessary(); + return project; + } + + internal bool IsProjectFile(string file) + { + return File.Exists(Path.GetFullPath(file)) && file.EndsWith(".csproj"); + } + + internal bool IsUrl(string file) + { + return Uri.TryCreate(file, UriKind.Absolute, out var _) && file.StartsWith("http"); + } + + internal async Task AddOpenAPIReference( + string tagName, + FileInfo projectFile, + string sourceFile, + CodeGenerator? codeGenerator, + string sourceUrl = null) + { + // EnsurePackagesInProjectAsync MUST happen before LoadProject, because otherwise the global state set by ProjectCollection doesn't pick up the nuget edits, and we end up losing them. + await EnsurePackagesInProjectAsync(projectFile, codeGenerator); + var project = LoadProject(projectFile); + var items = project.GetItems(tagName); + var fileItems = items.Where(i => string.Equals(GetFullPath(i.EvaluatedInclude), GetFullPath(sourceFile), StringComparison.Ordinal)); + + if (fileItems.Count() > 0) + { + Warning.Write($"One or more references to {sourceFile} already exist in '{project.FullPath}'. Duplicate references could lead to unexpected behavior."); + return; + } + + if (sourceUrl != null) + { + if (items.Any(i => string.Equals(i.GetMetadataValue(SourceUrlAttrName), sourceUrl))) + { + Warning.Write($"A reference to '{sourceUrl}' already exists in '{project.FullPath}'."); + return; + } + } + + var metadata = new Dictionary(); + + if (!string.IsNullOrEmpty(sourceUrl)) + { + metadata[SourceUrlAttrName] = sourceUrl; + } + + if (codeGenerator != null) + { + metadata[CodeGeneratorAttrName] = codeGenerator.ToString(); + } + + project.AddElementWithAttributes(tagName, sourceFile, metadata); + project.Save(); + } + + private async Task EnsurePackagesInProjectAsync(FileInfo projectFile, CodeGenerator? codeGenerator) + { + var urlPackages = await LoadPackageVersionsFromURLAsync(); + var attributePackages = GetServicePackages(codeGenerator); + + foreach (var kvp in attributePackages) + { + var packageId = kvp.Key; + var version = urlPackages != null && urlPackages.ContainsKey(packageId) ? urlPackages[packageId] : kvp.Value; + + var args = new[] { + "add", + "package", + packageId, + "--version", + version, + "--no-restore" + }; + + var muxer = DotNetMuxer.MuxerPathOrDefault(); + if (string.IsNullOrEmpty(muxer)) + { + throw new ArgumentException($"dotnet was not found on the path."); + } + + var startInfo = new ProcessStartInfo + { + FileName = muxer, + Arguments = string.Join(" ", args), + WorkingDirectory = projectFile.Directory.FullName, + RedirectStandardError = true, + RedirectStandardOutput = true, + }; + + var process = Process.Start(startInfo); + + var timeout = 20; + if (!process.WaitForExit(timeout * 1000)) + { + throw new ArgumentException($"Adding package `{packageId}` to `{projectFile.Directory}` took longer than {timeout} seconds."); + } + + if (process.ExitCode != 0) + { + Out.Write(process.StandardOutput.ReadToEnd()); + Error.Write(process.StandardError.ReadToEnd()); + throw new ArgumentException($"Could not add package `{packageId}` to `{projectFile.Directory}`"); + } + } + } + + internal async Task DownloadToFileAsync(string url, string destinationPath, bool overwrite) + { + using var response = await _httpClient.GetResponseAsync(url); + await WriteToFileAsync(await response.Stream, destinationPath, overwrite); + } + + internal async Task DownloadGivenOption(string url, CommandOption fileOption) + { + using var response = await _httpClient.GetResponseAsync(url); + + if (response.IsSuccessCode()) + { + string destinationPath; + if (fileOption.HasValue()) + { + destinationPath = fileOption.Value(); + } + else + { + var fileName = GetFileNameFromResponse(response, url); + var fullPath = GetFullPath(fileName); + var directory = Path.GetDirectoryName(fullPath); + destinationPath = GetUniqueFileName(directory, Path.GetFileNameWithoutExtension(fileName), Path.GetExtension(fileName)); + } + await WriteToFileAsync(await response.Stream, GetFullPath(destinationPath), overwrite: false); + + return destinationPath; + } + else + { + throw new ArgumentException($"The given url returned '{response.StatusCode}', indicating failure. The url might be wrong, or there might be a networking issue."); + } + } + + private string GetUniqueFileName(string directory, string fileName, string extension) + { + var uniqueName = fileName; + + var filePath = Path.Combine(directory, fileName + extension); + var exists = true; + var count = 0; + + do + { + if (!File.Exists(filePath)) + { + exists = false; + } + else + { + count++; + uniqueName = fileName + count; + filePath = Path.Combine(directory, uniqueName + extension); + } + } + while (exists); + + return uniqueName + extension; + } + + private string GetFileNameFromResponse(IHttpResponseMessageWrapper response, string url) + { + var contentDisposition = response.ContentDisposition(); + string result; + if (contentDisposition != null && contentDisposition.FileName != null) + { + var fileName = Path.GetFileName(contentDisposition.FileName); + if (!Path.HasExtension(fileName)) + { + fileName += DefaultExtension; + } + + result = fileName; + } + else + { + var uri = new Uri(url); + if (uri.Segments.Count() > 0 && uri.Segments.Last() != "/") + { + var lastSegment = uri.Segments.Last(); + if (!Path.HasExtension(lastSegment)) + { + lastSegment += DefaultExtension; + } + + result = lastSegment; + } + else + { + var parts = uri.Host.Split('.'); + + // There's no segment, use the domain name. + string domain; + switch (parts.Length) + { + case 1: + case 2: + // It's localhost if 1, no www if 2 + domain = parts.First(); + break; + case 3: + domain = parts[1]; + break; + default: + throw new NotImplementedException("We don't handle the case that the Host has more than three segments"); + } + + result = domain + DefaultExtension; + } + } + + return result; + } + + internal CodeGenerator? GetCodeGenerator(CommandOption codeGeneratorOption) + { + CodeGenerator? codeGenerator; + if (codeGeneratorOption.HasValue()) + { + codeGenerator = Enum.Parse(codeGeneratorOption.Value()); + } + else + { + codeGenerator = null; + } + + return codeGenerator; + } + + internal void ValidateCodeGenerator(CommandOption codeGeneratorOption) + { + if (codeGeneratorOption.HasValue()) + { + var value = codeGeneratorOption.Value(); + if (!Enum.TryParse(value, out CodeGenerator _)) + { + throw new ArgumentException($"Invalid value '{value}' given as code generator."); + } + } + } + + internal string GetFullPath(string path) + { + return Path.IsPathFullyQualified(path) + ? path + : Path.GetFullPath(path, WorkingDirectory); + } + + private async Task> LoadPackageVersionsFromURLAsync() + { + /* Example Json content + { + "Version" : "1.0", + "Packages" : { + "Microsoft.Azure.SignalR": "1.1.0-preview1-10442", + "Grpc.AspNetCore.Server": "0.1.22-pre2", + "Grpc.Net.ClientFactory": "0.1.22-pre2", + "Google.Protobuf": "3.8.0", + "Grpc.Tools": "1.22.0", + "NSwag.ApiDescription.Client": "13.0.3", + "Microsoft.Extensions.ApiDescription.Client": "0.3.0-preview7.19365.7", + "Newtonsoft.Json": "12.0.2" + } + }*/ + try + { + using var packageVersionStream = await (await _httpClient.GetResponseAsync(PackageVersionUrl)).Stream; + using var packageVersionDocument = await JsonDocument.ParseAsync(packageVersionStream); + var packageVersionsElement = packageVersionDocument.RootElement.GetProperty("Packages"); + var packageVersionsDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var packageVersion in packageVersionsElement.EnumerateObject()) + { + packageVersionsDictionary[packageVersion.Name] = packageVersion.Value.GetString(); + } + + return packageVersionsDictionary; + } + catch + { + // TODO (johluo): Consider logging a message indicating what went wrong and actions, if any, to be taken to resolve possible issues. + // Currently not logging anything since the fwlink is not published yet. + return null; + } + } + + private static IDictionary GetServicePackages(CodeGenerator? type) + { + CodeGenerator generator = type ?? CodeGenerator.NSwagCSharp; + var name = Enum.GetName(typeof(CodeGenerator), generator); + var attributes = typeof(Program).Assembly.GetCustomAttributes(); + + var packages = attributes.Where(a => a.CodeGenerators.Contains(generator)); + var result = new Dictionary(); + if (packages != null) + { + foreach (var package in packages) + { + result[package.Name] = package.Version; + } + } + + return result; + } + + private static byte[] GetHash(Stream stream) + { + SHA256 algorithm; + try + { + algorithm = SHA256.Create(); + } + catch (TargetInvocationException) + { + // SHA256.Create is documented to throw this exception on FIPS-compliant machines. See + // https://msdn.microsoft.com/en-us/library/z08hz7ad Fall back to a FIPS-compliant SHA256 algorithm. + algorithm = new SHA256CryptoServiceProvider(); + } + + using (algorithm) + { + return algorithm.ComputeHash(stream); + } + } + + private async Task WriteToFileAsync(Stream content, string destinationPath, bool overwrite) + { + if (content.CanSeek) + { + content.Seek(0, SeekOrigin.Begin); + } + + destinationPath = GetFullPath(destinationPath); + var destinationExists = File.Exists(destinationPath); + if (destinationExists && !overwrite) + { + throw new ArgumentException($"File '{destinationPath}' already exists. Aborting to avoid conflicts. Provide the '--output-file' argument with an unused file to resolve."); + } + + await Out.WriteLineAsync($"Downloading to '{destinationPath}'."); + var reachedCopy = false; + try + { + if (destinationExists) + { + // Check hashes before using the downloaded information. + var downloadHash = GetHash(content); + + byte[] destinationHash; + using (var destinationStream = File.OpenRead(destinationPath)) + { + destinationHash = GetHash(destinationStream); + } + + var sameHashes = downloadHash.Length == destinationHash.Length; + for (var i = 0; sameHashes && i < downloadHash.Length; i++) + { + sameHashes = downloadHash[i] == destinationHash[i]; + } + + if (sameHashes) + { + await Out.WriteLineAsync($"Not overwriting existing and matching file '{destinationPath}'."); + return; + } + } + else + { + // May need to create directory to hold the file. + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(destinationDirectory) && !Directory.Exists(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + } + + // Create or overwrite the destination file. + reachedCopy = true; + using var fileStream = new FileStream(destinationPath, FileMode.OpenOrCreate, FileAccess.Write); + fileStream.Seek(0, SeekOrigin.Begin); + if (content.CanSeek) + { + content.Seek(0, SeekOrigin.Begin); + } + await content.CopyToAsync(fileStream); + } + catch (Exception ex) + { + await Error.WriteLineAsync($"Downloading failed."); + await Error.WriteLineAsync(ex.ToString()); + if (reachedCopy) + { + File.Delete(destinationPath); + } + } + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/RefreshCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/RefreshCommand.cs new file mode 100644 index 000000000000..f21196f5f47e --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/RefreshCommand.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class RefreshCommand : BaseCommand + { + private const string CommandName = "refresh"; + + private const string SourceURLArgName = "source-URL"; + + public RefreshCommand(Application parent, IHttpClientWrapper httpClient) : base(parent, CommandName, httpClient) + { + _sourceFileArg = Argument(SourceURLArgName, $"The OpenAPI reference to refresh."); + } + + internal readonly CommandArgument _sourceFileArg; + + protected override async Task ExecuteCoreAsync() + { + var projectFile = ResolveProjectFile(ProjectFileOption); + + var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceURLArgName); + + var destination = FindReferenceFromUrl(projectFile, sourceFile); + await DownloadToFileAsync(sourceFile, destination, overwrite: true); + + return 0; + } + + private string FindReferenceFromUrl(FileInfo projectFile, string url) + { + var project = LoadProject(projectFile); + var openApiReferenceItems = project.GetItems(OpenApiReference); + + foreach (ProjectItem item in openApiReferenceItems) + { + var attrUrl = item.GetMetadataValue(SourceUrlAttrName); + if (string.Equals(attrUrl, url, StringComparison.Ordinal)) + { + return item.EvaluatedInclude; + } + } + + throw new ArgumentException("There was no OpenAPI reference to refresh with the given URL."); + } + + protected override bool ValidateArguments() + { + var sourceFile = Ensure.NotNullOrEmpty(_sourceFileArg.Value, SourceURLArgName); + if (!IsUrl(sourceFile)) + { + throw new ArgumentException($"'dotnet openapi refresh' must be given a URL"); + } + + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/RemoveCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/RemoveCommand.cs new file mode 100644 index 000000000000..e8e45d2b3256 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/RemoveCommand.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Build.Evaluation; +using Microsoft.DotNet.Openapi.Tools; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; + +namespace Microsoft.DotNet.OpenApi.Commands +{ + internal class RemoveCommand : BaseCommand + { + private const string CommandName = "remove"; + + private const string SourceArgName = "soruce"; + + public RemoveCommand(Application parent, IHttpClientWrapper httpClient) : base(parent, CommandName, httpClient) + { + _sourceProjectArg = Argument(SourceArgName, $"The OpenAPI reference to remove. Must represent a reference which is already in this project", multipleValues: true); + } + + internal readonly CommandArgument _sourceProjectArg; + + protected override Task ExecuteCoreAsync() + { + var projectFile = ResolveProjectFile(ProjectFileOption); + + var sourceFile = Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceArgName); + + if (IsProjectFile(sourceFile)) + { + RemoveServiceReference(OpenApiProjectReference, projectFile, sourceFile); + } + else + { + var file = RemoveServiceReference(OpenApiReference, projectFile, sourceFile); + + if (file != null) + { + File.Delete(GetFullPath(file)); + } + } + + return Task.FromResult(0); + } + + private string RemoveServiceReference(string tagName, FileInfo projectFile, string sourceFile) + { + var project = LoadProject(projectFile); + var openApiReferenceItems = project.GetItems(tagName); + + foreach (ProjectItem item in openApiReferenceItems) + { + var include = item.EvaluatedInclude; + var sourceUrl = item.HasMetadata(SourceUrlAttrName) ? item.GetMetadataValue(SourceUrlAttrName) : null; + if (string.Equals(include, sourceFile, StringComparison.OrdinalIgnoreCase) + || string.Equals(sourceUrl, sourceFile, StringComparison.OrdinalIgnoreCase)) + { + project.RemoveItem(item); + project.Save(); + return include; + } + } + + Warning.Write($"No OpenAPI reference was found with the file '{sourceFile}'"); + return null; + } + + protected override bool ValidateArguments() + { + Ensure.NotNullOrEmpty(_sourceProjectArg.Value, SourceArgName); + return true; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/DebugMode.cs b/src/Tools/Microsoft.dotnet-openapi/src/DebugMode.cs new file mode 100644 index 000000000000..5619fe01788a --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/DebugMode.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Microsoft.DotNet.OpenApi +{ + internal static class DebugMode + { + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + + Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); + while (!Debugger.IsAttached) + { + Thread.Sleep(TimeSpan.FromSeconds(3)); + } + } + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/HttpClientWrapper.cs b/src/Tools/Microsoft.dotnet-openapi/src/HttpClientWrapper.cs new file mode 100644 index 000000000000..4ca5a9ae266a --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/HttpClientWrapper.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.DotNet.OpenApi; +using Microsoft.DotNet.OpenApi.Commands; + +namespace Microsoft.DotNet.Openapi.Tools +{ + public class HttpClientWrapper : IHttpClientWrapper + { + private readonly HttpClient _client; + + public HttpClientWrapper(HttpClient client) + { + _client = client; + } + + public void Dispose() + { + _client.Dispose(); + } + + public async Task GetResponseAsync(string url) + { + var response = await _client.GetAsync(url); + + return new HttpResponseMessageWrapper(response); + } + + public Task GetStreamAsync(string url) + { + return _client.GetStreamAsync(url); + } + } + + public class HttpResponseMessageWrapper : IHttpResponseMessageWrapper + { + private HttpResponseMessage _response; + + public HttpResponseMessageWrapper(HttpResponseMessage response) + { + _response = response; + } + + public Task Stream => _response.Content.ReadAsStreamAsync(); + + public HttpStatusCode StatusCode => _response.StatusCode; + + public bool IsSuccessCode() => _response.IsSuccessStatusCode; + + public ContentDispositionHeaderValue ContentDisposition() + { + if (_response.Headers.TryGetValues(BaseCommand.ContentDispositionHeaderName, out var disposition)) + { + return new ContentDispositionHeaderValue(disposition.First()); + } + + return null; + } + + public void Dispose() + { + _response.Dispose(); + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/IHttpClientWrapper.cs b/src/Tools/Microsoft.dotnet-openapi/src/IHttpClientWrapper.cs new file mode 100644 index 000000000000..1cdd3589428c --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/IHttpClientWrapper.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.DotNet.OpenApi; + +namespace Microsoft.DotNet.Openapi.Tools +{ + internal interface IHttpClientWrapper : IDisposable + { + Task GetResponseAsync(string url); + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/IHttpResponseMessageWrapper.cs b/src/Tools/Microsoft.dotnet-openapi/src/IHttpResponseMessageWrapper.cs new file mode 100644 index 000000000000..07a4cf6355d7 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/IHttpResponseMessageWrapper.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.OpenApi +{ + public interface IHttpResponseMessageWrapper : IDisposable + { + Task Stream { get; } + ContentDispositionHeaderValue ContentDisposition(); + HttpStatusCode StatusCode { get; } + bool IsSuccessCode(); + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Internal/OpenapiDependencyAttribute.cs b/src/Tools/Microsoft.dotnet-openapi/src/Internal/OpenapiDependencyAttribute.cs new file mode 100644 index 000000000000..ae21fcec0bf8 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Internal/OpenapiDependencyAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.OpenApi; + +namespace Microsoft.DotNet.Openapi.Tools.Internal +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal class OpenApiDependencyAttribute : Attribute + { + public OpenApiDependencyAttribute(string name, string version, string codeGenerators) + { + Name = name; + Version = version; + CodeGenerators = codeGenerators.Split(';', StringSplitOptions.RemoveEmptyEntries).Select(c => Enum.Parse(c)).ToArray(); + } + + public string Name { get; set; } + public string Version { get; set; } + public IEnumerable CodeGenerators { get; set; } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Microsoft.dotnet-openapi.csproj b/src/Tools/Microsoft.dotnet-openapi/src/Microsoft.dotnet-openapi.csproj new file mode 100644 index 000000000000..ca416997da17 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Microsoft.dotnet-openapi.csproj @@ -0,0 +1,36 @@ + + + netcoreapp3.0 + exe + Command line tool to add an OpenAPI service reference + Microsoft.DotNet.Openapi.Tools + dotnet-openapi + Microsoft.dotnet-openapi + true + + false + + + + + + + + + + + + + + + <_Parameter1>NSwag.ApiDescription.Client + <_Parameter2>$(NSwagApiDescriptionClientPackageVersion) + <_Parameter3>NSwagCSharp;NSwagTypeScript + + + <_Parameter1>Newtonsoft.Json + <_Parameter2>$(NewtonsoftJsonPackageVersion) + <_Parameter3>NSwagCSharp;NSwagTypeScript + + + diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Program.cs b/src/Tools/Microsoft.dotnet-openapi/src/Program.cs new file mode 100644 index 000000000000..1dc4e5c74710 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net.Http; +using Microsoft.DotNet.Openapi.Tools; + +namespace Microsoft.DotNet.OpenApi +{ + public class Program + { + public static int Main(string[] args) + { + var outputWriter = new StringWriter(); + var errorWriter = new StringWriter(); + + DebugMode.HandleDebugSwitch(ref args); + + try + { + using var httpClient = new HttpClientWrapper(new HttpClient()); + var application = new Application( + Directory.GetCurrentDirectory(), + httpClient, + outputWriter, + errorWriter); + + var result = application.Execute(args); + + return result; + } + catch (Exception ex) + { + errorWriter.Write("Unexpected error:"); + errorWriter.WriteLine(ex.ToString()); + } + finally + { + var output = outputWriter.ToString(); + var error = errorWriter.ToString(); + + outputWriter.Dispose(); + errorWriter.Dispose(); + + Console.WriteLine(output); + Console.Error.WriteLine(error); + } + + return 1; + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/ProjectExtensions.cs b/src/Tools/Microsoft.dotnet-openapi/src/ProjectExtensions.cs new file mode 100644 index 000000000000..3428c8283969 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/ProjectExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Build.Evaluation; + +namespace Microsoft.DotNet.OpenApi +{ + public static class ProjectExtensions + { + public static void AddElementWithAttributes(this Project project, string tagName, string include, IDictionary metadata) + { + var item = project.AddItem(tagName, include).Single(); + foreach (var kvp in metadata) + { + item.Xml.AddMetadata(kvp.Key, kvp.Value, expressAsAttribute: true); + } + + project.Save(); + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Properties/AssemblyInfo.cs b/src/Tools/Microsoft.dotnet-openapi/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..0df0f149daa4 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.DotNet.Open.Api.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddFileTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddFileTests.cs new file mode 100644 index 000000000000..257a0fc82d7f --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddFileTests.cs @@ -0,0 +1,254 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.DotNet.OpenApi.Tests; +using Microsoft.Extensions.Internal; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.OpenApi.Add.Tests +{ + public class OpenApiAddFileTests : OpenApiTestBase + { + public OpenApiAddFileTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void OpenApi_Empty_ShowsHelp() + { + var app = GetApplication(); + var run = app.Execute(new string[] { }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + Assert.Contains("Usage: openapi ", _output.ToString()); + } + + [Fact] + public void OpenApi_NoProjectExists() + { + var app = GetApplication(); + _tempDir.Create(); + var run = app.Execute(new string[] { "add", "file", "randomfile.json" }); + + Assert.Contains("No project files were found in the current directory", _error.ToString()); + Assert.Equal(1, run); + } + + [Fact] + public void OpenApi_ExplicitProject_Missing() + { + var app = GetApplication(); + _tempDir.Create(); + var csproj = "fake.csproj"; + var run = app.Execute(new string[] { "add", "file", "--updateProject", csproj, "randomfile.json" }); + + Assert.Contains($"The project '{Path.Combine(_tempDir.Root, csproj)}' does not exist.", _error.ToString()); + Assert.Equal(1, run); + } + + [Fact] + public void OpenApi_Add_Empty_ShowsHelp() + { + var app = GetApplication(); + var run = app.Execute(new string[] { "add" }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + Assert.Contains("Usage: openapi add", _output.ToString()); + } + + [Fact] + public void OpenApi_Add_File_Empty_ShowsHelp() + { + var app = GetApplication(); + var run = app.Execute(new string[] { "add", "file", "--help" }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + Assert.Contains("Usage: openapi ", _output.ToString()); + } + + [Fact] + public async Task OpenApi_Add_ReuseItemGroup() + { + var project = CreateBasicProject(withOpenApi: true); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "file", project.NSwagJsonFile }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var secondRun = app.Execute(new[] { "add", "url", FakeOpenApiUrl }); + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, secondRun); + + var csproj = new FileInfo(project.Project.Path); + string content; + using (var csprojStream = csproj.OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + // Build project and make sure it compiles + using var buildProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "build"); + await buildProc.Exited; + Assert.True(buildProc.ExitCode == 0, $"Build failed: {buildProc.Output}"); + + + // Run project and make sure it doesn't crash + using var runProc = ProcessEx.Run(_outputHelper, _tempDir.Root, "dotnet", "run"); + Thread.Sleep(100); + Assert.False(runProc.HasExited, $"Run failed with: {runProc.Output}"); + } + + [Fact] + public async Task OpenApi_Add_FromJson() + { + var project = CreateBasicProject(withOpenApi: true); + var nswagJsonFile = project.NSwagJsonFile; + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "file", nswagJsonFile }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + // csproj contents + var csproj = new FileInfo(project.Project.Path); + using (var csprojStream = csproj.OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(jsonFile)); + using (var jsonStream = new FileInfo(jsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenAPI_Add_Url_NoContentDisposition() + { + var project = CreateBasicProject(withOpenApi: false); + var url = NoDispositionUrl; + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", url}); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "nodisposition.yaml"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(jsonFile)); + using (var jsonStream = new FileInfo(jsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenAPI_Add_Url_NoExtension_AssumesJson() + { + var project = CreateBasicProject(withOpenApi: false); + var url = NoExtensionUrl; + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", url }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "filename.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(jsonFile)); + using (var jsonStream = new FileInfo(jsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url_NoSegment() + { + var project = CreateBasicProject(withOpenApi: false); + var url = NoSegmentUrl; + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", url }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "contoso.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(jsonFile)); + using (var jsonStream = new FileInfo(jsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "filename.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(jsonFile)); + using (var jsonStream = new FileInfo(jsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url_SameName_UniqueFile() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var firstExpectedJsonName = "filename.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var firstJsonFile = Path.Combine(_tempDir.Root, firstExpectedJsonName); + Assert.True(File.Exists(firstJsonFile)); + using (var jsonStream = new FileInfo(firstJsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + + app = GetApplication(); + run = app.Execute(new[] { "add", "url", NoExtensionUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var secondExpectedJsonName = "filename1.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + Assert.Contains( + $@"", content); + } + + var secondJsonFile = Path.Combine(_tempDir.Root, secondExpectedJsonName); + Assert.True(File.Exists(secondJsonFile)); + using (var jsonStream = new FileInfo(secondJsonFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url_NSwagCSharp() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--code-generator", "NSwagCSharp" }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "filename.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var resultFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(resultFile)); + using (var jsonStream = new FileInfo(resultFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url_NSwagTypeScript() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--code-generator", "NSwagTypeScript" }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = "filename.json"; + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var resultFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(resultFile)); + using (var jsonStream = new FileInfo(resultFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_Url_OutputFile() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--output-file", Path.Combine("outputdir", "file.yaml") }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = Path.Combine("outputdir", "file.yaml"); + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var resultFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(resultFile)); + using (var jsonStream = new FileInfo(resultFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public async Task OpenApi_Add_URL_FileAlreadyExists_Fail() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var outputFile = Path.Combine("outputdir", "file.yaml"); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl, "--output-file", outputFile }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonName = Path.Combine("outputdir", "file.yaml"); + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + } + + var resultFile = Path.Combine(_tempDir.Root, expectedJsonName); + Assert.True(File.Exists(resultFile)); + using (var jsonStream = new FileInfo(resultFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + + // Second reference, same output + app = GetApplication(); + run = app.Execute(new[] { "add", "url", DifferentUrl, "--output-file", outputFile}); + Assert.Equal(1, run); + Assert.True(_error.ToString().Contains("Aborting to avoid conflicts."), $"Should have aborted to avoid conflicts"); + + // csproj contents + using (var csprojStream = new FileInfo(project.Project.Path).OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("", content); + Assert.DoesNotContain( + $@"", content); + } + + using (var jsonStream = new FileInfo(resultFile).OpenRead()) + using (var reader = new StreamReader(jsonStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Equal(Content, content); + } + } + + [Fact] + public void OpenApi_Add_URL_MultipleTimes_OnlyOneReference() + { + var project = CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + app = GetApplication(); + run = app.Execute(new[] { "add", "url", "--output-file", "openapi.yaml", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + // csproj contents + var csproj = new FileInfo(project.Project.Path); + using var csprojStream = csproj.OpenRead(); + using var reader = new StreamReader(csprojStream); + var content = reader.ReadToEnd(); + var escapedPkgRef = Regex.Escape("", content); + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRefreshTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRefreshTests.cs new file mode 100644 index 000000000000..d63d5015f6d1 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRefreshTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.OpenApi.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.OpenApi.Refresh.Tests +{ + public class OpenApiRefreshTests : OpenApiTestBase + { + public OpenApiRefreshTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task OpenApi_Refresh_Basic() + { + CreateBasicProject(withOpenApi: false); + + var app = GetApplication(); + var run = app.Execute(new[] { "add", "url", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var expectedJsonPath = Path.Combine(_tempDir.Root, "filename.json"); + var json = await File.ReadAllTextAsync(expectedJsonPath); + json += "trash"; + await File.WriteAllTextAsync(expectedJsonPath, json); + + var firstWriteTime = File.GetLastWriteTime(expectedJsonPath); + + Thread.Sleep(TimeSpan.FromSeconds(1)); + + app = GetApplication(); + run = app.Execute(new[] { "refresh", FakeOpenApiUrl }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + var secondWriteTime = File.GetLastWriteTime(expectedJsonPath); + Assert.True(firstWriteTime < secondWriteTime, $"File wasn't updated! {firstWriteTime} {secondWriteTime}"); + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRemoveTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRemoveTests.cs new file mode 100644 index 000000000000..5a110a7c6672 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiRemoveTests.cs @@ -0,0 +1,201 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.DotNet.OpenApi.Tests; +using Microsoft.Extensions.Tools.Internal; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.OpenApi.Remove.Tests +{ + public class OpenApiRemoveTests : OpenApiTestBase + { + public OpenApiRemoveTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task OpenApi_Remove_File() + { + var nswagJsonFile = "openapi.json"; + _tempDir + .WithCSharpProject("testproj") + .WithTargetFrameworks("netcoreapp3.0") + .Dir() + .WithContentFile(nswagJsonFile) + .WithContentFile("Startup.cs") + .Create(); + + var add = GetApplication(); + var run = add.Execute(new[] { "add", "file", nswagJsonFile }); + + Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}"); + Assert.Equal(0, run); + + // csproj contents + var csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj")); + using (var csprojStream = csproj.OpenRead()) + using (var reader = new StreamReader(csprojStream)) + { + var content = await reader.ReadToEndAsync(); + Assert.Contains("> DownloadMock() + { + var noExtension = new ContentDispositionHeaderValue("attachment"); + noExtension.Parameters.Add(new NameValueHeaderValue("filename", "filename")); + var extension = new ContentDispositionHeaderValue("attachment"); + extension.Parameters.Add(new NameValueHeaderValue("filename", "filename.json")); + var justAttachments = new ContentDispositionHeaderValue("attachment"); + + return new Dictionary> { + { FakeOpenApiUrl, Tuple.Create(Content, extension)}, + { DifferentUrl, Tuple.Create(DifferentUrlContent, null) }, + { PackageUrl, Tuple.Create(PackageUrlContent, null) }, + { NoDispositionUrl, Tuple.Create(Content, null) }, + { NoExtensionUrl, Tuple.Create(Content, noExtension) }, + { NoSegmentUrl, Tuple.Create(Content, justAttachments) } + }; + } + + public void Dispose() + { + _outputHelper.WriteLine(_output.ToString()); + _tempDir.Dispose(); + } + } + + public class TestHttpClientWrapper : IHttpClientWrapper + { + private readonly IDictionary> _results; + + public TestHttpClientWrapper(IDictionary> results) + { + _results = results; + } + + public void Dispose() + { + } + + public Task GetResponseAsync(string url) + { + var result = _results[url]; + byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1); + var stream = new MemoryStream(byteArray); + + return Task.FromResult(new TestHttpResponseMessageWrapper(stream, result.Item2)); + } + } + + public class TestHttpResponseMessageWrapper : IHttpResponseMessageWrapper + { + public Task Stream { get; } + + public HttpStatusCode StatusCode { get; } = HttpStatusCode.OK; + + public bool IsSuccessCode() + { + return true; + } + + private ContentDispositionHeaderValue _contentDisposition; + + public TestHttpResponseMessageWrapper( + MemoryStream stream, + ContentDispositionHeaderValue header) + { + Stream = Task.FromResult(stream); + _contentDisposition = header; + } + + public ContentDispositionHeaderValue ContentDisposition() + { + return _contentDisposition; + } + + public void Dispose() + { + } + } + + public class TemporaryNSwagProject + { + public TemporaryNSwagProject(TemporaryCSharpProject project, string jsonFile) + { + Project = project; + NSwagJsonFile = jsonFile; + } + + public TemporaryCSharpProject Project { get; set; } + public string NSwagJsonFile { get; set; } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/ProcessEx.cs b/src/Tools/Microsoft.dotnet-openapi/test/ProcessEx.cs new file mode 100644 index 000000000000..fddaac68a702 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/ProcessEx.cs @@ -0,0 +1,146 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Internal +{ + internal class ProcessEx : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly Process _process; + private readonly StringBuilder _stderrCapture = new StringBuilder(); + private readonly StringBuilder _stdoutCapture = new StringBuilder(); + private readonly object _pipeCaptureLock = new object(); + private BlockingCollection _stdoutLines = new BlockingCollection(); + private TaskCompletionSource _exited; + + private ProcessEx(ITestOutputHelper output, Process proc) + { + _output = output; + + _process = proc; + proc.EnableRaisingEvents = true; + proc.OutputDataReceived += OnOutputData; + proc.ErrorDataReceived += OnErrorData; + proc.Exited += OnProcessExited; + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + + _exited = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public Task Exited => _exited.Task; + + public bool HasExited => _process.HasExited; + + public string Output + { + get + { + lock (_pipeCaptureLock) + { + return _stdoutCapture.ToString(); + } + } + } + + public int ExitCode => _process.ExitCode; + + public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary envVars = null) + { + var startInfo = new ProcessStartInfo(command, args) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDirectory + }; + + if (envVars != null) + { + foreach (var envVar in envVars) + { + startInfo.EnvironmentVariables[envVar.Key] = envVar.Value; + } + } + + output.WriteLine($"==> {startInfo.FileName} {startInfo.Arguments} [{startInfo.WorkingDirectory}]"); + var proc = Process.Start(startInfo); + + return new ProcessEx(output, proc); + } + + private void OnErrorData(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + return; + } + + lock (_pipeCaptureLock) + { + _stderrCapture.AppendLine(e.Data); + } + + _output.WriteLine("[ERROR] " + e.Data); + } + + private void OnOutputData(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + return; + } + + lock (_pipeCaptureLock) + { + _stdoutCapture.AppendLine(e.Data); + } + + _output.WriteLine(e.Data); + + if (_stdoutLines != null) + { + _stdoutLines.Add(e.Data); + } + } + + private void OnProcessExited(object sender, EventArgs e) + { + _process.WaitForExit(); + _stdoutLines.CompleteAdding(); + _stdoutLines = null; + _exited.TrySetResult(_process.ExitCode); + } + + public void Dispose() + { + if (_process != null && !_process.HasExited) + { + _process.KillTree(); + } + + _process.CancelOutputRead(); + _process.CancelErrorRead(); + + _process.ErrorDataReceived -= OnErrorData; + _process.OutputDataReceived -= OnOutputData; + _process.Exited -= OnProcessExited; + _process.Dispose(); + + if(_stdoutLines != null) + { + _stdoutLines.Dispose(); + } + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/Properties/AssemblyInfo.cs b/src/Tools/Microsoft.dotnet-openapi/test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..76cbce868d3a --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/src/Tools/Microsoft.dotnet-openapi/test/TestContent/Startup.cs.txt b/src/Tools/Microsoft.dotnet-openapi/test/TestContent/Startup.cs.txt new file mode 100644 index 000000000000..edea5132c820 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/TestContent/Startup.cs.txt @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; + +namespace SimpleWebSite +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // Example 1 + services + .AddMvcCore() + .AddAuthorization() + .AddFormatterMappings(m => m.SetMediaTypeMappingForFormat("js", new MediaTypeHeaderValue("application/json"))) + .SetCompatibilityVersion(CompatibilityVersion.Latest); + } + + public void Configure(IApplicationBuilder app) + { + app.UseMvcWithDefaultRoute(); + } + + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseKestrel() + .UseIISIntegration(); + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/TestContent/openapi.json.txt b/src/Tools/Microsoft.dotnet-openapi/test/TestContent/openapi.json.txt new file mode 100644 index 000000000000..dff10b981749 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/TestContent/openapi.json.txt @@ -0,0 +1,514 @@ +{ + "x-generator": "NSwag v11.17.15.0 (NJsonSchema v9.10.53.0 (Newtonsoft.Json v10.0.0.0))", + "openapi": "2.0", + "info": { + "title": "My Title", + "version": "1.0.0" + }, + "host": "localhost:44370", + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "application/json-patch+json", + "text/json", + "application/*+json", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "Pet" + ], + "operationId": "Pet_AddPet", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "pet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + }, + "x-nullable": true + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + } + } + }, + "put": { + "tags": [ + "Pet" + ], + "operationId": "Pet_EditPet", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "name": "pet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + }, + "x-nullable": true + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + } + } + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "Pet" + ], + "operationId": "Pet_FindByStatus", + "consumes": [ + "application/json-patch+json", + "application/json", + "text/json", + "application/*+json" + ], + "parameters": [ + { + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "x-nullable": true + } + ], + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + } + } + } + }, + "/pet/findByCategory": { + "get": { + "tags": [ + "Pet" + ], + "operationId": "Pet_FindByCategory", + "parameters": [ + { + "type": "string", + "name": "category", + "in": "query", + "required": true, + "x-nullable": true + } + ], + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + } + } + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "Pet" + ], + "operationId": "Pet_FindById", + "parameters": [ + { + "type": "integer", + "name": "petId", + "in": "path", + "required": true, + "format": "int32", + "x-nullable": false + } + ], + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + }, + "404": { + "description": "" + } + } + }, + "post": { + "tags": [ + "Pet" + ], + "operationId": "Pet_EditPet2", + "parameters": [ + { + "type": "integer", + "name": "petId", + "in": "path", + "required": true, + "format": "int32", + "x-nullable": false + }, + { + "type": "integer", + "name": "Id", + "in": "formData", + "required": true, + "format": "int32", + "x-nullable": false + }, + { + "type": "integer", + "name": "Age", + "in": "formData", + "required": true, + "format": "int32", + "x-nullable": false + }, + { + "type": "integer", + "name": "Category.Id", + "in": "formData", + "required": true, + "format": "int32", + "x-nullable": false + }, + { + "type": "string", + "name": "Category.Name", + "in": "formData", + "required": true, + "x-nullable": true + }, + { + "type": "boolean", + "name": "HasVaccinations", + "in": "formData", + "required": true, + "x-nullable": false + }, + { + "type": "string", + "name": "Name", + "in": "formData", + "required": true, + "x-nullable": true + }, + { + "type": "array", + "name": "Images", + "in": "formData", + "required": true, + "collectionFormat": "multi", + "x-nullable": true, + "items": { + "$ref": "#/definitions/Image" + } + }, + { + "type": "array", + "name": "Tags", + "in": "formData", + "required": true, + "collectionFormat": "multi", + "x-nullable": true, + "items": { + "$ref": "#/definitions/Tag" + } + }, + { + "type": "string", + "name": "Status", + "in": "formData", + "required": true, + "x-nullable": true + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + }, + "404": { + "description": "" + } + } + }, + "delete": { + "tags": [ + "Pet" + ], + "operationId": "Pet_DeletePet", + "parameters": [ + { + "type": "integer", + "name": "petId", + "in": "path", + "required": true, + "format": "int32", + "x-nullable": false + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + }, + "404": { + "description": "" + } + } + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "Pet" + ], + "operationId": "Pet_UploadImage", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "type": "integer", + "name": "petId", + "in": "path", + "required": true, + "format": "int32", + "x-nullable": false + }, + { + "type": "file", + "name": "file", + "in": "formData", + "required": true, + "x-nullable": true + } + ], + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + }, + "400": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/SerializableError" + } + }, + "404": { + "description": "" + } + } + } + } + }, + "definitions": { + "Pet": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "age", + "hasVaccinations", + "name", + "status" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "age": { + "type": "integer", + "format": "int32", + "maximum": 150.0, + "minimum": 0.0 + }, + "category": { + "$ref": "#/definitions/Category" + }, + "hasVaccinations": { + "type": "boolean" + }, + "name": { + "type": "string", + "maxLength": 50, + "minLength": 2 + }, + "images": { + "type": "array", + "items": { + "$ref": "#/definitions/Image" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string" + } + } + }, + "Category": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "Image": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "url": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "SerializableError": { + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "type": "object", + "additionalProperties": {} + } + ] + }, + "ApiResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "code" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/test/dotnet-microsoft.openapi.Tests.csproj b/src/Tools/Microsoft.dotnet-openapi/test/dotnet-microsoft.openapi.Tests.csproj new file mode 100644 index 000000000000..75e36320f9ff --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/dotnet-microsoft.openapi.Tests.csproj @@ -0,0 +1,51 @@ + + + netcoreapp3.0 + Microsoft.DotNet.Open.Api.Tools.Tests + $(DefaultItemExcludes);TestProjects\**\* + DotNetAddOpenAPIReferenceToolsTests + + + + ..\src\Microsoft.dotnet-openapi.csproj + + + + + + + + + + + + + + + + + <_Parameter1>TestSettings:RestoreSources + <_Parameter2>$(RestoreSources) + + + <_Parameter1>TestSettings:RuntimeFrameworkVersion + <_Parameter2>$(RuntimeFrameworkVersion) + + + <_Parameter1>RepoRoot + <_Parameter2>$(RepoRoot) + + + + + + + + + + + + + + + diff --git a/src/Tools/Microsoft.dotnet-openapi/test/xunit.runner.json b/src/Tools/Microsoft.dotnet-openapi/test/xunit.runner.json new file mode 100644 index 000000000000..d25ea9035a29 --- /dev/null +++ b/src/Tools/Microsoft.dotnet-openapi/test/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "longRunningTestSeconds": 30, + "diagnosticMessages": true, + "maxParallelThreads": -1 +} diff --git a/src/Tools/README.md b/src/Tools/README.md index 3800c623f968..23418483037c 100644 --- a/src/Tools/README.md +++ b/src/Tools/README.md @@ -1,18 +1,24 @@ # DotNetTools -## Projects +## Bundled tools -The folder contains command-line tools for ASP.NET Core that are bundled* in the .NET Core CLI. Follow the links below for more details on each tool. +The folder contains command-line tools for ASP.NET Core. The following tools are bundled* in the .NET Core CLI. Follow the links below for more details on each tool. - - [dotnet-watch](dotnet-watch/README.md) - - [dotnet-user-secrets](dotnet-user-secrets/README.md) - - [dotnet-sql-cache](dotnet-sql-cache/README.md) - - [dotnet-dev-certs](dotnet-dev-certs/README.md) +- [dotnet-watch](dotnet-watch/README.md) +- [dotnet-user-secrets](dotnet-user-secrets/README.md) +- [dotnet-sql-cache](dotnet-sql-cache/README.md) +- [dotnet-dev-certs](dotnet-dev-certs/README.md) *\*This applies to .NET Core CLI 2.1.300-preview2 and up. For earlier versions of the CLI, these tools must be installed separately.* *For 2.0 CLI and earlier, see for details.* +## Non-bundled tools + +The following tools are produced by us but not bundled in the .NET Core CLI. They must be aquired independently. + +- [Microsoft.dotnet-openapi](Microsoft.dotnet-openapi/README.md) + This folder also contains the infrastructure for our partners' service reference features: - [Extensions.ApiDescription.Client](Extensions.ApiDescription.Client/README.md) MSBuild glue for OpenAPI code generation. @@ -29,10 +35,11 @@ dotnet watch dotnet user-secrets dotnet sql-cache dotnet dev-certs +dotnet openapi ``` Add `--help` to see more details. For example, -``` +```sh dotnet watch --help ``` diff --git a/src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs b/src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs index 6206c861c03f..f0c10cbfc5eb 100644 --- a/src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs +++ b/src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Reflection; +using System.Threading.Tasks; namespace Microsoft.Extensions.CommandLineUtils { diff --git a/src/Tools/Shared/CommandLine/Ensure.cs b/src/Tools/Shared/CommandLine/Ensure.cs index 5cb8ff7ec7c8..df94e8d01c09 100644 --- a/src/Tools/Shared/CommandLine/Ensure.cs +++ b/src/Tools/Shared/CommandLine/Ensure.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; diff --git a/src/Tools/dotnet-watch/test/Utilities/TemporaryCSharpProject.cs b/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs similarity index 91% rename from src/Tools/dotnet-watch/test/Utilities/TemporaryCSharpProject.cs rename to src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs index 7156cf85cf7f..aeb305c7379f 100644 --- a/src/Tools/dotnet-watch/test/Utilities/TemporaryCSharpProject.cs +++ b/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -6,12 +6,12 @@ using System.Diagnostics; using System.Text; -namespace Microsoft.DotNet.Watcher.Tools.Tests +namespace Microsoft.Extensions.Tools.Internal { public class TemporaryCSharpProject { private const string Template = - @" + @" {0} Exe @@ -23,19 +23,22 @@ public class TemporaryCSharpProject private readonly string _filename; private readonly TemporaryDirectory _directory; - private List _items = new List(); - private List _properties = new List(); + private readonly List _items = new List(); + private readonly List _properties = new List(); - public TemporaryCSharpProject(string name, TemporaryDirectory directory) + public TemporaryCSharpProject(string name, TemporaryDirectory directory, string sdk) { Name = name; _filename = name + ".csproj"; _directory = directory; + Sdk = sdk; } public string Name { get; } public string Path => System.IO.Path.Combine(_directory.Root, _filename); + public string Sdk { get; } + public TemporaryCSharpProject WithTargetFrameworks(params string[] tfms) { Debug.Assert(tfms.Length > 0); @@ -95,7 +98,7 @@ public TemporaryCSharpProject WithProjectReference(TemporaryCSharpProject refere public void Create() { - _directory.CreateFile(_filename, string.Format(Template, string.Join("\r\n", _properties), string.Join("\r\n", _items))); + _directory.CreateFile(_filename, string.Format(Template, string.Join("\r\n", _properties), string.Join("\r\n", _items), Sdk)); } public class ItemSpec diff --git a/src/Tools/dotnet-watch/test/Utilities/TemporaryDirectory.cs b/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs similarity index 78% rename from src/Tools/dotnet-watch/test/Utilities/TemporaryDirectory.cs rename to src/Tools/Shared/TestHelpers/TemporaryDirectory.cs index 692899817e8e..7cdef082f2e9 100644 --- a/src/Tools/dotnet-watch/test/Utilities/TemporaryDirectory.cs +++ b/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.IO; -namespace Microsoft.DotNet.Watcher.Tools.Tests +namespace Microsoft.Extensions.Tools.Internal { public class TemporaryDirectory : IDisposable { @@ -16,7 +16,7 @@ public class TemporaryDirectory : IDisposable public TemporaryDirectory() { - Root = Path.Combine(Path.GetTempPath(), "dotnet-watch-tests", Guid.NewGuid().ToString("N")); + Root = Path.Combine(Path.GetTempPath(), "dotnet-tool-tests", Guid.NewGuid().ToString("N")); } private TemporaryDirectory(string path, TemporaryDirectory parent) @@ -34,16 +34,16 @@ public TemporaryDirectory SubDir(string name) public string Root { get; } - public TemporaryCSharpProject WithCSharpProject(string name) + public TemporaryCSharpProject WithCSharpProject(string name, string sdk = "Microsoft.NET.Sdk") { - var project = new TemporaryCSharpProject(name, this); + var project = new TemporaryCSharpProject(name, this, sdk); _projects.Add(project); return project; } - public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project) + public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project, string sdk = "Microsoft.NET.Sdk") { - project = WithCSharpProject(name); + project = WithCSharpProject(name, sdk); return project; } @@ -53,6 +53,16 @@ public TemporaryDirectory WithFile(string name, string contents = "") return this; } + public TemporaryDirectory WithContentFile(string name) + { + using (var stream = File.OpenRead(Path.Combine("TestContent", $"{name}.txt"))) + using (var streamReader = new StreamReader(stream)) + { + _files[name] = streamReader.ReadToEnd(); + } + return this; + } + public TemporaryDirectory Up() { if (_parent == null) diff --git a/src/Tools/Tools.sln b/src/Tools/Tools.sln index 4af0386aeb2a..1de511f5cc1e 100644 --- a/src/Tools/Tools.sln +++ b/src/Tools/Tools.sln @@ -7,13 +7,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch", "dotnet-watc EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-watch.Tests", "dotnet-watch\test\dotnet-watch.Tests.csproj", "{63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-certs", "dotnet-dev-certs\src\dotnet-dev-certs.csproj", "{0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E01EE27B-6CF9-4707-9849-5BA2ABA825F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{7BBDBDA2-299F-4C36-8338-23C525901DE0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2C485EAF-E4DE-4D14-8AE1-330641E17D44}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests", "FirstRunCertGenerator\test\Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj", "{1EC6FA27-40A5-433F-8CA1-636E7ED8863E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dev-certs", "dotnet-dev-certs\src\dotnet-dev-certs.csproj", "{98550159-E04E-44EB-A969-E5BF12444B94}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sql-cache", "dotnet-sql-cache\src\dotnet-sql-cache.csproj", "{15FB0E39-1A28-4325-AD3C-76352516C80D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sql-cache", "dotnet-sql-cache\src\dotnet-sql-cache.csproj", "{216AF7F1-5B05-477E-B8D3-86F6059F268A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-secrets", "dotnet-user-secrets\src\dotnet-user-secrets.csproj", "{5FE62357-2915-4890-813A-D82656BDC4DD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-secrets.Tests", "dotnet-user-secrets\test\dotnet-user-secrets.Tests.csproj", "{25F8DCC4-4571-42F7-BA0F-5C2D5A802297}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.dotnet-openapi", "Microsoft.dotnet-openapi\src\Microsoft.dotnet-openapi.csproj", "{C806041C-30F2-4B27-918A-5FF3576B833B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-microsoft.openapi.Tests", "Microsoft.dotnet-openapi\test\dotnet-microsoft.openapi.Tests.csproj", "{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.Client", "Extensions.ApiDescription.Client", "{78610083-1FCE-47F5-AB4D-AF0E1313B351}" EndProject @@ -77,6 +85,30 @@ Global {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.Build.0 = Release|Any CPU + {98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98550159-E04E-44EB-A969-E5BF12444B94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98550159-E04E-44EB-A969-E5BF12444B94}.Release|Any CPU.Build.0 = Release|Any CPU + {216AF7F1-5B05-477E-B8D3-86F6059F268A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {216AF7F1-5B05-477E-B8D3-86F6059F268A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {216AF7F1-5B05-477E-B8D3-86F6059F268A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {216AF7F1-5B05-477E-B8D3-86F6059F268A}.Release|Any CPU.Build.0 = Release|Any CPU + {5FE62357-2915-4890-813A-D82656BDC4DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FE62357-2915-4890-813A-D82656BDC4DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FE62357-2915-4890-813A-D82656BDC4DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FE62357-2915-4890-813A-D82656BDC4DD}.Release|Any CPU.Build.0 = Release|Any CPU + {25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25F8DCC4-4571-42F7-BA0F-5C2D5A802297}.Release|Any CPU.Build.0 = Release|Any CPU + {C806041C-30F2-4B27-918A-5FF3576B833B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C806041C-30F2-4B27-918A-5FF3576B833B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C806041C-30F2-4B27-918A-5FF3576B833B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C806041C-30F2-4B27-918A-5FF3576B833B}.Release|Any CPU.Build.0 = Release|Any CPU + {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -88,6 +120,14 @@ Global {160A445F-7E1F-430D-9403-41F7F6F4A16E} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} {233119FC-E4C1-421C-89AE-1A445C5A947F} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} {EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} + {E16F10C8-5FC3-420B-AB33-D6E5CBE89B75} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} + {98550159-E04E-44EB-A969-E5BF12444B94} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {216AF7F1-5B05-477E-B8D3-86F6059F268A} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {5FE62357-2915-4890-813A-D82656BDC4DD} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {25F8DCC4-4571-42F7-BA0F-5C2D5A802297} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} + {C806041C-30F2-4B27-918A-5FF3576B833B} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EC668D8E-97B9-4758-9E5C-2E5DD6B9137B} diff --git a/src/Tools/build.cmd b/src/Tools/build.cmd new file mode 100644 index 000000000000..033fe6f61468 --- /dev/null +++ b/src/Tools/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %* diff --git a/src/Tools/build.sh b/src/Tools/build.sh new file mode 100755 index 000000000000..7046bb98a0fc --- /dev/null +++ b/src/Tools/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +repo_root="$DIR/../.." +"$repo_root/build.sh" --projects "$DIR/**/*.*proj" "$@" diff --git a/src/Tools/dotnet-dev-certs/src/Program.cs b/src/Tools/dotnet-dev-certs/src/Program.cs index 3854c4c59c42..6ecb1912cdf3 100644 --- a/src/Tools/dotnet-dev-certs/src/Program.cs +++ b/src/Tools/dotnet-dev-certs/src/Program.cs @@ -104,10 +104,10 @@ public static int Main(string[] args) app.HelpOption("-h|--help"); app.OnExecute(() => - { - app.ShowHelp(); - return Success; - }); + { + app.ShowHelp(); + return Success; + }); return app.Execute(args); } diff --git a/src/Tools/dotnet-watch/src/PrefixConsoleReporter.cs b/src/Tools/dotnet-watch/src/PrefixConsoleReporter.cs index b2453276efdc..cf13cd0733d4 100644 --- a/src/Tools/dotnet-watch/src/PrefixConsoleReporter.cs +++ b/src/Tools/dotnet-watch/src/PrefixConsoleReporter.cs @@ -11,18 +11,20 @@ public class PrefixConsoleReporter : ConsoleReporter { private object _lock = new object(); - public PrefixConsoleReporter(IConsole console, bool verbose, bool quiet) + private readonly string _prefix; + + public PrefixConsoleReporter(string prefix, IConsole console, bool verbose, bool quiet) : base(console, verbose, quiet) - { } + { + _prefix = prefix; + } protected override void WriteLine(TextWriter writer, string message, ConsoleColor? color) { - const string prefix = "watch : "; - lock (_lock) { Console.ForegroundColor = ConsoleColor.DarkGray; - writer.Write(prefix); + writer.Write(_prefix); Console.ResetColor(); base.WriteLine(writer, message, color); diff --git a/src/Tools/dotnet-watch/src/Program.cs b/src/Tools/dotnet-watch/src/Program.cs index 25317fb6b2c1..f27ffd878fa4 100644 --- a/src/Tools/dotnet-watch/src/Program.cs +++ b/src/Tools/dotnet-watch/src/Program.cs @@ -15,17 +15,17 @@ namespace Microsoft.DotNet.Watcher public class Program : IDisposable { private readonly IConsole _console; - private readonly string _workingDir; + private readonly string _workingDirectory; private readonly CancellationTokenSource _cts; private IReporter _reporter; - public Program(IConsole console, string workingDir) + public Program(IConsole console, string workingDirectory) { Ensure.NotNull(console, nameof(console)); - Ensure.NotNullOrEmpty(workingDir, nameof(workingDir)); + Ensure.NotNullOrEmpty(workingDirectory, nameof(workingDirectory)); _console = console; - _workingDir = workingDir; + _workingDirectory = workingDirectory; _cts = new CancellationTokenSource(); _console.CancelKeyPress += OnCancelKeyPress; _reporter = CreateReporter(verbose: true, quiet: false, console: _console); @@ -134,7 +134,7 @@ private async Task MainInternalAsync( string projectFile; try { - projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project); + projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project); } catch (FileNotFoundException ex) { @@ -177,7 +177,7 @@ private async Task ListFilesAsync( string projectFile; try { - projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project); + projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project); } catch (FileNotFoundException ex) { @@ -205,7 +205,7 @@ private async Task ListFilesAsync( } private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console) - => new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet); + => new PrefixConsoleReporter("watch : ", console, verbose || CliContext.IsGlobalVerbose(), quiet); public void Dispose() { diff --git a/src/Tools/dotnet-watch/src/dotnet-watch.csproj b/src/Tools/dotnet-watch/src/dotnet-watch.csproj index b289e142877f..e50c349324cd 100644 --- a/src/Tools/dotnet-watch/src/dotnet-watch.csproj +++ b/src/Tools/dotnet-watch/src/dotnet-watch.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -12,7 +12,7 @@ - + diff --git a/src/Tools/dotnet-watch/test/AssertEx.cs b/src/Tools/dotnet-watch/test/AssertEx.cs index 4d897058fd3d..d55406d8e9d4 100644 --- a/src/Tools/dotnet-watch/test/AssertEx.cs +++ b/src/Tools/dotnet-watch/test/AssertEx.cs @@ -1,11 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Xunit; using Xunit.Sdk; namespace Microsoft.DotNet.Watcher.Tools.Tests diff --git a/src/Tools/dotnet-watch/test/MsBuildFileSetFactoryTest.cs b/src/Tools/dotnet-watch/test/MsBuildFileSetFactoryTest.cs index c34111b23984..0d69f49d2ec9 100644 --- a/src/Tools/dotnet-watch/test/MsBuildFileSetFactoryTest.cs +++ b/src/Tools/dotnet-watch/test/MsBuildFileSetFactoryTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -113,17 +113,20 @@ public async Task SingleTfm() public async Task MultiTfm() { _tempDir - .SubDir("src") - .SubDir("Project1") - .WithCSharpProject("Project1", out var target) - .WithTargetFrameworks("netcoreapp1.0", "net451") - .WithProperty("EnableDefaultCompileItems", "false") - .WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'") - .WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'") - .Dir() - .WithFile("Class1.netcore.cs") - .WithFile("Class1.desktop.cs") - .WithFile("Class1.notincluded.cs"); + .SubDir("src") + .SubDir("Project1") + .WithCSharpProject("Project1", out var target) + .WithTargetFrameworks("netcoreapp1.0", "net451") + .WithProperty("EnableDefaultCompileItems", "false") + .WithItem("Compile", "Class1.netcore.cs", "'$(TargetFramework)'=='netcoreapp1.0'") + .WithItem("Compile", "Class1.desktop.cs", "'$(TargetFramework)'=='net451'") + .Dir() + .WithFile("Class1.netcore.cs") + .WithFile("Class1.desktop.cs") + .WithFile("Class1.notincluded.cs") + .Up() + .Up() + .Create(); var fileset = await GetFileSet(target); @@ -155,7 +158,10 @@ public async Task ProjectReferences_OneLevel() .WithTargetFrameworks("netcoreapp1.0", "net451") .WithProjectReference(proj2) .Dir() - .WithFile("Class1.cs"); + .WithFile("Class1.cs") + .Up() + .Up() + .Create(); var fileset = await GetFileSet(target); diff --git a/src/Tools/dotnet-watch/test/Utilities/TestProjectGraph.cs b/src/Tools/dotnet-watch/test/Utilities/TestProjectGraph.cs index a8e615d3c9b6..7e44ea643e0b 100644 --- a/src/Tools/dotnet-watch/test/Utilities/TestProjectGraph.cs +++ b/src/Tools/dotnet-watch/test/Utilities/TestProjectGraph.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools.Tests { @@ -10,7 +11,7 @@ public class TestProjectGraph { private readonly TemporaryDirectory _directory; private Action _onCreate; - private Dictionary _projects = new Dictionary(); + private readonly Dictionary _projects = new Dictionary(); public TestProjectGraph(TemporaryDirectory directory) { _directory = directory; @@ -28,8 +29,7 @@ public TemporaryCSharpProject Find(string projectName) public TemporaryCSharpProject GetOrCreate(string projectName) { - TemporaryCSharpProject sourceProj; - if (!_projects.TryGetValue(projectName, out sourceProj)) + if (!_projects.TryGetValue(projectName, out TemporaryCSharpProject sourceProj)) { sourceProj = _directory.SubDir(projectName).WithCSharpProject(projectName); _onCreate?.Invoke(sourceProj); @@ -38,4 +38,4 @@ public TemporaryCSharpProject GetOrCreate(string projectName) return sourceProj; } } -} \ No newline at end of file +} diff --git a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj index 913332b68d76..b51c978aa7ed 100644 --- a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj +++ b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0