diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index a136f3871f..d218c44531 100644 --- a/.azure/pipelines/jobs/default-build.yml +++ b/.azure/pipelines/jobs/default-build.yml @@ -55,6 +55,7 @@ parameters: artifacts: [] buildDirectory: '' buildScript: '' + installTar: true installNodeJs: true installJdk: true timeoutInMinutes: 180 @@ -151,6 +152,9 @@ jobs: Write-Host "##vso[task.setvariable variable=SeleniumProcessTrackingFolder]$(BuildDirectory)\artifacts\tmp\selenium\" ./eng/scripts/InstallGoogleChrome.ps1 displayName: Install Chrome + - ${{ if and(eq(parameters.installTar, 'true'), eq(parameters.agentOs, 'Windows')) }}: + - powershell: ./eng/scripts/InstallTar.ps1 + displayName: Find or install Tar - ${{ parameters.beforeBuild }} diff --git a/build.ps1 b/build.ps1 index 17020044ed..c515a84af5 100644 --- a/build.ps1 +++ b/build.ps1 @@ -307,6 +307,8 @@ if (-not $foundJdk -and $RunBuild -and ($All -or $BuildJava) -and -not $NoBuildJ # Initialize global variables need to be set before the import of Arcade is imported $restore = $RunRestore +# Though VS Code may indicate $nodeReuse, $warnAsError and $msbuildEngine are unused, tools.ps1 uses them. + # Disable node reuse - Workaround perpetual issues in node reuse and custom task assemblies $nodeReuse = $false $env:MSBUILDDISABLENODEREUSE=1 @@ -328,10 +330,10 @@ if ($CI) { } # tools.ps1 corrupts global state, so reset these values in case they carried over from a previous build -rm variable:global:_BuildTool -ea Ignore -rm variable:global:_DotNetInstallDir -ea Ignore -rm variable:global:_ToolsetBuildProj -ea Ignore -rm variable:global:_MSBuildExe -ea Ignore +Remove-Item variable:global:_BuildTool -ea Ignore +Remove-Item variable:global:_DotNetInstallDir -ea Ignore +Remove-Item variable:global:_ToolsetBuildProj -ea Ignore +Remove-Item variable:global:_MSBuildExe -ea Ignore # Import Arcade . "$PSScriptRoot/eng/common/tools.ps1" @@ -391,10 +393,10 @@ finally { } # tools.ps1 corrupts global state, so reset these values so they don't carry between invocations of build.ps1 - rm variable:global:_BuildTool -ea Ignore - rm variable:global:_DotNetInstallDir -ea Ignore - rm variable:global:_ToolsetBuildProj -ea Ignore - rm variable:global:_MSBuildExe -ea Ignore + Remove-Item variable:global:_BuildTool -ea Ignore + Remove-Item variable:global:_DotNetInstallDir -ea Ignore + Remove-Item variable:global:_ToolsetBuildProj -ea Ignore + Remove-Item variable:global:_MSBuildExe -ea Ignore if ($DumpProcesses -or $ci) { Stop-Job -Name DumpProcesses diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index 7740ec6140..5cc74a5f9d 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -27,6 +27,7 @@ Building ASP.NET Core on Windows requires: ```ps1 PS> ./eng/scripts/InstallJdk.ps1 ``` +* Chrome - Selenium-based tests require a version of Chrome to be installed. Download and install it from [https://www.google.com/chrome] ### macOS/Linux diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index f35501e1de..0b3cfa0c07 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,7 +2,7 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.6 + 2.2.7 @@ -77,12 +77,12 @@ - 2.2.6 + 2.2.7 - 2.2.6 + 2.2.7 @@ -303,10 +303,11 @@ - 2.2.0 + 2.2.7 + @@ -434,21 +435,21 @@ - 2.2.0 + 2.2.7 - - + + - + @@ -1197,12 +1198,12 @@ - 2.2.0 + 2.2.7 - + diff --git a/eng/Baseline.xml b/eng/Baseline.xml index 0946044016..b541b834a2 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,7 +4,7 @@ This file contains a list of all the packages and their versions which were rele build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch. --> - + @@ -12,8 +12,8 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch. - - + + @@ -39,7 +39,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch. - + @@ -53,7 +53,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch. - + @@ -122,7 +122,7 @@ build of ASP.NET Core 2.2.x. Update this list when preparing for a new patch. - + diff --git a/eng/PatchConfig.props b/eng/PatchConfig.props index 6cfba9b0b6..2c26521f8c 100644 --- a/eng/PatchConfig.props +++ b/eng/PatchConfig.props @@ -29,10 +29,6 @@ Later on, this will be checked using this condition: java:signalr; - - - - @aspnet/signalr; @@ -70,4 +66,8 @@ Later on, this will be checked using this condition: Microsoft.AspNetCore.SpaServices; + + + + diff --git a/eng/Versions.props b/eng/Versions.props index 4ffd700107..fff5d0df67 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -9,7 +9,7 @@ 5 0 0 - 1 + 2 diff --git a/eng/scripts/InstallTar.ps1 b/eng/scripts/InstallTar.ps1 new file mode 100644 index 0000000000..12159a8d0b --- /dev/null +++ b/eng/scripts/InstallTar.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + Finds or installs the Tar command on this system. +.DESCRIPTION + This script searches for Tar on this system. If not found, downloads and extracts Git to use its tar.exe. Prefers + global installation locations even if Git has been downloaded into this repo. +.PARAMETER GitVersion + The version of the Git to install. If not set, the default value is read from global.json. +.PARAMETER Force + Overwrite the existing installation if one exists in this repo and Tar isn't installed globally. +#> +param( + [string]$GitVersion, + [switch]$Force +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' # Workaround PowerShell/PowerShell#2138 + +Set-StrictMode -Version 1 + +# Find tar. If not found, install Git to get it. +$repoRoot = (Join-Path $PSScriptRoot "..\.." -Resolve) +$installDir = "$repoRoot\.tools\Git\win-x64" +$tarCommand = "$installDir\usr\bin\tar.exe" +$finalCommand = "$repoRoot\.tools\tar.exe" + +Write-Host "Windows version and other information..." +cmd.exe /c ver +systeminfo.exe +Write-Host "Processor Architecture: $env:PROCESSOR_ARCHITECTURE" + +Write-Host "Checking $env:SystemRoot\System32\tar.exe" +Get-ChildItem "$env:SystemRoot\System32\ta*.exe" +if (Test-Path "$env:SystemRoot\System32\tar.exe") { + Write-Host "Found $env:SystemRoot\System32\tar.exe" + $tarCommand = "$env:SystemRoot\System32\tar.exe" +} +elseif (Test-Path "$env:ProgramFiles\Git\usr\bin\tar.exe") { + $tarCommand = "$env:ProgramFiles\Git\usr\bin\tar.exe" +} +elseif (Test-Path "${env:ProgramFiles(x86)}\Git\usr\bin\tar.exe") { + $tarCommand = "${env:ProgramFiles(x86)}\Git\usr\bin\tar.exe" +} +elseif (Test-Path "$env:AGENT_HOMEDIRECTORY\externals\git\usr\bin\tar.exe") { + $tarCommand = "$env:AGENT_HOMEDIRECTORY\externals\git\usr\bin\tar.exe" +} +elseif ((Test-Path $tarCommand) -And (-Not $Force)) { + Write-Verbose "Repo-local Git installation and $tarCommand already exist, skipping Git install." +} +else { + if (-not $GitVersion) { + $globalJson = Get-Content "$repoRoot\global.json" | ConvertFrom-Json + $GitVersion = $globalJson.tools.Git + } + + $Uri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/git/Git-${GitVersion}-64-bit.zip" + + Import-Module -Name (Join-Path $PSScriptRoot "..\common\native\CommonLibrary.psm1" -Resolve) + $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri -InstallDirectory "$installDir\" -Force:$Force -Verbose + + if ($InstallStatus -Eq $False) { + Write-Error "Installation failed" + exit 1 + } +} + +New-Item "$repoRoot\.tools\" -ErrorAction SilentlyContinue -ItemType Directory +Copy-Item "$tarCommand" "$finalCommand" -Verbose +Write-Host "Tar now available at '$finalCommand'" + +if ($tarCommand -like '*\Git\*') { + $null >.\.tools\tar.fromGit +} diff --git a/eng/targets/ReferenceAssembly.targets b/eng/targets/ReferenceAssembly.targets index f516f1b07a..765cc16932 100644 --- a/eng/targets/ReferenceAssembly.targets +++ b/eng/targets/ReferenceAssembly.targets @@ -61,15 +61,21 @@ + <_GenApiFile>$([MSBuild]::NormalizePath('$(ArtifactsDir)', 'log', 'GenAPI.rsp')) <_GenAPICommand Condition="'$(MSBuildRuntimeType)' == 'core'">"$(DotNetTool)" --roll-forward-on-no-candidate-fx 2 "$(_GenAPIPath)" - <_GenAPICmd>$(_GenAPICommand) - <_GenAPICmd>$(_GenAPICmd) "$(TargetPath)" - <_GenAPICmd>$(_GenAPICmd) --lib-path "@(_ReferencePathDirectories)" - <_GenAPICmd>$(_GenAPICmd) --out "$(_RefSourceFileOutputPath)" - <_GenAPICmd>$(_GenAPICmd) --header-file "$(RepoRoot)/eng/LicenseHeader.txt" - <_GenAPICmd>$(_GenAPICmd) --exclude-api-list "$(RepoRoot)/eng/GenAPI.exclusions.txt" + <_GenAPICmd>$(_GenAPICommand) @"$(_GenApiFile)" + <_GenApiArguments> + + + @@ -96,4 +102,4 @@ - \ No newline at end of file + diff --git a/global.json b/global.json index 1ff988fe32..b0204615a6 100644 --- a/global.json +++ b/global.json @@ -12,6 +12,7 @@ "$(MicrosoftNETCoreAppRuntimeVersion)" ] }, + "Git": "2.22.0", "jdk": "11.0.3", "vs": { "version": "16.0", diff --git a/src/Components/Ignitor/src/BlazorClient.cs b/src/Components/Ignitor/src/BlazorClient.cs index 861e2ad0f2..df93faa3b3 100644 --- a/src/Components/Ignitor/src/BlazorClient.cs +++ b/src/Components/Ignitor/src/BlazorClient.cs @@ -88,7 +88,7 @@ namespace Ignitor public Task PrepareForNextBatch(TimeSpan? timeout) { - if (NextBatchReceived?.Completion != null) + if (NextBatchReceived != null && !NextBatchReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } @@ -100,7 +100,7 @@ namespace Ignitor public Task PrepareForNextJSInterop(TimeSpan? timeout) { - if (NextJSInteropReceived?.Completion != null) + if (NextJSInteropReceived != null && !NextJSInteropReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } @@ -112,7 +112,7 @@ namespace Ignitor public Task PrepareForNextDotNetInterop(TimeSpan? timeout) { - if (NextDotNetInteropCompletionReceived?.Completion != null) + if (NextDotNetInteropCompletionReceived != null && !NextDotNetInteropCompletionReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } @@ -124,7 +124,7 @@ namespace Ignitor public Task PrepareForNextCircuitError(TimeSpan? timeout) { - if (NextErrorReceived?.Completion != null) + if (NextErrorReceived != null && !NextErrorReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } @@ -136,7 +136,7 @@ namespace Ignitor public Task PrepareForNextDisconnect(TimeSpan? timeout) { - if (NextDisconnect?.Completion != null) + if (NextDisconnect != null && !NextDisconnect.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } @@ -499,56 +499,6 @@ namespace Ignitor return element; } - private class CancellableOperation - { - public CancellableOperation(TimeSpan? timeout) - { - Timeout = timeout; - Initialize(); - } - - public TimeSpan? Timeout { get; } - - public TaskCompletionSource Completion { get; set; } - - public CancellationTokenSource Cancellation { get; set; } - - public CancellationTokenRegistration CancellationRegistration { get; set; } - - private void Initialize() - { - Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); - Completion.Task.ContinueWith( - (task, state) => - { - var operation = (CancellableOperation)state; - operation.Dispose(); - }, - this, - TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues - if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue) - { - Cancellation = new CancellationTokenSource(Timeout.Value); - CancellationRegistration = Cancellation.Token.Register( - (self) => - { - var operation = (CancellableOperation)self; - operation.Completion.TrySetCanceled(operation.Cancellation.Token); - operation.Cancellation.Dispose(); - operation.CancellationRegistration.Dispose(); - }, - this); - } - } - - private void Dispose() - { - Completion = null; - Cancellation.Dispose(); - CancellationRegistration.Dispose(); - } - } - private string[] ReadMarkers(string content) { content = content.Replace("\r\n", "").Replace("\n", ""); diff --git a/src/Components/Ignitor/src/CancellableOperation.cs b/src/Components/Ignitor/src/CancellableOperation.cs new file mode 100644 index 0000000000..dad8cb5587 --- /dev/null +++ b/src/Components/Ignitor/src/CancellableOperation.cs @@ -0,0 +1,64 @@ +// 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; +using System.Threading.Tasks; + +namespace Ignitor +{ + internal class CancellableOperation + { + public CancellableOperation(TimeSpan? timeout) + { + Timeout = timeout; + + Completion = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + Completion.Task.ContinueWith( + (task, state) => + { + var operation = (CancellableOperation)state; + operation.Dispose(); + }, + this, + TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues + + if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue) + { + Cancellation = new CancellationTokenSource(Timeout.Value); + CancellationRegistration = Cancellation.Token.Register( + (self) => + { + var operation = (CancellableOperation)self; + operation.Completion.TrySetCanceled(operation.Cancellation.Token); + operation.Cancellation.Dispose(); + operation.CancellationRegistration.Dispose(); + }, + this); + } + } + + public TimeSpan? Timeout { get; } + + public TaskCompletionSource Completion { get; } + + public CancellationTokenSource Cancellation { get; } + + public CancellationTokenRegistration CancellationRegistration { get; } + + public bool Disposed { get; private set; } + + private void Dispose() + { + if (Disposed) + { + return; + } + + Disposed = true; + Completion.TrySetCanceled(Cancellation.Token); + Cancellation.Dispose(); + CancellationRegistration.Dispose(); + } + } +} diff --git a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj index 00c9026c5a..a60b73a3cd 100644 --- a/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/ref/Microsoft.AspNetCore.App.Ref.csproj @@ -171,14 +171,26 @@ This package is an internal implementation of the .NET Core SDK and is not meant Inputs="@(RefPackContent)" Outputs="$(ZipArchiveOutputPath);$(TarArchiveOutputPath)" Condition="'$(IsPackable)' == 'true'"> + + <_TarCommand>tar + <_TarCommand Condition="Exists('$(RepoRoot).tools\tar.exe')">$(RepoRoot).tools\tar.exe + + + <_TarArchiveOutputPath>$(TarArchiveOutputPath) + <_TarArchiveOutputPath + Condition="Exists('$(repoRoot)\.tools\tar.fromGit')">/$(TarArchiveOutputPath.Replace('\','/').Replace(':','')) + + + - + + + diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs index a453a2d7be..8b3239c5df 100644 --- a/src/Shared/E2ETesting/BrowserFixture.cs +++ b/src/Shared/E2ETesting/BrowserFixture.cs @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.E2ETesting var instance = await SeleniumStandaloneServer.GetInstanceAsync(output); var attempt = 0; - var maxAttempts = 3; + const int maxAttempts = 3; do { try @@ -132,18 +132,16 @@ namespace Microsoft.AspNetCore.E2ETesting return (driver, logs); } - catch + catch (Exception ex) { - if (attempt >= maxAttempts) - { - throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive"); - } + output.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}"); } + attempt++; + } while (attempt < maxAttempts); - // We will never get here. Keeping the compiler happy. - throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is unresponsive"); + throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive"); } } } diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs index 1b6abc9f27..9e076cefc3 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ApiDescription.Client public void Execute_ReturnsExpectedItem() { // Arrange - string input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" + + var input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" + "CodeGenerator=NSwagCSharp|Namespace=ConsoleClient|Options=|OutputPath=" + "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs|" + "OriginalItemSpec=../files/azureMonitor.json|FirstForGenerator=true"; @@ -22,8 +22,8 @@ namespace Microsoft.Extensions.ApiDescription.Client Input = input, }; - string expectedIdentity = "../files/azureMonitor.json"; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedIdentity = "../files/azureMonitor.json"; + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "azureMonitorClient" }, { "CodeGenerator", "NSwagCSharp" }, diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs index 7bc5cc7fb9..3602ea1c56 100644 --- a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "NSwagClient" }, { "CodeGenerator", "NSwagCSharp" }, @@ -85,7 +85,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -143,7 +143,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "obj", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", "NSwagClient" }, { "CodeGenerator", "NSwagCSharp" }, @@ -201,7 +201,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -351,7 +351,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -414,7 +414,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "bin", }; - IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className }, { "CodeGenerator", "NSwagCSharp" }, @@ -481,7 +481,7 @@ namespace Microsoft.Extensions.ApiDescription.Client OutputDirectory = "obj", }; - IDictionary expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className12 }, { "CodeGenerator", codeGenerator13 }, @@ -496,7 +496,7 @@ namespace Microsoft.Extensions.ApiDescription.Client $"OutputPath={outputPath1}|ClassName={className12}|Namespace={@namespace}" }, }; - IDictionary expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className12 }, { "CodeGenerator", codeGenerator2 }, @@ -511,7 +511,7 @@ namespace Microsoft.Extensions.ApiDescription.Client $"OutputPath={outputPath2}|ClassName={className12}|Namespace={@namespace}" }, }; - IDictionary expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal) + var expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal) { { "ClassName", className3 }, { "CodeGenerator", codeGenerator13 }, diff --git a/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs new file mode 100644 index 0000000000..adc889ccac --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/MetadataSerializerTest.cs @@ -0,0 +1,314 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.ApiDescription.Client +{ + // ItemSpec values always have '\\' converted to '/' on input when running on non-Windows. It is not possible to + // retrieve the original (unconverted) item spec value. In other respects, item spec values are treated identically + // to custom metadata values. + // + // ITaskItem members aka the implicitly-implemented methods and properties in TaskItem expect _escaped_ values on + // input and return _literal_ values. This includes TaskItem constructors and CloneCustomMetadata() (which returns + // a new dictionary containing literal values). TaskItem stores all values in their escaped form. + // + // Added ITaskItem2 members e.g. CloneCustomMetadataEscaped(), GetMetadataValueEscaped(...) and + // EvaluatedIncludeEscaped return escaped values. Of all TaskItem methods, only SetMetadataValueLiteral(...) + // accepts a literal input value. + // + // Metadata names are never escaped. + // + // MetadataSerializer expects literal values on input. + public class MetadataSerializerTest + { + // Maps literal to escaped values. + public static TheoryData EscapedValuesMapping { get; } = new TheoryData + { + { "No escaping necessary for =.", "No escaping necessary for =." }, + { "Value needs escaping? (yes)", "Value needs escaping%3f %28yes%29" }, + { "$ comes earlier; @ comes later.", "%24 comes earlier%3b %40 comes later." }, + { + "A '%' *character* needs escaping %-escaping.", + "A %27%25%27 %2acharacter%2a needs escaping %25-escaping." + }, + }; + + public static TheoryData EscapedValues + { + get + { + var result = new TheoryData(); + foreach (var entry in EscapedValuesMapping) + { + result.Add((string)entry[1]); + } + + return result; + } + } + + public static TheoryData LiteralValues + { + get + { + var result = new TheoryData(); + foreach (var entry in EscapedValuesMapping) + { + result.Add((string)entry[0]); + } + + return result; + } + } + + [Theory] + [MemberData(nameof(LiteralValues))] + public void SetMetadata_UpdatesTaskAsExpected(string value) + { + // Arrange + var item = new TaskItem("My Identity"); + var key = "My key"; + + // Act + MetadataSerializer.SetMetadata(item, key, value); + + // Assert + Assert.Equal(value, item.GetMetadata(key)); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SetMetadata_UpdatesTaskAsExpected_WithLegacyItem(string value, string escapedValue) + { + // Arrange + var item = new Mock(MockBehavior.Strict); + var key = "My key"; + item.Setup(i => i.SetMetadata(key, escapedValue)).Verifiable(); + + // Act + MetadataSerializer.SetMetadata(item.Object, key, value); + + // Assert + item.Verify(i => i.SetMetadata(key, escapedValue), Times.Once); + } + + [Fact] + public void DeserializeMetadata_ReturnsExpectedTask() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var input = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + var expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(identity, item.ItemSpec); + var metadata = Assert.IsAssignableFrom>(item.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void DeserializeMetadata_ReturnsExpectedTask_WhenEscaping(string value, string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var input = $"Identity={identity}|Value={escapedValue}"; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(identity, item.ItemSpec); + Assert.Equal(value, item.GetMetadata("Value")); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void DeserializeMetadata_ReturnsExpectedTask_WhenEscapingIdentity(string value, string escapedValue) + { + // Arrange + var input = $"Identity={escapedValue}|Value=a value"; + + // Act + var item = MetadataSerializer.DeserializeMetadata(input); + + // Assert + Assert.Equal(value, item.ItemSpec); + Assert.Equal("a value", item.GetMetadata("Value")); + } + + [Fact] + public void SerializeMetadata_ReturnsExpectedString() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + var input = new TaskItem(identity, metadata); + var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData(nameof(EscapedValues))] + public void SerializeMetadata_ReturnsExpectedString_WhenEscaping(string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var expectedResult = $"Identity={identity}|Value={escapedValue}"; + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", escapedValue } }; + var input = new TaskItem(identity, metadata); + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData(nameof(EscapedValues))] + public void SerializeMetadata_ReturnsExpectedString_WhenEscapingIdentity(string escapedValue) + { + // Arrange + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } }; + var expectedResult = $"Identity={escapedValue}|Value=a value"; + var input = new TaskItem(escapedValue, metadata); + + // Act + var result = MetadataSerializer.SerializeMetadata(input); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem() + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", identity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" + + "Options=|OriginalItemSpec=../files/azureMonitor.json|" + + "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscaping( + string value, + string escapedValue) + { + // Arrange + var identity = "../files/azureMonitor.json"; + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", value } }; + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={identity}|Value={escapedValue}"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + + [Theory] + [MemberData(nameof(EscapedValuesMapping))] + public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscapingIdentity( + string value, + string escapedValue) + { + // Arrange + var metadata = new SortedDictionary(StringComparer.Ordinal) { { "Value", "a value" } }; + var input = new Mock(MockBehavior.Strict); + input.SetupGet(i => i.ItemSpec).Returns(value).Verifiable(); + input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable(); + + var expectedResult = $"Identity={escapedValue}|Value=a value"; + + // Act + var result = MetadataSerializer.SerializeMetadata(input.Object); + + // Assert + Assert.Equal(expectedResult, result); + input.VerifyGet(i => i.ItemSpec, Times.Once); + input.Verify(i => i.CloneCustomMetadata(), Times.Once); + } + } +} diff --git a/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs index 7123f0d04e..92fe39df4f 100644 --- a/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs +++ b/src/Tools/Microsoft.dotnet-openapi/src/Commands/BaseCommand.cs @@ -6,9 +6,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Reflection; using System.Security.Cryptography; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using Microsoft.DotNet.Openapi.Tools; @@ -240,13 +243,13 @@ namespace Microsoft.DotNet.OpenApi.Commands internal async Task DownloadToFileAsync(string url, string destinationPath, bool overwrite) { - using var response = await _httpClient.GetResponseAsync(url); + using var response = await RetryRequest(() => _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); + using var response = await RetryRequest(() => _httpClient.GetResponseAsync(url)); if (response.IsSuccessCode()) { @@ -272,6 +275,56 @@ namespace Microsoft.DotNet.OpenApi.Commands } } + /// + /// Retries every 1 sec for 60 times by default. + /// + /// + /// + /// + /// + private static async Task RetryRequest( + Func> retryBlock, + CancellationToken cancellationToken = default, + int retryCount = 60) + { + for (var retry = 0; retry < retryCount; retry++) + { + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Failed to connect, retry canceled.", cancellationToken); + } + + try + { + var response = await retryBlock().ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + // Automatically retry on 503. May be application is still booting. + continue; + } + + return response; // Went through successfully + } + catch (Exception exception) + { + if (retry == retryCount - 1) + { + throw; + } + else + { + if (exception is HttpRequestException || exception is WebException) + { + await Task.Delay(1 * 1000); //Wait for a while before retry. + } + } + } + } + + throw new OperationCanceledException("Failed to connect, retry limit exceeded."); + } + private string GetUniqueFileName(string directory, string fileName, string extension) { var uniqueName = fileName; diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs index 95369d7683..ff8000263d 100644 --- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs +++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs @@ -440,8 +440,8 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests var url = BrokenUrl; var run = app.Execute(new[] { "add", "url", url }); - Assert.Equal(_error.ToString(), $"The given url returned 'NotFound', " + - "indicating failure. The url might be wrong, or there might be a networking issue."+Environment.NewLine); + Assert.Equal($"The given url returned 'NotFound', " + + "indicating failure. The url might be wrong, or there might be a networking issue."+Environment.NewLine, _error.ToString()); Assert.Equal(1, run); var expectedJsonName = "dingos.json";