diff --git a/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets b/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets index af590565c61990cbf500a323c1901796874370a7..f3640745069e06da42733bb4ae5c9384aed495f7 100644 --- a/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets +++ b/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets @@ -84,6 +84,7 @@ <Target Name="_InnerGenerateOpenApiCode" DependsOnTargets="_GetCurrentOpenApiReference;$(GeneratorTarget)" /> + <!-- Note target will **always** execute when generator uses the OutputPath as a directory. --> <Target Name="_GenerateOpenApiCode" Condition="$(OpenApiGenerateCodeAtDesignTime) OR ('$(DesignTimeBuild)' != 'true' AND '$(BuildingProject)' == 'true')" Inputs="@(OpenApiReference)" @@ -100,44 +101,57 @@ <ItemGroup> <_Files Remove="@(_Files)" /> <_Files Include="@(OpenApiReference -> '%(OutputPath)')" - Condition="$([System.IO.File]::Exists('%(OpenApiReference.OutputPath)'))"> - <OutputPathExtension>$([System.IO.Path]::GetExtension('%(OpenApiReference.OutputPath)'))</OutputPathExtension> - </_Files> + Condition="$([System.IO.File]::Exists('%(OpenApiReference.OutputPath)'))" /> <_Directories Remove="@(_Directories)" /> <_Directories Include="@(OpenApiReference -> '%(OutputPath)')" Condition="Exists('%(OpenApiReference.OutputPath)') AND ! $([System.IO.File]::Exists('%(OpenApiReference.OutputPath)'))" /> + </ItemGroup> + + <ItemGroup> + <_Files SourceDocument="%(OriginalItemSpec)" RemoveMetadata="OriginalItemSpec" /> + <_Directories SourceDocument="%(OriginalItemSpec)" RemoveMetadata="OriginalItemSpec" /> + </ItemGroup> + <ItemGroup> <!-- If OutputPath is a file, add it directly to relevant items. --> <TypeScriptCompile Include="@(_Files)" Exclude="@(TypeScriptCompile)" - Condition="'%(_Files.OutputPathExtension)' == '.ts' OR '%(_Files.OutputPathExtension)' == '.tsx'"> - <SourceDocument>%(_Files.FullPath)</SourceDocument> - </TypeScriptCompile> + Condition="'%(_Files.Extension)' == '.ts' OR '%(_Files.Extension)' == '.tsx'" /> <Compile Include="@(_Files)" Exclude="@(Compile)" - Condition="'$(DefaultLanguageSourceExtension)' != '.ts' AND '%(_Files.OutputPathExtension)' == '$(DefaultLanguageSourceExtension)'"> - <SourceDocument>%(OpenApiReference.FullPath)</SourceDocument> - </Compile> + Condition="'$(DefaultLanguageSourceExtension)' != '.ts' AND '%(_Files.Extension)' == '$(DefaultLanguageSourceExtension)'" /> + </ItemGroup> - <!-- Otherwise, add all descendant files with the expected extension. --> - <TypeScriptCompile Include="@(_Directories -> '%(Identity)/**/*.ts;%(Identity)/**/*.tsx')" - Exclude="@(TypeScriptCompile)"> - <SourceDocument>%(_Directories.FullPath)</SourceDocument> - </TypeScriptCompile> + <!-- + Otherwise, add all descendant files with the expected extension. Glob into _Directories before updating + TypeScriptCompile or Compile items. See <https://github.com/dotnet/msbuild/issues/3274> and workaround in + <https://stackoverflow.com/questions/48868060/can-a-task-itemgroup-glob-files>. Unfortunately, this workaround + loses SourceDocument and other metadata. + --> + <PropertyGroup> + <_TypeScriptCompileItemsFromDirectories>@(_Directories -> '%(Identity)/**/*.ts;%(Identity)/**/*.tsx')</_TypeScriptCompileItemsFromDirectories> + <_CompileItemsFromDirectories>@(_Directories -> '%(Identity)/**/*$(DefaultLanguageSourceExtension)')</_CompileItemsFromDirectories> + </PropertyGroup> - <Compile Include="@(_Directories -> '%(Identity)/**/*.$(DefaultLanguageSourceExtension)')" + <ItemGroup> + <TypeScriptCompile Include="$(_TypeScriptCompileItemsFromDirectories)" Exclude="@(TypeScriptCompile)" /> + + <Compile Include="$(_CompileItemsFromDirectories)" Exclude="@(Compile)" - Condition="'$(DefaultLanguageSourceExtension)' != '.ts'"> - <SourceDocument>%(_Directories.FullPath)</SourceDocument> - </Compile> + Condition="'$(DefaultLanguageSourceExtension)' != '.ts'" /> - <FileWrites Exclude="@(FileWrites)" - Include="@(_Files);@(_Directories -> '%(Identity)/**/*.ts;%(Identity)/**/*.tsx;%(Identity)/**/*.$(DefaultLanguageSourceExtension)')" /> + <FileWrites Include="@(_Files);$(_TypeScriptCompileItemsFromDirectories);$(_CompileItemsFromDirectories)" + Exclude="@(FileWrites)" /> <_Files Remove="@(_Files)" /> <_Directories Remove="@(_Directories)" /> </ItemGroup> + + <PropertyGroup> + <_TypeScriptCompileItemsFromDirectories /> + <_CompileItemsFromDirectories /> + </PropertyGroup> </Target> <!-- Inform users of breaking changes in this file and Microsoft.Extensions.ApiDescription.Client.props. --> diff --git a/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj b/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj index db09c31bee67b519664aeb4f57697e406ccd75f5..1ccce0e63658bd39ce23be067f8de602fe8f05ec 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj +++ b/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj @@ -1,5 +1,4 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <DefaultItemExcludes>$(DefaultItemExcludes);TestProjects\**\*</DefaultItemExcludes> @@ -7,15 +6,20 @@ </PropertyGroup> <ItemGroup> - <Content Include="$(MSBuildThisFileDirectory)..\src\build\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="build" /> - <Content Include="$(MSBuildThisFileDirectory)..\src\buildMultiTargeting\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="buildMultiTargeting" /> - <Content Include="TestProjects\**\*" CopyToOutputDirectory="PreserveNewest" /> + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>TargetFramework</_Parameter1> + <_Parameter2>$(TargetFramework)</_Parameter2> + </AssemblyAttribute> - <Reference Include="Microsoft.Extensions.ApiDescription.Client" /> - </ItemGroup> + <Compile Include="$(SharedSourceRoot)Process\*.cs" LinkBase="ProcessHelpers" /> + <Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" + Link="ProcessHelpers\DotNetMuxer.cs" /> + <Compile Include="$(ToolSharedSourceRoot)TestHelpers\Temporary*.cs" LinkBase="TestHelpers" /> - <Target Name="CleanTestProjects" BeforeTargets="CoreCompile"> - <RemoveDir Directories="$(TargetDir)TestProjects" /> - </Target> + <Content Include="..\src\build\**\*" LinkBase="build" /> + <Content Include="..\src\buildMultiTargeting\**\*" LinkBase="buildMultiTargeting" /> + <Content Include="TestProjects\**\*" /> + <Reference Include="Microsoft.Extensions.ApiDescription.Client" /> + </ItemGroup> </Project> diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TargetTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/TargetTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..4de0605a76f5d0943fe6b0d7ffa8a6122107a80b --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TargetTest.cs @@ -0,0 +1,503 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Internal; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Tools.Internal; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.ApiDescription.Client; + +public class TargetTest : IDisposable +{ + private static Assembly _assembly = typeof(TargetTest).Assembly; + private static string _assemblyLocation = Path.GetDirectoryName(_assembly.Location); + private static string _targetFramework = _assembly.GetCustomAttributes<AssemblyMetadataAttribute>() + .Single(m => m.Key == "TargetFramework") + .Value; + + private ITestOutputHelper _output; + private TemporaryDirectory _temporaryDirectory; + + public TargetTest(ITestOutputHelper output) + { + _output = output; + _temporaryDirectory = new TemporaryDirectory(); + + var build = _temporaryDirectory.SubDir("build"); + var files = _temporaryDirectory.SubDir("files"); + var tasks = _temporaryDirectory.SubDir("tasks").SubDir("netstandard2.0"); + _temporaryDirectory.Create(); + + // Populate temporary build folder. + var directory = new DirectoryInfo(Path.Combine(_assemblyLocation, "build")); + foreach (var file in directory.GetFiles()) + { + file.CopyTo(Path.Combine(build.Root, file.Name), overwrite: true); + } + directory = new DirectoryInfo(Path.Combine(_assemblyLocation, "TestProjects", "build")); + foreach (var file in directory.GetFiles()) + { + file.CopyTo(Path.Combine(build.Root, file.Name), overwrite: true); + } + + // Populate temporary files folder. + directory = new DirectoryInfo(Path.Combine(_assemblyLocation, "TestProjects", "files")); + foreach (var file in directory.GetFiles()) + { + file.CopyTo(Path.Combine(files.Root, file.Name), overwrite: true); + } + + // Populate temporary tasks folder. + directory = new DirectoryInfo(_assemblyLocation); + foreach (var file in directory.GetFiles("Microsoft.Extensions.ApiDescription.Client.???")) + { + file.CopyTo(Path.Combine(tasks.Root, file.Name), overwrite: true); + } + } + + [Fact] + public async Task AddsExpectedItems() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains($"Compile: {Path.Combine("obj", "azureMonitorClient.cs")}", process.Output); + Assert.Contains($"FileWrites: {Path.Combine("obj", "azureMonitorClient.cs")}", process.Output); + Assert.DoesNotContain("TypeScriptCompile:", process.Output); + } + + [Fact] + public async Task AddsExpectedItems_WithCodeGenerator() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + CodeGenerator = "NSwagTypeScript", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.DoesNotContain(" Compile:", process.Output); + Assert.Contains($"FileWrites: {Path.Combine("obj", "azureMonitorClient.ts")}", process.Output); + Assert.Contains($"TypeScriptCompile: {Path.Combine("obj", "azureMonitorClient.ts")}", process.Output); + } + + [Fact] + public async Task AddsExpectedItems_WithMultipleFiles() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json;files/NSwag.json;files/swashbuckle.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains($"Compile: {Path.Combine("obj", "azureMonitorClient.cs")}", process.Output); + Assert.Contains($"Compile: {Path.Combine("obj", "NSwagClient.cs")}", process.Output); + Assert.Contains($"Compile: {Path.Combine("obj", "swashbuckleClient.cs")}", process.Output); + Assert.Contains($"FileWrites: {Path.Combine("obj", "azureMonitorClient.cs")}", process.Output); + Assert.Contains($"FileWrites: {Path.Combine("obj", "NSwagClient.cs")}", process.Output); + Assert.Contains($"FileWrites: {Path.Combine("obj", "swashbuckleClient.cs")}", process.Output); + Assert.DoesNotContain("TypeScriptCompile:", process.Output); + } + + [Fact] + public async Task AddsExpectedItems_WithMultipleFilesFromGenerator() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + CodeGenerator = "CustomCSharp", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains($"Compile: {Path.Combine("obj", "azureMonitorClient.cs", "Generated1.cs")}", process.Output); + Assert.Contains($"Compile: {Path.Combine("obj", "azureMonitorClient.cs", "Generated2.cs")}", process.Output); + Assert.Contains( + $"FileWrites: {Path.Combine("obj", "azureMonitorClient.cs", "Generated1.cs")}", + process.Output); + Assert.Contains( + $"FileWrites: {Path.Combine("obj", "azureMonitorClient.cs", "Generated2.cs")}", + process.Output); + Assert.DoesNotContain("TypeScriptCompile:", process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithOpenApiGenerateCodeOptions() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithProperty("OpenApiGenerateCodeOptions", "--an-option") + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '--an-option' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithOpenApiCodeDirectory() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithProperty("OpenApiCodeDirectory", "generated") + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("generated", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithClassName() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + ClassName = "AzureMonitor" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + + // Note ClassName does **not** override OutputPath. + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.AzureMonitor' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithCodeGenerator() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec { + Include = "files/azureMonitor.json", + CodeGenerator="NSwagTypeScript" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagTypeScript " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.ts")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithNamespace() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + Namespace = "SomeNamespace" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'SomeNamespace.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithOptions() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec { + Include = "files/azureMonitor.json", + Options="--an-option" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '--an-option' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithOutputPath() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + OutputPath = "Custom.cs" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + + // Note OutputPath also overrides ClassName. + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.Custom' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "Custom.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithMultipleFiles() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json;files/NSwag.json;files/swashbuckle.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "NSwag.json")} " + + "Class: 'test.NSwagClient' FirstForGenerator: 'false' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "NSwagClient.cs")}'", + process.Output); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "swashbuckle.json")} " + + "Class: 'test.swashbuckleClient' FirstForGenerator: 'false' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "swashbuckleClient.cs")}'", + process.Output); + } + + [Fact] + public async Task ExecutesGeneratorTarget_WithMultipleGenerators() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }) + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + CodeGenerator = "NSwagTypeScript" + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.Contains( + "GenerateNSwagCSharp " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.cs")}'", + process.Output); + Assert.Contains( + "GenerateNSwagTypeScript " + + $"{Path.Combine(_temporaryDirectory.Root, "files", "azureMonitor.json")} " + + "Class: 'test.azureMonitorClient' FirstForGenerator: 'true' " + + $"Options: '' OutputPath: '{Path.Combine("obj", "azureMonitorClient.ts")}'", + process.Output); + } + + [Fact] + public async Task SkipsGeneratorTarget_InSubsequentBuilds() + { + // Arrange + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithProperty("OpenApiGenerateCodeOnBuild", "false") + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + // Act 1 + using var firstProcess = await RunBuild(); + + // Assert 1 aka Guards + Assert.Equal(0, firstProcess.ExitCode); + Assert.Empty(firstProcess.Error); + + // Act 2 + using var secondProcess = await RunBuild(); + + // Assert 2 + Assert.Equal(0, secondProcess.ExitCode); + Assert.Empty(secondProcess.Error); + Assert.DoesNotContain("GenerateNSwagCSharp ", secondProcess.Output); + + // Act 3 + using var thirdProcess = await RunBuild(); + + // Assert 2 + Assert.Equal(0, thirdProcess.ExitCode); + Assert.Empty(thirdProcess.Error); + Assert.DoesNotContain("GenerateNSwagCSharp ", thirdProcess.Output); + } + + [Fact] + public async Task SkipsGeneratorTarget_WithOpenApiGenerateCodeOnBuild() + { + var project = new TemporaryOpenApiProject("test", _temporaryDirectory, "Microsoft.NET.Sdk") + .WithTargetFrameworks(_targetFramework) + .WithProperty("OpenApiGenerateCodeOnBuild", "false") + .WithItem(new TemporaryOpenApiProject.ItemSpec + { + Include = "files/azureMonitor.json", + }); + _temporaryDirectory.WithCSharpProject(project); + project.Create(); + + using var process = await RunBuild(); + + Assert.Equal(0, process.ExitCode); + Assert.Empty(process.Error); + Assert.DoesNotContain("GenerateNSwagCSharp ", process.Output); + } + + public void Dispose() + { + _temporaryDirectory.Dispose(); + } + + private async Task<ProcessEx> RunBuild() + { + var process = ProcessEx.Run( + _output, + _temporaryDirectory.Root, + DotNetMuxer.MuxerPathOrDefault(), + "build"); + await process.Exited; + + return process; + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TemporaryOpenApiProject.cs b/src/Tools/Extensions.ApiDescription.Client/test/TemporaryOpenApiProject.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e6739779928b6f8f343c919e454ee1a2f34498d --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TemporaryOpenApiProject.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.Extensions.Tools.Internal; + +public class TemporaryOpenApiProject : TemporaryCSharpProject +{ + public TemporaryOpenApiProject(string name, TemporaryDirectory directory, string sdk) + : base(name, directory, sdk) + { + } + + protected override string Template => +@"<Project Sdk=""{2}""> + <Import Project=""build/Microsoft.Extensions.ApiDescription.Client.props"" /> + + <PropertyGroup> + {0} + </PropertyGroup> + <ItemGroup> + {1} + </ItemGroup> + + <!-- Check _CreateCompileItemsForOpenApiReferences output items. --> + <Target Name=""_WriteWrites"" AfterTargets=""_CreateCompileItemsForOpenApiReferences""> + <Message Importance=""high"" + Text=""Compile: %(Compile.Identity)"" + Condition="" '@(Compile)' != '' "" /> + <Message Importance=""high"" Text=""FileWrites: %(FileWrites.Identity)"" /> + <Message Importance=""high"" + Text=""TypeScriptCompile: %(TypeScriptCompile.Identity)"" + Condition="" '@(TypeScriptCompile)' != '' "" /> + </Target> + + <Import Project=""build/Microsoft.Extensions.ApiDescription.Client.targets"" /> + <Import Project=""build/Fakes.targets"" /> +</Project>"; + + protected override void AddAdditionalAttributes(StringBuilder sb, TemporaryCSharpProject.ItemSpec item) + { + var openApiItem = item as ItemSpec; + if (openApiItem.ClassName != null) + { + sb.Append(" ClassName=\"").Append(openApiItem.ClassName).Append('"'); + } + + if (openApiItem.CodeGenerator != null) + { + sb.Append(" CodeGenerator=\"").Append(openApiItem.CodeGenerator).Append('"'); + } + + if (openApiItem.Namespace != null) + { + sb.Append(" Namespace=\"").Append(openApiItem.Namespace).Append('"'); + } + + if (openApiItem.Options != null) + { + sb.Append(" Options=\"").Append(openApiItem.Options).Append('"'); + } + + if (openApiItem.OutputPath != null) + { + sb.Append(" OutputPath=\"").Append(openApiItem.OutputPath).Append('"'); + } + } + + public new class ItemSpec : TemporaryCSharpProject.ItemSpec + { + public ItemSpec() : base() + { + Name = "OpenApiReference"; + } + + // Metadata specific to OpenApiReference items. + public string ClassName { get; set; } + + public string CodeGenerator { get; set; } + + public string Namespace { get; set; } + + public string Options { get; set; } + + public string OutputPath { get; set; } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj deleted file mode 100644 index 18c332c188eb0575c3678f6bbe25396b8f3ea707..0000000000000000000000000000000000000000 --- a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj +++ /dev/null @@ -1,38 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <Import Project="../../build/Microsoft.Extensions.ApiDescription.Client.props" - Condition="Exists('../../build/Microsoft.Extensions.ApiDescription.Client.props')" /> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> - <IsTestAssetProject>true</IsTestAssetProject> - </PropertyGroup> - - <ItemGroup> - <OpenApiReference Include="../files/azureMonitor.json" /> - <OpenApiReference Include="../files/NSwag.json" /> - <OpenApiReference Include="../files/swashbuckle.json" /> - </ItemGroup> - - <!-- Position tasks assembly where Microsoft.Extensions.ApiDescription.Client.props expects it. --> - <Target Name="CopyTargets" BeforeTargets="BeforeBuild"> - <ItemGroup> - <_Files Include="../../Microsoft.Extensions.ApiDescription.Client.*" - Exclude="../../Microsoft.Extensions.ApiDescription.Client.Tests.*" /> - </ItemGroup> - - <Copy - SourceFiles="@(_Files)" - DestinationFolder="../../tasks/netstandard2.0/" - SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" - OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" - Retries="$(CopyRetryCount)" - RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" - UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" - UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" /> - </Target> - - <Import Project="../../build/Microsoft.Extensions.ApiDescription.Client.targets" - Condition="Exists('../../build/Microsoft.Extensions.ApiDescription.Client.targets')" /> - <Import Project="../build/Fakes.targets" Condition="EXISTS('../build/Fakes.targets')" /> -</Project> diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs deleted file mode 100644 index 35f6c0593ed4618cebb4aea16e806b0ade30e8ef..0000000000000000000000000000000000000000 --- a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace ConsoleClient; - -class Program -{ - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } -} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets index 78597f2ac60d97aa2d2209ada98bc42cbf525f02..480fb5b573165d9a36d74c0c03c1cdfcbcadfd13 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets @@ -1,40 +1,52 @@ -<?xml version="1.0" encoding="utf-8" standalone="no"?> +<?xml version="1.0" encoding="utf-8" standalone="no"?> <Project> - <!-- NSwag's OpenApiReference support for C# (default target). --> - - <Target Name="GenerateNSwagCSharp"> + <Target Name="_GetMetadataProperties"> + <ItemGroup> + <_HeaderMetadata Include="Licensed to the .NET Foundation under one or more agreements." /> + <_HeaderMetadata Include="The .NET Foundation licenses this file to you under the MIT license." /> + + <_Metadata Include="__TargetName__" /> + <_Metadata Include="%(CurrentOpenApiReference.FullPath)" /> + <_Metadata Include="Class: '%(CurrentOpenApiReference.Namespace).%(CurrentOpenApiReference.ClassName)'" /> + <_Metadata Include="FirstForGenerator: '%(CurrentOpenApiReference.FirstForGenerator)'" /> + <_Metadata Include="Options: '%(CurrentOpenApiReference.Options)'" /> + <_Metadata Include="OutputPath: '%(CurrentOpenApiReference.OutputPath)'" /> + </ItemGroup> <PropertyGroup> - <_Metadata>'%(CurrentOpenApiReference.FullPath)'</_Metadata> - <_Metadata>$(_Metadata) Class: '%(CurrentOpenApiReference.Namespace).%(CurrentOpenApiReference.ClassName)'</_Metadata> - <_Metadata>$(_Metadata) FirstForGenerator: '%(CurrentOpenApiReference.FirstForGenerator)'</_Metadata> - <_Metadata>$(_Metadata) Options: '%(CurrentOpenApiReference.Options)'</_Metadata> - <_Metadata>$(_Metadata) OutputPath: '%(CurrentOpenApiReference.OutputPath)'</_Metadata> + <_Message>@(_Metadata, ' ')</_Message> + <_Lines>@(_HeaderMetadata -> '// %(Identity)', '%0A') + +</_Lines> </PropertyGroup> + </Target> + + <!-- NSwag's OpenApiReference support for C# (default target). --> - <Message Importance="high" Text="GenerateNSwagCSharp $(_Metadata)" /> + <Target Name="GenerateNSwagCSharp" DependsOnTargets="_GetMetadataProperties"> + <Message Importance="high" Text="$(_Message.Replace('__TargetName__', 'GenerateNSwagCSharp'))" /> + <WriteLinesToFile File="%(CurrentOpenApiReference.OutputPath)" + Lines="$(_Lines)" + Overwrite="true" /> </Target> <!-- NSwag's OpenApiReference support for TypeScript. --> - <Target Name="GenerateNSwagTypeScript"> - <PropertyGroup> - <_Metadata>'%(FullPath)' Class: '%(Namespace).%(ClassName)'</_Metadata> - <_Metadata>$(_Metadata) FirstForGenerator: '%(FirstForGenerator)' Options: '%(Options)'</_Metadata> - <_Metadata>$(_Metadata) OutputPath: '%(OutputPath)'</_Metadata> - </PropertyGroup> - - <Message Importance="high" Text="GenerateNSwagTypeScript $(_Metadata)" /> + <Target Name="GenerateNSwagTypeScript" DependsOnTargets="_GetMetadataProperties"> + <Message Importance="high" Text="$(_Message.Replace('__TargetName__', 'GenerateNSwagTypeScript'))" /> + <WriteLinesToFile File="%(CurrentOpenApiReference.OutputPath)" + Lines="$(_Lines)" + Overwrite="true" /> </Target> <!-- Custom OpenApiReference support for C#. --> - <Target Name="GenerateCustomCSharp"> - <PropertyGroup> - <_Metadata>'%(FullPath)' Class: '%(Namespace).%(ClassName)'</_Metadata> - <_Metadata>$(_Metadata) FirstForGenerator: '%(FirstForGenerator)' Options: '%(Options)'</_Metadata> - <_Metadata>$(_Metadata) OutputPath: '%(OutputPath)'</_Metadata> - </PropertyGroup> - - <Message Importance="high" Text="GenerateCustomCSharp $(_Metadata)" /> + <Target Name="GenerateCustomCSharp" DependsOnTargets="_GetMetadataProperties"> + <Message Importance="high" Text="$(_Message.Replace('__TargetName__', 'GenerateCustomCSharp'))" /> + <WriteLinesToFile File="%(CurrentOpenApiReference.OutputPath)/Generated1.cs" + Lines="$(_Lines)" + Overwrite="true" /> + <WriteLinesToFile File="%(CurrentOpenApiReference.OutputPath)/Generated2.cs" + Lines="$(_Lines)" + Overwrite="true" /> </Target> </Project> diff --git a/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs b/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs index ad2ce63b1f2b760ff72cd72272129f516472607e..09039b9a9cc54fa817377f953d5567dec6619920 100644 --- a/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs +++ b/src/Tools/Shared/TestHelpers/TemporaryCSharpProject.cs @@ -13,17 +13,6 @@ namespace Microsoft.Extensions.Tools.Internal; public class TemporaryCSharpProject { - private const string Template = -@"<Project Sdk=""{2}""> - <PropertyGroup> - {0} - <OutputType>Exe</OutputType> - </PropertyGroup> - <ItemGroup> - {1} - </ItemGroup> -</Project>"; - private readonly string _filename; private readonly TemporaryDirectory _directory; private readonly List<string> _items = new List<string>(); @@ -42,6 +31,17 @@ public class TemporaryCSharpProject public string Sdk { get; } + protected virtual string Template => +@"<Project Sdk=""{2}""> + <PropertyGroup> + {0} + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> + {1} + </ItemGroup> +</Project>"; + public TemporaryCSharpProject WithTargetFrameworks(params string[] tfms) { Debug.Assert(tfms.Length > 0); @@ -82,11 +82,16 @@ public class TemporaryCSharpProject if (item.Exclude != null) sb.Append(" Exclude=\"").Append(item.Exclude).Append('"'); if (item.Condition != null) sb.Append(" Exclude=\"").Append(item.Condition).Append('"'); if (!item.Watch) sb.Append(" Watch=\"false\" "); + AddAdditionalAttributes(sb, item); sb.Append(" />"); _items.Add(sb.ToString()); return this; } + protected virtual void AddAdditionalAttributes(StringBuilder sb, ItemSpec item) + { + } + public TemporaryCSharpProject WithProjectReference(TemporaryCSharpProject reference, bool watch = true) { if (ReferenceEquals(this, reference)) diff --git a/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs b/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs index d040908db8a6922230155ece0ddb174440c9e758..6869a362252ab55b798f524ada1deca33a46b29e 100644 --- a/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs +++ b/src/Tools/Shared/TestHelpers/TemporaryDirectory.cs @@ -18,7 +18,7 @@ public class TemporaryDirectory : IDisposable public TemporaryDirectory() { - Root = Path.Combine(Path.GetTempPath(), "dotnet-tool-tests", Guid.NewGuid().ToString("N")); + Root = Path.Combine(ResolveLinks(Path.GetTempPath()), "dotnet-tool-tests", Guid.NewGuid().ToString("N")); } private TemporaryDirectory(string path, TemporaryDirectory parent) @@ -39,13 +39,12 @@ public class TemporaryDirectory : IDisposable public TemporaryCSharpProject WithCSharpProject(string name, string sdk = "Microsoft.NET.Sdk") { var project = new TemporaryCSharpProject(name, this, sdk); - _projects.Add(project); - return project; + return WithCSharpProject(project); } - public TemporaryCSharpProject WithCSharpProject(string name, out TemporaryCSharpProject project, string sdk = "Microsoft.NET.Sdk") + public TemporaryCSharpProject WithCSharpProject(TemporaryCSharpProject project) { - project = WithCSharpProject(name, sdk); + _projects.Add(project); return project; } @@ -115,4 +114,34 @@ public class TemporaryDirectory : IDisposable Console.Error.WriteLine($"Test cleanup failed to delete '{Root}'"); } } + + private static string ResolveLinks(string path) + { + if (!Directory.Exists(path)) + { + return path; + } + + var info = new DirectoryInfo(path); + var segments = new List<string>(); + while (true) + { + if (info.LinkTarget is not null) + { + // Found a link, use it until we reach root. Portions of resolved path may also be links. + info = new DirectoryInfo(info.LinkTarget); + } + + segments.Add(info.Name); + if (info.Parent is null) + { + break; + } + + info = info.Parent; + } + + segments.Reverse(); + return Path.Combine(segments.ToArray()); + } }