Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Templates testing framework to test through API #5677

Merged
merged 6 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Microsoft.TemplateEngine.sln
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.TemplateEngine.Co
tools\Shared\Microsoft.TemplateEngine.CommandUtils\TestCommand.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\TestCommand.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateApiVerifier", "tools\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj", "{449B9DDA-F18C-411E-9A74-3930652BB78A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "template-samples", "template-samples", "{3A2D12C2-0455-4471-9EBB-91749BA3A60F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Samples", "dotnet-template-samples\Microsoft.TemplateEngine.Samples.csproj", "{DFB06A25-719F-4C8B-B84D-55D2D601BEF6}"
Expand Down Expand Up @@ -474,6 +476,18 @@ Global
{E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x64.Build.0 = Release|Any CPU
{E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x86.ActiveCfg = Release|Any CPU
{E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x86.Build.0 = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x64.ActiveCfg = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x64.Build.0 = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x86.ActiveCfg = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x86.Build.0 = Debug|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|Any CPU.Build.0 = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x64.ActiveCfg = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x64.Build.0 = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x86.ActiveCfg = Release|Any CPU
{449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x86.Build.0 = Release|Any CPU
{DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -524,6 +538,7 @@ Global
{D190251C-5649-4DD6-A158-16D119116352} = {C5186341-2064-49AA-B398-CDF4302D2823}
{43053BC4-32B4-4404-B62D-410F367CE0CE} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B}
{EE8CD472-D8C4-4CD0-BC84-6C305F5971AE} = {43053BC4-32B4-4404-B62D-410F367CE0CE}
{449B9DDA-F18C-411E-9A74-3930652BB78A} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B}
{DFB06A25-719F-4C8B-B84D-55D2D601BEF6} = {3A2D12C2-0455-4471-9EBB-91749BA3A60F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
11 changes: 10 additions & 1 deletion src/Microsoft.TemplateEngine.Edge/DefaultEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Edge
{
Expand All @@ -18,7 +19,10 @@ public sealed class DefaultEnvironment : IEnvironment
private const int DefaultBufferWidth = 160;
private readonly IReadOnlyDictionary<string, string> _environmentVariables;

public DefaultEnvironment()
public DefaultEnvironment() : this(null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a thought - would it be better to do own implementation? Maybe something as ExtendedEnvironment?

another thought: https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.TemplateEngine.Cli/CliEnvironment.cs we can get rid of this custom implementation with this class now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were few options:

  • Unseal the DefaultEnvironment and extend.
  • Copy paste DefaultEnvironment logic and add custom logic
  • Add the logic into DefaultEnvironment

The last one felt as the one with least dept incured. Though I might miss some obvious reason why it might not be an optional choice - so I'm open to alternative thoughts :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference is the first one, just to intentionally keep it separate, so it is not misused, however I don't insist.

{ }

public DefaultEnvironment(IReadOnlyDictionary<string, string>? additionalVirtualEnvironemnt)
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
{
Dictionary<string, string> variables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
IDictionary env = System.Environment.GetEnvironmentVariables();
Expand All @@ -28,6 +32,11 @@ public DefaultEnvironment()
variables[key] = (env[key] as string) ?? string.Empty;
}

if (additionalVirtualEnvironemnt != null)
{
variables.Merge(additionalVirtualEnvironemnt);
}

_environmentVariables = variables;
NewLine = System.Environment.NewLine;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.TemplateEngine.Edge/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Microsoft.TemplateEngine.Edge.Settings.ITemplateInfoHostJsonCache.HostData.get -> string?
Microsoft.TemplateEngine.Edge.DefaultEnvironment.DefaultEnvironment(System.Collections.Generic.IReadOnlyDictionary<string!, string!>? additionalVirtualEnvironemnt) -> void
Microsoft.TemplateEngine.Edge.Settings.ITemplateInfoHostJsonCache.HostData.get -> string?
60 changes: 51 additions & 9 deletions src/Microsoft.TemplateEngine.IDE/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@
using System.Threading.Tasks;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.Installer;
using Microsoft.TemplateEngine.Abstractions.TemplateFiltering;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Edge;
using Microsoft.TemplateEngine.Edge.Settings;
using Microsoft.TemplateEngine.Utils;
using Microsoft.TemplateEngine.Edge.Template;
using ITemplateCreationResult = Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult;
using ITemplateMatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo;
using MatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo;
using TemplateCreator = Microsoft.TemplateEngine.Edge.Template.TemplateCreator;
using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters;

namespace Microsoft.TemplateEngine.IDE
{
public class Bootstrapper : IDisposable
{
private readonly ITemplateEngineHost _host;
private readonly TemplateCreator _templateCreator;
private readonly Edge.Settings.TemplatePackageManager _templatePackagesManager;
private readonly TemplatePackageManager _templatePackagesManager;
private readonly EngineEnvironmentSettings _engineEnvironmentSettings;

/// <summary>
Expand All @@ -35,19 +37,25 @@ public class Bootstrapper : IDisposable
/// <param name="loadDefaultComponents">if true, the default components (providers, installers, generator) will be loaded. Same as calling <see cref="LoadDefaultComponents()"/> after instance is created.</param>
/// <param name="hostSettingsLocation">the file path to store host specific settings. Use null for default location.
/// Note: this parameter changes only directory of host and host version specific settings. Global settings path remains unchanged.</param>
public Bootstrapper(ITemplateEngineHost host, bool virtualizeConfiguration, bool loadDefaultComponents = true, string? hostSettingsLocation = null)
/// <param name="additionalVirtualEnvironemnt">optional set of environment variables to be used.</param>
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
JanKrivanek marked this conversation as resolved.
Show resolved Hide resolved
public Bootstrapper(
ITemplateEngineHost host,
bool virtualizeConfiguration,
bool loadDefaultComponents = true,
string? hostSettingsLocation = null,
IReadOnlyDictionary<string, string>? additionalVirtualEnvironemnt = null)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
IEnvironment environment = new DefaultEnvironment(additionalVirtualEnvironemnt);

if (string.IsNullOrWhiteSpace(hostSettingsLocation))
{
_engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: virtualizeConfiguration);
_engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: virtualizeConfiguration, environment: environment);
}
else
{
string hostSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier);
string hostVersionSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier, host.Version);
IEnvironment environment = new DefaultEnvironment();
IPathInfo pathInfo = new DefaultPathInfo(environment, host, hostSettingsDir: hostSettingsDir, hostVersionSettingsDir: hostVersionSettingsDir);
_engineEnvironmentSettings = new EngineEnvironmentSettings(
host,
Expand All @@ -57,7 +65,7 @@ public Bootstrapper(ITemplateEngineHost host, bool virtualizeConfiguration, bool
}

_templateCreator = new TemplateCreator(_engineEnvironmentSettings);
_templatePackagesManager = new Edge.Settings.TemplatePackageManager(_engineEnvironmentSettings);
_templatePackagesManager = new TemplatePackageManager(_engineEnvironmentSettings);
if (loadDefaultComponents)
{
LoadDefaultComponents();
Expand All @@ -73,7 +81,7 @@ public void LoadDefaultComponents()
{
AddComponent(component.Type, component.Instance);
}
foreach ((Type Type, IIdentifiedComponent Instance) component in Edge.Components.AllComponents)
foreach ((Type Type, IIdentifiedComponent Instance) component in Components.AllComponents)
{
AddComponent(component.Type, component.Instance);
}
Expand Down Expand Up @@ -103,7 +111,7 @@ public Task<IReadOnlyList<ITemplateInfo>> GetTemplatesAsync(CancellationToken ca
/// <summary>
/// Gets list of available templates, if <paramref name="filters"/> is provided returns only matching templates.
/// </summary>
/// <param name="filters">List of filters to apply. See <see cref="WellKnownSearchFilters"/> for predefined filters.</param>
/// <param name="filters">List of filters to apply. See <see cref="Utils.WellKnownSearchFilters"/> for predefined filters.</param>
/// <param name="exactMatchesOnly">
/// true: templates should match all filters; false: templates should match any filter.
/// </param>
Expand Down Expand Up @@ -158,6 +166,40 @@ public Task<ITemplateCreationResult> CreateAsync(
cancellationToken: cancellationToken);
}

/// <summary>
/// Instantiates the template.
/// </summary>
/// <param name="info">The template to instantiate.</param>
/// <param name="name">The name to use.</param>
/// <param name="outputPath">The output directory for template instantiation.</param>
/// <param name="inputParameters">The template parameters.</param>
/// <param name="baselineName">The baseline configuration to use.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns><see cref="ITemplateCreationResult"/> containing information on created template or error occurred.</returns>
#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
public Task<ITemplateCreationResult> CreateAsync(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a thought - should we make other one obsolete?
also we are hardly using baselines, not sure if we should keep it and leave the warning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other CreateAsync (with params as dictionary) has legitimate use for quick testing with set of params - I'd keep it as is

ITemplateInfo info,
string? name,
string outputPath,
InputDataSet? inputParameters,
string? baselineName = null,
CancellationToken cancellationToken = default)
#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
{
return _templateCreator.InstantiateAsync(
info,
name,
fallbackName: null,
outputPath: outputPath,
inputParameters: inputParameters,
forceCreation: false,
baselineName: baselineName,
dryRun: false,
cancellationToken: cancellationToken);
}

/// <summary>
/// Dry runs the template with given parameters.
/// </summary>
Expand Down
1 change: 0 additions & 1 deletion src/Microsoft.TemplateEngine.IDE/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Microsoft.TemplateEngine.IDE.Bootstrapper.Uninstall(string! path) -> System.Coll
Microsoft.TemplateEngine.IDE.Bootstrapper.Uninstall(System.Collections.Generic.IEnumerable<string!>! paths) -> System.Collections.Generic.IEnumerable<string!>!
Microsoft.TemplateEngine.IDE.Bootstrapper.UninstallTemplatePackagesAsync(System.Collections.Generic.IEnumerable<Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage!>! managedPackages, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult!>!>!
Microsoft.TemplateEngine.IDE.Bootstrapper.UpdateTemplatePackagesAsync(System.Collections.Generic.IEnumerable<Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest!>! updateRequests, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult!>!>!
Microsoft.TemplateEngine.IDE.Bootstrapper.Bootstrapper(Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! host, bool virtualizeConfiguration, bool loadDefaultComponents = true, string? hostSettingsLocation = null) -> void
Microsoft.TemplateEngine.IDE.Bootstrapper.GetManagedTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage!>!>!
Microsoft.TemplateEngine.IDE.Bootstrapper.GetTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage!>!>!
Microsoft.TemplateEngine.IDE.Bootstrapper.GetTemplatesAsync(System.Collections.Generic.IEnumerable<System.Func<Microsoft.TemplateEngine.Abstractions.ITemplateInfo!, Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo?>!>! filters, bool exactMatchesOnly = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo!>!>!
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.TemplateEngine.IDE/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

Microsoft.TemplateEngine.IDE.Bootstrapper.Bootstrapper(Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! host, bool virtualizeConfiguration, bool loadDefaultComponents = true, string? hostSettingsLocation = null, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? additionalVirtualEnvironemnt = null) -> void
Microsoft.TemplateEngine.IDE.Bootstrapper.CreateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string? name, string! outputPath, Microsoft.TemplateEngine.Edge.Template.InputDataSet? inputParameters, string? baselineName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult!>!
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier;
using Microsoft.TemplateEngine.TestHelper;
using Microsoft.TemplateEngine.Tests;
using Xunit.Abstractions;
Expand All @@ -17,6 +18,10 @@ public ExampleTemplateTest(ITestOutputHelper log)
_log = new XunitLoggerProvider(log).CreateLogger("TestRun");
}

// Following 2 tests share identical snapshot folder - that's the reason for the additional
// naming parameters (DoNotPrependCallerMethodNameToScenarioName, DoNotAppendTemplateArgsToScenarioName, ScenarioName)
// The identity of snapshots ilustrates that execution through API and through full blown command leads to identical results

[Fact]
public async void VerificationEngineSampleDogfoodTest()
{
Expand All @@ -33,10 +38,47 @@ public async void VerificationEngineSampleDogfoodTest()
SnapshotsDirectory = "Snapshots",
OutputDirectory = workingDir,
VerifyCommandOutput = true,
DoNotPrependTemplateNameToScenarioName = true,
DoNotPrependCallerMethodNameToScenarioName = true,
DoNotAppendTemplateArgsToScenarioName = true,
ScenarioName = "SampleDogfoodTest",
// This is here just for testing and documentation purposes - showing functionality of differing snapshots for arch
UniqueFor = UniqueForOption.Architecture,
}
.WithCustomScrubbers(
ScrubbersDefinition.Empty
.AddScrubber(sb => sb.Replace("B is enabled", "*******"))
.AddScrubber((path, content) =>
{
if (path.Replace(Path.DirectorySeparatorChar, '/') == "std-streams/stdout.txt")
{
content.Replace("SampleTestTemplate", "%TEMPLATE%");
}
}));

VerificationEngine engine = new VerificationEngine(_log);
await engine.Execute(options);
}

[Fact]
public async void VerificationEngineSampleDogfoodTest_ExecThroughApi()
{
string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty));
string templateShortName = "TestAssets.SampleTestTemplate";

//get the template location
string templateLocation = Path.Combine(TestTemplatesLocation, "TestTemplate");

TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName)
{
TemplatePath = templateLocation,
SnapshotsDirectory = "Snapshots",
OutputDirectory = workingDir,
VerifyCommandOutput = true,
DoNotPrependCallerMethodNameToScenarioName = true,
ScenarioName = "SampleDogfoodTest",
UniqueFor = UniqueForOption.Architecture,
}
.WithInstantiationThroughTemplateCreatorApi(new Dictionary<string, string?>() { { "paramB", "true" } })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YuliiaKovalova this can be inspiration how the imported sample templates might be tested (only templateName, TemplatePath and WithInstantiationThroughTemplateCreatorApi will be needed - rest is here to unit test the actual testing functionality)

.WithCustomScrubbers(
ScrubbersDefinition.Empty
.AddScrubber(sb => sb.Replace("B is enabled", "*******"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ProjectReference Include="$(ToolsDir)Microsoft.TemplateEngine.Authoring.TemplateVerifier\Microsoft.TemplateEngine.Authoring.TemplateVerifier.csproj" />
<ProjectReference Include="$(TestDir)Microsoft.TemplateEngine.TestHelper\Microsoft.TemplateEngine.TestHelper.csproj" />
<ProjectReference Include="$(TestDir)Microsoft.TemplateEngine.TestTemplates\Microsoft.TemplateEngine.TestTemplates.csproj" />
<ProjectReference Include="$(ToolsDir)Microsoft.TemplateEngine.Authoring.TemplateApiVerifier\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,6 @@ public IEngineEnvironmentSettings CreateEnvironment(
{
locale = "en-US";
}
var builtIns = new List<(Type, IIdentifiedComponent)>();
vlada-shubina marked this conversation as resolved.
Show resolved Hide resolved
if (additionalComponents != null)
{
builtIns.AddRange(additionalComponents);
}
builtIns.AddRange(Edge.Components.AllComponents);
if (loadDefaultGenerator)
{
builtIns.AddRange(Orchestrator.RunnableProjects.Components.AllComponents);
}

IEnumerable<ILoggerProvider> loggerProviders = new[] { new XunitLoggerProvider(_testOutputHelper) };
if (addLoggerProviders != null)
Expand Down
Loading