Merge branch 'release/2.2' into merge/release/2.1-to-release/2.2\n\nCommit migrated from a83df959fc

This commit is contained in:
William Godbe 2019-11-22 11:10:54 -08:00 committed by GitHub
commit 39273cd311
100 changed files with 6164 additions and 50 deletions

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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,
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,5 @@
{
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
]
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>();
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,5 @@
{
"AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
"Types": [
]
}

View File

@ -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));
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -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;
}
}
}

View File

@ -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
{

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -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;
}
}
}
}

View File

@ -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";
}
}
}

View File

@ -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));
}

View File

@ -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));
}
}
}

View File

@ -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
}));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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";
}
}

View File

@ -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()

View File

@ -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();
}
}
}
}
}

View File

@ -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;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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;
}
}
}

View File

@ -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.";
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
{
}
}

View File

@ -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
{
}
}

View File

@ -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);
}
}
}

View File

@ -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
{
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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,
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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.");
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<IsProductComponent>true</IsProductComponent>
</PropertyGroup>
</Project>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>