Merge branch 'release/2.2' into merge/release/2.1-to-release/2.2\n\nCommit migrated from a83df959fc
This commit is contained in:
commit
39273cd311
|
|
@ -31,12 +31,13 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
|
|||
/// </summary>
|
||||
public override void Load()
|
||||
{
|
||||
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (Source.FileProvider == null)
|
||||
{
|
||||
if (Source.Optional)
|
||||
{
|
||||
Data = data;
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
|
@ -63,10 +64,12 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
|
|||
{
|
||||
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
|
||||
{
|
||||
Data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
|
||||
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
|
@ -28,7 +30,7 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
|
|||
public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
|
||||
{
|
||||
var e = Assert.Throws<ArgumentException>(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
|
||||
Assert.Contains("The directory name", e.Message);
|
||||
Assert.Contains("The path must be absolute.", e.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -177,6 +179,39 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
|
|||
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BindingDoesNotThrowIfReloadedDuringBinding()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Number", "-2"),
|
||||
new TestFile("Text", "Foo"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
MyOptions options = null;
|
||||
|
||||
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
|
||||
{
|
||||
_ = Task.Run(() => { while (!cts.IsCancellationRequested) config.Reload(); });
|
||||
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
options = config.Get<MyOptions>();
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(-2, options.Number);
|
||||
Assert.Equal("Foo", options.Text);
|
||||
}
|
||||
|
||||
private sealed class MyOptions
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
class TestFileProvider : IFileProvider
|
||||
|
|
@ -305,4 +340,4 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
|
|||
return new MemoryStream(Encoding.UTF8.GetBytes(_contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.KeyPerFile" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@
|
|||
<ProjectReference Include="..\..\Manifest.MSBuildTask\src\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SignedPackageFile Include="$(TargetPath)" Certificate="$(AssemblySigningCertName)" />
|
||||
<SignedPackageFile Include="Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PopulateNuspec" BeforeTargets="GenerateNuspec" DependsOnTargets="BuiltProjectOutputGroup;DocumentationProjectOutputGroup;DebugSymbolsProjectOutputGroup;">
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public sealed class HealthCheckContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="HealthCheckRegistration"/> of the currently executing <see cref="IHealthCheck"/>.
|
||||
/// </summary>
|
||||
public HealthCheckRegistration Registration { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the registration information associated with an <see cref="IHealthCheck"/> implementation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The health check registration is provided as a separate object so that application developers can customize
|
||||
/// how health check implementations are configured.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The registration is provided to an <see cref="IHealthCheck"/> implementation during execution through
|
||||
/// <see cref="HealthCheckContext.Registration"/>. This allows a health check implementation to access named
|
||||
/// options or perform other operations based on the registered name.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class HealthCheckRegistration
|
||||
{
|
||||
private Func<IServiceProvider, IHealthCheck> _factory;
|
||||
private string _name;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="instance">The <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported upon failure of the health check. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable<string> tags)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
FailureStatus = failureStatus ?? HealthStatus.Unhealthy;
|
||||
Tags = new HashSet<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
Factory = (_) => instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="factory">A delegate used to create the <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
public HealthCheckRegistration(
|
||||
string name,
|
||||
Func<IServiceProvider, IHealthCheck> factory,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
FailureStatus = failureStatus ?? HealthStatus.Unhealthy;
|
||||
Tags = new HashSet<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
Factory = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate used to create the <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IHealthCheck> Factory
|
||||
{
|
||||
get => _factory;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_factory = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="HealthStatus"/> that should be reported upon failure of the health check.
|
||||
/// </summary>
|
||||
public HealthStatus FailureStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the health check name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of tags that can be used for filtering health checks.
|
||||
/// </summary>
|
||||
public ISet<string> Tags { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a health check.
|
||||
/// </summary>
|
||||
public struct HealthCheckResult
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="status"/>,
|
||||
/// <paramref name="exception"/>, <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A value indicating the status of the component that was checked.</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public HealthCheckResult(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
Status = status;
|
||||
Description = description;
|
||||
Exception = exception;
|
||||
Data = data ?? _emptyReadOnlyDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional key-value pairs describing the health of the component.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the status of the component that was checked.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the status of the component that was checked.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
|
||||
public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a degraded component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a degraged component.</returns>
|
||||
public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of executing a group of <see cref="IHealthCheck"/> instances.
|
||||
/// </summary>
|
||||
public sealed class HealthReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new <see cref="HealthReport"/> from the specified results.
|
||||
/// </summary>
|
||||
/// <param name="entries">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
|
||||
/// <param name="totalDuration">A value indicating the time the health check service took to execute.</param>
|
||||
public HealthReport(IReadOnlyDictionary<string, HealthReportEntry> entries, TimeSpan totalDuration)
|
||||
{
|
||||
Entries = entries;
|
||||
Status = CalculateAggregateStatus(entries.Values);
|
||||
TotalDuration = totalDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The keys in this dictionary map the name of each executed health check to a <see cref="HealthReportEntry"/> for the
|
||||
/// result data retruned from the corresponding health check.
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<string, HealthReportEntry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HealthStatus"/> representing the aggregate status of all the health checks. The value of <see cref="Status"/>
|
||||
/// will be the most servere status reported by a health check. If no checks were executed, the value is always <see cref="HealthStatus.Healthy"/>.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time the health check service took to execute.
|
||||
/// </summary>
|
||||
public TimeSpan TotalDuration { get; }
|
||||
|
||||
private HealthStatus CalculateAggregateStatus(IEnumerable<HealthReportEntry> entries)
|
||||
{
|
||||
// This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list
|
||||
var currentValue = HealthStatus.Healthy;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (currentValue > entry.Status)
|
||||
{
|
||||
currentValue = entry.Status;
|
||||
}
|
||||
|
||||
if (currentValue == HealthStatus.Unhealthy)
|
||||
{
|
||||
// Game over, man! Game over!
|
||||
// (We hit the worst possible status, so there's no need to keep iterating)
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an entry in a <see cref="HealthReport"/>. Corresponds to the result of a single <see cref="IHealthCheck"/>.
|
||||
/// </summary>
|
||||
public struct HealthReportEntry
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthReportEntry"/> with the specified values for <paramref name="status"/>, <paramref name="exception"/>,
|
||||
/// <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A value indicating the health status of the component that was checked.</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="duration">A value indicating the health execution duration.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public HealthReportEntry(HealthStatus status, string description, TimeSpan duration, Exception exception, IReadOnlyDictionary<string, object> data)
|
||||
{
|
||||
Status = status;
|
||||
Description = description;
|
||||
Duration = duration;
|
||||
Exception = exception;
|
||||
Data = data ?? _emptyReadOnlyDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional key-value pairs describing the health of the component.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the status of the component that was checked.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the health check execution duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="System.Exception"/> representing the exception that was thrown when checking for status (if any).
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the health status of the component that was checked.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the reported status of a health check result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A status of <see cref="Unhealthy"/> should be considered the default value for a failing health check. Application
|
||||
/// developers may configure a health check to report a different status as desired.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthStatus.Degraded"/> is
|
||||
/// greater than <see cref="HealthStatus.Unhealthy"/> but less than <see cref="HealthStatus.Healthy"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum HealthStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was unhealthy, or an unhandled
|
||||
/// exception was thrown while executing the health check.
|
||||
/// </summary>
|
||||
Unhealthy = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was in a degraded state.
|
||||
/// </summary>
|
||||
Degraded = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was healthy.
|
||||
/// </summary>
|
||||
Healthy = 2,
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a health check, which can be used to check the status of a component in the application, such as a backend service, database or some internal
|
||||
/// state.
|
||||
/// </summary>
|
||||
public interface IHealthCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="context">A context object associated with the current execution.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a publisher of <see cref="HealthReport"/> information.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default health checks implementation provided an <c>IHostedService</c> implementation that can
|
||||
/// be used to execute health checks at regular intervals and provide the resulting <see cref="HealthReport"/>
|
||||
/// data to all registered <see cref="IHealthCheckPublisher"/> instances.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To provide an <see cref="IHealthCheckPublisher"/> implementation, register an instance or type as a singleton
|
||||
/// service in the dependency injection container.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="IHealthCheckPublisher"/> instances are provided with a <see cref="HealthReport"/> after executing
|
||||
/// health checks in a background thread. The use of <see cref="IHealthCheckPublisher"/> depend on hosting in
|
||||
/// an application using <c>IWebHost</c> or generic host (<c>IHost</c>). Execution of <see cref="IHealthCheckPublisher"/>
|
||||
/// instance is not related to execution of health checks via a middleware.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IHealthCheckPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes the provided <paramref name="report"/>.
|
||||
/// </summary>
|
||||
/// <param name="report">The <see cref="HealthReport"/>. The result of executing a set of health checks.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>A <see cref="Task"/> which will complete when publishing is complete.</returns>
|
||||
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Abstractions for defining health checks in .NET applications
|
||||
|
||||
Commonly Used Types
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
|
||||
</Description>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class DefaultHealthCheckService : HealthCheckService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IOptions<HealthCheckServiceOptions> _options;
|
||||
private readonly ILogger<DefaultHealthCheckService> _logger;
|
||||
|
||||
public DefaultHealthCheckService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<HealthCheckServiceOptions> options,
|
||||
ILogger<DefaultHealthCheckService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// We're specifically going out of our way to do this at startup time. We want to make sure you
|
||||
// get any kind of health-check related error as early as possible. Waiting until someone
|
||||
// actually tries to **run** health checks would be real baaaaad.
|
||||
ValidateRegistrations(_options.Value.Registrations);
|
||||
}
|
||||
public override async Task<HealthReport> CheckHealthAsync(
|
||||
Func<HealthCheckRegistration, bool> predicate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var registrations = _options.Value.Registrations;
|
||||
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
var context = new HealthCheckContext();
|
||||
var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var totalTime = ValueStopwatch.StartNew();
|
||||
Log.HealthCheckProcessingBegin(_logger);
|
||||
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
if (predicate != null && !predicate(registration))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var healthCheck = registration.Factory(scope.ServiceProvider);
|
||||
|
||||
// If the health check does things like make Database queries using EF or backend HTTP calls,
|
||||
// it may be valuable to know that logs it generates are part of a health check. So we start a scope.
|
||||
using (_logger.BeginScope(new HealthCheckLogScope(registration.Name)))
|
||||
{
|
||||
var stopwatch = ValueStopwatch.StartNew();
|
||||
context.Registration = registration;
|
||||
|
||||
Log.HealthCheckBegin(_logger, registration);
|
||||
|
||||
HealthReportEntry entry;
|
||||
try
|
||||
{
|
||||
var result = await healthCheck.CheckHealthAsync(context, cancellationToken);
|
||||
var duration = stopwatch.GetElapsedTime();
|
||||
|
||||
entry = new HealthReportEntry(
|
||||
status: result.Status,
|
||||
description: result.Description,
|
||||
duration: duration,
|
||||
exception: result.Exception,
|
||||
data: result.Data);
|
||||
|
||||
Log.HealthCheckEnd(_logger, registration, entry, duration);
|
||||
Log.HealthCheckData(_logger, registration, entry);
|
||||
}
|
||||
|
||||
// Allow cancellation to propagate.
|
||||
catch (Exception ex) when (ex as OperationCanceledException == null)
|
||||
{
|
||||
var duration = stopwatch.GetElapsedTime();
|
||||
entry = new HealthReportEntry(
|
||||
status: HealthStatus.Unhealthy,
|
||||
description: ex.Message,
|
||||
duration: duration,
|
||||
exception: ex,
|
||||
data: null);
|
||||
|
||||
Log.HealthCheckError(_logger, registration, ex, duration);
|
||||
}
|
||||
|
||||
entries[registration.Name] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
var totalElapsedTime = totalTime.GetElapsedTime();
|
||||
var report = new HealthReport(entries, totalElapsedTime);
|
||||
Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime);
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRegistrations(IEnumerable<HealthCheckRegistration> registrations)
|
||||
{
|
||||
// Scan the list for duplicate names to provide a better error if there are duplicates.
|
||||
var duplicateNames = registrations
|
||||
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
if (duplicateNames.Count > 0)
|
||||
{
|
||||
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations));
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
|
||||
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
|
||||
|
||||
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
|
||||
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
|
||||
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
|
||||
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckProcessingBegin,
|
||||
"Running health checks");
|
||||
|
||||
private static readonly Action<ILogger, double, HealthStatus, Exception> _healthCheckProcessingEnd = LoggerMessage.Define<double, HealthStatus>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckProcessingEnd,
|
||||
"Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckBegin,
|
||||
"Running health check {HealthCheckName}");
|
||||
|
||||
// These are separate so they can have different log levels
|
||||
private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'";
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndHealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndDegraded = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Warning,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndUnhealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndFailed = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckError,
|
||||
"Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
||||
|
||||
public static void HealthCheckProcessingBegin(ILogger logger)
|
||||
{
|
||||
_healthCheckProcessingBegin(logger, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration)
|
||||
{
|
||||
_healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration)
|
||||
{
|
||||
_healthCheckBegin(logger, registration.Name, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration)
|
||||
{
|
||||
switch (entry.Status)
|
||||
{
|
||||
case HealthStatus.Healthy:
|
||||
_healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
|
||||
case HealthStatus.Degraded:
|
||||
_healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
|
||||
case HealthStatus.Unhealthy:
|
||||
_healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
|
||||
{
|
||||
_healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry)
|
||||
{
|
||||
if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
logger.Log(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckData,
|
||||
new HealthCheckDataLogValue(registration.Name, entry.Data),
|
||||
null,
|
||||
(state, ex) => state.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class HealthCheckDataLogValue : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly List<KeyValuePair<string, object>> _values;
|
||||
|
||||
private string _formatted;
|
||||
|
||||
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> values)
|
||||
{
|
||||
_name = name;
|
||||
_values = values.ToList();
|
||||
|
||||
// We add the name as a kvp so that you can filter by health check name in the logs.
|
||||
// This is the same parameter name used in the other logs.
|
||||
_values.Add(new KeyValuePair<string, object>("HealthCheckName", name));
|
||||
}
|
||||
|
||||
public KeyValuePair<string, object> this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
{
|
||||
throw new IndexOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return _values[index];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => _values.Count;
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return _values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return _values.GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_formatted == null)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"Health check data for {_name}:");
|
||||
|
||||
var values = _values;
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var kvp = values[i];
|
||||
builder.Append(" ");
|
||||
builder.Append(kvp.Key);
|
||||
builder.Append(": ");
|
||||
|
||||
builder.AppendLine(kvp.Value?.ToString());
|
||||
}
|
||||
|
||||
_formatted = builder.ToString();
|
||||
}
|
||||
|
||||
return _formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
|
||||
/// implement the check.
|
||||
/// </summary>
|
||||
internal sealed class DelegateHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="DelegateHealthCheck"/> from the specified delegate.
|
||||
/// </summary>
|
||||
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
|
||||
public DelegateHealthCheck(Func<CancellationToken, Task<HealthCheckResult>> check)
|
||||
{
|
||||
_check = check ?? throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="context">A context object associated with the current execution.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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 Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering <see cref="HealthCheckService"/> in an <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static class HealthCheckServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="HealthCheckService"/> to the container, using the provided delegate to register
|
||||
/// health checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation is idempotent - multiple invocations will still only result in a single
|
||||
/// <see cref="HealthCheckService"/> instance in the <see cref="IServiceCollection"/>. It can be invoked
|
||||
/// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
|
||||
/// </remarks>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="HealthCheckService"/> to.</param>
|
||||
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
|
||||
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
|
||||
services.TryAddSingleton<IHostedService, HealthCheckPublisherHostedService>();
|
||||
return new HealthChecksBuilder(services);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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 Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
internal class HealthChecksBuilder : IHealthChecksBuilder
|
||||
{
|
||||
public HealthChecksBuilder(IServiceCollection services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
public IHealthChecksBuilder Add(HealthCheckRegistration registration)
|
||||
{
|
||||
if (registration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(registration));
|
||||
}
|
||||
|
||||
Services.Configure<HealthCheckServiceOptions>(options =>
|
||||
{
|
||||
options.Registrations.Add(registration);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides basic extension methods for registering <see cref="IHealthCheck"/> instances in an <see cref="IHealthChecksBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HealthChecksBuilderAddCheckExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="instance">An <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
IHealthCheck instance,
|
||||
HealthStatus? failureStatus = null,
|
||||
IEnumerable<string> tags = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.GetServiceOrCreateInstance{T}(IServiceProvider)"/> to create the health check
|
||||
/// instance when needed. If a service of type <typeparamref name="T"/> is registred in the dependency injection container
|
||||
/// with any liftime it will be used. Otherwise an instance of type <typeparamref name="T"/> will be constructed with
|
||||
/// access to services from the dependency injection container.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus = null,
|
||||
IEnumerable<string> tags = null) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance<T>(s), failureStatus, tags));
|
||||
}
|
||||
|
||||
// NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't
|
||||
// play super well with params.
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return AddTypeActivatedCheck<T>(builder, name, failureStatus: null, tags: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
params object[] args) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return AddTypeActivatedCheck<T>(builder, name, failureStatus, tags: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags,
|
||||
params object[] args) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance<T>(s, args), failureStatus, tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering delegates with the <see cref="IHealthChecksBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HealthChecksBuilderDelegateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<HealthCheckResult> check,
|
||||
IEnumerable<string> tags = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check()));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, HealthCheckResult> check,
|
||||
IEnumerable<string> tags = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct)));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => check());
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => check(ct));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 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 Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder used to register health checks.
|
||||
/// </summary>
|
||||
public interface IHealthChecksBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="HealthCheckRegistration"/> for a health check.
|
||||
/// </summary>
|
||||
/// <param name="registration">The <see cref="HealthCheckRegistration"/>.</param>
|
||||
IHealthChecksBuilder Add(HealthCheckRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class HealthCheckLogScope : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
public string HealthCheckName { get; }
|
||||
|
||||
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
|
||||
|
||||
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="HealthCheckLogScope"/> with the provided name.
|
||||
/// </summary>
|
||||
/// <param name="healthCheckName">The name of the health check being executed.</param>
|
||||
public HealthCheckLogScope(string healthCheckName)
|
||||
{
|
||||
HealthCheckName = healthCheckName;
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||
{
|
||||
yield return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal sealed class HealthCheckPublisherHostedService : IHostedService
|
||||
{
|
||||
private readonly HealthCheckService _healthCheckService;
|
||||
private readonly IOptions<HealthCheckPublisherOptions> _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHealthCheckPublisher[] _publishers;
|
||||
|
||||
private CancellationTokenSource _stopping;
|
||||
private Timer _timer;
|
||||
|
||||
public HealthCheckPublisherHostedService(
|
||||
HealthCheckService healthCheckService,
|
||||
IOptions<HealthCheckPublisherOptions> options,
|
||||
ILogger<HealthCheckPublisherHostedService> logger,
|
||||
IEnumerable<IHealthCheckPublisher> publishers)
|
||||
{
|
||||
if (healthCheckService == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(healthCheckService));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (publishers == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(publishers));
|
||||
}
|
||||
|
||||
_healthCheckService = healthCheckService;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_publishers = publishers.ToArray();
|
||||
|
||||
_stopping = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
internal bool IsStopping => _stopping.IsCancellationRequested;
|
||||
|
||||
internal bool IsTimerRunning => _timer != null;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_publishers.Length == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
|
||||
// fire before other code runs.
|
||||
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_stopping.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions thrown as a result of a cancellation.
|
||||
}
|
||||
|
||||
if (_publishers.Length == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
|
||||
private async void Timer_Tick(object state)
|
||||
{
|
||||
await RunAsync();
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal async Task RunAsync()
|
||||
{
|
||||
var duration = ValueStopwatch.StartNew();
|
||||
Logger.HealthCheckPublisherProcessingBegin(_logger);
|
||||
|
||||
CancellationTokenSource cancellation = null;
|
||||
try
|
||||
{
|
||||
var timeout = _options.Value.Timeout;
|
||||
|
||||
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
|
||||
cancellation.CancelAfter(timeout);
|
||||
|
||||
await RunAsyncCore(cancellation.Token);
|
||||
|
||||
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
|
||||
}
|
||||
catch (OperationCanceledException) when (IsStopping)
|
||||
{
|
||||
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||
// a timeout and we want to log it.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is an error, publishing failed.
|
||||
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsyncCore(CancellationToken cancellationToken)
|
||||
{
|
||||
// Forcibly yield - we want to unblock the timer thread.
|
||||
await Task.Yield();
|
||||
|
||||
// The health checks service does it's own logging, and doesn't throw exceptions.
|
||||
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
|
||||
|
||||
var publishers = _publishers;
|
||||
var tasks = new Task[publishers.Length];
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
var duration = ValueStopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Logger.HealthCheckPublisherBegin(_logger, publisher);
|
||||
|
||||
await publisher.PublishAsync(report, cancellationToken);
|
||||
Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
|
||||
}
|
||||
catch (OperationCanceledException) when (IsStopping)
|
||||
{
|
||||
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||
// a timeout and we want to log it.
|
||||
}
|
||||
catch (OperationCanceledException ocex)
|
||||
{
|
||||
Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
|
||||
throw ocex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
|
||||
public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
|
||||
public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
|
||||
|
||||
public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
|
||||
public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
|
||||
public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
|
||||
public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
|
||||
}
|
||||
|
||||
private static class Logger
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherProcessingBegin,
|
||||
"Running health check publishers");
|
||||
|
||||
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherProcessingEnd,
|
||||
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherBegin,
|
||||
"Running health check publisher '{HealthCheckPublisher}'");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherEnd,
|
||||
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckPublisherError,
|
||||
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckPublisherTimeout,
|
||||
"Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
|
||||
|
||||
public static void HealthCheckPublisherProcessingBegin(ILogger logger)
|
||||
{
|
||||
_healthCheckPublisherProcessingBegin(logger, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
|
||||
{
|
||||
_healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
|
||||
{
|
||||
_healthCheckPublisherBegin(logger, publisher, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||
{
|
||||
_healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
|
||||
{
|
||||
_healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||
{
|
||||
_healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the default service that executes <see cref="IHealthCheckPublisher"/> instances.
|
||||
/// </summary>
|
||||
public sealed class HealthCheckPublisherOptions
|
||||
{
|
||||
private TimeSpan _delay;
|
||||
private TimeSpan _period;
|
||||
|
||||
public HealthCheckPublisherOptions()
|
||||
{
|
||||
_delay = TimeSpan.FromSeconds(5);
|
||||
_period = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial delay applied after the application starts before executing
|
||||
/// <see cref="IHealthCheckPublisher"/> instances. The delay is applied once at startup, and does
|
||||
/// not apply to subsequent iterations. The default value is 5 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan Delay
|
||||
{
|
||||
get => _delay;
|
||||
set
|
||||
{
|
||||
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Delay)} must not be infinite.", nameof(value));
|
||||
}
|
||||
|
||||
_delay = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the period of <see cref="IHealthCheckPublisher"/> execution. The default value is
|
||||
/// 30 seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="Period"/> cannot be set to a value lower than 1 second.
|
||||
/// </remarks>
|
||||
public TimeSpan Period
|
||||
{
|
||||
get => _period;
|
||||
set
|
||||
{
|
||||
if (value < TimeSpan.FromSeconds(1))
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Period)} must be greater than or equal to one second.", nameof(value));
|
||||
}
|
||||
|
||||
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value));
|
||||
}
|
||||
|
||||
_period = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a predicate that is used to filter the set of health checks executed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="Predicate"/> is <c>null</c>, the health check publisher service will run all
|
||||
/// registered health checks - this is the default behavior. To run a subset of health checks,
|
||||
/// provide a function that filters the set of checks. The predicate will be evaluated each period.
|
||||
/// </remarks>
|
||||
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout for executing the health checks an all <see cref="IHealthCheckPublisher"/>
|
||||
/// instances. Use <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> to execute with no timeout.
|
||||
/// The default value is 30 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances
|
||||
/// registered in the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default implementation of <see cref="HealthCheckService"/> is registered in the dependency
|
||||
/// injection container as a singleton service by calling
|
||||
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="IHealthChecksBuilder"/> returned by
|
||||
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>
|
||||
/// provides a convenience API for registering health checks.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="IHealthCheck"/> implementations can be registered through extension methods provided by
|
||||
/// <see cref="IHealthChecksBuilder"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class HealthCheckService
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs all the health checks in the application and returns the aggregated status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="HealthReport"/> containing the results.
|
||||
/// </returns>
|
||||
public Task<HealthReport> CheckHealthAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CheckHealthAsync(predicate: null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the provided health checks and returns the aggregated status
|
||||
/// </summary>
|
||||
/// <param name="predicate">
|
||||
/// A predicate that can be used to include health checks based on user-defined criteria.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="HealthReport"/> containing the results.
|
||||
/// </returns>
|
||||
public abstract Task<HealthReport> CheckHealthAsync(
|
||||
Func<HealthCheckRegistration, bool> predicate,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the default implementation of <see cref="HealthCheckService"/>
|
||||
/// </summary>
|
||||
public sealed class HealthCheckServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the health check registrations.
|
||||
/// </summary>
|
||||
public ICollection<HealthCheckRegistration> Registrations { get; } = new List<HealthCheckRegistration>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Components for performing health checks in .NET applications
|
||||
|
||||
Commonly Used Types:
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
|
||||
</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)NonCapturingTimer\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ValueStopwatch\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class DefaultHealthCheckServiceTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
|
||||
{
|
||||
// Arrange
|
||||
//
|
||||
// Doing this the old fashioned way so we can verify that the exception comes
|
||||
// from the constructor.
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddOptions();
|
||||
serviceCollection.AddHealthChecks()
|
||||
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())));
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var options = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
|
||||
var logger = services.GetRequiredService<ILogger<DefaultHealthCheckService>>();
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<ArgumentException>(() => new DefaultHealthCheckService(scopeFactory, options, logger));
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>()
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
|
||||
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
|
||||
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries.OrderBy(kvp => kvp.Key),
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("DegradedCheck", actual.Key);
|
||||
Assert.Equal(DegradedMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("HealthyCheck", actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("UnhealthyCheck", actual.Key);
|
||||
Assert.Equal(UnhealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(exception, actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
|
||||
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
|
||||
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("HealthyCheck", actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SetsRegistrationForEachCheck()
|
||||
{
|
||||
// Arrange
|
||||
var thrownException = new InvalidOperationException("Whoops!");
|
||||
var faultedException = new InvalidOperationException("Ohnoes!");
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddCheck<NameCapturingCheck>("A");
|
||||
b.AddCheck<NameCapturingCheck>("B");
|
||||
b.AddCheck<NameCapturingCheck>("C");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("A", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "A")));
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("B", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "B")));
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("C", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "C")));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_Cancellation_CanPropagate()
|
||||
{
|
||||
// Arrange
|
||||
var insideCheck = new TaskCompletionSource<object>();
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("cancels", async ct =>
|
||||
{
|
||||
insideCheck.SetResult(null);
|
||||
|
||||
await Task.Delay(10000, ct);
|
||||
return HealthCheckResult.Unhealthy();
|
||||
});
|
||||
});
|
||||
|
||||
var cancel = new CancellationTokenSource();
|
||||
var task = service.CheckHealthAsync(cancel.Token);
|
||||
|
||||
// After this returns we know the check has started
|
||||
await insideCheck.Task;
|
||||
|
||||
cancel.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var thrownException = new InvalidOperationException("Whoops!");
|
||||
var faultedException = new InvalidOperationException("Ohnoes!");
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("Throws", ct => throw thrownException);
|
||||
b.AddAsyncCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
|
||||
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Throws", actual.Key);
|
||||
Assert.Equal(thrownException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(thrownException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Faults", actual.Key);
|
||||
Assert.Equal(faultedException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(faultedException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Succeeds", actual.Key);
|
||||
Assert.Null(actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var check = new DelegateHealthCheck(cancellationToken =>
|
||||
{
|
||||
Assert.Collection(sink.Scopes,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName);
|
||||
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("HealthCheckName", item.Key);
|
||||
Assert.Equal("TestScope", item.Value);
|
||||
});
|
||||
});
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
});
|
||||
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
// Override the logger factory for testing
|
||||
b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
|
||||
b.AddCheck("TestScope", check);
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Entries, actual =>
|
||||
{
|
||||
Assert.Equal("TestScope", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnTransientService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddTransient<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnScopedService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddScoped<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddSingleton<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
private static DefaultHealthCheckService CreateHealthChecksService(Action<IHealthChecksBuilder> configure)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
|
||||
var builder = services.AddHealthChecks();
|
||||
if (configure != null)
|
||||
{
|
||||
configure(builder);
|
||||
}
|
||||
|
||||
return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<HealthCheckService>();
|
||||
}
|
||||
|
||||
private class AnotherService { }
|
||||
|
||||
private class CheckWithServiceDependency : IHealthCheck
|
||||
{
|
||||
public CheckWithServiceDependency(AnotherService _)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
}
|
||||
}
|
||||
|
||||
private class NameCapturingCheck : IHealthCheck
|
||||
{
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var data = new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", context.Registration.Name },
|
||||
};
|
||||
return Task.FromResult(HealthCheckResult.Healthy(data: data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
// Integration tests for extension methods on IHealthCheckBuilder
|
||||
//
|
||||
// We test the longest overload of each 'family' of Add...Check methods, since they chain to each other.
|
||||
public class HealthChecksBuilderTest
|
||||
{
|
||||
[Fact]
|
||||
public void AddCheck_Instance()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new DelegateHealthCheck((_) =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
});
|
||||
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance);
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.Same(instance, registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddCheck_T_TypeActivated()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<TestHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddCheck_T_Service()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new TestHealthCheck();
|
||||
|
||||
var services = CreateServices();
|
||||
services.AddSingleton(instance);
|
||||
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.Same(instance, registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddTypeActivatedCheck()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddTypeActivatedCheck<TestHealthCheckWithArgs>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
|
||||
var check = Assert.IsType<TestHealthCheckWithArgs>(registration.Factory(serviceProvider));
|
||||
Assert.Equal(5, check.I);
|
||||
Assert.Equal("hi", check.S);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDelegateCheck_NoArg()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", tags: new[] { "tag", }, check: () =>
|
||||
{
|
||||
return HealthCheckResult.Healthy();
|
||||
});
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDelegateCheck_CancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", (_) =>
|
||||
{
|
||||
return HealthCheckResult.Degraded();
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAsyncDelegateCheck_NoArg()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddAsyncCheck("test", () =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAsyncDelegateCheck_CancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddAsyncCheck("test", (_) =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Unhealthy());
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
|
||||
// Act
|
||||
var options = services.BuildServiceProvider().GetRequiredService<IOptions<HealthCheckServiceOptions>>();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
options.Value.Registrations,
|
||||
actual => Assert.Equal("Foo", actual.Name),
|
||||
actual => Assert.Equal("Bar", actual.Name));
|
||||
}
|
||||
|
||||
private IServiceCollection CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
return services;
|
||||
}
|
||||
|
||||
private class TestHealthCheck : IHealthCheck
|
||||
{
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHealthCheckWithArgs : IHealthCheck
|
||||
{
|
||||
public TestHealthCheckWithArgs(int i, string s)
|
||||
{
|
||||
I = i;
|
||||
S = s;
|
||||
}
|
||||
|
||||
public int I { get; set; }
|
||||
|
||||
public string S { get; set; }
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public class ServiceCollectionExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
services.AddHealthChecks();
|
||||
services.AddHealthChecks();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(services.OrderBy(s => s.ServiceType.FullName),
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||
Assert.Equal(typeof(HealthCheckService), actual.ServiceType);
|
||||
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
|
||||
Assert.Null(actual.ImplementationInstance);
|
||||
Assert.Null(actual.ImplementationFactory);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||
Assert.Equal(typeof(IHostedService), actual.ServiceType);
|
||||
Assert.Equal(typeof(HealthCheckPublisherHostedService), actual.ImplementationType);
|
||||
Assert.Null(actual.ImplementationInstance);
|
||||
Assert.Null(actual.ImplementationFactory);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthCheckPublisherHostedServiceTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAsync_WithoutPublishers_DoesNotStartTimer()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new IHealthCheckPublisher[]
|
||||
{
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WithPublishers_StartsTimer()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new IHealthCheckPublisher[]
|
||||
{
|
||||
new TestPublisher(),
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers()
|
||||
{
|
||||
// Arrange
|
||||
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock0.Task, },
|
||||
new TestPublisher() { Wait = unblock1.Task, },
|
||||
new TestPublisher() { Wait = unblock2.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, configure: (options) =>
|
||||
{
|
||||
options.Delay = TimeSpan.FromMilliseconds(0);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock0.SetResult(null);
|
||||
unblock1.SetResult(null);
|
||||
unblock2.SetResult(null);
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsync_CancelsExecution()
|
||||
{
|
||||
// Arrange
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, }
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Start execution
|
||||
var running = service.RunAsync();
|
||||
|
||||
// Wait for the publisher to see the cancellation token
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
Assert.Single(publishers[0].Entries);
|
||||
|
||||
// Act
|
||||
await service.StopAsync(); // Trigger cancellation
|
||||
|
||||
// Assert
|
||||
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WaitsForCompletion_Single()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
// Not testing logs here to avoid differences in logging order
|
||||
[Fact]
|
||||
public async Task RunAsync_WaitsForCompletion_Multiple()
|
||||
{
|
||||
// Arrange
|
||||
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock0.Task, },
|
||||
new TestPublisher() { Wait = unblock1.Task, },
|
||||
new TestPublisher() { Wait = unblock2.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock0.SetResult(null);
|
||||
unblock1.SetResult(null);
|
||||
unblock2.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_PublishersCanTimeout()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink, configure: (options) =>
|
||||
{
|
||||
options.Timeout = TimeSpan.FromMilliseconds(50);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherTimeout, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_CanFilterHealthChecks()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher(),
|
||||
new TestPublisher(),
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, configure: (options) =>
|
||||
{
|
||||
options.Predicate = (r) => r.Name == "one";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_HandlesExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherError, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
// Not testing logging here to avoid flaky ordering issues
|
||||
[Fact]
|
||||
public async Task RunAsync_HandlesExceptions_Multiple()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
new TestPublisher(),
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
private HealthCheckPublisherHostedService CreateService(
|
||||
IHealthCheckPublisher[] publishers,
|
||||
Action<HealthCheckPublisherOptions> configure = null,
|
||||
TestSink sink = null)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddOptions();
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddHealthChecks()
|
||||
.AddCheck("one", () => { return HealthCheckResult.Healthy(); })
|
||||
.AddCheck("two", () => { return HealthCheckResult.Healthy(); });
|
||||
|
||||
// Choosing big values for tests to make sure that we're not dependent on the defaults.
|
||||
// All of the tests that rely on the timer will set their own values for speed.
|
||||
serviceCollection.Configure<HealthCheckPublisherOptions>(options =>
|
||||
{
|
||||
options.Delay = TimeSpan.FromMinutes(5);
|
||||
options.Period = TimeSpan.FromMinutes(5);
|
||||
options.Timeout = TimeSpan.FromMinutes(5);
|
||||
});
|
||||
|
||||
if (publishers != null)
|
||||
{
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
serviceCollection.AddSingleton<IHealthCheckPublisher>(publishers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (configure != null)
|
||||
{
|
||||
serviceCollection.Configure(configure);
|
||||
}
|
||||
|
||||
if (sink != null)
|
||||
{
|
||||
serviceCollection.AddSingleton<ILoggerFactory>(new TestLoggerFactory(sink, enabled: true));
|
||||
}
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
return services.GetServices<IHostedService>().OfType< HealthCheckPublisherHostedService>().Single();
|
||||
}
|
||||
|
||||
private static async Task AssertCancelledAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken));
|
||||
}
|
||||
|
||||
private class TestPublisher : IHealthCheckPublisher
|
||||
{
|
||||
private TaskCompletionSource<object> _started;
|
||||
|
||||
public TestPublisher()
|
||||
{
|
||||
_started = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public List<(HealthReport report, CancellationToken cancellationToken)> Entries { get; } = new List<(HealthReport report, CancellationToken cancellationToken)>();
|
||||
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public Task Started => _started.Task;
|
||||
|
||||
public Task Wait { get; set; }
|
||||
|
||||
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
Entries.Add((report, cancellationToken));
|
||||
|
||||
// Signal that we've started
|
||||
_started.SetResult(null);
|
||||
|
||||
if (Wait != null)
|
||||
{
|
||||
await Wait;
|
||||
}
|
||||
|
||||
if (Exception != null)
|
||||
{
|
||||
throw Exception;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthReportTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(HealthStatus.Healthy)]
|
||||
[InlineData(HealthStatus.Degraded)]
|
||||
[InlineData(HealthStatus.Unhealthy)]
|
||||
public void Status_MatchesWorstStatusInResults(HealthStatus status)
|
||||
{
|
||||
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
|
||||
{
|
||||
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) },
|
||||
{"Bar", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue,null, null) },
|
||||
{"Baz", new HealthReportEntry(status, exception: null, description: null,duration:TimeSpan.MinValue, data: null) },
|
||||
{"Quick", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
{"Quack", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
{"Quock", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
}, totalDuration: TimeSpan.MinValue);
|
||||
|
||||
Assert.Equal(status, result.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(200)]
|
||||
[InlineData(300)]
|
||||
[InlineData(400)]
|
||||
public void TotalDuration_MatchesTotalDurationParameter(int milliseconds)
|
||||
{
|
||||
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
|
||||
{
|
||||
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
|
||||
}, totalDuration: TimeSpan.FromMilliseconds(milliseconds));
|
||||
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(milliseconds), result.TotalDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="$(RepositoryRoot)src\Logging\Logging.Testing\src\build\Microsoft.Extensions.Logging.Testing.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ namespace Microsoft.Extensions.ObjectPool
|
|||
|
||||
public override T Get()
|
||||
{
|
||||
T item = _firstItem;
|
||||
|
||||
var item = _firstItem;
|
||||
if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
|
||||
{
|
||||
item = GetViaScan();
|
||||
|
|
@ -55,12 +54,10 @@ namespace Microsoft.Extensions.ObjectPool
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private T GetViaScan()
|
||||
{
|
||||
ObjectWrapper[] items = _items;
|
||||
|
||||
var items = _items;
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
T item = items[i];
|
||||
|
||||
var item = items[i].Element;
|
||||
if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item)
|
||||
{
|
||||
return item;
|
||||
|
|
@ -88,21 +85,17 @@ namespace Microsoft.Extensions.ObjectPool
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ReturnViaScan(T obj)
|
||||
{
|
||||
ObjectWrapper[] items = _items;
|
||||
|
||||
var items = _items;
|
||||
for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// PERF: the struct wrapper avoids array-covariance-checks from the runtime when assigning to elements of the array.
|
||||
[DebuggerDisplay("{Element}")]
|
||||
private struct ObjectWrapper
|
||||
{
|
||||
public T Element;
|
||||
|
||||
public ObjectWrapper(T item) => Element = item;
|
||||
|
||||
public static implicit operator T(ObjectWrapper wrapper) => wrapper.Element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.ObjectPool.Test
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
public class DefaultObjectPoolTest
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
// 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;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
public class ThreadingTest
|
||||
{
|
||||
private CancellationTokenSource _cts;
|
||||
private DefaultObjectPool<Item> _pool;
|
||||
private bool _foundError;
|
||||
|
||||
[Fact]
|
||||
public void RunThreadingTest()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
_pool = new DefaultObjectPool<Item>(new DefaultPooledObjectPolicy<Item>(), 10);
|
||||
|
||||
var threads = new Thread[8];
|
||||
for (var i = 0; i < threads.Length; i++)
|
||||
{
|
||||
threads[i] = new Thread(Run);
|
||||
}
|
||||
|
||||
for (var i = 0; i < threads.Length; i++)
|
||||
{
|
||||
threads[i].Start();
|
||||
}
|
||||
|
||||
// Run for 1000ms
|
||||
_cts.CancelAfter(1000);
|
||||
|
||||
// Wait for all threads to complete
|
||||
for (var i = 0; i < threads.Length; i++)
|
||||
{
|
||||
threads[i].Join();
|
||||
}
|
||||
|
||||
Assert.False(_foundError, "Race condition found. An item was shared across threads.");
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
var obj = _pool.Get();
|
||||
if (obj.i != 0)
|
||||
{
|
||||
_foundError = true;
|
||||
}
|
||||
obj.i = 123;
|
||||
|
||||
var obj2 = _pool.Get();
|
||||
if (obj2.i != 0)
|
||||
{
|
||||
_foundError = true;
|
||||
}
|
||||
obj2.i = 321;
|
||||
|
||||
obj.Reset();
|
||||
_pool.Return(obj);
|
||||
|
||||
obj2.Reset();
|
||||
_pool.Return(obj2);
|
||||
}
|
||||
}
|
||||
|
||||
private class Item
|
||||
{
|
||||
public int i = 0;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,38 +2,72 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Running;
|
||||
using System.Collections.Generic;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Toolchains.InProcess;
|
||||
|
||||
namespace BenchmarkDotNet.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
|
||||
internal class AspNetCoreBenchmarkAttribute : Attribute, IConfigSource
|
||||
{
|
||||
public static bool UseValidationConfig { get; set; }
|
||||
|
||||
public Type ConfigType { get; }
|
||||
public Type ValidationConfigType { get; }
|
||||
|
||||
public AspNetCoreBenchmarkAttribute() : this(typeof(DefaultCoreConfig))
|
||||
public AspNetCoreBenchmarkAttribute()
|
||||
: this(typeof(DefaultCoreConfig))
|
||||
{
|
||||
}
|
||||
|
||||
public AspNetCoreBenchmarkAttribute(Type configType) : this(configType, typeof(DefaultCoreValidationConfig))
|
||||
public AspNetCoreBenchmarkAttribute(Type configType)
|
||||
: this(configType, typeof(DefaultCoreValidationConfig))
|
||||
{
|
||||
}
|
||||
|
||||
public AspNetCoreBenchmarkAttribute(Type configType, Type validationConfigType)
|
||||
{
|
||||
ConfigType = configType;
|
||||
ValidationConfigType = validationConfigType;
|
||||
ConfigTypes = new Dictionary<string, Type>()
|
||||
{
|
||||
{ NamedConfiguration.Default, typeof(DefaultCoreConfig) },
|
||||
{ NamedConfiguration.Validation, typeof(DefaultCoreValidationConfig) },
|
||||
{ NamedConfiguration.Profile, typeof(DefaultCoreProfileConfig) },
|
||||
{ NamedConfiguration.Debug, typeof(DefaultCoreDebugConfig) },
|
||||
{ NamedConfiguration.PerfLab, typeof(DefaultCorePerfLabConfig) },
|
||||
};
|
||||
|
||||
if (configType != null)
|
||||
{
|
||||
ConfigTypes[NamedConfiguration.Default] = configType;
|
||||
}
|
||||
|
||||
if (validationConfigType != null)
|
||||
{
|
||||
ConfigTypes[NamedConfiguration.Validation] = validationConfigType;
|
||||
}
|
||||
}
|
||||
|
||||
public IConfig Config => (IConfig) Activator.CreateInstance(UseValidationConfig ? ValidationConfigType : ConfigType, Array.Empty<object>());
|
||||
public IConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!ConfigTypes.TryGetValue(ConfigName ?? NamedConfiguration.Default, out var configType))
|
||||
{
|
||||
var message = $"Could not find a configuration matching {ConfigName}. " +
|
||||
$"Known configurations: {string.Join(", ", ConfigTypes.Keys)}";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
return (IConfig)Activator.CreateInstance(configType, Array.Empty<object>());
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, Type> ConfigTypes { get; }
|
||||
|
||||
public static string ConfigName { get; set; } = NamedConfiguration.Default;
|
||||
|
||||
public static class NamedConfiguration
|
||||
{
|
||||
public static readonly string Default = "default";
|
||||
public static readonly string Validation = "validation";
|
||||
public static readonly string Profile = "profile";
|
||||
public static readonly string Debug = "debug";
|
||||
public static readonly string PerfLab = "perflab";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ namespace BenchmarkDotNet.Attributes
|
|||
Add(JitOptimizationsValidator.FailOnError);
|
||||
|
||||
Add(Job.Core
|
||||
#if NETCOREAPP2_1
|
||||
.With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21))
|
||||
#else
|
||||
.With(CsProjCoreToolchain.From(new NetCoreAppSettings("netcoreapp2.2", null, ".NET Core 2.2")))
|
||||
#endif
|
||||
.With(new GcMode { Server = true })
|
||||
.With(RunStrategy.Throughput));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Validators;
|
||||
|
||||
namespace BenchmarkDotNet.Attributes
|
||||
{
|
||||
internal class DefaultCoreDebugConfig : ManualConfig
|
||||
{
|
||||
public DefaultCoreDebugConfig()
|
||||
{
|
||||
Add(ConsoleLogger.Default);
|
||||
Add(JitOptimizationsValidator.DontFailOnError);
|
||||
|
||||
Add(Job.InProcess
|
||||
.With(RunStrategy.Throughput));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using BenchmarkDotNet.Exporters;
|
||||
using BenchmarkDotNet.Exporters.Csv;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Validators;
|
||||
|
||||
namespace BenchmarkDotNet.Attributes
|
||||
{
|
||||
internal class DefaultCorePerfLabConfig : ManualConfig
|
||||
{
|
||||
public DefaultCorePerfLabConfig()
|
||||
{
|
||||
Add(ConsoleLogger.Default);
|
||||
|
||||
Add(MemoryDiagnoser.Default);
|
||||
Add(StatisticColumn.OperationsPerSecond);
|
||||
Add(new ParamsSummaryColumn());
|
||||
Add(DefaultColumnProviders.Statistics, DefaultColumnProviders.Diagnosers, DefaultColumnProviders.Target);
|
||||
|
||||
// TODO: When upgrading to BDN 0.11.1, use Add(DefaultColumnProviders.Descriptor);
|
||||
// DefaultColumnProviders.Target is deprecated
|
||||
|
||||
Add(JitOptimizationsValidator.FailOnError);
|
||||
|
||||
Add(Job.InProcess
|
||||
.With(RunStrategy.Throughput));
|
||||
|
||||
Add(MarkdownExporter.GitHub);
|
||||
|
||||
Add(new CsvExporter(
|
||||
CsvSeparator.Comma,
|
||||
new Reports.SummaryStyle
|
||||
{
|
||||
PrintUnitsInHeader = true,
|
||||
PrintUnitsInContent = false,
|
||||
TimeUnit = Horology.TimeUnit.Microsecond,
|
||||
SizeUnit = SizeUnit.KB
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// 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 BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Diagnosers;
|
||||
using BenchmarkDotNet.Engines;
|
||||
using BenchmarkDotNet.Exporters;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Validators;
|
||||
|
||||
namespace BenchmarkDotNet.Attributes
|
||||
{
|
||||
internal class DefaultCoreProfileConfig : ManualConfig
|
||||
{
|
||||
public DefaultCoreProfileConfig()
|
||||
{
|
||||
Add(ConsoleLogger.Default);
|
||||
Add(MarkdownExporter.GitHub);
|
||||
|
||||
Add(MemoryDiagnoser.Default);
|
||||
Add(StatisticColumn.OperationsPerSecond);
|
||||
Add(DefaultColumnProviders.Instance);
|
||||
|
||||
Add(JitOptimizationsValidator.FailOnError);
|
||||
|
||||
Add(Job.InProcess
|
||||
.With(RunStrategy.Throughput));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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 BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace BenchmarkDotNet.Attributes
|
||||
{
|
||||
public class ParamsSummaryColumn : IColumn
|
||||
{
|
||||
public string Id => nameof(ParamsSummaryColumn);
|
||||
public string ColumnName { get; } = "Params";
|
||||
public bool IsDefault(Summary summary, Benchmark benchmark) => false;
|
||||
public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Parameters.DisplayInfo;
|
||||
public bool IsAvailable(Summary summary) => true;
|
||||
public bool AlwaysShow => true;
|
||||
public ColumnCategory Category => ColumnCategory.Params;
|
||||
public int PriorityInCategory => 0;
|
||||
public override string ToString() => ColumnName;
|
||||
public bool IsNumeric => false;
|
||||
public UnitType UnitType => UnitType.Dimensionless;
|
||||
public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => GetValue(summary, benchmark);
|
||||
public string Legend => $"Summary of all parameter values";
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,14 @@
|
|||
// 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.Reflection;
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Running;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Toolchains.InProcess;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner
|
||||
{
|
||||
|
|
@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner
|
|||
{
|
||||
BeforeMain(args);
|
||||
|
||||
CheckValidate(ref args);
|
||||
AssignConfiguration(ref args);
|
||||
var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly)
|
||||
.Run(args, ManualConfig.CreateEmpty());
|
||||
|
||||
|
|
@ -66,16 +65,35 @@ namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner
|
|||
return 1;
|
||||
}
|
||||
|
||||
private static void CheckValidate(ref string[] args)
|
||||
private static void AssignConfiguration(ref string[] args)
|
||||
{
|
||||
var argsList = args.ToList();
|
||||
if (argsList.Remove("--validate") || argsList.Remove("--validate-fast"))
|
||||
{
|
||||
// Compat: support the old style of passing a config that is used by our build system.
|
||||
SuppressConsole();
|
||||
AspNetCoreBenchmarkAttribute.UseValidationConfig = true;
|
||||
AspNetCoreBenchmarkAttribute.ConfigName = AspNetCoreBenchmarkAttribute.NamedConfiguration.Validation;
|
||||
args = argsList.ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
var index = argsList.IndexOf("--config");
|
||||
if (index >= 0 && index < argsList.Count -1)
|
||||
{
|
||||
AspNetCoreBenchmarkAttribute.ConfigName = argsList[index + 1];
|
||||
argsList.RemoveAt(index + 1);
|
||||
argsList.RemoveAt(index);
|
||||
args = argsList.ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
args = argsList.ToArray();
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
Console.WriteLine("Using the debug config since you are debugging. I hope that's OK!");
|
||||
Console.WriteLine("Specify a configuration with --config <name> to override");
|
||||
AspNetCoreBenchmarkAttribute.ConfigName = AspNetCoreBenchmarkAttribute.NamedConfiguration.Debug;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SuppressConsole()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Internal
|
||||
{
|
||||
// A convenience API for interacting with System.Threading.Timer in a way
|
||||
// that doesn't capture the ExecutionContext. We should be using this (or equivalent)
|
||||
// everywhere we use timers to avoid rooting any values stored in asynclocals.
|
||||
internal static class NonCapturingTimer
|
||||
{
|
||||
public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
|
||||
{
|
||||
if (callback == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer
|
||||
bool restoreFlow = false;
|
||||
try
|
||||
{
|
||||
if (!ExecutionContext.IsFlowSuppressed())
|
||||
{
|
||||
ExecutionContext.SuppressFlow();
|
||||
restoreFlow = true;
|
||||
}
|
||||
|
||||
return new Timer(callback, state, dueTime, period);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Restore the current ExecutionContext
|
||||
if (restoreFlow)
|
||||
{
|
||||
ExecutionContext.RestoreFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// 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.
|
||||
|
||||
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||
#if NETCOREAPP2_2
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Internal
|
||||
{
|
||||
public class NonCapturingTimerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task NonCapturingTimer_DoesntCaptureExecutionContext()
|
||||
{
|
||||
// Arrange
|
||||
var message = new AsyncLocal<string>();
|
||||
message.Value = "Hey, this is a value stored in the execuion context";
|
||||
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
|
||||
// Act
|
||||
var timer = NonCapturingTimer.Create((_) =>
|
||||
{
|
||||
// Observe the value based on the current execution context
|
||||
tcs.SetResult(message.Value);
|
||||
}, state: null, dueTime: TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan);
|
||||
|
||||
// Assert
|
||||
var messageFromTimer = await tcs.Task;
|
||||
timer.Dispose();
|
||||
|
||||
// ExecutionContext didn't flow to timer callback
|
||||
Assert.Null(messageFromTimer);
|
||||
|
||||
// ExecutionContext was restored
|
||||
Assert.NotNull(await Task.Run(() => message.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// 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.Globalization;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class CultureReplacer : IDisposable
|
||||
{
|
||||
private const string _defaultCultureName = "en-GB";
|
||||
private const string _defaultUICultureName = "en-US";
|
||||
private static readonly CultureInfo _defaultCulture = new CultureInfo(_defaultCultureName);
|
||||
private readonly CultureInfo _originalCulture;
|
||||
private readonly CultureInfo _originalUICulture;
|
||||
private readonly long _threadId;
|
||||
|
||||
// Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture
|
||||
// We want to be able to find issues where the InvariantCulture is used, but a specific culture should be.
|
||||
//
|
||||
// UICulture => Language
|
||||
public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName)
|
||||
: this(new CultureInfo(culture), new CultureInfo(uiCulture))
|
||||
{
|
||||
}
|
||||
|
||||
public CultureReplacer(CultureInfo culture, CultureInfo uiCulture)
|
||||
{
|
||||
_originalCulture = CultureInfo.CurrentCulture;
|
||||
_originalUICulture = CultureInfo.CurrentUICulture;
|
||||
_threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
CultureInfo.CurrentUICulture = uiCulture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the culture that is used as the default value for CultureInfo.DefaultThreadCurrentCulture when CultureReplacer is used.
|
||||
/// </summary>
|
||||
public static string DefaultCultureName
|
||||
{
|
||||
get { return _defaultCultureName; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentUICulture when CultureReplacer is used.
|
||||
/// </summary>
|
||||
public static string DefaultUICultureName
|
||||
{
|
||||
get { return _defaultUICultureName; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentCulture when CultureReplacer is used.
|
||||
/// </summary>
|
||||
public static CultureInfo DefaultCulture
|
||||
{
|
||||
get { return _defaultCulture; }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Assert.True(Thread.CurrentThread.ManagedThreadId == _threadId,
|
||||
"The current thread is not the same as the thread invoking the constructor. This should never happen.");
|
||||
CultureInfo.CurrentCulture = _originalCulture;
|
||||
CultureInfo.CurrentUICulture = _originalUICulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
// 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
// TODO: eventually want: public partial class Assert : Xunit.Assert
|
||||
public static class ExceptionAssert
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that an exception of the given type (or optionally a derived type) is thrown.
|
||||
/// </summary>
|
||||
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static TException Throws<TException>(Action testCode)
|
||||
where TException : Exception
|
||||
{
|
||||
return VerifyException<TException>(RecordException(testCode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an exception of the given type is thrown.
|
||||
/// Also verifies that the exception message matches.
|
||||
/// </summary>
|
||||
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static TException Throws<TException>(Action testCode, string exceptionMessage)
|
||||
where TException : Exception
|
||||
{
|
||||
var ex = Throws<TException>(testCode);
|
||||
VerifyExceptionMessage(ex, exceptionMessage);
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an exception of the given type is thrown.
|
||||
/// Also verifies that the exception message matches.
|
||||
/// </summary>
|
||||
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static async Task<TException> ThrowsAsync<TException>(Func<Task> testCode, string exceptionMessage)
|
||||
where TException : Exception
|
||||
{
|
||||
// The 'testCode' Task might execute asynchronously in a different thread making it hard to enforce the thread culture.
|
||||
// The correct way to verify exception messages in such a scenario would be to run the task synchronously inside of a
|
||||
// culture enforced block.
|
||||
var ex = await Assert.ThrowsAsync<TException>(testCode);
|
||||
VerifyExceptionMessage(ex, exceptionMessage);
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an exception of the given type is thrown.
|
||||
/// Also verified that the exception message matches.
|
||||
/// </summary>
|
||||
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static TException Throws<TException>(Func<object> testCode, string exceptionMessage)
|
||||
where TException : Exception
|
||||
{
|
||||
return Throws<TException>(() => { testCode(); }, exceptionMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an <see cref="ArgumentException"/>.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage)
|
||||
{
|
||||
return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, exceptionMessage);
|
||||
}
|
||||
|
||||
private static TException ThrowsArgumentInternal<TException>(
|
||||
Action testCode,
|
||||
string paramName,
|
||||
string exceptionMessage)
|
||||
where TException : ArgumentException
|
||||
{
|
||||
var ex = Throws<TException>(testCode);
|
||||
if (paramName != null)
|
||||
{
|
||||
Assert.Equal(paramName, ex.ParamName);
|
||||
}
|
||||
VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an <see cref="ArgumentException"/>.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static Task<ArgumentException> ThrowsArgumentAsync(Func<Task> testCode, string paramName, string exceptionMessage)
|
||||
{
|
||||
return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, exceptionMessage);
|
||||
}
|
||||
|
||||
private static async Task<TException> ThrowsArgumentAsyncInternal<TException>(
|
||||
Func<Task> testCode,
|
||||
string paramName,
|
||||
string exceptionMessage)
|
||||
where TException : ArgumentException
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<TException>(testCode);
|
||||
if (paramName != null)
|
||||
{
|
||||
Assert.Equal(paramName, ex.ParamName);
|
||||
}
|
||||
VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an <see cref="ArgumentNullException"/>.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName)
|
||||
{
|
||||
var ex = Throws<ArgumentNullException>(testCode);
|
||||
if (paramName != null)
|
||||
{
|
||||
Assert.Equal(paramName, ex.ParamName);
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot
|
||||
/// be null or empty.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName)
|
||||
{
|
||||
return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, "Value cannot be null or empty.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot
|
||||
/// be null or empty.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static Task<ArgumentException> ThrowsArgumentNullOrEmptyAsync(Func<Task> testCode, string paramName)
|
||||
{
|
||||
return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, "Value cannot be null or empty.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
|
||||
/// be null or empty string.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName)
|
||||
{
|
||||
return ThrowsArgumentInternal<ArgumentException>(testCode, paramName, "Value cannot be null or an empty string.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
|
||||
/// be null or empty string.
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static Task<ArgumentException> ThrowsArgumentNullOrEmptyStringAsync(Func<Task> testCode, string paramName)
|
||||
{
|
||||
return ThrowsArgumentAsyncInternal<ArgumentException>(testCode, paramName, "Value cannot be null or an empty string.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it).
|
||||
/// </summary>
|
||||
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||
/// <param name="actualValue">The actual value provided</param>
|
||||
/// <returns>The exception that was thrown, when successful</returns>
|
||||
public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, object actualValue = null)
|
||||
{
|
||||
var ex = ThrowsArgumentInternal<ArgumentOutOfRangeException>(testCode, paramName, exceptionMessage);
|
||||
|
||||
if (paramName != null)
|
||||
{
|
||||
Assert.Equal(paramName, ex.ParamName);
|
||||
}
|
||||
|
||||
if (actualValue != null)
|
||||
{
|
||||
Assert.Equal(actualValue, ex.ActualValue);
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
// We've re-implemented all the xUnit.net Throws code so that we can get this
|
||||
// updated implementation of RecordException which silently unwraps any instances
|
||||
// of AggregateException. In addition to unwrapping exceptions, this method ensures
|
||||
// that tests are executed in with a known set of Culture and UICulture. This prevents
|
||||
// tests from failing when executed on a non-English machine.
|
||||
private static Exception RecordException(Action testCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (new CultureReplacer())
|
||||
{
|
||||
testCode();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return UnwrapException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static Exception UnwrapException(Exception exception)
|
||||
{
|
||||
var aggEx = exception as AggregateException;
|
||||
return aggEx != null ? aggEx.GetBaseException() : exception;
|
||||
}
|
||||
|
||||
private static TException VerifyException<TException>(Exception exception)
|
||||
{
|
||||
var tie = exception as TargetInvocationException;
|
||||
if (tie != null)
|
||||
{
|
||||
exception = tie.InnerException;
|
||||
}
|
||||
Assert.NotNull(exception);
|
||||
return Assert.IsAssignableFrom<TException>(exception);
|
||||
}
|
||||
|
||||
private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false)
|
||||
{
|
||||
if (expectedMessage != null)
|
||||
{
|
||||
if (!partialMatch)
|
||||
{
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Contains(expectedMessage, exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight version of HttpClient implemented using Socket and SslStream.
|
||||
/// </summary>
|
||||
public static class HttpClientSlim
|
||||
{
|
||||
public static async Task<string> GetStringAsync(string requestUri, bool validateCertificate = true)
|
||||
=> await GetStringAsync(new Uri(requestUri), validateCertificate).ConfigureAwait(false);
|
||||
|
||||
public static async Task<string> GetStringAsync(Uri requestUri, bool validateCertificate = true)
|
||||
{
|
||||
using (var stream = await GetStream(requestUri, validateCertificate).ConfigureAwait(false))
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
await writer.WriteAsync($"GET {requestUri.PathAndQuery} HTTP/1.0\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync($"Host: {GetHost(requestUri)}\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync("\r\n").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await ReadResponse(stream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetHost(Uri requestUri)
|
||||
{
|
||||
var authority = requestUri.Authority;
|
||||
if (requestUri.HostNameType == UriHostNameType.IPv6)
|
||||
{
|
||||
// Make sure there's no % scope id. https://github.com/aspnet/KestrelHttpServer/issues/2637
|
||||
var address = IPAddress.Parse(requestUri.Host);
|
||||
address = new IPAddress(address.GetAddressBytes()); // Drop scope Id.
|
||||
if (requestUri.IsDefaultPort)
|
||||
{
|
||||
authority = $"[{address}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
authority = $"[{address}]:{requestUri.Port.ToString(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
return authority;
|
||||
}
|
||||
|
||||
public static async Task<string> PostAsync(string requestUri, HttpContent content, bool validateCertificate = true)
|
||||
=> await PostAsync(new Uri(requestUri), content, validateCertificate).ConfigureAwait(false);
|
||||
|
||||
public static async Task<string> PostAsync(Uri requestUri, HttpContent content, bool validateCertificate = true)
|
||||
{
|
||||
using (var stream = await GetStream(requestUri, validateCertificate))
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
await writer.WriteAsync($"POST {requestUri.PathAndQuery} HTTP/1.0\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync($"Host: {requestUri.Authority}\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync($"Content-Type: {content.Headers.ContentType}\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync($"Content-Length: {content.Headers.ContentLength}\r\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync("\r\n").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await content.CopyToAsync(stream).ConfigureAwait(false);
|
||||
|
||||
return await ReadResponse(stream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadResponse(Stream stream)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true,
|
||||
bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
var response = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var status = GetStatus(response);
|
||||
new HttpResponseMessage(status).EnsureSuccessStatusCode();
|
||||
|
||||
var body = response.Substring(response.IndexOf("\r\n\r\n") + 4);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpStatusCode GetStatus(string response)
|
||||
{
|
||||
var statusStart = response.IndexOf(' ') + 1;
|
||||
var statusEnd = response.IndexOf(' ', statusStart) - 1;
|
||||
var statusLength = statusEnd - statusStart + 1;
|
||||
|
||||
if (statusLength < 1)
|
||||
{
|
||||
throw new InvalidDataException($"No StatusCode found in '{response}'");
|
||||
}
|
||||
|
||||
return (HttpStatusCode)int.Parse(response.Substring(statusStart, statusLength));
|
||||
}
|
||||
|
||||
private static async Task<Stream> GetStream(Uri requestUri, bool validateCertificate)
|
||||
{
|
||||
var socket = await GetSocket(requestUri);
|
||||
var stream = new NetworkStream(socket, ownsSocket: true);
|
||||
|
||||
if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false, userCertificateValidationCallback:
|
||||
validateCertificate ? null : (RemoteCertificateValidationCallback)((a, b, c, d) => true));
|
||||
|
||||
await sslStream.AuthenticateAsClientAsync(requestUri.Host, clientCertificates: null,
|
||||
enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12,
|
||||
checkCertificateRevocation: validateCertificate).ConfigureAwait(false);
|
||||
return sslStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Socket> GetSocket(Uri requestUri)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Socket>();
|
||||
|
||||
var socketArgs = new SocketAsyncEventArgs();
|
||||
socketArgs.RemoteEndPoint = new DnsEndPoint(requestUri.DnsSafeHost, requestUri.Port);
|
||||
socketArgs.Completed += (s, e) => tcs.TrySetResult(e.ConnectSocket);
|
||||
|
||||
// Must use static ConnectAsync(), since instance Connect() does not support DNS names on OSX/Linux.
|
||||
if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, socketArgs))
|
||||
{
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var socket = socketArgs.ConnectSocket;
|
||||
|
||||
if (socket == null)
|
||||
{
|
||||
throw new SocketException((int)socketArgs.SocketError);
|
||||
}
|
||||
else
|
||||
{
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Various helpers for writing tests that use ASP.NET Core.</Description>
|
||||
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore</PackageTags>
|
||||
<EnableApiCheck>false</EnableApiCheck>
|
||||
<IsPackable>true</IsPackable>
|
||||
<!-- This package is internal, so we don't generate a package baseline. Always build against the latest dependencies. -->
|
||||
<UseLatestPackageReferences>true</UseLatestPackageReferences>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Win32.Registry" />
|
||||
<Reference Include="System.ValueTuple" />
|
||||
<Reference Include="xunit.assert" />
|
||||
<Reference Include="xunit.extensibility.execution" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net46'">
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="contentFiles\cs\netstandard2.0\EventSourceTestCollection.cs" />
|
||||
<Content Include="contentFiles\cs\netstandard2.0\EventSourceTestCollection.cs">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>contentFiles\cs\netstandard2.0\</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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.AspNetCore.Testing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// 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.Globalization;
|
||||
using System.Reflection;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces the current culture and UI culture for the test.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ReplaceCultureAttribute : BeforeAfterTestAttribute
|
||||
{
|
||||
private const string _defaultCultureName = "en-GB";
|
||||
private const string _defaultUICultureName = "en-US";
|
||||
private CultureInfo _originalCulture;
|
||||
private CultureInfo _originalUICulture;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current culture and UI culture to en-GB and en-US respectively.
|
||||
/// </summary>
|
||||
public ReplaceCultureAttribute() :
|
||||
this(_defaultCultureName, _defaultUICultureName)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current culture and UI culture based on specified values.
|
||||
/// </summary>
|
||||
public ReplaceCultureAttribute(string currentCulture, string currentUICulture)
|
||||
{
|
||||
Culture = new CultureInfo(currentCulture);
|
||||
UICulture = new CultureInfo(currentUICulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CultureInfo.CurrentCulture"/> for the test. Defaults to en-GB.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We
|
||||
/// want to be able to find bugs where we're accidentally relying on the Invariant instead of the
|
||||
/// user's culture.
|
||||
/// </remarks>
|
||||
public CultureInfo Culture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CultureInfo.CurrentUICulture"/> for the test. Defaults to en-US.
|
||||
/// </summary>
|
||||
public CultureInfo UICulture { get; }
|
||||
|
||||
public override void Before(MethodInfo methodUnderTest)
|
||||
{
|
||||
_originalCulture = CultureInfo.CurrentCulture;
|
||||
_originalUICulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
CultureInfo.CurrentCulture = Culture;
|
||||
CultureInfo.CurrentUICulture = UICulture;
|
||||
}
|
||||
|
||||
public override void After(MethodInfo methodUnderTest)
|
||||
{
|
||||
CultureInfo.CurrentCulture = _originalCulture;
|
||||
CultureInfo.CurrentUICulture = _originalUICulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public static class TaskExtensions
|
||||
{
|
||||
public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = default(int))
|
||||
{
|
||||
// Don't create a timer if the task is already completed
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
|
||||
{
|
||||
cts.Cancel();
|
||||
return await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(
|
||||
CreateMessage(timeout, filePath, lineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task TimeoutAfter(this Task task, TimeSpan timeout,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = default(int))
|
||||
{
|
||||
// Don't create a timer if the task is already completed
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
await task;
|
||||
return;
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
|
||||
{
|
||||
cts.Cancel();
|
||||
await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber)
|
||||
=> string.IsNullOrEmpty(filePath)
|
||||
? $"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms."
|
||||
: $"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms.";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class TestPathUtilities
|
||||
{
|
||||
public static string GetSolutionRootDirectory(string solution)
|
||||
{
|
||||
var applicationBasePath = AppContext.BaseDirectory;
|
||||
var directoryInfo = new DirectoryInfo(applicationBasePath);
|
||||
|
||||
do
|
||||
{
|
||||
var projectFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, $"{solution}.sln"));
|
||||
if (projectFileInfo.Exists)
|
||||
{
|
||||
return projectFileInfo.DirectoryName;
|
||||
}
|
||||
|
||||
directoryInfo = directoryInfo.Parent;
|
||||
}
|
||||
while (directoryInfo.Parent != null);
|
||||
|
||||
throw new Exception($"Solution file {solution}.sln could not be found in {applicationBasePath} or its parent directories.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public static class TestPlatformHelper
|
||||
{
|
||||
public static bool IsMono =>
|
||||
Type.GetType("Mono.Runtime") != null;
|
||||
|
||||
public static bool IsWindows =>
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
public static bool IsLinux =>
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
|
||||
public static bool IsMac =>
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.Tracing
|
||||
{
|
||||
public class CollectingEventListener : EventListener
|
||||
{
|
||||
private ConcurrentQueue<EventWrittenEventArgs> _events = new ConcurrentQueue<EventWrittenEventArgs>();
|
||||
|
||||
private object _lock = new object();
|
||||
|
||||
private Dictionary<string, EventSource> _existingSources = new Dictionary<string, EventSource>(StringComparer.OrdinalIgnoreCase);
|
||||
private HashSet<string> _requestedEventSources = new HashSet<string>();
|
||||
|
||||
public void CollectFrom(string eventSourceName)
|
||||
{
|
||||
lock(_lock)
|
||||
{
|
||||
// Check if it's already been created
|
||||
if(_existingSources.TryGetValue(eventSourceName, out var existingSource))
|
||||
{
|
||||
// It has, so just enable it now
|
||||
CollectFrom(existingSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
// It hasn't, so queue this request for when it is created
|
||||
_requestedEventSources.Add(eventSourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CollectFrom(EventSource eventSource) => EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All);
|
||||
|
||||
public IReadOnlyList<EventWrittenEventArgs> GetEventsWritten() => _events.ToArray();
|
||||
|
||||
protected override void OnEventSourceCreated(EventSource eventSource)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Add this to the list of existing sources for future CollectEventsFrom requests.
|
||||
_existingSources[eventSource.Name] = eventSource;
|
||||
|
||||
// Check if we have a pending request to enable it
|
||||
if (_requestedEventSources.Contains(eventSource.Name))
|
||||
{
|
||||
CollectFrom(eventSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEventWritten(EventWrittenEventArgs eventData)
|
||||
{
|
||||
_events.Enqueue(eventData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.Tracing
|
||||
{
|
||||
public class EventAssert
|
||||
{
|
||||
private readonly int _expectedId;
|
||||
private readonly string _expectedName;
|
||||
private readonly EventLevel _expectedLevel;
|
||||
private readonly IList<(string name, Action<object> asserter)> _payloadAsserters = new List<(string, Action<object>)>();
|
||||
|
||||
public EventAssert(int expectedId, string expectedName, EventLevel expectedLevel)
|
||||
{
|
||||
_expectedId = expectedId;
|
||||
_expectedName = expectedName;
|
||||
_expectedLevel = expectedLevel;
|
||||
}
|
||||
|
||||
public static void Collection(IEnumerable<EventWrittenEventArgs> events, params EventAssert[] asserts)
|
||||
{
|
||||
Assert.Collection(
|
||||
events,
|
||||
asserts.Select(a => a.CreateAsserter()).ToArray());
|
||||
}
|
||||
|
||||
public static EventAssert Event(int id, string name, EventLevel level)
|
||||
{
|
||||
return new EventAssert(id, name, level);
|
||||
}
|
||||
|
||||
public EventAssert Payload(string name, object expectedValue) => Payload(name, actualValue => Assert.Equal(expectedValue, actualValue));
|
||||
|
||||
public EventAssert Payload(string name, Action<object> asserter)
|
||||
{
|
||||
_payloadAsserters.Add((name, asserter));
|
||||
return this;
|
||||
}
|
||||
|
||||
private Action<EventWrittenEventArgs> CreateAsserter() => Execute;
|
||||
|
||||
private void Execute(EventWrittenEventArgs evt)
|
||||
{
|
||||
Assert.Equal(_expectedId, evt.EventId);
|
||||
Assert.Equal(_expectedName, evt.EventName);
|
||||
Assert.Equal(_expectedLevel, evt.Level);
|
||||
|
||||
Action<string> CreateNameAsserter((string name, Action<object> asserter) val)
|
||||
{
|
||||
return actualValue => Assert.Equal(val.name, actualValue);
|
||||
}
|
||||
|
||||
Assert.Collection(evt.PayloadNames, _payloadAsserters.Select(CreateNameAsserter).ToArray());
|
||||
Assert.Collection(evt.Payload, _payloadAsserters.Select(t => t.asserter).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Tracing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.Tracing
|
||||
{
|
||||
// This collection attribute is what makes the "magic" happen. It forces xunit to run all tests that inherit from this
|
||||
// base class sequentially, preventing conflicts (since EventSource/EventListener is a process-global concept).
|
||||
[Collection(CollectionName)]
|
||||
public abstract class EventSourceTestBase : IDisposable
|
||||
{
|
||||
public const string CollectionName = "Microsoft.AspNetCore.Testing.Tracing.EventSourceTestCollection";
|
||||
|
||||
private readonly CollectingEventListener _listener;
|
||||
|
||||
public EventSourceTestBase()
|
||||
{
|
||||
_listener = new CollectingEventListener();
|
||||
}
|
||||
|
||||
protected void CollectFrom(string eventSourceName)
|
||||
{
|
||||
_listener.CollectFrom(eventSourceName);
|
||||
}
|
||||
|
||||
protected void CollectFrom(EventSource eventSource)
|
||||
{
|
||||
_listener.CollectFrom(eventSource);
|
||||
}
|
||||
|
||||
protected IReadOnlyList<EventWrittenEventArgs> GetEvents() => _listener.GetEventsWritten();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Microsoft.AspNetCore.Testing.Tracing
|
||||
{
|
||||
// This file comes from Microsoft.AspNetCore.Testing and has to be defined in the test assembly.
|
||||
// It enables EventSourceTestBase's parallel isolation functionality.
|
||||
|
||||
[Xunit.CollectionDefinition(EventSourceTestBase.CollectionName, DisableParallelization = true)]
|
||||
public class EventSourceTestCollection
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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 Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
[XunitTestCaseDiscoverer("Microsoft.AspNetCore.Testing.xunit." + nameof(ConditionalFactDiscoverer), "Microsoft.AspNetCore.Testing")]
|
||||
public class ConditionalFactAttribute : FactAttribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
internal class ConditionalFactDiscoverer : FactDiscoverer
|
||||
{
|
||||
private readonly IMessageSink _diagnosticMessageSink;
|
||||
|
||||
public ConditionalFactDiscoverer(IMessageSink diagnosticMessageSink)
|
||||
: base(diagnosticMessageSink)
|
||||
{
|
||||
_diagnosticMessageSink = diagnosticMessageSink;
|
||||
}
|
||||
|
||||
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
|
||||
{
|
||||
var skipReason = testMethod.EvaluateSkipConditions();
|
||||
return skipReason != null
|
||||
? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod)
|
||||
: base.CreateTestCase(discoveryOptions, testMethod, factAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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 Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
[XunitTestCaseDiscoverer("Microsoft.AspNetCore.Testing.xunit." + nameof(ConditionalTheoryDiscoverer), "Microsoft.AspNetCore.Testing")]
|
||||
public class ConditionalTheoryAttribute : TheoryAttribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 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 Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
internal class ConditionalTheoryDiscoverer : TheoryDiscoverer
|
||||
{
|
||||
public ConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink)
|
||||
: base(diagnosticMessageSink)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
|
||||
{
|
||||
var skipReason = testMethod.EvaluateSkipConditions();
|
||||
return skipReason != null
|
||||
? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) }
|
||||
: base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
|
||||
{
|
||||
var skipReason = testMethod.EvaluateSkipConditions();
|
||||
if (skipReason == null && dataRow?.Length > 0)
|
||||
{
|
||||
var obj = dataRow[0];
|
||||
if (obj != null)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
var property = type.GetProperty("Skip");
|
||||
if (property != null && property.PropertyType.Equals(typeof(string)))
|
||||
{
|
||||
skipReason = property.GetValue(obj) as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skipReason != null ?
|
||||
base.CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, skipReason)
|
||||
: base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class DockerOnlyAttribute : Attribute, ITestCondition
|
||||
{
|
||||
public string SkipReason { get; } = "This test can only run in a Docker container.";
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// we currently don't have a good way to detect if running in a Windows container
|
||||
return false;
|
||||
}
|
||||
|
||||
const string procFile = "/proc/1/cgroup";
|
||||
if (!File.Exists(procFile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lines = File.ReadAllLines(procFile);
|
||||
// typically the last line in the file is "1:name=openrc:/docker"
|
||||
return lines.Reverse().Any(l => l.EndsWith("name=openrc:/docker", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// 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.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips a test when the value of an environment variable matches any of the supplied values.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class EnvironmentVariableSkipConditionAttribute : Attribute, ITestCondition
|
||||
{
|
||||
private readonly string _variableName;
|
||||
private readonly string[] _values;
|
||||
private string _currentValue;
|
||||
private readonly IEnvironmentVariable _environmentVariable;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="EnvironmentVariableSkipConditionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="variableName">Name of the environment variable.</param>
|
||||
/// <param name="values">Value(s) of the environment variable to match for the test to be skipped</param>
|
||||
public EnvironmentVariableSkipConditionAttribute(string variableName, params string[] values)
|
||||
: this(new EnvironmentVariable(), variableName, values)
|
||||
{
|
||||
}
|
||||
|
||||
// To enable unit testing
|
||||
internal EnvironmentVariableSkipConditionAttribute(
|
||||
IEnvironmentVariable environmentVariable,
|
||||
string variableName,
|
||||
params string[] values)
|
||||
{
|
||||
if (environmentVariable == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(environmentVariable));
|
||||
}
|
||||
if (variableName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(variableName));
|
||||
}
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
_variableName = variableName;
|
||||
_values = values;
|
||||
_environmentVariable = environmentVariable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips the test only if the value of the variable matches any of the supplied values. Default is <c>True</c>.
|
||||
/// </summary>
|
||||
public bool SkipOnMatch { get; set; } = true;
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get
|
||||
{
|
||||
_currentValue = _environmentVariable.Get(_variableName);
|
||||
var hasMatched = _values.Any(value => string.Compare(value, _currentValue, ignoreCase: true) == 0);
|
||||
|
||||
if (SkipOnMatch)
|
||||
{
|
||||
return hasMatched;
|
||||
}
|
||||
else
|
||||
{
|
||||
return !hasMatched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SkipReason
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = _currentValue == null ? "(null)" : _currentValue;
|
||||
return $"Test skipped on environment variable with name '{_variableName}' and value '{value}' " +
|
||||
$"for the '{nameof(SkipOnMatch)}' value of '{SkipOnMatch}'.";
|
||||
}
|
||||
}
|
||||
|
||||
private struct EnvironmentVariable : IEnvironmentVariable
|
||||
{
|
||||
public string Get(string name)
|
||||
{
|
||||
return Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class FrameworkSkipConditionAttribute : Attribute, ITestCondition
|
||||
{
|
||||
private readonly RuntimeFrameworks _excludedFrameworks;
|
||||
|
||||
public FrameworkSkipConditionAttribute(RuntimeFrameworks excludedFrameworks)
|
||||
{
|
||||
_excludedFrameworks = excludedFrameworks;
|
||||
}
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get
|
||||
{
|
||||
return CanRunOnThisFramework(_excludedFrameworks);
|
||||
}
|
||||
}
|
||||
|
||||
public string SkipReason { get; set; } = "Test cannot run on this runtime framework.";
|
||||
|
||||
private static bool CanRunOnThisFramework(RuntimeFrameworks excludedFrameworks)
|
||||
{
|
||||
if (excludedFrameworks == RuntimeFrameworks.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#if NET461 || NET46
|
||||
if (excludedFrameworks.HasFlag(RuntimeFrameworks.Mono) &&
|
||||
TestPlatformHelper.IsMono)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (excludedFrameworks.HasFlag(RuntimeFrameworks.CLR))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#elif NETSTANDARD2_0
|
||||
if (excludedFrameworks.HasFlag(RuntimeFrameworks.CoreCLR))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
#error Target frameworks need to be updated.
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// 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.AspNetCore.Testing.xunit
|
||||
{
|
||||
internal interface IEnvironmentVariable
|
||||
{
|
||||
string Get(string name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.AspNetCore.Testing.xunit
|
||||
{
|
||||
public interface ITestCondition
|
||||
{
|
||||
bool IsMet { get; }
|
||||
|
||||
string SkipReason { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips a test if the OS is the given type (Windows) and the OS version is less than specified.
|
||||
/// E.g. Specifying Window 10.0 skips on Win 8, but not on Linux. Combine with OSSkipConditionAttribute as needed.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class MinimumOSVersionAttribute : Attribute, ITestCondition
|
||||
{
|
||||
private readonly OperatingSystems _excludedOperatingSystem;
|
||||
private readonly Version _minVersion;
|
||||
private readonly OperatingSystems _osPlatform;
|
||||
private readonly Version _osVersion;
|
||||
|
||||
public MinimumOSVersionAttribute(OperatingSystems operatingSystem, string minVersion) :
|
||||
this(
|
||||
operatingSystem,
|
||||
GetCurrentOS(),
|
||||
GetCurrentOSVersion(),
|
||||
Version.Parse(minVersion))
|
||||
{
|
||||
}
|
||||
|
||||
// to enable unit testing
|
||||
internal MinimumOSVersionAttribute(
|
||||
OperatingSystems operatingSystem, OperatingSystems osPlatform, Version osVersion, Version minVersion)
|
||||
{
|
||||
if (operatingSystem != OperatingSystems.Windows)
|
||||
{
|
||||
throw new NotImplementedException("Min version support is only implemented for Windows.");
|
||||
}
|
||||
_excludedOperatingSystem = operatingSystem;
|
||||
_minVersion = minVersion;
|
||||
_osPlatform = osPlatform;
|
||||
_osVersion = osVersion;
|
||||
|
||||
SkipReason = $"This test requires {_excludedOperatingSystem} {_minVersion} or later.";
|
||||
}
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get
|
||||
{
|
||||
// Do not skip other OS's, Use OSSkipConditionAttribute or a separate MinimumOSVersionAttribute for that.
|
||||
if (_osPlatform != _excludedOperatingSystem)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _osVersion >= _minVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public string SkipReason { get; set; }
|
||||
|
||||
private static OperatingSystems GetCurrentOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return OperatingSystems.Windows;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return OperatingSystems.Linux;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return OperatingSystems.MacOSX;
|
||||
}
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private static Version GetCurrentOSVersion()
|
||||
{
|
||||
// currently not used on other OS's
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Win10+
|
||||
var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
|
||||
var major = key.GetValue("CurrentMajorVersionNumber") as int?;
|
||||
var minor = key.GetValue("CurrentMinorVersionNumber") as int?;
|
||||
|
||||
if (major.HasValue && minor.HasValue)
|
||||
{
|
||||
return new Version(major.Value, minor.Value);
|
||||
}
|
||||
|
||||
// CurrentVersion doesn't work past Win8.1
|
||||
var current = key.GetValue("CurrentVersion") as string;
|
||||
if (!string.IsNullOrEmpty(current) && Version.TryParse(current, out var currentVersion))
|
||||
{
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
// Environment.OSVersion doesn't work past Win8.
|
||||
return Environment.OSVersion.Version;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Version();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// 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 System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public class OSSkipConditionAttribute : Attribute, ITestCondition
|
||||
{
|
||||
private readonly OperatingSystems _excludedOperatingSystem;
|
||||
private readonly IEnumerable<string> _excludedVersions;
|
||||
private readonly OperatingSystems _osPlatform;
|
||||
private readonly string _osVersion;
|
||||
|
||||
public OSSkipConditionAttribute(OperatingSystems operatingSystem, params string[] versions) :
|
||||
this(
|
||||
operatingSystem,
|
||||
GetCurrentOS(),
|
||||
GetCurrentOSVersion(),
|
||||
versions)
|
||||
{
|
||||
}
|
||||
|
||||
// to enable unit testing
|
||||
internal OSSkipConditionAttribute(
|
||||
OperatingSystems operatingSystem, OperatingSystems osPlatform, string osVersion, params string[] versions)
|
||||
{
|
||||
_excludedOperatingSystem = operatingSystem;
|
||||
_excludedVersions = versions ?? Enumerable.Empty<string>();
|
||||
_osPlatform = osPlatform;
|
||||
_osVersion = osVersion;
|
||||
}
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get
|
||||
{
|
||||
var currentOSInfo = new OSInfo()
|
||||
{
|
||||
OperatingSystem = _osPlatform,
|
||||
Version = _osVersion,
|
||||
};
|
||||
|
||||
var skip = (_excludedOperatingSystem & currentOSInfo.OperatingSystem) == currentOSInfo.OperatingSystem;
|
||||
if (_excludedVersions.Any())
|
||||
{
|
||||
skip = skip
|
||||
&& _excludedVersions.Any(ex => _osVersion.StartsWith(ex, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Since a test would be excuted only if 'IsMet' is true, return false if we want to skip
|
||||
return !skip;
|
||||
}
|
||||
}
|
||||
|
||||
public string SkipReason { get; set; } = "Test cannot run on this operating system.";
|
||||
|
||||
static private OperatingSystems GetCurrentOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return OperatingSystems.Windows;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return OperatingSystems.Linux;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return OperatingSystems.MacOSX;
|
||||
}
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
static private string GetCurrentOSVersion()
|
||||
{
|
||||
// currently not used on other OS's
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return Environment.OSVersion.Version.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private class OSInfo
|
||||
{
|
||||
public OperatingSystems OperatingSystem { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[Flags]
|
||||
public enum OperatingSystems
|
||||
{
|
||||
Linux = 1,
|
||||
MacOSX = 2,
|
||||
Windows = 4,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
[Flags]
|
||||
public enum RuntimeFrameworks
|
||||
{
|
||||
None = 0,
|
||||
Mono = 1 << 0,
|
||||
CLR = 1 << 1,
|
||||
CoreCLR = 1 << 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 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 Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public class SkippedTestCase : XunitTestCase
|
||||
{
|
||||
private string _skipReason;
|
||||
|
||||
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
|
||||
public SkippedTestCase() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public SkippedTestCase(string skipReason, IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, ITestMethod testMethod, object[] testMethodArguments = null)
|
||||
: base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments)
|
||||
{
|
||||
_skipReason = skipReason;
|
||||
}
|
||||
|
||||
protected override string GetSkipReason(IAttributeInfo factAttribute)
|
||||
=> _skipReason ?? base.GetSkipReason(factAttribute);
|
||||
|
||||
public override void Deserialize(IXunitSerializationInfo data)
|
||||
{
|
||||
base.Deserialize(data);
|
||||
_skipReason = data.GetValue<string>(nameof(_skipReason));
|
||||
}
|
||||
|
||||
public override void Serialize(IXunitSerializationInfo data)
|
||||
{
|
||||
base.Serialize(data);
|
||||
data.AddValue(nameof(_skipReason), _skipReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Linq;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public static class TestMethodExtensions
|
||||
{
|
||||
public static string EvaluateSkipConditions(this ITestMethod testMethod)
|
||||
{
|
||||
var testClass = testMethod.TestClass.Class;
|
||||
var assembly = testMethod.TestClass.TestCollection.TestAssembly.Assembly;
|
||||
var conditionAttributes = testMethod.Method
|
||||
.GetCustomAttributes(typeof(ITestCondition))
|
||||
.Concat(testClass.GetCustomAttributes(typeof(ITestCondition)))
|
||||
.Concat(assembly.GetCustomAttributes(typeof(ITestCondition)))
|
||||
.OfType<ReflectionAttributeInfo>()
|
||||
.Select(attributeInfo => attributeInfo.Attribute);
|
||||
|
||||
foreach (ITestCondition condition in conditionAttributes)
|
||||
{
|
||||
if (!condition.IsMet)
|
||||
{
|
||||
return condition.SkipReason;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public static class WindowsVersions
|
||||
{
|
||||
public const string Win7 = "6.1";
|
||||
|
||||
public const string Win2008R2 = Win7;
|
||||
|
||||
public const string Win8 = "6.2";
|
||||
|
||||
public const string Win81 = "6.3";
|
||||
|
||||
public const string Win10 = "10.0";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System.Diagnostics.Tracing;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing.Tracing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.Tests
|
||||
{
|
||||
// We are verifying here that when event listener tests are spread among multiple classes, they still
|
||||
// work, even when run in parallel. To do that we have a bunch of tests in different classes (since
|
||||
// that affects parallelism) and do some Task.Yielding in them.
|
||||
public class CollectingEventListenerTests
|
||||
{
|
||||
public abstract class CollectingTestBase : EventSourceTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task CollectingEventListenerTest()
|
||||
{
|
||||
CollectFrom("Microsoft-AspNetCore-Testing-Test");
|
||||
|
||||
await Task.Yield();
|
||||
TestEventSource.Log.Test();
|
||||
await Task.Yield();
|
||||
TestEventSource.Log.TestWithPayload(42, 4.2);
|
||||
await Task.Yield();
|
||||
|
||||
var events = GetEvents();
|
||||
EventAssert.Collection(events,
|
||||
EventAssert.Event(1, "Test", EventLevel.Informational),
|
||||
EventAssert.Event(2, "TestWithPayload", EventLevel.Verbose)
|
||||
.Payload("payload1", 42)
|
||||
.Payload("payload2", 4.2));
|
||||
}
|
||||
}
|
||||
|
||||
// These tests are designed to interfere with the collecting ones by running in parallel and writing events
|
||||
public abstract class NonCollectingTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task CollectingEventListenerTest()
|
||||
{
|
||||
await Task.Yield();
|
||||
TestEventSource.Log.Test();
|
||||
await Task.Yield();
|
||||
TestEventSource.Log.TestWithPayload(42, 4.2);
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectingTests
|
||||
{
|
||||
public class A : CollectingTestBase { }
|
||||
public class B : CollectingTestBase { }
|
||||
public class C : CollectingTestBase { }
|
||||
public class D : CollectingTestBase { }
|
||||
public class E : CollectingTestBase { }
|
||||
public class F : CollectingTestBase { }
|
||||
public class G : CollectingTestBase { }
|
||||
}
|
||||
|
||||
public class NonCollectingTests
|
||||
{
|
||||
public class A : NonCollectingTestBase { }
|
||||
public class B : NonCollectingTestBase { }
|
||||
public class C : NonCollectingTestBase { }
|
||||
public class D : NonCollectingTestBase { }
|
||||
public class E : NonCollectingTestBase { }
|
||||
public class F : NonCollectingTestBase { }
|
||||
public class G : NonCollectingTestBase { }
|
||||
}
|
||||
}
|
||||
|
||||
[EventSource(Name = "Microsoft-AspNetCore-Testing-Test")]
|
||||
public class TestEventSource : EventSource
|
||||
{
|
||||
public static readonly TestEventSource Log = new TestEventSource();
|
||||
|
||||
private TestEventSource()
|
||||
{
|
||||
}
|
||||
|
||||
[Event(eventId: 1, Level = EventLevel.Informational, Message = "Test")]
|
||||
public void Test() => WriteEvent(1);
|
||||
|
||||
[Event(eventId: 2, Level = EventLevel.Verbose, Message = "Test")]
|
||||
public void TestWithPayload(int payload1, double payload2) => WriteEvent(2, payload1, payload2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// 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 Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class ConditionalFactTest : IClassFixture<ConditionalFactTest.ConditionalFactAsserter>
|
||||
{
|
||||
public ConditionalFactTest(ConditionalFactAsserter collector)
|
||||
{
|
||||
Asserter = collector;
|
||||
}
|
||||
|
||||
private ConditionalFactAsserter Asserter { get; }
|
||||
|
||||
[Fact]
|
||||
public void TestAlwaysRun()
|
||||
{
|
||||
// This is required to ensure that the type at least gets initialized.
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[ConditionalFact(Skip = "Test is always skipped.")]
|
||||
public void ConditionalFactSkip()
|
||||
{
|
||||
Assert.True(false, "This test should always be skipped.");
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_2
|
||||
[ConditionalFact]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.CLR)]
|
||||
public void ThisTestMustRunOnCoreCLR()
|
||||
{
|
||||
Asserter.TestRan = true;
|
||||
}
|
||||
#elif NET461 || NET46
|
||||
[ConditionalFact]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
|
||||
public void ThisTestMustRunOnCLR()
|
||||
{
|
||||
Asserter.TestRan = true;
|
||||
}
|
||||
#else
|
||||
#error Target frameworks need to be updated.
|
||||
#endif
|
||||
|
||||
public class ConditionalFactAsserter : IDisposable
|
||||
{
|
||||
public bool TestRan { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Assert.True(TestRan, "If this assertion fails, a conditional fact wasn't discovered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// 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 Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class ConditionalTheoryTest : IClassFixture<ConditionalTheoryTest.ConditionalTheoryAsserter>
|
||||
{
|
||||
public ConditionalTheoryTest(ConditionalTheoryAsserter asserter)
|
||||
{
|
||||
Asserter = asserter;
|
||||
}
|
||||
|
||||
public ConditionalTheoryAsserter Asserter { get; }
|
||||
|
||||
[ConditionalTheory(Skip = "Test is always skipped.")]
|
||||
[InlineData(0)]
|
||||
public void ConditionalTheorySkip(int arg)
|
||||
{
|
||||
Assert.True(false, "This test should always be skipped.");
|
||||
}
|
||||
|
||||
private static int _conditionalTheoryRuns = 0;
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2, Skip = "Skip these data")]
|
||||
public void ConditionalTheoryRunOncePerDataLine(int arg)
|
||||
{
|
||||
_conditionalTheoryRuns++;
|
||||
Assert.True(_conditionalTheoryRuns <= 2, $"Theory should run 2 times, but ran {_conditionalTheoryRuns} times.");
|
||||
}
|
||||
|
||||
[ConditionalTheory, Trait("Color", "Blue")]
|
||||
[InlineData(1)]
|
||||
public void ConditionalTheoriesShouldPreserveTraits(int arg)
|
||||
{
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[ConditionalTheory(Skip = "Skip this")]
|
||||
[MemberData(nameof(GetInts))]
|
||||
public void ConditionalTheoriesWithSkippedMemberData(int arg)
|
||||
{
|
||||
Assert.True(false, "This should never run");
|
||||
}
|
||||
|
||||
private static int _conditionalMemberDataRuns = 0;
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(4)]
|
||||
[MemberData(nameof(GetInts))]
|
||||
public void ConditionalTheoriesWithMemberData(int arg)
|
||||
{
|
||||
_conditionalMemberDataRuns++;
|
||||
Assert.True(_conditionalTheoryRuns <= 3, $"Theory should run 2 times, but ran {_conditionalMemberDataRuns} times.");
|
||||
}
|
||||
|
||||
public static TheoryData<int> GetInts
|
||||
=> new TheoryData<int> { 0, 1 };
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[MemberData(nameof(GetActionTestData))]
|
||||
public void ConditionalTheoryWithFuncs(Func<int, int> func)
|
||||
{
|
||||
Assert.True(false, "This should never run");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestAlwaysRun()
|
||||
{
|
||||
// This is required to ensure that this type at least gets initialized.
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_2
|
||||
[ConditionalTheory]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.CLR)]
|
||||
[MemberData(nameof(GetInts))]
|
||||
public void ThisTestMustRunOnCoreCLR(int value)
|
||||
{
|
||||
Asserter.TestRan = true;
|
||||
}
|
||||
#elif NET461 || NET46
|
||||
[ConditionalTheory]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
|
||||
[MemberData(nameof(GetInts))]
|
||||
public void ThisTestMustRunOnCLR(int value)
|
||||
{
|
||||
Asserter.TestRan = true;
|
||||
}
|
||||
#else
|
||||
#error Target frameworks need to be updated.
|
||||
#endif
|
||||
|
||||
public static TheoryData<Func<int, int>> GetActionTestData
|
||||
=> new TheoryData<Func<int, int>>
|
||||
{
|
||||
(i) => i * 1
|
||||
};
|
||||
|
||||
public class ConditionalTheoryAsserter : IDisposable
|
||||
{
|
||||
public bool TestRan { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Assert.True(TestRan, "If this assertion fails, a conditional theory wasn't discovered.");
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[MemberData(nameof(SkippableData))]
|
||||
public void WithSkipableData(Skippable skippable)
|
||||
{
|
||||
Assert.Null(skippable.Skip);
|
||||
Assert.Equal(1, skippable.Data);
|
||||
}
|
||||
|
||||
public static TheoryData<Skippable> SkippableData => new TheoryData<Skippable>
|
||||
{
|
||||
new Skippable() { Data = 1 },
|
||||
new Skippable() { Data = 2, Skip = "This row should be skipped." }
|
||||
};
|
||||
|
||||
public class Skippable : IXunitSerializable
|
||||
{
|
||||
public Skippable() { }
|
||||
public int Data { get; set; }
|
||||
public string Skip { get; set; }
|
||||
|
||||
public void Serialize(IXunitSerializationInfo info)
|
||||
{
|
||||
info.AddValue(nameof(Data), Data, typeof(int));
|
||||
}
|
||||
|
||||
public void Deserialize(IXunitSerializationInfo info)
|
||||
{
|
||||
Data = info.GetValue<int>(nameof(Data));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Data.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class DockerTests
|
||||
{
|
||||
[ConditionalFact]
|
||||
[DockerOnly]
|
||||
[Trait("Docker", "true")]
|
||||
public void DoesNotRunOnWindows()
|
||||
{
|
||||
Assert.False(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public class EnvironmentVariableSkipConditionTest
|
||||
{
|
||||
private readonly string _skipReason = "Test skipped on environment variable with name '{0}' and value '{1}'" +
|
||||
$" for the '{nameof(EnvironmentVariableSkipConditionAttribute.SkipOnMatch)}' value of '{{2}}'.";
|
||||
|
||||
[Theory]
|
||||
[InlineData("false")]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void IsMet_DoesNotMatch(string environmentVariableValue)
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable(environmentVariableValue),
|
||||
"Run",
|
||||
"true");
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.False(isMet);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("True")]
|
||||
[InlineData("TRUE")]
|
||||
[InlineData("true")]
|
||||
public void IsMet_DoesCaseInsensitiveMatch_OnValue(string environmentVariableValue)
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable(environmentVariableValue),
|
||||
"Run",
|
||||
"true");
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.True(isMet);
|
||||
Assert.Equal(
|
||||
string.Format(_skipReason, "Run", environmentVariableValue, attribute.SkipOnMatch),
|
||||
attribute.SkipReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMet_DoesSuccessfulMatch_OnNull()
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable(null),
|
||||
"Run",
|
||||
"true", null); // skip the test when the variable 'Run' is explicitly set to 'true' or is null (default)
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.True(isMet);
|
||||
Assert.Equal(
|
||||
string.Format(_skipReason, "Run", "(null)", attribute.SkipOnMatch),
|
||||
attribute.SkipReason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("false")]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void IsMet_MatchesOnMultipleSkipValues(string environmentVariableValue)
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable(environmentVariableValue),
|
||||
"Run",
|
||||
"false", "", null);
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.True(isMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMet_DoesNotMatch_OnMultipleSkipValues()
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable("100"),
|
||||
"Build",
|
||||
"125", "126");
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.False(isMet);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CentOS")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void IsMet_Matches_WhenSkipOnMatchIsFalse(string environmentVariableValue)
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable(environmentVariableValue),
|
||||
"LinuxFlavor",
|
||||
"Ubuntu14.04")
|
||||
{
|
||||
// Example: Run this test on all OSes except on "Ubuntu14.04"
|
||||
SkipOnMatch = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.True(isMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMet_DoesNotMatch_WhenSkipOnMatchIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new EnvironmentVariableSkipConditionAttribute(
|
||||
new TestEnvironmentVariable("Ubuntu14.04"),
|
||||
"LinuxFlavor",
|
||||
"Ubuntu14.04")
|
||||
{
|
||||
// Example: Run this test on all OSes except on "Ubuntu14.04"
|
||||
SkipOnMatch = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var isMet = attribute.IsMet;
|
||||
|
||||
// Assert
|
||||
Assert.False(isMet);
|
||||
}
|
||||
|
||||
private struct TestEnvironmentVariable : IEnvironmentVariable
|
||||
{
|
||||
public TestEnvironmentVariable(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; private set; }
|
||||
|
||||
public string Get(string name)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class ExceptionAssertTest
|
||||
{
|
||||
[Fact]
|
||||
[ReplaceCulture("fr-FR", "fr-FR")]
|
||||
public void AssertArgumentNullOrEmptyString_WorksInNonEnglishCultures()
|
||||
{
|
||||
// Arrange
|
||||
Action action = () =>
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or an empty string.", "foo");
|
||||
};
|
||||
|
||||
// Act and Assert
|
||||
ExceptionAssert.ThrowsArgumentNullOrEmptyString(action, "foo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture("fr-FR", "fr-FR")]
|
||||
public void AssertArgumentOutOfRangeException_WorksInNonEnglishCultures()
|
||||
{
|
||||
// Arrange
|
||||
Action action = () =>
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("foo", 10, "exception message.");
|
||||
};
|
||||
|
||||
// Act and Assert
|
||||
ExceptionAssert.ThrowsArgumentOutOfRange(action, "foo", "exception message.", 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class HttpClientSlimTest
|
||||
{
|
||||
private static byte[] _defaultResponse = Encoding.ASCII.GetBytes("test");
|
||||
|
||||
[Fact]
|
||||
public async Task GetStringAsyncHttp()
|
||||
{
|
||||
using (var host = StartHost(out var address))
|
||||
{
|
||||
Assert.Equal("test", await HttpClientSlim.GetStringAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStringAsyncThrowsForErrorResponse()
|
||||
{
|
||||
using (var host = StartHost(out var address, statusCode: 500))
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(() => HttpClientSlim.GetStringAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAsyncHttp()
|
||||
{
|
||||
using (var host = StartHost(out var address, handler: context => context.Request.InputStream.CopyToAsync(context.Response.OutputStream)))
|
||||
{
|
||||
Assert.Equal("test post", await HttpClientSlim.PostAsync(address, new StringContent("test post")));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAsyncThrowsForErrorResponse()
|
||||
{
|
||||
using (var host = StartHost(out var address, statusCode: 500))
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(
|
||||
() => HttpClientSlim.PostAsync(address, new StringContent("")));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ipv6ScopeIdsFilteredOut()
|
||||
{
|
||||
var requestUri = new Uri("http://[fe80::5d2a:d070:6fd6:1bac%7]:5003/");
|
||||
Assert.Equal("[fe80::5d2a:d070:6fd6:1bac]:5003", HttpClientSlim.GetHost(requestUri));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHostExcludesDefaultPort()
|
||||
{
|
||||
var requestUri = new Uri("http://[fe80::5d2a:d070:6fd6:1bac%7]:80/");
|
||||
Assert.Equal("[fe80::5d2a:d070:6fd6:1bac]", HttpClientSlim.GetHost(requestUri));
|
||||
}
|
||||
|
||||
private HttpListener StartHost(out string address, int statusCode = 200, Func<HttpListenerContext, Task> handler = null)
|
||||
{
|
||||
var listener = new HttpListener();
|
||||
var random = new Random();
|
||||
address = null;
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// HttpListener doesn't support requesting port 0 (dynamic).
|
||||
// Requesting port 0 from Sockets and then passing that to HttpListener is racy.
|
||||
// Just keep trying until we find a free one.
|
||||
address = $"http://127.0.0.1:{random.Next(1024, ushort.MaxValue)}/";
|
||||
listener.Prefixes.Add(address);
|
||||
listener.Start();
|
||||
break;
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
// Address in use
|
||||
listener.Close();
|
||||
listener = new HttpListener();
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(listener.IsListening, "IsListening");
|
||||
|
||||
_ = listener.GetContextAsync().ContinueWith(async task =>
|
||||
{
|
||||
var context = task.Result;
|
||||
context.Response.StatusCode = statusCode;
|
||||
|
||||
if (handler == null)
|
||||
{
|
||||
await context.Response.OutputStream.WriteAsync(_defaultResponse, 0, _defaultResponse.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
await handler(context);
|
||||
}
|
||||
|
||||
context.Response.Close();
|
||||
});
|
||||
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
|
||||
<!-- allow skipped tests -->
|
||||
<NoWarn>$(NoWarn);xUnit1004</NoWarn>
|
||||
<!-- allow unused theory parameters -->
|
||||
<NoWarn>$(NoWarn);xUnit1026</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\src\contentFiles\cs\netstandard2.0\EventSourceTestCollection.cs" Link="EventSourceTestCollection.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\Microsoft.AspNetCore.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public class OSSkipConditionAttributeTest
|
||||
{
|
||||
[Fact]
|
||||
public void Skips_WhenOnlyOperatingSystemIsSupplied()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Windows,
|
||||
OperatingSystems.Windows,
|
||||
"2.5");
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotSkip_WhenOperatingSystemDoesNotMatch()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Linux,
|
||||
OperatingSystems.Windows,
|
||||
"2.5");
|
||||
|
||||
// Assert
|
||||
Assert.True(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotSkip_WhenVersionsDoNotMatch()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Windows,
|
||||
OperatingSystems.Windows,
|
||||
"2.5",
|
||||
"10.0");
|
||||
|
||||
// Assert
|
||||
Assert.True(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotSkip_WhenOnlyVersionsMatch()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Linux,
|
||||
OperatingSystems.Windows,
|
||||
"2.5",
|
||||
"2.5");
|
||||
|
||||
// Assert
|
||||
Assert.True(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2.5", "2.5")]
|
||||
[InlineData("blue", "Blue")]
|
||||
public void Skips_WhenVersionsMatches(string currentOSVersion, string skipVersion)
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Windows,
|
||||
OperatingSystems.Windows,
|
||||
currentOSVersion,
|
||||
skipVersion);
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skips_WhenVersionsMatchesOutOfMultiple()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(
|
||||
OperatingSystems.Windows,
|
||||
OperatingSystems.Windows,
|
||||
"2.5",
|
||||
"10.0", "3.4", "2.5");
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttribute.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skips_BothMacOSXAndLinux()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttributeLinux = new OSSkipConditionAttribute(OperatingSystems.Linux | OperatingSystems.MacOSX, OperatingSystems.Linux, string.Empty);
|
||||
var osSkipAttributeMacOSX = new OSSkipConditionAttribute(OperatingSystems.Linux | OperatingSystems.MacOSX, OperatingSystems.MacOSX, string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttributeLinux.IsMet);
|
||||
Assert.False(osSkipAttributeMacOSX.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skips_BothMacOSXAndWindows()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(OperatingSystems.Windows | OperatingSystems.MacOSX, OperatingSystems.Windows, string.Empty);
|
||||
var osSkipAttributeMacOSX = new OSSkipConditionAttribute(OperatingSystems.Windows | OperatingSystems.MacOSX, OperatingSystems.MacOSX, string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttribute.IsMet);
|
||||
Assert.False(osSkipAttributeMacOSX.IsMet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skips_BothWindowsAndLinux()
|
||||
{
|
||||
// Act
|
||||
var osSkipAttribute = new OSSkipConditionAttribute(OperatingSystems.Linux | OperatingSystems.Windows, OperatingSystems.Windows, string.Empty);
|
||||
var osSkipAttributeLinux = new OSSkipConditionAttribute(OperatingSystems.Linux | OperatingSystems.Windows, OperatingSystems.Linux, string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.False(osSkipAttribute.IsMet);
|
||||
Assert.False(osSkipAttributeLinux.IsMet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing.xunit
|
||||
{
|
||||
public class OSSkipConditionTest
|
||||
{
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
public void TestSkipLinux()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
|
||||
"Test should not be running on Linux");
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
public void TestSkipMacOSX()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX),
|
||||
"Test should not be running on MacOSX.");
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
|
||||
public void RunTest_DoesNotRunOnWin7OrWin2008R2()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.ToString().StartsWith("6.1"),
|
||||
"Test should not be running on Win7 or Win2008R2.");
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
public void TestSkipWindows()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
"Test should not be running on Windows.");
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
|
||||
public void TestSkipLinuxAndMacOSX()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
|
||||
"Test should not be running on Linux.");
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX),
|
||||
"Test should not be running on MacOSX.");
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[InlineData(1)]
|
||||
public void TestTheorySkipLinux(int arg)
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
|
||||
"Test should not be running on Linux");
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData(1)]
|
||||
public void TestTheorySkipMacOS(int arg)
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX),
|
||||
"Test should not be running on MacOSX.");
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
[InlineData(1)]
|
||||
public void TestTheorySkipWindows(int arg)
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
"Test should not be running on Windows.");
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
|
||||
[InlineData(1)]
|
||||
public void TestTheorySkipLinuxAndMacOSX(int arg)
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux),
|
||||
"Test should not be running on Linux.");
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX),
|
||||
"Test should not be running on MacOSX.");
|
||||
}
|
||||
}
|
||||
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
public class OSSkipConditionClassTest
|
||||
{
|
||||
[ConditionalFact]
|
||||
public void TestSkipClassWindows()
|
||||
{
|
||||
Assert.False(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
|
||||
"Test should not be running on Windows.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// 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.Globalization;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class RepalceCultureAttributeTest
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultsTo_EnGB_EnUS()
|
||||
{
|
||||
// Arrange
|
||||
var culture = new CultureInfo("en-GB");
|
||||
var uiCulture = new CultureInfo("en-US");
|
||||
|
||||
// Act
|
||||
var replaceCulture = new ReplaceCultureAttribute();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(culture, replaceCulture.Culture);
|
||||
Assert.Equal(uiCulture, replaceCulture.UICulture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesSuppliedCultureAndUICulture()
|
||||
{
|
||||
// Arrange
|
||||
var culture = "de-DE";
|
||||
var uiCulture = "fr-CA";
|
||||
|
||||
// Act
|
||||
var replaceCulture = new ReplaceCultureAttribute(culture, uiCulture);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new CultureInfo(culture), replaceCulture.Culture);
|
||||
Assert.Equal(new CultureInfo(uiCulture), replaceCulture.UICulture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeforeAndAfterTest_ReplacesCulture()
|
||||
{
|
||||
// Arrange
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
var originalUICulture = CultureInfo.CurrentUICulture;
|
||||
var culture = "de-DE";
|
||||
var uiCulture = "fr-CA";
|
||||
var replaceCulture = new ReplaceCultureAttribute(culture, uiCulture);
|
||||
|
||||
// Act
|
||||
replaceCulture.Before(methodUnderTest: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new CultureInfo(culture), CultureInfo.CurrentCulture);
|
||||
Assert.Equal(new CultureInfo(uiCulture), CultureInfo.CurrentUICulture);
|
||||
|
||||
// Act
|
||||
replaceCulture.After(methodUnderTest: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(originalCulture, CultureInfo.CurrentCulture);
|
||||
Assert.Equal(originalUICulture, CultureInfo.CurrentUICulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class TaskExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task TimeoutAfterTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<TimeoutException>(async () => await Task.Delay(1000).TimeoutAfter(TimeSpan.FromMilliseconds(50)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class TestPathUtilitiesTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetSolutionRootDirectory_ResolvesSolutionRoot()
|
||||
{
|
||||
// Directory.GetCurrentDirectory() gives:
|
||||
// Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\netcoreapp2.0
|
||||
// Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\net461
|
||||
// Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\net46
|
||||
var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "..", ".."));
|
||||
|
||||
Assert.Equal(expectedPath, TestPathUtilities.GetSolutionRootDirectory("Extensions"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSolutionRootDirectory_Throws_IfNotFound()
|
||||
{
|
||||
var exception = Assert.Throws<Exception>(() => TestPathUtilities.GetSolutionRootDirectory("NotTesting"));
|
||||
Assert.Equal($"Solution file NotTesting.sln could not be found in {AppContext.BaseDirectory} or its parent directories.", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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 Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Testing
|
||||
{
|
||||
public class TestPlatformHelperTest
|
||||
{
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
public void IsLinux_TrueOnLinux()
|
||||
{
|
||||
Assert.True(TestPlatformHelper.IsLinux);
|
||||
Assert.False(TestPlatformHelper.IsMac);
|
||||
Assert.False(TestPlatformHelper.IsWindows);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.Windows)]
|
||||
public void IsMac_TrueOnMac()
|
||||
{
|
||||
Assert.False(TestPlatformHelper.IsLinux);
|
||||
Assert.True(TestPlatformHelper.IsMac);
|
||||
Assert.False(TestPlatformHelper.IsWindows);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
public void IsWindows_TrueOnWindows()
|
||||
{
|
||||
Assert.False(TestPlatformHelper.IsLinux);
|
||||
Assert.False(TestPlatformHelper.IsMac);
|
||||
Assert.True(TestPlatformHelper.IsWindows);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.CLR | RuntimeFrameworks.CoreCLR | RuntimeFrameworks.None)]
|
||||
public void IsMono_TrueOnMono()
|
||||
{
|
||||
Assert.True(TestPlatformHelper.IsMono);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||
public void IsMono_FalseElsewhere()
|
||||
{
|
||||
Assert.False(TestPlatformHelper.IsMono);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsProductComponent>true</IsProductComponent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
Loading…
Reference in New Issue