Merge branch 'release/3.0-preview9' => 'release/3.0' (#13283)
This commit is contained in:
commit
208b50afca
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<!-- These projects are meant to be executed by tests. -->
|
||||
<ProjectToExclude Include="
|
||||
$(RepoRoot)src\Tools\dotnet-watch\test\TestProjects\**\*.csproj;
|
||||
$(RepoRoot)src\Tools\Tests.Common\TestProjects\**\*.csproj;
|
||||
$(RepoRoot)src\Razor\Razor.Design\test\testassets\**\*.*proj;
|
||||
$(RepoRoot)src\submodules\**\*.*proj;
|
||||
$(RepoRoot)src\Installers\**\*.*proj;
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ and are generated based on the last package release.
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="MSBuild">
|
||||
<LatestPackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildPackageVersion)" />
|
||||
<LatestPackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />
|
||||
<LatestPackageReference Include="Microsoft.Build.Locator" Version="$(MicrosoftBuildLocatorPackageVersion)" />
|
||||
<LatestPackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCorePackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -172,6 +174,7 @@ and are generated based on the last package release.
|
|||
<LatestPackageReference Include="Moq" Version="$(MoqPackageVersion)" />
|
||||
<LatestPackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||
<LatestPackageReference Include="Newtonsoft.Json.Bson" Version="$(NewtonsoftJsonBsonPackageVersion)" />
|
||||
<LatestPackageReference Include="NSwag.ApiDescription.Client" Version="$(NSwagApiDescriptionClientPackageVersion)" />
|
||||
<LatestPackageReference Include="Selenium.Support" Version="$(SeleniumSupportPackageVersion)" />
|
||||
<LatestPackageReference Include="Selenium.WebDriver" Version="$(SeleniumWebDriverPackageVersion)" />
|
||||
<LatestPackageReference Include="Selenium.WebDriver.ChromeDriver" Version="$(SeleniumWebDriverChromeDriverPackageVersion)" />
|
||||
|
|
|
|||
|
|
@ -97,6 +97,12 @@
|
|||
<_DotNetFilesToExclude Include="$(RedistNetCorePath)dotnet.exe" CertificateName="None" />
|
||||
<FileSignInfo Include="@(_DotNetFilesToExclude->'%(FileName)%(Extension)'->Distinct())" CertificateName="None" />
|
||||
|
||||
<!--
|
||||
We include the Microsoft.Build.Locator.dll assembly in our global tool 'Microsoft.dotnet-openapi'.
|
||||
It is already signed by that team, so we don't need to sign it.
|
||||
-->
|
||||
<FileSignInfo Include="Microsoft.Build.Locator.dll" CertificateName="None" />
|
||||
|
||||
<!--
|
||||
We include the Microsoft.Data.SqlClient.dll assembly in our global tool 'dotnet-sql-cache'.
|
||||
It is already signed by that team, so we don't need to sign it.
|
||||
|
|
|
|||
|
|
@ -199,7 +199,9 @@
|
|||
<!-- Partner teams -->
|
||||
<MicrosoftAzureKeyVaultPackageVersion>2.3.2</MicrosoftAzureKeyVaultPackageVersion>
|
||||
<MicrosoftAzureStorageBlobPackageVersion>10.0.1</MicrosoftAzureStorageBlobPackageVersion>
|
||||
<MicrosoftBuildPackageVersion>15.8.166</MicrosoftBuildPackageVersion>
|
||||
<MicrosoftBuildFrameworkPackageVersion>15.8.166</MicrosoftBuildFrameworkPackageVersion>
|
||||
<MicrosoftBuildLocatorPackageVersion>1.2.6</MicrosoftBuildLocatorPackageVersion>
|
||||
<MicrosoftBuildUtilitiesCorePackageVersion>15.8.166</MicrosoftBuildUtilitiesCorePackageVersion>
|
||||
<MicrosoftCodeAnalysisCommonPackageVersion>3.0.0</MicrosoftCodeAnalysisCommonPackageVersion>
|
||||
<MicrosoftCodeAnalysisCSharpPackageVersion>3.0.0</MicrosoftCodeAnalysisCSharpPackageVersion>
|
||||
|
|
@ -224,7 +226,7 @@
|
|||
<CastleCorePackageVersion>4.2.1</CastleCorePackageVersion>
|
||||
<FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion>
|
||||
<GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion>
|
||||
<GrpcAspNetCorePackageVersion>0.1.22-pre3</GrpcAspNetCorePackageVersion>
|
||||
<GrpcAspNetCorePackageVersion>0.2.23-pre1</GrpcAspNetCorePackageVersion>
|
||||
<IdentityServer4AspNetIdentityPackageVersion>3.0.0-preview7.33</IdentityServer4AspNetIdentityPackageVersion>
|
||||
<IdentityServer4EntityFrameworkPackageVersion>3.0.0-preview7.33</IdentityServer4EntityFrameworkPackageVersion>
|
||||
<IdentityServer4PackageVersion>3.0.0-preview7.33</IdentityServer4PackageVersion>
|
||||
|
|
@ -235,6 +237,7 @@
|
|||
<MonoCecilPackageVersion>0.10.1</MonoCecilPackageVersion>
|
||||
<NewtonsoftJsonBsonPackageVersion>1.0.2</NewtonsoftJsonBsonPackageVersion>
|
||||
<NewtonsoftJsonPackageVersion>12.0.1</NewtonsoftJsonPackageVersion>
|
||||
<NSwagApiDescriptionClientPackageVersion>13.0.4</NSwagApiDescriptionClientPackageVersion>
|
||||
<SeleniumSupportPackageVersion>3.12.1</SeleniumSupportPackageVersion>
|
||||
<SeleniumWebDriverMicrosoftDriverPackageVersion>17.17134.0</SeleniumWebDriverMicrosoftDriverPackageVersion>
|
||||
<SeleniumWebDriverChromeDriverPackageVersion>2.43.0</SeleniumWebDriverChromeDriverPackageVersion>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
|
||||
internal class RouteEntry
|
||||
{
|
||||
public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,11 @@ namespace Microsoft.AspNetCore.Components
|
|||
/// </summary>
|
||||
internal static int RouteComparison(RouteEntry x, RouteEntry y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var xTemplate = x.Template;
|
||||
var yTemplate = y.Template;
|
||||
if (xTemplate.Segments.Length != y.Template.Segments.Length)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
[DebuggerDisplay("{TemplateText}")]
|
||||
internal class RouteTemplate
|
||||
{
|
||||
public RouteTemplate(string templateText, TemplateSegment[] segments)
|
||||
|
|
|
|||
|
|
@ -366,6 +366,41 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
Assert.Equal("a/brilliant", routeTable.Routes[0].Template.TemplateText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotThrowIfStableSortComparesRouteWithItself()
|
||||
{
|
||||
// Test for https://github.com/aspnet/AspNetCore/issues/13313
|
||||
// Arrange & Act
|
||||
var builder = new TestRouteTableBuilder();
|
||||
builder.AddRoute("r16");
|
||||
builder.AddRoute("r05");
|
||||
builder.AddRoute("r09");
|
||||
builder.AddRoute("r00");
|
||||
builder.AddRoute("r13");
|
||||
builder.AddRoute("r02");
|
||||
builder.AddRoute("r03");
|
||||
builder.AddRoute("r10");
|
||||
builder.AddRoute("r15");
|
||||
builder.AddRoute("r14");
|
||||
builder.AddRoute("r12");
|
||||
builder.AddRoute("r07");
|
||||
builder.AddRoute("r11");
|
||||
builder.AddRoute("r08");
|
||||
builder.AddRoute("r06");
|
||||
builder.AddRoute("r04");
|
||||
builder.AddRoute("r01");
|
||||
|
||||
var routeTable = builder.Build();
|
||||
|
||||
// Act
|
||||
Assert.Equal(17, routeTable.Routes.Length);
|
||||
for (var i = 0; i < 17; i++)
|
||||
{
|
||||
var templateText = "r" + i.ToString().PadLeft(2, '0');
|
||||
Assert.Equal(templateText, routeTable.Routes[i].Template.TemplateText);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/literal", "/Literal/")]
|
||||
[InlineData("/{parameter}", "/{parameter}/")]
|
||||
|
|
|
|||
|
|
@ -649,12 +649,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
_intializationStarted = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.InitializationFailed,
|
||||
EventIds.InitializationStarted,
|
||||
"Circuit initialization started.");
|
||||
|
||||
_intializationSucceded = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.InitializationFailed,
|
||||
EventIds.InitializationSucceeded,
|
||||
"Circuit initialization succeeded.");
|
||||
|
||||
_intializationFailed = LoggerMessage.Define(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
// 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.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.E2ETest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests
|
||||
{
|
||||
public class ComponentHubInvalidEventTest : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
public ComponentHubInvalidEventTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
: base(serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
}
|
||||
|
||||
protected async override Task InitializeAsync()
|
||||
{
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
|
||||
Assert.Equal(2, Batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data.";
|
||||
|
||||
var eventDescriptor = Serialize(new WebEventDescriptor()
|
||||
{
|
||||
BrowserRendererId = 0,
|
||||
EventHandlerId = 3,
|
||||
EventArgsType = "mouse",
|
||||
});
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"DispatchBrowserEvent",
|
||||
eventDescriptor,
|
||||
"{sadfadsf]"));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'."));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event.";
|
||||
|
||||
var eventDescriptor = Serialize(new WebEventDescriptor()
|
||||
{
|
||||
BrowserRendererId = 0,
|
||||
EventHandlerId = 1990,
|
||||
EventArgsType = "mouse",
|
||||
});
|
||||
|
||||
var eventArgs = new MouseEventArgs
|
||||
{
|
||||
Type = "click",
|
||||
Detail = 1,
|
||||
ScreenX = 47,
|
||||
ScreenY = 258,
|
||||
ClientX = 47,
|
||||
ClientY = 155,
|
||||
};
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"DispatchBrowserEvent",
|
||||
eventDescriptor,
|
||||
Serialize(eventArgs)));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
|
||||
(LogLevel.Debug,
|
||||
"There was an error dispatching the event '1990' to the application.",
|
||||
"There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'.";
|
||||
|
||||
|
||||
Client.ConfirmRenderBatch = false;
|
||||
await Client.ClickAsync("counter");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"OnRenderCompleted",
|
||||
1846,
|
||||
null));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message);
|
||||
Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message);
|
||||
}
|
||||
|
||||
private string Serialize<T>(T browserEventDescriptor) =>
|
||||
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,7 @@
|
|||
// 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.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -13,58 +11,22 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
|||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
public class ComponentHubReliabilityTest : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10);
|
||||
private readonly AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
: base(serverFixture, output)
|
||||
{
|
||||
_serverFixture = serverFixture;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
CreateDefaultConfiguration();
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
public ITestOutputHelper Output { get; set; }
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
public TestSink TestSink { get; set; }
|
||||
|
||||
private void CreateDefaultConfiguration()
|
||||
{
|
||||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
|
||||
Client.OnCircuitError += (error) => Errors.Add(error);
|
||||
Client.LoggerProvider = new XunitLoggerProvider(Output);
|
||||
Client.FormatError = (error) =>
|
||||
{
|
||||
var logs = string.Join(Environment.NewLine, Logs);
|
||||
return new Exception(error + Environment.NewLine + logs);
|
||||
};
|
||||
|
||||
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
|
||||
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
TestSink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context)
|
||||
{
|
||||
var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception);
|
||||
Logs.Enqueue(log);
|
||||
Output.WriteLine(log.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -72,10 +34,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "The circuit host '.*?' has already been initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
var descriptors = await Client.GetPrerenderDescriptors(baseUri);
|
||||
|
||||
// Act
|
||||
|
|
@ -96,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "The uris provided are invalid.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var uri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
|
||||
var descriptors = await Client.GetPrerenderDescriptors(uri);
|
||||
|
|
@ -117,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "The circuit failed to initialize.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var uri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
|
||||
var descriptors = await Client.GetPrerenderDescriptors(uri);
|
||||
|
|
@ -138,7 +101,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
|
||||
Assert.Empty(Batches);
|
||||
|
|
@ -164,7 +127,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
|
||||
Assert.Empty(Batches);
|
||||
|
|
@ -188,7 +151,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
|
||||
Assert.Empty(Batches);
|
||||
|
|
@ -206,125 +169,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization"));
|
||||
}
|
||||
|
||||
private async Task GoToTestComponent(IList<Batch> batches)
|
||||
{
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
|
||||
Assert.Single(batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
|
||||
Assert.Equal(2, batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data.";
|
||||
|
||||
var eventDescriptor = Serialize(new WebEventDescriptor()
|
||||
{
|
||||
BrowserRendererId = 0,
|
||||
EventHandlerId = 3,
|
||||
EventArgsType = "mouse",
|
||||
});
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
Assert.Equal(2, Batches.Count);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"DispatchBrowserEvent",
|
||||
eventDescriptor,
|
||||
"{sadfadsf]"));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'."));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event.";
|
||||
|
||||
var eventDescriptor = Serialize(new WebEventDescriptor()
|
||||
{
|
||||
BrowserRendererId = 0,
|
||||
EventHandlerId = 1990,
|
||||
EventArgsType = "mouse",
|
||||
});
|
||||
|
||||
var eventArgs = new MouseEventArgs
|
||||
{
|
||||
Type = "click",
|
||||
Detail = 1,
|
||||
ScreenX = 47,
|
||||
ScreenY = 258,
|
||||
ClientX = 47,
|
||||
ClientY = 155,
|
||||
};
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
Assert.Equal(2, Batches.Count);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"DispatchBrowserEvent",
|
||||
eventDescriptor,
|
||||
Serialize(eventArgs)));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
|
||||
(LogLevel.Debug,
|
||||
"There was an error dispatching the event '1990' to the application.",
|
||||
"There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'.";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
Assert.Equal(2, Batches.Count);
|
||||
|
||||
Client.ConfirmRenderBatch = false;
|
||||
await Client.ClickAsync("counter");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"OnRenderCompleted",
|
||||
1846,
|
||||
null));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message);
|
||||
Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
|
||||
Assert.Empty(Batches);
|
||||
|
|
@ -347,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
|
||||
Assert.Empty(Batches);
|
||||
|
|
@ -373,7 +223,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
|
||||
"Location change to 'http://example.com' failed.";
|
||||
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
|
@ -402,7 +252,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
|
||||
"Location change failed.";
|
||||
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
|
@ -421,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "LocationChangeFailed");
|
||||
Assert.Equal(LogLevel.Error, entry.LogLevel);
|
||||
Assert.Matches($"Location change to '{new Uri(_serverFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message);
|
||||
Assert.Matches($"Location change to '{new Uri(ServerFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -436,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Unhandled exception in circuit .*";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
|
@ -467,7 +317,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
{
|
||||
// Arrange
|
||||
var expectedError = "Unhandled exception in circuit .*";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
|
@ -493,47 +343,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Logs,
|
||||
e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TestSink.MessageLogged -= LogMessages;
|
||||
}
|
||||
|
||||
private string Serialize<T>(T browserEventDescriptor) =>
|
||||
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
|
||||
|
||||
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
|
||||
private class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
EventId = eventId;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public EventId EventId { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
|
||||
}
|
||||
}
|
||||
|
||||
private class Batch
|
||||
{
|
||||
public Batch(int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Ignitor;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
// Base class for Ignitor-based tests.
|
||||
public abstract class IgnitorTest<TFixture> : IClassFixture<TFixture>, IAsyncLifetime
|
||||
where TFixture : ServerFixture
|
||||
{
|
||||
private static readonly TimeSpan DefaultTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(30);
|
||||
|
||||
protected IgnitorTest(TFixture serverFixture, ITestOutputHelper output)
|
||||
{
|
||||
ServerFixture = serverFixture;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
protected BlazorClient Client { get; private set; }
|
||||
|
||||
protected ConcurrentQueue<LogMessage> Logs { get; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
protected ITestOutputHelper Output { get; }
|
||||
|
||||
protected TFixture ServerFixture { get; }
|
||||
|
||||
protected TimeSpan Timeout { get; set; } = DefaultTimeout;
|
||||
|
||||
private TestSink TestSink { get; set; }
|
||||
|
||||
protected IReadOnlyCollection<CapturedRenderBatch> Batches => Client?.Operations?.Batches;
|
||||
|
||||
protected IReadOnlyCollection<string> DotNetCompletions => Client?.Operations?.DotNetCompletions;
|
||||
|
||||
protected IReadOnlyCollection<string> Errors => Client?.Operations?.Errors;
|
||||
|
||||
protected IReadOnlyCollection<CapturedJSInteropCall> JSInteropCalls => Client?.Operations?.JSInteropCalls;
|
||||
|
||||
// Called to initialize the fixture as part of InitializeAsync.
|
||||
protected virtual void InitializeFixture(TFixture serverFixture)
|
||||
{
|
||||
}
|
||||
|
||||
async Task IAsyncLifetime.InitializeAsync()
|
||||
{
|
||||
Client = new BlazorClient()
|
||||
{
|
||||
CaptureOperations = true,
|
||||
DefaultOperationTimeout = Timeout,
|
||||
};
|
||||
Client.LoggerProvider = new XunitLoggerProvider(Output);
|
||||
Client.FormatError = (error) =>
|
||||
{
|
||||
var logs = string.Join(Environment.NewLine, Logs);
|
||||
return new Exception(error + Environment.NewLine + logs);
|
||||
};
|
||||
|
||||
InitializeFixture(ServerFixture);
|
||||
_ = ServerFixture.RootUri; // This is needed for the side-effects of starting the server.
|
||||
|
||||
if (ServerFixture is WebHostServerFixture hostFixture)
|
||||
{
|
||||
TestSink = hostFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
TestSink.MessageLogged += TestSink_MessageLogged;
|
||||
}
|
||||
|
||||
await InitializeAsync();
|
||||
}
|
||||
|
||||
async Task IAsyncLifetime.DisposeAsync()
|
||||
{
|
||||
if (TestSink != null)
|
||||
{
|
||||
TestSink.MessageLogged -= TestSink_MessageLogged;
|
||||
}
|
||||
|
||||
await DisposeAsync();
|
||||
}
|
||||
|
||||
protected virtual Task InitializeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual Task DisposeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void TestSink_MessageLogged(WriteContext context)
|
||||
{
|
||||
var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception);
|
||||
Logs.Enqueue(log);
|
||||
Output.WriteLine(log.ToString());
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
|
||||
protected sealed class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
EventId = eventId;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public EventId EventId { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// 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.Linq;
|
||||
using System.Text.Json;
|
||||
|
|
@ -21,55 +20,26 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.All)]
|
||||
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
public class InteropReliabilityTests : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30);
|
||||
private readonly AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
: base(serverFixture, output)
|
||||
{
|
||||
_serverFixture = serverFixture;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
CreateDefaultConfiguration();
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
public ITestOutputHelper Output { get; set; }
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private List<DotNetCompletion> DotNetCompletions = new List<DotNetCompletion>();
|
||||
private List<JSInteropCall> JSInteropCalls = new List<JSInteropCall>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
public TestSink TestSink { get; set; }
|
||||
|
||||
private void CreateDefaultConfiguration()
|
||||
protected async override Task InitializeAsync()
|
||||
{
|
||||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
|
||||
Client.DotNetInteropCompletion += (method) => DotNetCompletions.Add(new DotNetCompletion(method));
|
||||
Client.JSInterop += (asyncHandle, identifier, argsJson) => JSInteropCalls.Add(new JSInteropCall(asyncHandle, identifier, argsJson));
|
||||
Client.OnCircuitError += (error) => Errors.Add(error);
|
||||
Client.LoggerProvider = new XunitLoggerProvider(Output);
|
||||
Client.FormatError = (error) =>
|
||||
{
|
||||
var logs = string.Join(Environment.NewLine, Logs);
|
||||
return new Exception(error + Environment.NewLine + logs);
|
||||
};
|
||||
var rootUri = ServerFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
|
||||
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
TestSink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context)
|
||||
{
|
||||
var log = new LogMessage(context.LogLevel, context.Message, context.Exception);
|
||||
Logs.Enqueue(log);
|
||||
Output.WriteLine(log.ToString());
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
|
||||
Assert.Equal(2, Batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -79,7 +49,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -90,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +71,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -113,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -125,18 +92,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"NotifyLocationChanged",
|
||||
null,
|
||||
JsonSerializer.Serialize(new[] { _serverFixture.RootUri }));
|
||||
JsonSerializer.Serialize(new[] { ServerFixture.RootUri }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -148,18 +113,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"",
|
||||
"NotifyLocationChanged",
|
||||
null,
|
||||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -171,18 +134,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"",
|
||||
null,
|
||||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
|
@ -196,8 +157,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -206,7 +165,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedDotNetObjectRef);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef);
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -216,7 +175,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == "[\"1\",true,\"tnatropmI\"]");
|
||||
Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]");
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -225,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
3, // non existing ref
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -238,8 +197,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
"BasicTestApp",
|
||||
|
|
@ -247,7 +204,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedImportantDotNetObjectRef);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedImportantDotNetObjectRef);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -258,19 +215,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)]
|
||||
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details.";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-malformed");
|
||||
|
||||
|
|
@ -278,11 +232,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.AsyncHandle;
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
true,
|
||||
$"[{id}, true, \"{{\"]");
|
||||
await Client.ExpectRenderBatch(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
true,
|
||||
$"[{id}, true, \"{{\"]");
|
||||
});
|
||||
|
||||
var text = Assert.Single(
|
||||
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
|
||||
|
|
@ -295,10 +252,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task JSInteropCompletionSuccess()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-success");
|
||||
|
|
@ -307,27 +260,27 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.AsyncHandle;
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id++,
|
||||
true,
|
||||
$"[{id}, true, null]");
|
||||
await Client.ExpectRenderBatch(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
true,
|
||||
$"[{id}, true, null]");
|
||||
});
|
||||
|
||||
Assert.Single(
|
||||
Client.FindElementById("errormessage-success").Children.OfType<TextNode>(),
|
||||
e => "" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeJSSucceeded"), logEvents);
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSSucceeded");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JSInteropThrowsInUserCode()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-failure");
|
||||
|
|
@ -349,9 +302,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Client.FindElementById("errormessage-failure").Children.OfType<TextNode>(),
|
||||
e => "There was an error invoking sendFailureCallbackReturn" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeJSFailed"), logEvents);
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSFailed");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
|
||||
Assert.DoesNotContain(logEvents, m => m.logLevel > LogLevel.Information);
|
||||
Assert.DoesNotContain(Logs, m => m.LogLevel > LogLevel.Information);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
|
@ -360,10 +314,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task MalformedJSInteropCallbackDisposesCircuit()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-malformed");
|
||||
|
|
@ -386,7 +336,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
|
||||
e => "" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeDispatchException"), logEvents);
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeDispatchException");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
|
|
@ -402,8 +353,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -413,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"[ \"invalidPayload\"}");
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -425,8 +374,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -436,7 +383,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"[ { \"data\": {\"}} ]");
|
||||
|
||||
// Assert
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
|
|
@ -444,10 +391,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
|
|
@ -458,9 +401,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
null);
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
|
||||
logEvents);
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
|
|
@ -473,10 +415,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task DispatchingEventsWithInvalidEventDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
|
|
@ -487,9 +425,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"{}");
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
|
||||
logEvents);
|
||||
var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
|
||||
Assert.Equal(LogLevel.Debug, entry.LogLevel);
|
||||
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
|
|
@ -502,10 +439,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task DispatchingEventsWithInvalidEventArgs()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
||||
// Act
|
||||
var browserDescriptor = new WebEventDescriptor()
|
||||
|
|
@ -524,9 +457,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
});
|
||||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => e.eventIdName == "DispatchEventFailedToParseEventData" && e.logLevel == LogLevel.Debug &&
|
||||
e.exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
|
||||
Logs,
|
||||
e => e.EventId.Name == "DispatchEventFailedToParseEventData" && e.LogLevel == LogLevel.Debug &&
|
||||
e.Exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
|
||||
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
|
|
@ -539,10 +472,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task DispatchingEventsWithInvalidEventHandlerId()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
||||
// Act
|
||||
var mouseEventArgs = new MouseEventArgs()
|
||||
|
|
@ -566,9 +495,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
});
|
||||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug &&
|
||||
e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'."));
|
||||
Logs,
|
||||
e => e.EventId.Name == "DispatchEventFailedToDispatchEvent" && e.LogLevel == LogLevel.Debug &&
|
||||
e.Exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'."));
|
||||
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
|
|
@ -581,21 +510,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
|
||||
|
||||
await Task.Delay(1000);
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => LogLevel.Error == e.logLevel &&
|
||||
"CircuitUnhandledException" == e.eventIdName &&
|
||||
"Handler threw an exception" == e.exception.Message);
|
||||
Logs,
|
||||
e => LogLevel.Error == e.LogLevel &&
|
||||
"CircuitUnhandledException" == e.EventId.Name &&
|
||||
"Handler threw an exception" == e.Exception.Message);
|
||||
|
||||
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
|
||||
// a circuit that's gone.
|
||||
|
|
@ -605,7 +531,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
});
|
||||
}
|
||||
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, IList<Batch> batches) =>
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection<CapturedRenderBatch> batches) =>
|
||||
ValidateClientKeepsWorking(Client, () => batches.Count);
|
||||
|
||||
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
|
||||
|
|
@ -615,75 +541,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
Assert.Equal(currentBatches + 1, countAccessor());
|
||||
}
|
||||
|
||||
private async Task GoToTestComponent(IList<Batch> batches)
|
||||
{
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
|
||||
Assert.Single(batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
|
||||
Assert.Equal(2, batches.Count);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TestSink.MessageLogged -= LogMessages;
|
||||
}
|
||||
|
||||
private class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LogLevel}: {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
|
||||
}
|
||||
}
|
||||
|
||||
private class Batch
|
||||
{
|
||||
public Batch(int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
|
||||
private class DotNetCompletion
|
||||
{
|
||||
public DotNetCompletion(string message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string Message { get; }
|
||||
}
|
||||
|
||||
private class JSInteropCall
|
||||
{
|
||||
public JSInteropCall(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
AsyncHandle = asyncHandle;
|
||||
Identifier = identifier;
|
||||
ArgsJson = argsJson;
|
||||
}
|
||||
|
||||
public int AsyncHandle { get; }
|
||||
public string Identifier { get; }
|
||||
public string ArgsJson { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,55 +2,33 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Ignitor;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
public class RemoteRendererBufferLimitTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
public class RemoteRendererBufferLimitTest : IgnitorTest<AspNetSiteServerFixture>
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture)
|
||||
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
: base(serverFixture, output)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_serverFixture = serverFixture;
|
||||
|
||||
// Needed here for side-effects
|
||||
_ = _serverFixture.RootUri;
|
||||
|
||||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
|
||||
|
||||
Sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
Sink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
|
||||
// We use a stack so that we can search the logs in reverse order
|
||||
private ConcurrentStack<LogMessage> Logs { get; set; } = new ConcurrentStack<LogMessage>();
|
||||
|
||||
public TestSink Sink { get; private set; }
|
||||
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged()
|
||||
{
|
||||
// Arrange
|
||||
var baseUri = new Uri(_serverFixture.RootUri, "/subdir");
|
||||
var baseUri = new Uri(ServerFixture.RootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
|
|
@ -75,48 +53,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Client.ConfirmRenderBatch = true;
|
||||
|
||||
// This will resume the render batches.
|
||||
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id));
|
||||
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches.Last().Id));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent);
|
||||
Assert.Equal(fullCount + 1, Batches.Count);
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception));
|
||||
|
||||
[DebuggerDisplay("{Message,nq}")]
|
||||
private class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
|
||||
private class Batch
|
||||
{
|
||||
public Batch(int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Sink != null)
|
||||
{
|
||||
Sink.MessageLogged -= LogMessages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
{
|
||||
int currentCount = 0;
|
||||
string errorMalformed = "";
|
||||
string errorSuccess = "";
|
||||
string errorSuccess = "(this will be cleared)";
|
||||
string errorFailure = "";
|
||||
bool showConstructorThrow;
|
||||
bool showAttachThrow;
|
||||
|
|
@ -116,6 +116,10 @@
|
|||
var result = await JSRuntime.InvokeAsync<object>(
|
||||
"sendSuccessCallbackReturn",
|
||||
Array.Empty<object>());
|
||||
|
||||
// Make sure we trigger a render when the interop call completes.
|
||||
// The test uses a render to synchronize.
|
||||
errorSuccess = "";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
|
@ -33,8 +31,20 @@ namespace Ignitor
|
|||
});
|
||||
}
|
||||
|
||||
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||
public TimeSpan? DefaultConnectTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public TimeSpan? DefaultConnectionTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public TimeSpan? DefaultOperationTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines whether the client will capture data such
|
||||
/// as render batches, interop calls, and errors for later inspection.
|
||||
/// </summary>
|
||||
public bool CaptureOperations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collections of operation results that are captured when <see cref="CaptureOperations"/>
|
||||
/// is true.
|
||||
/// </summary>
|
||||
public Operations Operations { get; private set; }
|
||||
|
||||
public Func<string, Exception> FormatError { get; set; }
|
||||
|
||||
|
|
@ -44,23 +54,23 @@ namespace Ignitor
|
|||
|
||||
private TaskCompletionSource<object> TaskCompletionSource { get; }
|
||||
|
||||
private CancellableOperation NextBatchReceived { get; set; }
|
||||
private CancellableOperation<CapturedRenderBatch> NextBatchReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextErrorReceived { get; set; }
|
||||
private CancellableOperation<string> NextErrorReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDisconnect { get; set; }
|
||||
private CancellableOperation<Exception> NextDisconnect { get; set; }
|
||||
|
||||
private CancellableOperation NextJSInteropReceived { get; set; }
|
||||
private CancellableOperation<CapturedJSInteropCall> NextJSInteropReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDotNetInteropCompletionReceived { get; set; }
|
||||
private CancellableOperation<string> NextDotNetInteropCompletionReceived { get; set; }
|
||||
|
||||
public ILoggerProvider LoggerProvider { get; set; }
|
||||
|
||||
public bool ConfirmRenderBatch { get; set; } = true;
|
||||
|
||||
public event Action<int, string, string> JSInterop;
|
||||
public event Action<CapturedJSInteropCall> JSInterop;
|
||||
|
||||
public event Action<int, byte[]> RenderBatchReceived;
|
||||
public event Action<CapturedRenderBatch> RenderBatchReceived;
|
||||
|
||||
public event Action<string> DotNetInteropCompletion;
|
||||
|
||||
|
|
@ -70,66 +80,66 @@ namespace Ignitor
|
|||
|
||||
public ElementHive Hive { get; set; } = new ElementHive();
|
||||
|
||||
public bool ImplicitWait => DefaultLatencyTimeout != null;
|
||||
public bool ImplicitWait => DefaultOperationTimeout != null;
|
||||
|
||||
public HubConnection HubConnection { get; set; }
|
||||
|
||||
public Task PrepareForNextBatch(TimeSpan? timeout)
|
||||
public Task<CapturedRenderBatch> PrepareForNextBatch(TimeSpan? timeout)
|
||||
{
|
||||
if (NextBatchReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextBatchReceived = new CancellableOperation(timeout);
|
||||
NextBatchReceived = new CancellableOperation<CapturedRenderBatch>(timeout);
|
||||
|
||||
return NextBatchReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextJSInterop(TimeSpan? timeout)
|
||||
public Task<CapturedJSInteropCall> PrepareForNextJSInterop(TimeSpan? timeout)
|
||||
{
|
||||
if (NextJSInteropReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextJSInteropReceived = new CancellableOperation(timeout);
|
||||
NextJSInteropReceived = new CancellableOperation<CapturedJSInteropCall>(timeout);
|
||||
|
||||
return NextJSInteropReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextDotNetInterop(TimeSpan? timeout)
|
||||
public Task<string> PrepareForNextDotNetInterop(TimeSpan? timeout)
|
||||
{
|
||||
if (NextDotNetInteropCompletionReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextDotNetInteropCompletionReceived = new CancellableOperation(timeout);
|
||||
NextDotNetInteropCompletionReceived = new CancellableOperation<string>(timeout);
|
||||
|
||||
return NextDotNetInteropCompletionReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextCircuitError(TimeSpan? timeout)
|
||||
public Task<string> PrepareForNextCircuitError(TimeSpan? timeout)
|
||||
{
|
||||
if (NextErrorReceived?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextErrorReceived = new CancellableOperation(timeout);
|
||||
NextErrorReceived = new CancellableOperation<string>(timeout);
|
||||
|
||||
return NextErrorReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextDisconnect(TimeSpan? timeout)
|
||||
public Task<Exception> PrepareForNextDisconnect(TimeSpan? timeout)
|
||||
{
|
||||
if (NextDisconnect?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextDisconnect = new CancellableOperation(timeout);
|
||||
NextDisconnect = new CancellableOperation<Exception>(timeout);
|
||||
|
||||
return NextDisconnect.Completion.Task;
|
||||
}
|
||||
|
|
@ -160,115 +170,162 @@ namespace Ignitor
|
|||
return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
|
||||
}
|
||||
|
||||
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<CapturedRenderBatch> ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForRenderBatch(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<CapturedJSInteropCall> ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForJSInterop(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<string> ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForDotNetInterop(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<string> ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForCircuitError(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
public async Task ExpectCircuitErrorAndDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
// NOTE: timeout is used for each operation individually.
|
||||
await ExpectDisconnect(async () =>
|
||||
{
|
||||
await ExpectCircuitError(action, timeout);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
public async Task ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
public async Task<Exception> ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForDisconnect(timeout);
|
||||
await action();
|
||||
await task;
|
||||
return await task;
|
||||
}
|
||||
|
||||
private Task WaitForRenderBatch(TimeSpan? timeout = null)
|
||||
public async Task<(string error, Exception exception)> ExpectCircuitErrorAndDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
string error = null;
|
||||
|
||||
// NOTE: timeout is used for each operation individually.
|
||||
var exception = await ExpectDisconnect(async () =>
|
||||
{
|
||||
error = await ExpectCircuitError(action, timeout);
|
||||
}, timeout);
|
||||
|
||||
return (error, exception);
|
||||
}
|
||||
|
||||
private async Task<CapturedRenderBatch> WaitForRenderBatch(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
if (DefaultOperationTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
return PrepareForNextBatch(timeout ?? DefaultLatencyTimeout);
|
||||
try
|
||||
{
|
||||
return await PrepareForNextBatch(timeout ?? DefaultOperationTimeout);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw FormatError("Timed out while waiting for batch.");
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task WaitForJSInterop(TimeSpan? timeout = null)
|
||||
private async Task<CapturedJSInteropCall> WaitForJSInterop(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
if (DefaultOperationTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
await PrepareForNextJSInterop(timeout ?? DefaultLatencyTimeout);
|
||||
try
|
||||
{
|
||||
return await PrepareForNextJSInterop(timeout ?? DefaultOperationTimeout);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw FormatError("Timed out while waiting for JS Interop.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task WaitForDotNetInterop(TimeSpan? timeout = null)
|
||||
private async Task<string> WaitForDotNetInterop(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
if (DefaultOperationTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
await PrepareForNextDotNetInterop(timeout ?? DefaultLatencyTimeout);
|
||||
try
|
||||
{
|
||||
return await PrepareForNextDotNetInterop(timeout ?? DefaultOperationTimeout);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw FormatError("Timed out while waiting for .NET interop.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task WaitForCircuitError(TimeSpan? timeout = null)
|
||||
private async Task<string> WaitForCircuitError(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
if (DefaultOperationTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
await PrepareForNextCircuitError(timeout ?? DefaultLatencyTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForDisconnect(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
try
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
return await PrepareForNextCircuitError(timeout ?? DefaultOperationTimeout);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw FormatError("Timed out while waiting for circuit error.");
|
||||
}
|
||||
}
|
||||
|
||||
await PrepareForNextDisconnect(timeout ?? DefaultLatencyTimeout);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Exception> WaitForDisconnect(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultOperationTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await PrepareForNextDisconnect(timeout ?? DefaultOperationTimeout);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw FormatError("Timed out while waiting for disconnect.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync(Uri uri, bool connectAutomatically = true)
|
||||
|
|
@ -294,6 +351,11 @@ namespace Ignitor
|
|||
HubConnection.On<string>("JS.Error", OnError);
|
||||
HubConnection.Closed += OnClosedAsync;
|
||||
|
||||
if (CaptureOperations)
|
||||
{
|
||||
Operations = new Operations();
|
||||
}
|
||||
|
||||
if (!connectAutomatically)
|
||||
{
|
||||
return true;
|
||||
|
|
@ -302,35 +364,41 @@ namespace Ignitor
|
|||
var descriptors = await GetPrerenderDescriptors(uri);
|
||||
await ExpectRenderBatch(
|
||||
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri, descriptors),
|
||||
DefaultConnectTimeout);
|
||||
DefaultConnectionTimeout);
|
||||
return CircuitId != null;
|
||||
}
|
||||
|
||||
private void OnEndInvokeDotNet(string completion)
|
||||
private void OnEndInvokeDotNet(string message)
|
||||
{
|
||||
DotNetInteropCompletion?.Invoke(completion);
|
||||
Operations?.DotNetCompletions.Enqueue(message);
|
||||
DotNetInteropCompletion?.Invoke(message);
|
||||
|
||||
NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
JSInterop?.Invoke(asyncHandle, identifier, argsJson);
|
||||
var call = new CapturedJSInteropCall(asyncHandle, identifier, argsJson);
|
||||
Operations?.JSInteropCalls.Enqueue(call);
|
||||
JSInterop?.Invoke(call);
|
||||
|
||||
NextJSInteropReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
private void OnRenderBatch(int batchId, byte[] batchData)
|
||||
private void OnRenderBatch(int id, byte[] data)
|
||||
{
|
||||
RenderBatchReceived?.Invoke(batchId, batchData);
|
||||
var capturedBatch = new CapturedRenderBatch(id, data);
|
||||
|
||||
var batch = RenderBatchReader.Read(batchData);
|
||||
Operations?.Batches.Enqueue(capturedBatch);
|
||||
RenderBatchReceived?.Invoke(capturedBatch);
|
||||
|
||||
var batch = RenderBatchReader.Read(data);
|
||||
|
||||
Hive.Update(batch);
|
||||
|
||||
if (ConfirmRenderBatch)
|
||||
{
|
||||
_ = ConfirmBatch(batchId);
|
||||
_ = ConfirmBatch(id);
|
||||
}
|
||||
|
||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
||||
|
|
@ -343,8 +411,12 @@ namespace Ignitor
|
|||
|
||||
private void OnError(string error)
|
||||
{
|
||||
Operations?.Errors.Enqueue(error);
|
||||
OnCircuitError?.Invoke(error);
|
||||
|
||||
// If we get an error, forcibly terminate anything else we're waiting for. These
|
||||
// tests should only encounter errors in specific situations, and this ensures that
|
||||
// we fail with a good message.
|
||||
var exception = FormatError?.Invoke(error) ?? new Exception(error);
|
||||
NextBatchReceived?.Completion?.TrySetException(exception);
|
||||
NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception);
|
||||
|
|
@ -415,7 +487,7 @@ namespace Ignitor
|
|||
return element;
|
||||
}
|
||||
|
||||
private class CancellableOperation
|
||||
private class CancellableOperation<TResult>
|
||||
{
|
||||
public CancellableOperation(TimeSpan? timeout)
|
||||
{
|
||||
|
|
@ -423,24 +495,32 @@ namespace Ignitor
|
|||
Initialize();
|
||||
}
|
||||
|
||||
public TimeSpan? Timeout { get; }
|
||||
|
||||
public TaskCompletionSource<TResult> Completion { get; set; }
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
Completion = new TaskCompletionSource<TResult>(TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
Completion.Task.ContinueWith(
|
||||
(task, state) =>
|
||||
{
|
||||
var operation = (CancellableOperation)state;
|
||||
var operation = (CancellableOperation<TResult>)state;
|
||||
operation.Dispose();
|
||||
},
|
||||
this,
|
||||
TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
|
||||
if (Timeout != null)
|
||||
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;
|
||||
var operation = (CancellableOperation<TResult>)self;
|
||||
operation.Completion.TrySetCanceled(operation.Cancellation.Token);
|
||||
operation.Cancellation.Dispose();
|
||||
operation.CancellationRegistration.Dispose();
|
||||
|
|
@ -455,14 +535,6 @@ namespace Ignitor
|
|||
Cancellation.Dispose();
|
||||
CancellationRegistration.Dispose();
|
||||
}
|
||||
|
||||
public TimeSpan? Timeout { get; }
|
||||
|
||||
public TaskCompletionSource<object> Completion { get; set; }
|
||||
|
||||
public CancellationTokenSource Cancellation { get; set; }
|
||||
|
||||
public CancellationTokenRegistration CancellationRegistration { get; set; }
|
||||
}
|
||||
|
||||
private string[] ReadMarkers(string content)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
namespace Ignitor
|
||||
{
|
||||
public class CapturedJSInteropCall
|
||||
{
|
||||
public CapturedJSInteropCall(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
AsyncHandle = asyncHandle;
|
||||
Identifier = identifier;
|
||||
ArgsJson = argsJson;
|
||||
}
|
||||
|
||||
public int AsyncHandle { get; }
|
||||
public string Identifier { get; }
|
||||
public string ArgsJson { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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 Ignitor
|
||||
{
|
||||
public class CapturedRenderBatch
|
||||
{
|
||||
public CapturedRenderBatch(int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.Concurrent;
|
||||
|
||||
namespace Ignitor
|
||||
{
|
||||
public sealed class Operations
|
||||
{
|
||||
public ConcurrentQueue<CapturedRenderBatch> Batches { get; } = new ConcurrentQueue<CapturedRenderBatch>();
|
||||
|
||||
public ConcurrentQueue<string> DotNetCompletions { get; } = new ConcurrentQueue<string>();
|
||||
|
||||
public ConcurrentQueue<string> Errors { get; } = new ConcurrentQueue<string>();
|
||||
|
||||
public ConcurrentQueue<CapturedJSInteropCall> JSInteropCalls { get; } = new ConcurrentQueue<CapturedJSInteropCall>();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
|
@ -38,9 +34,9 @@ namespace Ignitor
|
|||
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
// Click the counter button 1000 times
|
||||
client.RenderBatchReceived += (int batchId, byte[] data) =>
|
||||
client.RenderBatchReceived += (batch) =>
|
||||
{
|
||||
if (batchId < 1000)
|
||||
if (batch.Id < 1000)
|
||||
{
|
||||
var _ = client.ClickAsync("thecounter");
|
||||
}
|
||||
|
|
@ -56,8 +52,8 @@ namespace Ignitor
|
|||
return 0;
|
||||
}
|
||||
|
||||
private static void OnJSInterop(int callId, string identifier, string argsJson) =>
|
||||
Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")");
|
||||
private static void OnJSInterop(CapturedJSInteropCall call) =>
|
||||
Console.WriteLine("JS Invoke: " + call.Identifier + " (" + call.ArgsJson + ")");
|
||||
|
||||
public Program()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,7 +56,14 @@
|
|||
</div>
|
||||
<footer class="footer border-top pl-3 text-muted">
|
||||
<div class="container">
|
||||
© @DateTime.Now.Year - @Environment.ApplicationName - <a asp-area="" asp-page="Privacy">Privacy</a>
|
||||
© @DateTime.Now.Year - @Environment.ApplicationName -
|
||||
@{
|
||||
var foundPrivacy = Url.Page("/Privacy", new { area = "" });
|
||||
}
|
||||
@if (foundPrivacy != null)
|
||||
{
|
||||
<a asp-area="" asp-page="/Privacy">Privacy</a>
|
||||
}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - Company.WebApplication1</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -46,8 +46,8 @@
|
|||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - Company.WebApplication1</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -45,8 +45,8 @@
|
|||
© copyrightYear - Company.WebApplication1 - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="~/lib/jquery/dist/jquery.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - Company.WebApplication1</title>
|
||||
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -42,8 +42,8 @@
|
|||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.Extensions.Internal
|
|||
|
||||
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
|
||||
{
|
||||
RunProcessAndWaitForExit(
|
||||
"pgrep",
|
||||
$"-P {parentId}",
|
||||
timeout,
|
||||
out var stdout);
|
||||
|
||||
if (!string.IsNullOrEmpty(stdout))
|
||||
try
|
||||
{
|
||||
using (var reader = new StringReader(stdout))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var text = reader.ReadLine();
|
||||
if (text == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RunProcessAndWaitForExit(
|
||||
"pgrep",
|
||||
$"-P {parentId}",
|
||||
timeout,
|
||||
out var stdout);
|
||||
|
||||
if (int.TryParse(text, out var id))
|
||||
if (!string.IsNullOrEmpty(stdout))
|
||||
{
|
||||
using (var reader = new StringReader(stdout))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
children.Add(id);
|
||||
// Recursively get the children
|
||||
GetAllChildIdsUnix(id, children, timeout);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -23,13 +23,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
|||
_logger = logger ?? NullLogger<DefaultHubProtocolResolver>.Instance;
|
||||
_availableProtocols = new Dictionary<string, IHubProtocol>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// We might get duplicates in _hubProtocols, but we're going to check it and overwrite in just a sec.
|
||||
_hubProtocols = availableProtocols.ToList();
|
||||
foreach (var protocol in _hubProtocols)
|
||||
foreach (var protocol in availableProtocols)
|
||||
{
|
||||
Log.RegisteredSignalRProtocol(_logger, protocol.Name, protocol.GetType());
|
||||
_availableProtocols[protocol.Name] = protocol;
|
||||
}
|
||||
_hubProtocols = _availableProtocols.Values.ToList();
|
||||
}
|
||||
|
||||
public virtual IHubProtocol GetProtocol(string protocolName, IReadOnlyList<string> supportedProtocols)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,22 @@ namespace Microsoft.AspNetCore.SignalR.Common.Protocol.Tests
|
|||
Assert.Same(jsonProtocol2, resolvedProtocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllProtocolsOnlyReturnsLatestOfSameType()
|
||||
{
|
||||
var jsonProtocol1 = new NewtonsoftJsonHubProtocol();
|
||||
var jsonProtocol2 = new NewtonsoftJsonHubProtocol();
|
||||
var resolver = new DefaultHubProtocolResolver(new[] {
|
||||
jsonProtocol1,
|
||||
jsonProtocol2
|
||||
}, NullLogger<DefaultHubProtocolResolver>.Instance);
|
||||
|
||||
var hubProtocols = resolver.AllProtocols;
|
||||
Assert.Equal(1, hubProtocols.Count);
|
||||
|
||||
Assert.Same(jsonProtocol2, hubProtocols[0]);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> HubProtocolNames => HubProtocolHelpers.AllProtocols.Select(p => new object[] {p.Name});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# Microsoft.dotnet-openapi
|
||||
|
||||
`Microsoft.dotnet-openapi` is a tool for managing OpenAPI references within your project.
|
||||
|
||||
## Commands
|
||||
|
||||
### Add Commands
|
||||
|
||||
<!-- TODO: Restore after https://github.com/aspnet/AspNetCore/issues/12738
|
||||
#### Add Project
|
||||
|
||||
##### Options
|
||||
|
||||
| Short option | Long option | Description | Example |
|
||||
|-------|------|-------|---------|
|
||||
| -v|--verbose | Show verbose output. |dotnet openapi add project *-v* ../Ref/ProjRef.csproj |
|
||||
| -p|--project | The project to operate on. |dotnet openapi add project *--project .\Ref.csproj* ../Ref/ProjRef.csproj |
|
||||
|
||||
##### Arguments
|
||||
|
||||
| Argument | Description | Example |
|
||||
|-------------|-------------|---------|
|
||||
| source-file | The source to create a reference from. Must be a project file. |dotnet openapi add project *../Ref/ProjRef.csproj* | -->
|
||||
|
||||
#### 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* <http://contoso.com/openapi.json> |
|
||||
| -p|--updateProject | The project to operate on. |dotnet openapi add url *--updateProject .\Ref.csproj* <http://contoso.com/openapi.json> |
|
||||
| -o|--output-file | Where to place the local copy of the OpenAPI file. |dotnet openapi add url <https://contoso.com/openapi.json> *--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 <https://contoso.com/openapi.json> |
|
||||
|
||||
### 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* <https://contoso.com/openapi.json> |
|
||||
| -p|--updateProject | The project to operate on. | dotnet openapi refresh *--updateProject .\Ref.csproj* <https://contoso.com/openapi.json> |
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Argument | Description | Example |
|
||||
| ------------|-------------|---------|
|
||||
| source-file | The URL to refresh the reference from. | dotnet openapi refresh *<https://contoso.com/openapi.json>* |
|
||||
|
|
@ -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<AssemblyInformationalVersionAttribute>();
|
||||
return attribute.InformationalVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> ExecuteCoreAsync()
|
||||
{
|
||||
ShowHelp();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
protected override bool ValidateArguments()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> ExecuteCoreAsync();
|
||||
|
||||
protected abstract bool ValidateArguments();
|
||||
|
||||
private async Task<int> 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<string, string>();
|
||||
|
||||
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<string> 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<CodeGenerator>(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<IDictionary<string, string>> 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<string, string>(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<string, string> GetServicePackages(CodeGenerator? type)
|
||||
{
|
||||
CodeGenerator generator = type ?? CodeGenerator.NSwagCSharp;
|
||||
var name = Enum.GetName(typeof(CodeGenerator), generator);
|
||||
var attributes = typeof(Program).Assembly.GetCustomAttributes<OpenApiDependencyAttribute>();
|
||||
|
||||
var packages = attributes.Where(a => a.CodeGenerators.Contains(generator));
|
||||
var result = new Dictionary<string, string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IHttpResponseMessageWrapper> GetResponseAsync(string url)
|
||||
{
|
||||
var response = await _client.GetAsync(url);
|
||||
|
||||
return new HttpResponseMessageWrapper(response);
|
||||
}
|
||||
|
||||
public Task<Stream> GetStreamAsync(string url)
|
||||
{
|
||||
return _client.GetStreamAsync(url);
|
||||
}
|
||||
}
|
||||
|
||||
public class HttpResponseMessageWrapper : IHttpResponseMessageWrapper
|
||||
{
|
||||
private HttpResponseMessage _response;
|
||||
|
||||
public HttpResponseMessageWrapper(HttpResponseMessage response)
|
||||
{
|
||||
_response = response;
|
||||
}
|
||||
|
||||
public Task<Stream> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IHttpResponseMessageWrapper> GetResponseAsync(string url);
|
||||
}
|
||||
}
|
||||
|
|
@ -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> Stream { get; }
|
||||
ContentDispositionHeaderValue ContentDisposition();
|
||||
HttpStatusCode StatusCode { get; }
|
||||
bool IsSuccessCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CodeGenerator>(c)).ToArray();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Version { get; set; }
|
||||
public IEnumerable<CodeGenerator> CodeGenerators { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<OutputType>exe</OutputType>
|
||||
<Description>Command line tool to add an OpenAPI service reference</Description>
|
||||
<RootNamespace>Microsoft.DotNet.Openapi.Tools</RootNamespace>
|
||||
<AssemblyName>dotnet-openapi</AssemblyName>
|
||||
<PackageId>Microsoft.dotnet-openapi</PackageId>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<!-- This package is for internal use only. It contains a CLI tool. -->
|
||||
<IsShippingPackage>false</IsShippingPackage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build" ExcludeAssets="runtime" />
|
||||
<Reference Include="Microsoft.Build.Locator" />
|
||||
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="Microsoft.DotNet.Openapi.Tools.Internal.OpenApiDependencyAttribute">
|
||||
<_Parameter1>NSwag.ApiDescription.Client</_Parameter1>
|
||||
<_Parameter2>$(NSwagApiDescriptionClientPackageVersion)</_Parameter2>
|
||||
<_Parameter3>NSwagCSharp;NSwagTypeScript</_Parameter3>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="Microsoft.DotNet.Openapi.Tools.Internal.OpenApiDependencyAttribute">
|
||||
<_Parameter1>Newtonsoft.Json</_Parameter1>
|
||||
<_Parameter2>$(NewtonsoftJsonPackageVersion)</_Parameter2>
|
||||
<_Parameter3>NSwagCSharp;NSwagTypeScript</_Parameter3>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string> metadata)
|
||||
{
|
||||
var item = project.AddItem(tagName, include).Single();
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
item.Xml.AddMetadata(kvp.Key, kvp.Value, expressAsAttribute: true);
|
||||
}
|
||||
|
||||
project.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiReference Include=\"{project.NSwagJsonFile}\"", content);
|
||||
}
|
||||
var projXml = new XmlDocument();
|
||||
projXml.Load(csproj.FullName);
|
||||
|
||||
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiReference);
|
||||
Assert.Same(openApiRefs[0].ParentNode, openApiRefs[1].ParentNode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenApi_Add_File_EquivilentPaths()
|
||||
{
|
||||
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);
|
||||
|
||||
app = GetApplication();
|
||||
var absolute = Path.GetFullPath(nswagJsonFile, project.Project.Dir().Root);
|
||||
run = app.Execute(new[] { "add", "file", absolute });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
var csproj = new FileInfo(project.Project.Path);
|
||||
var projXml = new XmlDocument();
|
||||
projXml.Load(csproj.FullName);
|
||||
|
||||
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiReference);
|
||||
Assert.Single(openApiRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenApi_Add_NSwagTypeScript()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: true);
|
||||
var nswagJsonFile = project.NSwagJsonFile;
|
||||
|
||||
var app = GetApplication();
|
||||
var run = app.Execute(new[] { "add", "file", nswagJsonFile, "--code-generator", "NSwagTypeScript" });
|
||||
|
||||
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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\" CodeGenerator=\"NSwagTypeScript\" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
|
||||
}
|
||||
|
||||
// Build project and make sure it compiles
|
||||
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_File_UseProjectOption()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: true);
|
||||
var nswagJsonFIle = project.NSwagJsonFile;
|
||||
|
||||
var app = GetApplication();
|
||||
var run = app.Execute(new[] { "add", "file", "--updateProject", project.Project.Path, 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFIle}\"", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenApi_Add_MultipleTimes_OnlyOneReference()
|
||||
{
|
||||
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);
|
||||
|
||||
app = GetApplication();
|
||||
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();
|
||||
var escapedPkgRef = Regex.Escape("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
|
||||
Assert.Single(Regex.Matches(content, escapedPkgRef));
|
||||
var escapedApiRef = Regex.Escape($"<OpenApiReference Include=\"{nswagJsonFile}\"");
|
||||
Assert.Single(Regex.Matches(content, escapedApiRef));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// 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 System.Xml;
|
||||
using Microsoft.DotNet.OpenApi.Tests;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.DotNet.OpenApi.Add.Tests
|
||||
{
|
||||
public class OpenApiAddProjectTests : OpenApiTestBase
|
||||
{
|
||||
public OpenApiAddProjectTests(ITestOutputHelper output) : base(output){}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
|
||||
public async Task OpenApi_Add_GlobbingOpenApi()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: true);
|
||||
|
||||
using (var refProj1 = project.Project.Dir().SubDir("refProj1"))
|
||||
using (var refProj2 = project.Project.Dir().SubDir("refProj2"))
|
||||
{
|
||||
var project1 = refProj1.WithCSharpProject("refProj");
|
||||
project1
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.Create();
|
||||
|
||||
var project2 = refProj2.WithCSharpProject("refProj2");
|
||||
project2
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.Create();
|
||||
|
||||
var app = GetApplication();
|
||||
|
||||
var run = app.Execute(new[] { "add", "project", project1.Path, project2.Path});
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
// csproj contents
|
||||
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
|
||||
using (var reader = new StreamReader(csprojStream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiProjectReference Include=\"{project1.Path}\"", content);
|
||||
Assert.Contains($"<OpenApiProjectReference Include=\"{project2.Path}\"", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
|
||||
public void OpenApi_Add_Project_EquivilentPaths()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: false);
|
||||
|
||||
using (var refProj = new TemporaryDirectory())
|
||||
{
|
||||
var refProjName = "refProj";
|
||||
var csproj = refProj.WithCSharpProject(refProjName);
|
||||
csproj
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.Create();
|
||||
|
||||
var app = GetApplication();
|
||||
var run = app.Execute(new[] { "add", "project", csproj.Path});
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
app = GetApplication();
|
||||
run = app.Execute(new[] { "add", "project", Path.Combine(csproj.Path, "..", "refProj.csproj")});
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
var projXml = new XmlDocument();
|
||||
projXml.Load(project.Project.Path);
|
||||
|
||||
var openApiRefs = projXml.GetElementsByTagName(Commands.BaseCommand.OpenApiProjectReference);
|
||||
Assert.Single(openApiRefs);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
|
||||
public async Task OpenApi_Add_FromCsProj()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: false);
|
||||
|
||||
using (var refProj = new TemporaryDirectory())
|
||||
{
|
||||
var refProjName = "refProj";
|
||||
refProj
|
||||
.WithCSharpProject(refProjName)
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.Create();
|
||||
|
||||
var app = GetApplication();
|
||||
var refProjFile = Path.Join(refProj.Root, $"{refProjName}.csproj");
|
||||
var run = app.Execute(new[] { "add", "project", refProjFile });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
// csproj contents
|
||||
using(var csprojStream = new FileInfo(project.Project.Path).OpenRead())
|
||||
using(var reader = new StreamReader(csprojStream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
// 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.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.OpenApi.Tests;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.DotNet.OpenApi.Add.Tests
|
||||
{
|
||||
public class OpenApiAddURLTests : OpenApiTestBase
|
||||
{
|
||||
public OpenApiAddURLTests(ITestOutputHelper output) : base(output){ }
|
||||
|
||||
[Fact]
|
||||
public async Task OpenApi_Add_Url_WithContentDisposition()
|
||||
{
|
||||
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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{url}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{firstExpectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{firstExpectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{secondExpectedJsonName}"" SourceUrl=""{NoExtensionUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" CodeGenerator=""NSwagCSharp"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" CodeGenerator=""NSwagTypeScript"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{FakeOpenApiUrl}"" />", content);
|
||||
Assert.DoesNotContain(
|
||||
$@"<OpenApiReference Include=""{expectedJsonName}"" SourceUrl=""{DifferentUrl}"" CodeGenerator=""NSwagCSharp"" />", 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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
|
||||
Assert.Single(Regex.Matches(content, escapedPkgRef));
|
||||
var escapedApiRef = Regex.Escape($"SourceUrl=\"{FakeOpenApiUrl}\"");
|
||||
Assert.Single(Regex.Matches(content, escapedApiRef));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAPi_Add_URL_InvalidUrl()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: false);
|
||||
|
||||
var app = GetApplication(realHttp: true);
|
||||
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(1, run);
|
||||
|
||||
var expectedJsonName = "dingos.json";
|
||||
|
||||
// csproj contents
|
||||
using (var csprojStream = new FileInfo(project.Project.Path).OpenRead())
|
||||
using (var reader = new StreamReader(csprojStream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.DoesNotContain("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.DoesNotContain($@"<OpenApiReference", content);
|
||||
}
|
||||
|
||||
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
|
||||
Assert.False(File.Exists(jsonFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenApi_Add_URL_ActualResponse()
|
||||
{
|
||||
var project = CreateBasicProject(withOpenApi: false);
|
||||
|
||||
var app = GetApplication(realHttp: true);
|
||||
var url = ActualUrl;
|
||||
var run = app.Execute(new[] { "add", "url", url });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
app = GetApplication(realHttp: true);
|
||||
run = app.Execute(new[] { "add", "url", url });
|
||||
|
||||
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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"");
|
||||
Assert.Single(Regex.Matches(content, escapedPkgRef));
|
||||
var escapedApiRef = Regex.Escape($"SourceUrl=\"{url}\"");
|
||||
Assert.Single(Regex.Matches(content, escapedApiRef));
|
||||
Assert.Contains(
|
||||
$@"<OpenApiReference Include=""api-with-examples.yaml"" SourceUrl=""{ActualUrl}"" />", content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
|
||||
}
|
||||
|
||||
var remove = GetApplication();
|
||||
var removeRun = remove.Execute(new[] { "remove", nswagJsonFile });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, removeRun);
|
||||
|
||||
// csproj contents
|
||||
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();
|
||||
// Don't remove the package reference, they might have taken other dependencies on it
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.DoesNotContain($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
|
||||
}
|
||||
Assert.False(File.Exists(Path.Combine(_tempDir.Root, nswagJsonFile)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenApi_Remove_ViaUrl()
|
||||
{
|
||||
_tempDir
|
||||
.WithCSharpProject("testproj")
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.WithContentFile("Startup.cs")
|
||||
.Create();
|
||||
|
||||
var add = GetApplication();
|
||||
var run = add.Execute(new[] { "add", "url", FakeOpenApiUrl });
|
||||
|
||||
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();
|
||||
// Don't remove the package reference, they might have taken other dependencies on it
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
}
|
||||
|
||||
var remove = GetApplication();
|
||||
var removeRun = remove.Execute(new[] { "remove", FakeOpenApiUrl });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, removeRun);
|
||||
|
||||
// csproj contents
|
||||
csproj = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj"));
|
||||
using var removedCsprojStream = csproj.OpenRead();
|
||||
using var removedReader = new StreamReader(removedCsprojStream);
|
||||
var removedContent = await removedReader.ReadToEndAsync();
|
||||
// Don't remove the package reference, they might have taken other dependencies on it
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", removedContent);
|
||||
Assert.DoesNotContain($"<OpenApiReference", removedContent);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12738")]
|
||||
public async Task OpenApi_Remove_Project()
|
||||
{
|
||||
_tempDir
|
||||
.WithCSharpProject("testproj")
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.WithContentFile("Startup.cs")
|
||||
.Create();
|
||||
|
||||
using var refProj = new TemporaryDirectory();
|
||||
var refProjName = "refProj";
|
||||
refProj
|
||||
.WithCSharpProject(refProjName)
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.Create();
|
||||
|
||||
var app = GetApplication();
|
||||
var refProjFile = Path.Join(refProj.Root, $"{refProjName}.csproj");
|
||||
var run = app.Execute(new[] { "add", "project", refProjFile });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
// csproj contents
|
||||
using (var csprojStream = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj")).OpenRead())
|
||||
using (var reader = new StreamReader(csprojStream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.Contains($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
|
||||
}
|
||||
|
||||
var remove = GetApplication();
|
||||
run = app.Execute(new[] { "remove", refProjFile });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
// csproj contents
|
||||
using (var csprojStream = new FileInfo(Path.Join(_tempDir.Root, "testproj.csproj")).OpenRead())
|
||||
using (var reader = new StreamReader(csprojStream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.DoesNotContain($"<OpenApiProjectReference Include=\"{refProjFile}\"", content);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenApi_Remove_Multiple()
|
||||
{
|
||||
var nswagJsonFile = "openapi.json";
|
||||
var swagFile2 = "swag2.json";
|
||||
_tempDir
|
||||
.WithCSharpProject("testproj")
|
||||
.WithTargetFrameworks("netcoreapp3.0")
|
||||
.Dir()
|
||||
.WithContentFile(nswagJsonFile)
|
||||
.WithFile(swagFile2)
|
||||
.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);
|
||||
|
||||
add = GetApplication();
|
||||
run = add.Execute(new[] { "add", "file", swagFile2 });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, run);
|
||||
|
||||
var remove = GetApplication();
|
||||
var removeRun = remove.Execute(new[] { "remove", nswagJsonFile, swagFile2 });
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(_error.ToString()), $"Threw error: {_error.ToString()}");
|
||||
Assert.Equal(0, removeRun);
|
||||
|
||||
// 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();
|
||||
// Don't remove the package reference, they might have taken other dependencies on it
|
||||
Assert.Contains("<PackageReference Include=\"NSwag.ApiDescription.Client\" Version=\"", content);
|
||||
Assert.DoesNotContain($"<OpenApiReference Include=\"{nswagJsonFile}\"", content);
|
||||
}
|
||||
Assert.False(File.Exists(Path.Combine(_tempDir.Root, nswagJsonFile)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Openapi.Tools;
|
||||
using Microsoft.Extensions.Tools.Internal;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.DotNet.OpenApi.Tests
|
||||
{
|
||||
public class OpenApiTestBase : IDisposable
|
||||
{
|
||||
protected readonly TemporaryDirectory _tempDir;
|
||||
protected readonly TextWriter _output = new StringWriter();
|
||||
protected readonly TextWriter _error = new StringWriter();
|
||||
protected readonly ITestOutputHelper _outputHelper;
|
||||
|
||||
protected const string Content = @"{""x-generator"": ""NSwag""}";
|
||||
protected const string ActualUrl = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/api-with-examples.yaml";
|
||||
protected const string BrokenUrl = "https://www.microsoft.com/en-us/dingos.json";
|
||||
protected const string FakeOpenApiUrl = "https://contoso.com/openapi.json";
|
||||
protected const string NoDispositionUrl = "https://contoso.com/nodisposition.yaml";
|
||||
protected const string NoExtensionUrl = "https://contoso.com/noextension";
|
||||
protected const string NoSegmentUrl = "https://contoso.com";
|
||||
protected const string DifferentUrl = "https://contoso.com/different.json";
|
||||
protected const string PackageUrl = "https://go.microsoft.com/fwlink/?linkid=2099561";
|
||||
protected const string DifferentUrlContent = @"
|
||||
{
|
||||
""x-generator"": ""NSwag""
|
||||
}";
|
||||
protected const string PackageUrlContent = @"
|
||||
{
|
||||
""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""
|
||||
}
|
||||
}";
|
||||
|
||||
public OpenApiTestBase(ITestOutputHelper output)
|
||||
{
|
||||
_tempDir = new TemporaryDirectory();
|
||||
_outputHelper = output;
|
||||
}
|
||||
|
||||
public TemporaryNSwagProject CreateBasicProject(bool withOpenApi)
|
||||
{
|
||||
var nswagJsonFile = "openapi.json";
|
||||
var project = _tempDir
|
||||
.WithCSharpProject("testproj", sdk: "Microsoft.NET.Sdk.Web")
|
||||
.WithTargetFrameworks("netcoreapp3.0");
|
||||
var tmp = project.Dir();
|
||||
|
||||
if (withOpenApi)
|
||||
{
|
||||
tmp = tmp.WithContentFile(nswagJsonFile);
|
||||
}
|
||||
|
||||
tmp.WithContentFile("Startup.cs")
|
||||
.Create();
|
||||
|
||||
return new TemporaryNSwagProject(project, nswagJsonFile);
|
||||
}
|
||||
|
||||
internal Application GetApplication(bool realHttp = false)
|
||||
{
|
||||
IHttpClientWrapper wrapper;
|
||||
if (realHttp)
|
||||
{
|
||||
wrapper = new HttpClientWrapper(new HttpClient());
|
||||
}
|
||||
else
|
||||
{
|
||||
wrapper = new TestHttpClientWrapper(DownloadMock());
|
||||
}
|
||||
return new Application(
|
||||
_tempDir.Root, wrapper, _output, _error);
|
||||
}
|
||||
|
||||
private IDictionary<string, Tuple<string, ContentDispositionHeaderValue>> 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<string, Tuple<string, ContentDispositionHeaderValue>> {
|
||||
{ FakeOpenApiUrl, Tuple.Create(Content, extension)},
|
||||
{ DifferentUrl, Tuple.Create<string, ContentDispositionHeaderValue>(DifferentUrlContent, null) },
|
||||
{ PackageUrl, Tuple.Create<string, ContentDispositionHeaderValue>(PackageUrlContent, null) },
|
||||
{ NoDispositionUrl, Tuple.Create<string, ContentDispositionHeaderValue>(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<string, Tuple<string, ContentDispositionHeaderValue>> _results;
|
||||
|
||||
public TestHttpClientWrapper(IDictionary<string, Tuple<string, ContentDispositionHeaderValue>> results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IHttpResponseMessageWrapper> GetResponseAsync(string url)
|
||||
{
|
||||
var result = _results[url];
|
||||
byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1);
|
||||
var stream = new MemoryStream(byteArray);
|
||||
|
||||
return Task.FromResult<IHttpResponseMessageWrapper>(new TestHttpResponseMessageWrapper(stream, result.Item2));
|
||||
}
|
||||
}
|
||||
|
||||
public class TestHttpResponseMessageWrapper : IHttpResponseMessageWrapper
|
||||
{
|
||||
public Task<Stream> 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>(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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> _stdoutLines = new BlockingCollection<string>();
|
||||
private TaskCompletionSource<int> _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<int>(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<string, string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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<Startup>()
|
||||
.UseKestrel()
|
||||
.UseIISIntegration();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<AssemblyName>Microsoft.DotNet.Open.Api.Tools.Tests</AssemblyName>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);TestProjects\**\*</DefaultItemExcludes>
|
||||
<TestGroupName>DotNetAddOpenAPIReferenceToolsTests</TestGroupName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OpenAPIToolCSProjPath>..\src\Microsoft.dotnet-openapi.csproj</OpenAPIToolCSProjPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(ToolSharedSourceRoot)TestHelpers\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" />
|
||||
<Content Include="TestContent\*" LinkBase="TestContent\" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Compile Include="$(SharedSourceRoot)Process\ProcessExtensions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build" ExcludeAssets="runtime" />
|
||||
<ProjectReference Include="$(OpenAPIToolCSProjPath)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>TestSettings:RestoreSources</_Parameter1>
|
||||
<_Parameter2>$(RestoreSources)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>TestSettings:RuntimeFrameworkVersion</_Parameter1>
|
||||
<_Parameter2>$(RuntimeFrameworkVersion)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>RepoRoot</_Parameter1>
|
||||
<_Parameter2>$(RepoRoot)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CleanTestProjects" BeforeTargets="CoreCompile">
|
||||
<RemoveDir Directories="$(TargetDir)TestProjects" Condition="Exists('$(TargetDir)TestProjects')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishDotNetOpenApiOnBuild" BeforeTargets="Build" Condition="'$(DotNetBuildFromSource)' != 'true'">
|
||||
<MSBuild Projects="$(OpenAPIToolCSProjPath)" Targets="Publish" Properties="PublishDir=$(OutputPath)\tool\;Configuration=$(Configuration)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishDotNetOpenApiOnPublish" BeforeTargets="Publish" Condition="'$(DotNetBuildFromSource)' != 'true'">
|
||||
<MSBuild Projects="$(OpenAPIToolCSProjPath)" Targets="Publish" Properties="PublishDir=$(PublishDir)\tool\;Configuration=$(Configuration)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"longRunningTestSeconds": 30,
|
||||
"diagnosticMessages": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
|
|
@ -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 <https://github.com/aspnet/DotNetTools/tree/rel/2.0.0/README.md> 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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.CommandLineUtils
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||
namespace Microsoft.Extensions.Tools.Internal
|
||||
{
|
||||
public class TemporaryCSharpProject
|
||||
{
|
||||
private const string Template =
|
||||
@"<Project Sdk=""Microsoft.NET.Sdk"">
|
||||
@"<Project Sdk=""{2}"">
|
||||
<PropertyGroup>
|
||||
{0}
|
||||
<OutputType>Exe</OutputType>
|
||||
|
|
@ -23,19 +23,22 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
|
||||
private readonly string _filename;
|
||||
private readonly TemporaryDirectory _directory;
|
||||
private List<string> _items = new List<string>();
|
||||
private List<string> _properties = new List<string>();
|
||||
private readonly List<string> _items = new List<string>();
|
||||
private readonly List<string> _properties = new List<string>();
|
||||
|
||||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
|
||||
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
|
||||
|
|
@ -5,7 +5,7 @@ using System;
|
|||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
|
||||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
|
||||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
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)
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@ECHO OFF
|
||||
SET RepoRoot=%~dp0..\..
|
||||
%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %*
|
||||
|
|
@ -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" "$@"
|
||||
|
|
@ -104,10 +104,10 @@ namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
|||
app.HelpOption("-h|--help");
|
||||
|
||||
app.OnExecute(() =>
|
||||
{
|
||||
app.ShowHelp();
|
||||
return Success;
|
||||
});
|
||||
{
|
||||
app.ShowHelp();
|
||||
return Success;
|
||||
});
|
||||
|
||||
return app.Execute(args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,20 @@ namespace Microsoft.DotNet.Watcher
|
|||
{
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.DotNet.Watcher
|
|||
string projectFile;
|
||||
try
|
||||
{
|
||||
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
|
||||
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
|
|
@ -177,7 +177,7 @@ namespace Microsoft.DotNet.Watcher
|
|||
string projectFile;
|
||||
try
|
||||
{
|
||||
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDir, project);
|
||||
projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
|
|
@ -205,7 +205,7 @@ namespace Microsoft.DotNet.Watcher
|
|||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)Process\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Process\ProcessExtensions.cs" />
|
||||
<Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" />
|
||||
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
.WithTargetFrameworks("netcoreapp1.0", "net451")
|
||||
.WithProjectReference(proj2)
|
||||
.Dir()
|
||||
.WithFile("Class1.cs");
|
||||
.WithFile("Class1.cs")
|
||||
.Up()
|
||||
.Up()
|
||||
.Create();
|
||||
|
||||
var fileset = await GetFileSet(target);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
{
|
||||
private readonly TemporaryDirectory _directory;
|
||||
private Action<TemporaryCSharpProject> _onCreate;
|
||||
private Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
|
||||
private readonly Dictionary<string, TemporaryCSharpProject> _projects = new Dictionary<string, TemporaryCSharpProject>();
|
||||
public TestProjectGraph(TemporaryDirectory directory)
|
||||
{
|
||||
_directory = directory;
|
||||
|
|
@ -28,8 +29,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
|
||||
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 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
|||
return sourceProj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
|
|||
Loading…
Reference in New Issue