From 81b11e8d110464d4c0cbd3c57261e5c0f3bb93df Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 4 Apr 2018 18:46:03 -0700 Subject: [PATCH] Implement file logging integration with xunit - Add filename handling for assembly test log --- .appveyor.yml | 1 + .travis.yml | 1 + NuGetPackageVerifier.json | 11 +- build/dependencies.props | 33 ++-- .../AssemblyTestLog.cs | 37 +++- .../LoggedTest.cs | 34 +++- ...icrosoft.Extensions.Logging.Testing.csproj | 7 +- .../Properties/AssemblyInfo.cs | 6 + .../TestLoggerProvider.cs | 24 +++ .../Xunit/LogLevelAttribute.cs | 18 ++ .../Xunit/LoggedConditionalFactDiscoverer.cs | 28 +++ .../LoggedConditionalTheoryDiscoverer.cs | 41 +++++ .../Xunit/LoggedFactDiscoverer.cs | 18 ++ .../Xunit/LoggedTestAssemblyRunner.cs | 31 ++++ .../Xunit/LoggedTestCase.cs | 36 ++++ .../Xunit/LoggedTestCaseRunner.cs | 42 +++++ .../Xunit/LoggedTestClassRunner.cs | 36 ++++ .../Xunit/LoggedTestCollectionRunner.cs | 33 ++++ .../Xunit/LoggedTestFramework.cs | 26 +++ .../Xunit/LoggedTestFrameworkDiscoverer.cs | 80 ++++++++ .../Xunit/LoggedTestFrameworkExecutor.cs | 26 +++ .../Xunit/LoggedTestInvoker.cs | 86 +++++++++ .../Xunit/LoggedTestMethodRunner.cs | 36 ++++ .../Xunit/LoggedTestRunner.cs | 33 ++++ .../Xunit/LoggedTheoryDiscoverer.cs | 29 +++ .../Xunit/LoggedTheoryTestCase.cs | 35 ++++ .../Xunit/LoggedTheoryTestCaseRunner.cs | 41 +++++ ...Microsoft.Extensions.Logging.Testing.props | 8 + .../AssemblyTestLogTests.cs | 174 +++++++++--------- .../LogValuesAssertTest.cs | 1 - .../LoggedTestXunitTests.cs | 119 ++++++++++++ ...ft.Extensions.Logging.Testing.Tests.csproj | 2 +- 32 files changed, 1020 insertions(+), 113 deletions(-) create mode 100644 src/Microsoft.Extensions.Logging.Testing/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/TestLoggerProvider.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LogLevelAttribute.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalFactDiscoverer.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalTheoryDiscoverer.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedFactDiscoverer.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestAssemblyRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCase.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCaseRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestClassRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCollectionRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFramework.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkDiscoverer.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkExecutor.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestInvoker.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestMethodRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryDiscoverer.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCase.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCaseRunner.cs create mode 100644 src/Microsoft.Extensions.Logging.Testing/build/Microsoft.Extensions.Logging.Testing.props create mode 100644 test/Microsoft.Extensions.Logging.Testing.Tests/LoggedTestXunitTests.cs diff --git a/.appveyor.yml b/.appveyor.yml index 4eea96ab..372e8c8a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -12,6 +12,7 @@ environment: global: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: 1 + ASPNETCORE_TEST_LOG_DIR: "$APPVEYOR_BUILD_FOLDER\\artifacts\\logs" test: 'off' deploy: 'off' os: Visual Studio 2017 diff --git a/.travis.yml b/.travis.yml index 64bdbb44..99d90aa7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ env: global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: 1 + - ASPNETCORE_TEST_LOG_DIR: "$APPVEYOR_BUILD_FOLDER\\artifacts\\logs" mono: none os: - linux diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index b153ab15..281f41dd 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -2,6 +2,15 @@ "Default": { "rules": [ "DefaultCompositeRule" - ] + ], + "packages": { + "Microsoft.Extensions.Logging.Testing": { + "Exclusions": { + "BUILD_ITEMS_FRAMEWORK": { + "*": "Props file intentionally targets any framework since the content is the same for both netstandard2.0 and net461." + } + } + } + } } } \ No newline at end of file diff --git a/build/dependencies.props b/build/dependencies.props index e7968f51..5ea0098f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,31 +4,32 @@ 2.1.0-preview3-17004 - 2.1.0-preview3-32176 + 2.1.0-preview3-32192 2.6.1 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview2-26403-06 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 - 2.1.0-preview3-32176 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview2-26406-04 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 + 2.1.0-preview3-32192 2.0.0 - 2.1.0-preview2-26403-06 + 2.1.0-preview2-26406-04 15.6.1 4.7.49 11.0.2 1.4.0 3.2.0 - 4.5.0-preview2-26403-05 - 1.6.0-preview2-26403-05 - 4.5.0-preview2-26403-05 + 4.5.0-preview2-26406-04 + 1.6.0-preview2-26406-04 + 4.5.0-preview2-26406-04 2.0.1 2.3.1 + 2.3.1 2.3.1 2.4.0-beta.1.build3945 diff --git a/src/Microsoft.Extensions.Logging.Testing/AssemblyTestLog.cs b/src/Microsoft.Extensions.Logging.Testing/AssemblyTestLog.cs index fef92af0..a7d4992a 100644 --- a/src/Microsoft.Extensions.Logging.Testing/AssemblyTestLog.cs +++ b/src/Microsoft.Extensions.Logging.Testing/AssemblyTestLog.cs @@ -5,9 +5,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Extensions.Logging; @@ -20,6 +22,14 @@ public class AssemblyTestLog : IDisposable public static readonly string OutputDirectoryEnvironmentVariableName = "ASPNETCORE_TEST_LOG_DIR"; private static readonly string LogFileExtension = ".log"; private static readonly int MaxPathLength = 245; + private static char[] InvalidFileChars = new char[] + { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/', ' ', (char)127 + }; private static readonly object _lock = new object(); private static readonly Dictionary _logs = new Dictionary(); @@ -42,9 +52,12 @@ private AssemblyTestLog(ILoggerFactory globalLoggerFactory, ILogger globalLogger public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartTestLog(output, className, out loggerFactory, LogLevel.Debug, testName); - public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null) + public IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null) => + StartTestLog(output, className, out loggerFactory, minLogLevel, out var _, testName); + + internal IDisposable StartTestLog(ITestOutputHelper output, string className, out ILoggerFactory loggerFactory, LogLevel minLogLevel, out string resolvedTestName, [CallerMemberName] string testName = null) { - var serviceProvider = CreateLoggerServices(output, className, minLogLevel, testName); + var serviceProvider = CreateLoggerServices(output, className, minLogLevel, out resolvedTestName, testName); var factory = serviceProvider.GetRequiredService(); loggerFactory = factory; var logger = loggerFactory.CreateLogger("TestLifetime"); @@ -72,11 +85,13 @@ public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string class public ILoggerFactory CreateLoggerFactory(ITestOutputHelper output, string className, LogLevel minLogLevel, [CallerMemberName] string testName = null) { - return CreateLoggerServices(output, className, minLogLevel, testName).GetRequiredService(); + return CreateLoggerServices(output, className, minLogLevel, out var _, testName).GetRequiredService(); } - public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string className, LogLevel minLogLevel, [CallerMemberName] string testName = null) + public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string className, LogLevel minLogLevel, out string normalizedTestName, [CallerMemberName] string testName = null) { + normalizedTestName = string.Empty; + // Try to shorten the class name using the assembly name if (className.StartsWith(_assemblyName + ".")) { @@ -87,6 +102,7 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl if (!string.IsNullOrEmpty(_baseDirectory)) { var testOutputDirectory = Path.Combine(GetAssemblyBaseDirectory(_assemblyName, _baseDirectory), className); + testName = RemoveIllegalFileChars(testName); if (testOutputDirectory.Length + testName.Length + LogFileExtension.Length >= MaxPathLength) { @@ -118,11 +134,13 @@ public IServiceProvider CreateLoggerServices(ITestOutputHelper output, string cl if (!File.Exists(testOutputFile)) { _globalLogger.LogWarning($"To resolve log file collision, the enumerated file {testOutputFile} will be used."); + testName = $"{testName}.{i}"; break; } } } + normalizedTestName = testName; serilogLoggerProvider = ConfigureFileLogging(testOutputFile); } @@ -234,6 +252,17 @@ private static SerilogLoggerProvider ConfigureFileLogging(string fileName) return new SerilogLoggerProvider(serilogger, dispose: true); } + private static string RemoveIllegalFileChars(string s) + { + var sb = new StringBuilder(); + + foreach (var c in s) + { + sb.Append(InvalidFileChars.Contains(c) ? '_' : c); + } + return sb.ToString(); + } + public void Dispose() { (_serviceProvider as IDisposable)?.Dispose(); diff --git a/src/Microsoft.Extensions.Logging.Testing/LoggedTest.cs b/src/Microsoft.Extensions.Logging.Testing/LoggedTest.cs index a9850fc6..c92c13b2 100644 --- a/src/Microsoft.Extensions.Logging.Testing/LoggedTest.cs +++ b/src/Microsoft.Extensions.Logging.Testing/LoggedTest.cs @@ -4,24 +4,50 @@ using System; using System.Reflection; using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; namespace Microsoft.Extensions.Logging.Testing { public abstract class LoggedTest { - private readonly ITestOutputHelper _output; + private ILoggerFactory _loggerFactory; - public LoggedTest(ITestOutputHelper output) + // Obsolete but keeping for back compat + public LoggedTest(ITestOutputHelper output = null) { - _output = output; + TestOutputHelper = output; } + // Internal for testing + internal string TestMethodTestName { get; set; } + + public ILogger Logger { get; set; } + + public ILoggerFactory LoggerFactory + { + get + { + return _loggerFactory; + } + set + { + _loggerFactory = value; + AddTestLogging = services => services.AddSingleton(_loggerFactory); + } + } + + public ITestOutputHelper TestOutputHelper { get; set; } + + public ITestSink TestSink { get; set; } + + public Action AddTestLogging { get; private set; } = services => { }; + public IDisposable StartLog(out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartLog(out loggerFactory, LogLevel.Information, testName); public IDisposable StartLog(out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null) { - return AssemblyTestLog.ForAssembly(GetType().GetTypeInfo().Assembly).StartTestLog(_output, GetType().FullName, out loggerFactory, minLogLevel, testName); + return AssemblyTestLog.ForAssembly(GetType().GetTypeInfo().Assembly).StartTestLog(TestOutputHelper, GetType().FullName, out loggerFactory, minLogLevel, testName); } } } diff --git a/src/Microsoft.Extensions.Logging.Testing/Microsoft.Extensions.Logging.Testing.csproj b/src/Microsoft.Extensions.Logging.Testing/Microsoft.Extensions.Logging.Testing.csproj index 20c19d80..b26e3065 100644 --- a/src/Microsoft.Extensions.Logging.Testing/Microsoft.Extensions.Logging.Testing.csproj +++ b/src/Microsoft.Extensions.Logging.Testing/Microsoft.Extensions.Logging.Testing.csproj @@ -2,7 +2,7 @@ Helpers for writing tests that use Microsoft.Extensions.Logging. Contains null implementations of the abstractions that do nothing, as well as test implementations that are observable. - netstandard2.0 + netstandard2.0;net461 $(NoWarn);CS1591 $(PackageTags);testing false @@ -14,11 +14,16 @@ + + + + + diff --git a/src/Microsoft.Extensions.Logging.Testing/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Logging.Testing/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a5cc6c1d --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Properties/AssemblyInfo.cs @@ -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.Extensions.Logging.Testing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.Extensions.Logging.Testing/TestLoggerProvider.cs b/src/Microsoft.Extensions.Logging.Testing/TestLoggerProvider.cs new file mode 100644 index 00000000..758ff324 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/TestLoggerProvider.cs @@ -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. + +namespace Microsoft.Extensions.Logging.Testing +{ + public class TestLoggerProvider : ILoggerProvider + { + private readonly ITestSink _sink; + + public TestLoggerProvider(ITestSink sink) + { + _sink = sink; + } + + public ILogger CreateLogger(string categoryName) + { + return new TestLogger(categoryName, _sink, enabled: true); + } + + public void Dispose() + { + } + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LogLevelAttribute.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LogLevelAttribute.cs new file mode 100644 index 00000000..9f6f6213 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LogLevelAttribute.cs @@ -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; + +namespace Microsoft.Extensions.Logging.Testing +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class LogLevelAttribute : Attribute + { + public LogLevelAttribute(LogLevel logLevel) + { + LogLevel = logLevel; + } + + public LogLevel LogLevel { get; } + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalFactDiscoverer.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalFactDiscoverer.cs new file mode 100644 index 00000000..ebb9ecf9 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalFactDiscoverer.cs @@ -0,0 +1,28 @@ +// 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.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedConditionalFactDiscoverer : LoggedFactDiscoverer + { + private readonly IMessageSink _diagnosticMessageSink; + + public LoggedConditionalFactDiscoverer(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); + } + + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalTheoryDiscoverer.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalTheoryDiscoverer.cs new file mode 100644 index 00000000..da5685bb --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedConditionalTheoryDiscoverer.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.AspNetCore.Testing.xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedConditionalTheoryDiscoverer : LoggedTheoryDiscoverer + { + public LoggedConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + } + + protected override IEnumerable 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 CreateTestCasesForDataRow( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, IAttributeInfo theoryAttribute, + object[] dataRow) + { + var skipReason = testMethod.EvaluateSkipConditions(); + return skipReason != null + ? base.CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, skipReason) + : base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow); + } + + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedFactDiscoverer.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedFactDiscoverer.cs new file mode 100644 index 00000000..c52d99c8 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedFactDiscoverer.cs @@ -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 Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedFactDiscoverer : FactDiscoverer + { + public LoggedFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) + { + } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + => new LoggedTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestAssemblyRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestAssemblyRunner.cs new file mode 100644 index 00000000..39dff8fc --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestAssemblyRunner.cs @@ -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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestAssemblyRunner : XunitTestAssemblyRunner + { + public LoggedTestAssemblyRunner( + ITestAssembly testAssembly, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) + { + } + + protected override Task RunTestCollectionAsync( + IMessageBus messageBus, + ITestCollection testCollection, + IEnumerable testCases, + CancellationTokenSource cancellationTokenSource) + => new LoggedTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCase.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCase.cs new file mode 100644 index 00000000..b8d3684c --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCase.cs @@ -0,0 +1,36 @@ +// 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.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestCase : XunitTestCase + { + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public LoggedTestCase() : base() + { + } + + public LoggedTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + ITestMethod testMethod, + object[] testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments) + { + } + + public override Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + => new LoggedTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCaseRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCaseRunner.cs new file mode 100644 index 00000000..20cde614 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCaseRunner.cs @@ -0,0 +1,42 @@ +// 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.Reflection; +using System.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestCaseRunner : XunitTestCaseRunner + { + public LoggedTestCaseRunner( + IXunitTestCase testCase, + string displayName, + string skipReason, + object[] constructorArguments, + object[] testMethodArguments, + IMessageBus messageBus, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource) + { + } + + protected override XunitTestRunner CreateTestRunner( + ITest test, + IMessageBus messageBus, + Type testClass, + object[] constructorArguments, + MethodInfo testMethod, + object[] testMethodArguments, + string skipReason, + IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + => new LoggedTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, + skipReason, beforeAfterAttributes, new ExceptionAggregator(aggregator), cancellationTokenSource); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestClassRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestClassRunner.cs new file mode 100644 index 00000000..e7c30264 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestClassRunner.cs @@ -0,0 +1,36 @@ +// 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 Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestClassRunner : XunitTestClassRunner + { + public LoggedTestClassRunner( + ITestClass testClass, + IReflectionTypeInfo @class, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + ITestCaseOrderer testCaseOrderer, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, + IDictionary collectionFixtureMappings) + : base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings) + { + } + + protected override Task RunTestMethodAsync( + ITestMethod testMethod, + IReflectionMethodInfo method, + IEnumerable testCases, + object[] constructorArguments) + => new LoggedTestMethodRunner(testMethod, Class, method, testCases, DiagnosticMessageSink, MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, constructorArguments).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCollectionRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCollectionRunner.cs new file mode 100644 index 00000000..c3713d42 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestCollectionRunner.cs @@ -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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestCollectionRunner : XunitTestCollectionRunner + { + private readonly IMessageSink _diagnosticMessageSink; + + public LoggedTestCollectionRunner( + ITestCollection testCollection, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + ITestCaseOrderer testCaseOrderer, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) + { + // Base class doesn't expose this, so capture it here. + _diagnosticMessageSink = diagnosticMessageSink; + } + + protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases) + => new LoggedTestClassRunner(testClass, @class, testCases, _diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFramework.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFramework.cs new file mode 100644 index 00000000..dc5737d3 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFramework.cs @@ -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 System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestFramework : XunitTestFramework + { + public LoggedTestFramework(IMessageSink messageSink) : base(messageSink) + { + } + + protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assemblyInfo) + { + return new LoggedTestFrameworkDiscoverer(assemblyInfo, SourceInformationProvider, DiagnosticMessageSink); + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + { + return new LoggedTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + } + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkDiscoverer.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkDiscoverer.cs new file mode 100644 index 00000000..bc02355c --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkDiscoverer.cs @@ -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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer + { + private IDictionary Discoverers { get; } + + public LoggedTestFrameworkDiscoverer( + IAssemblyInfo assemblyInfo, + ISourceInformationProvider sourceProvider, + IMessageSink diagnosticMessageSink, + IXunitTestCollectionFactory collectionFactory = null) + : base(assemblyInfo, sourceProvider, diagnosticMessageSink, collectionFactory) + { + Discoverers = new Dictionary() + { + { typeof(ConditionalTheoryAttribute), new LoggedConditionalTheoryDiscoverer(diagnosticMessageSink) }, + { typeof(ConditionalFactAttribute), new LoggedConditionalFactDiscoverer(diagnosticMessageSink) }, + { typeof(TheoryAttribute), new LoggedTheoryDiscoverer(diagnosticMessageSink) }, + { typeof(FactAttribute), new LoggedFactDiscoverer(diagnosticMessageSink) } + }; + } + + protected override bool FindTestsForMethod( + ITestMethod testMethod, + bool includeSourceInformation, + IMessageBus messageBus, + ITestFrameworkDiscoveryOptions discoveryOptions) + { + if (typeof(LoggedTest).IsAssignableFrom(testMethod.TestClass.Class.ToRuntimeType())) + { + var factAttributes = testMethod.Method.GetCustomAttributes(typeof(FactAttribute)); + if (factAttributes.Count() > 1) + { + var message = $"Test method '{testMethod.TestClass.Class.Name}.{testMethod.Method.Name}' has multiple [Fact]-derived attributes"; + var testCase = new ExecutionErrorTestCase(DiagnosticMessageSink, TestMethodDisplay.ClassAndMethod, testMethod, message); + return ReportDiscoveredTestCase(testCase, includeSourceInformation, messageBus); + } + + var factAttribute = factAttributes.FirstOrDefault(); + if (factAttribute == null) + { + return true; + } + + var factAttributeType = (factAttribute as IReflectionAttributeInfo)?.Attribute.GetType(); + if (!Discoverers.TryGetValue(factAttributeType, out var discoverer)) + { + return base.FindTestsForMethod(testMethod, includeSourceInformation, messageBus, discoveryOptions); + } + else + { + foreach (var testCase in discoverer.Discover(discoveryOptions, testMethod, factAttribute)) + { + if (!ReportDiscoveredTestCase(testCase, includeSourceInformation, messageBus)) + { + return false; + } + } + + return true; + } + } + else + { + return base.FindTestsForMethod(testMethod, includeSourceInformation, messageBus, discoveryOptions); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkExecutor.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkExecutor.cs new file mode 100644 index 00000000..ece623fa --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestFrameworkExecutor.cs @@ -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 System.Collections.Generic; +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestFrameworkExecutor : XunitTestFrameworkExecutor + { + public LoggedTestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink) + : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) + { + using (var assemblyRunner = new LoggedTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions)) + { + await assemblyRunner.RunAsync(); + } + } + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestInvoker.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestInvoker.cs new file mode 100644 index 00000000..67136f16 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestInvoker.cs @@ -0,0 +1,86 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestInvoker : XunitTestInvoker + { + private TestOutputHelper _output; + + public LoggedTestInvoker( + ITest test, + IMessageBus messageBus, + Type testClass, + object[] constructorArguments, + MethodInfo testMethod, + object[] testMethodArguments, + IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, cancellationTokenSource) + { + } + + protected override Task BeforeTestMethodInvokedAsync() + { + if (_output != null) + { + _output.Initialize(MessageBus, Test); + } + + return base.BeforeTestMethodInvokedAsync(); + } + + protected override async Task AfterTestMethodInvokedAsync() + { + await base.AfterTestMethodInvokedAsync(); + + if (_output != null) + { + _output.Uninitialize(); + } + } + + protected override object CreateTestClass() + { + var testClass = base.CreateTestClass(); + + if (testClass is LoggedTest loggedTestClass) + { + var classType = loggedTestClass.GetType(); + var logLevelAttribute = TestMethod.GetCustomAttribute(typeof(LogLevelAttribute)) as LogLevelAttribute; + var testName = TestMethodArguments.Aggregate(TestMethod.Name, (a, b) => $"{a}-{(b ?? "null")}"); + + // Try resolving ITestOutputHelper from constructor arguments + loggedTestClass.TestOutputHelper = ConstructorArguments?.SingleOrDefault(a => typeof(ITestOutputHelper).IsAssignableFrom(a.GetType())) as ITestOutputHelper; + + // None resolved so create a new one and retain a reference to it for initialization/uninitialization + if (loggedTestClass.TestOutputHelper == null) + { + loggedTestClass.TestOutputHelper = _output = new TestOutputHelper(); + } + + AssemblyTestLog + .ForAssembly(classType.GetTypeInfo().Assembly) + .StartTestLog(loggedTestClass.TestOutputHelper, classType.FullName, out var loggerFactory, logLevelAttribute?.LogLevel ?? LogLevel.Debug, out var resolvedTestName, testName); + + loggedTestClass.LoggerFactory = loggerFactory; + loggedTestClass.TestMethodTestName = resolvedTestName; + loggedTestClass.Logger = loggerFactory.CreateLogger(classType); + loggedTestClass.TestSink = new TestSink(); + loggerFactory.AddProvider(new TestLoggerProvider(loggedTestClass.TestSink)); + } + + return testClass; + } + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestMethodRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestMethodRunner.cs new file mode 100644 index 00000000..2ba29882 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestMethodRunner.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestMethodRunner : XunitTestMethodRunner + { + private IMessageSink DiagnosticMessageSink { get; } + private object[] ConstructorArguments { get; } + + public LoggedTestMethodRunner( + ITestMethod testMethod, + IReflectionTypeInfo @class, + IReflectionMethodInfo method, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, + object[] constructorArguments) + : base(testMethod, @class, method, testCases, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource, constructorArguments) + { + DiagnosticMessageSink = diagnosticMessageSink; + ConstructorArguments = constructorArguments; + } + + protected override Task RunTestCaseAsync(IXunitTestCase testCase) + => testCase.RunAsync(DiagnosticMessageSink, MessageBus, ConstructorArguments, new ExceptionAggregator(Aggregator), CancellationTokenSource); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestRunner.cs new file mode 100644 index 00000000..07ad0978 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTestRunner.cs @@ -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 System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTestRunner : XunitTestRunner + { + public LoggedTestRunner( + ITest test, + IMessageBus messageBus, + Type testClass, + object[] constructorArguments, + MethodInfo testMethod, object[] + testMethodArguments, string skipReason, + IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) + { + } + + protected override Task InvokeTestMethodAsync(ExceptionAggregator aggregator) + => new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryDiscoverer.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryDiscoverer.cs new file mode 100644 index 00000000..535099c6 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryDiscoverer.cs @@ -0,0 +1,29 @@ +// 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.Extensions.Logging.Testing +{ + public class LoggedTheoryDiscoverer : TheoryDiscoverer + { + public LoggedTheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) + { + } + + protected override IEnumerable CreateTestCasesForDataRow( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo theoryAttribute, + object[] dataRow) + => new[] { new LoggedTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow) }; + + protected override IEnumerable CreateTestCasesForTheory( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo theoryAttribute) + => new[] { new LoggedTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) }; + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCase.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCase.cs new file mode 100644 index 00000000..db78f093 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCase.cs @@ -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; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTheoryTestCase : XunitTheoryTestCase + { + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public LoggedTheoryTestCase() : base() + { + } + + public LoggedTheoryTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, testMethod) + { + } + + public override Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + => new LoggedTheoryTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource).RunAsync(); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCaseRunner.cs b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCaseRunner.cs new file mode 100644 index 00000000..f1f92e1d --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/Xunit/LoggedTheoryTestCaseRunner.cs @@ -0,0 +1,41 @@ +// 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.Reflection; +using System.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Extensions.Logging.Testing +{ + public class LoggedTheoryTestCaseRunner : XunitTheoryTestCaseRunner + { + public LoggedTheoryTestCaseRunner( + IXunitTestCase testCase, + string displayName, + string skipReason, + object[] constructorArguments, + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(testCase, displayName, skipReason, constructorArguments, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource) + { + } + + protected override XunitTestRunner CreateTestRunner( + ITest test, + IMessageBus messageBus, + Type testClass, + object[] constructorArguments, + MethodInfo testMethod, + object[] testMethodArguments, + string skipReason, + IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + => new LoggedTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, new ExceptionAggregator(aggregator), cancellationTokenSource); + } +} diff --git a/src/Microsoft.Extensions.Logging.Testing/build/Microsoft.Extensions.Logging.Testing.props b/src/Microsoft.Extensions.Logging.Testing/build/Microsoft.Extensions.Logging.Testing.props new file mode 100644 index 00000000..165a6fc1 --- /dev/null +++ b/src/Microsoft.Extensions.Logging.Testing/build/Microsoft.Extensions.Logging.Testing.props @@ -0,0 +1,8 @@ + + +   +     <_Parameter1>Microsoft.Extensions.Logging.Testing.LoggedTestFramework +     <_Parameter2>Microsoft.Extensions.Logging.Testing +   + + \ No newline at end of file diff --git a/test/Microsoft.Extensions.Logging.Testing.Tests/AssemblyTestLogTests.cs b/test/Microsoft.Extensions.Logging.Testing.Tests/AssemblyTestLogTests.cs index ffb642a6..0987848c 100644 --- a/test/Microsoft.Extensions.Logging.Testing.Tests/AssemblyTestLogTests.cs +++ b/test/Microsoft.Extensions.Logging.Testing.Tests/AssemblyTestLogTests.cs @@ -9,8 +9,8 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; using Xunit; -using Xunit.Abstractions; namespace Microsoft.Extensions.Logging.Testing.Tests { @@ -18,10 +18,6 @@ public class AssemblyTestLogTests : LoggedTest { private static readonly Assembly ThisAssembly = typeof(AssemblyTestLog).GetTypeInfo().Assembly; - public AssemblyTestLogTests(ITestOutputHelper output) : base(output) - { - } - [Fact] public void ForAssembly_ReturnsSameInstanceForSameAssembly() { @@ -52,37 +48,50 @@ public void TestLogWritesToITestOutputHelper() } [Fact] - public Task TestLogWritesToGlobalLogFile() => - RunTestLogFunctionalTest((tempDir, loggerFactory) => - { - // Because this test writes to a file, it is a functional test and should be logged - // but it's also testing the test logging facility. So this is pretty meta ;) - var logger = loggerFactory.CreateLogger("Test"); + private Task TestLogEscapesIllegalFileNames() => + RunTestLogFunctionalTest((tempDir) => + { + var illegalTestName = "Testing-https://localhost:5000"; + var escapedTestName = "Testing-https___localhost_5000"; + using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", baseDirectory: tempDir)) + using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, resolvedTestName: out var resolvedTestname, testName: illegalTestName)) + { + Assert.Equal(escapedTestName, resolvedTestname); + } + }); - using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) + [Fact] + public Task TestLogWritesToGlobalLogFile() => + RunTestLogFunctionalTest((tempDir) => { - logger.LogInformation("Created test log in {baseDirectory}", tempDir); + // Because this test writes to a file, it is a functional test and should be logged + // but it's also testing the test logging facility. So this is pretty meta ;) + var logger = LoggerFactory.CreateLogger("Test"); - using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) { - var testLogger = testLoggerFactory.CreateLogger("TestLogger"); - testLogger.LogInformation("Information!"); - testLogger.LogTrace("Trace!"); + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + { + var testLogger = testLoggerFactory.CreateLogger("TestLogger"); + testLogger.LogInformation("Information!"); + testLogger.LogTrace("Trace!"); + } } - } - logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - var globalLogPath = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "global.log"); - var testLog = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log"); + var globalLogPath = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "global.log"); + var testLog = Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log"); - Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist"); - Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist"); + Assert.True(File.Exists(globalLogPath), $"Expected global log file {globalLogPath} to exist"); + Assert.True(File.Exists(testLog), $"Expected test log file {testLog} to exist"); - var globalLogContent = MakeConsistent(File.ReadAllText(globalLogPath)); - var testLogContent = MakeConsistent(File.ReadAllText(testLog)); + var globalLogContent = MakeConsistent(File.ReadAllText(globalLogPath)); + var testLogContent = MakeConsistent(File.ReadAllText(testLog)); - Assert.Equal(@"[GlobalTestLog] [Information] Global Test Logging initialized. Set the 'ASPNETCORE_TEST_LOG_DIR' Environment Variable in order to create log files on disk. + Assert.Equal(@"[GlobalTestLog] [Information] Global Test Logging initialized. Set the 'ASPNETCORE_TEST_LOG_DIR' Environment Variable in order to create log files on disk. [GlobalTestLog] [Information] Starting test ""FakeTestName"" [GlobalTestLog] [Information] Finished test ""FakeTestName"" in DURATION ", globalLogContent, ignoreLineEndingDifferences: true); @@ -91,91 +100,86 @@ [TestLogger] [Information] Information! [TestLogger] [Verbose] Trace! [TestLifetime] [Information] Finished test ""FakeTestName"" in DURATION ", testLogContent, ignoreLineEndingDifferences: true); - }); + }); [Fact] public Task TestLogTruncatesTestNameToAvoidLongPaths() => - RunTestLogFunctionalTest((tempDir, loggerFactory) => - { - var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50); - var logger = loggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) + RunTestLogFunctionalTest((tempDir) => { - logger.LogInformation("Created test log in {baseDirectory}", tempDir); - - using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName)) + var longTestName = new string('0', 50) + new string('1', 50) + new string('2', 50) + new string('3', 50) + new string('4', 50); + var logger = LoggerFactory.CreateLogger("Test"); + using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) { - testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); - } - } - logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + logger.LogInformation("Created test log in {baseDirectory}", tempDir); - var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass")).EnumerateFiles(); - var testLog = Assert.Single(testLogFiles); - var testFileName = Path.GetFileNameWithoutExtension(testLog.Name); + using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: longTestName)) + { + testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); + } + } + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - // The first half of the file comes from the beginning of the test name passed to the logger - Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2)); - // The last half of the file comes from the ending of the test name passed to the logger - Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); + var testLogFiles = new DirectoryInfo(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass")).EnumerateFiles(); + var testLog = Assert.Single(testLogFiles); + var testFileName = Path.GetFileNameWithoutExtension(testLog.Name); - var testLogContent = MakeConsistent(File.ReadAllText(testLog.FullName)); - }); + // The first half of the file comes from the beginning of the test name passed to the logger + Assert.Equal(longTestName.Substring(0, testFileName.Length / 2), testFileName.Substring(0, testFileName.Length / 2)); + // The last half of the file comes from the ending of the test name passed to the logger + Assert.Equal(longTestName.Substring(longTestName.Length - testFileName.Length / 2, testFileName.Length / 2), testFileName.Substring(testFileName.Length - testFileName.Length / 2, testFileName.Length / 2)); + }); [Fact] public Task TestLogEnumerateFilenamesToAvoidCollisions() => - RunTestLogFunctionalTest((tempDir, loggerFactory) => - { - var logger = loggerFactory.CreateLogger("Test"); - using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) + RunTestLogFunctionalTest((tempDir) => { - logger.LogInformation("Created test log in {baseDirectory}", tempDir); - - for (var i = 0; i < 10; i++) + var logger = LoggerFactory.CreateLogger("Test"); + using (var testAssemblyLog = AssemblyTestLog.Create("FakeTestAssembly", tempDir)) { - using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + logger.LogInformation("Created test log in {baseDirectory}", tempDir); + + for (var i = 0; i < 10; i++) { - testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); + using (testAssemblyLog.StartTestLog(output: null, className: "FakeTestAssembly.FakeTestClass", loggerFactory: out var testLoggerFactory, minLogLevel: LogLevel.Trace, testName: "FakeTestName")) + { + testLoggerFactory.CreateLogger("TestLogger").LogInformation("Information!"); + } } } - } - logger.LogInformation("Finished test log in {baseDirectory}", tempDir); + logger.LogInformation("Finished test log in {baseDirectory}", tempDir); - // The first log file exists - Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log"))); + // The first log file exists + Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.log"))); - // Subsequent files exist - for (var i = 0; i < 9; i++) - { - Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.{i}.log"))); - } - }); + // Subsequent files exist + for (var i = 0; i < 9; i++) + { + Assert.True(File.Exists(Path.Combine(tempDir, "FakeTestAssembly", RuntimeInformation.FrameworkDescription.TrimStart('.'), "FakeTestClass", $"FakeTestName.{i}.log"))); + } + }); private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+"); private static readonly Regex DurationRegex = new Regex(@"[^ ]+s$"); - private async Task RunTestLogFunctionalTest(Action action, [CallerMemberName] string testName = null) + private async Task RunTestLogFunctionalTest(Action action, [CallerMemberName] string testName = null) { - using (StartLog(out var loggerFactory, testName)) + var tempDir = Path.Combine(Path.GetTempPath(), $"TestLogging_{Guid.NewGuid().ToString("N")}"); + try { - var tempDir = Path.Combine(Path.GetTempPath(), $"TestLogging_{Guid.NewGuid().ToString("N")}"); - try - { - action(tempDir, loggerFactory); - } - finally + action(tempDir); + } + finally + { + if (Directory.Exists(tempDir)) { - if (Directory.Exists(tempDir)) + try { - try - { - Directory.Delete(tempDir, recursive: true); - } - catch - { - await Task.Delay(100); - Directory.Delete(tempDir, recursive: true); - } + Directory.Delete(tempDir, recursive: true); + } + catch + { + await Task.Delay(100); + Directory.Delete(tempDir, recursive: true); } } } diff --git a/test/Microsoft.Extensions.Logging.Testing.Tests/LogValuesAssertTest.cs b/test/Microsoft.Extensions.Logging.Testing.Tests/LogValuesAssertTest.cs index 781c85df..b5e1d987 100644 --- a/test/Microsoft.Extensions.Logging.Testing.Tests/LogValuesAssertTest.cs +++ b/test/Microsoft.Extensions.Logging.Testing.Tests/LogValuesAssertTest.cs @@ -1,7 +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; using System.Collections.Generic; using System.Linq; using Xunit; diff --git a/test/Microsoft.Extensions.Logging.Testing.Tests/LoggedTestXunitTests.cs b/test/Microsoft.Extensions.Logging.Testing.Tests/LoggedTestXunitTests.cs new file mode 100644 index 00000000..f9d024ea --- /dev/null +++ b/test/Microsoft.Extensions.Logging.Testing.Tests/LoggedTestXunitTests.cs @@ -0,0 +1,119 @@ +// 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 Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging.Testing.Tests +{ + public class LoggedTestXunitTests : LoggedTest + { + private readonly ITestOutputHelper _output; + + public LoggedTestXunitTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void LoggedTestTestOutputHelperSameInstanceAsInjectedConstructorArg() + { + Assert.Same(_output, TestOutputHelper); + } + + [Fact] + public void LoggedFactInitializesLoggedTestProperties() + { + Assert.NotNull(Logger); + Assert.NotNull(LoggerFactory); + Assert.NotNull(TestSink); + Assert.NotNull(TestOutputHelper); + } + + [Theory] + [InlineData("Hello world")] + public void LoggedTheoryInitializesLoggedTestProperties(string argument) + { + Assert.NotNull(Logger); + Assert.NotNull(LoggerFactory); + Assert.NotNull(TestSink); + Assert.NotNull(TestOutputHelper); + // Use the test argument + Assert.NotNull(argument); + } + + [ConditionalFact] + public void ConditionalLoggedFactGetsInitializedLoggerFactory() + { + Assert.NotNull(Logger); + Assert.NotNull(LoggerFactory); + Assert.NotNull(TestSink); + Assert.NotNull(TestOutputHelper); + } + + [ConditionalTheory] + [InlineData("Hello world")] + public void LoggedConditionalTheoryInitializesLoggedTestProperties(string argument) + { + Assert.NotNull(Logger); + Assert.NotNull(LoggerFactory); + Assert.NotNull(TestSink); + Assert.NotNull(TestOutputHelper); + // Use the test argument + Assert.NotNull(argument); + } + + [Fact] + [LogLevel(LogLevel.Information)] + public void LoggedFactFilteredByLogLevel() + { + Logger.LogInformation("Information"); + Logger.LogDebug("Debug"); + + var message = Assert.Single(TestSink.Writes); + Assert.Equal(LogLevel.Information, message.LogLevel); + Assert.Equal("Information", message.Formatter(message.State, null)); + } + + [Theory] + [InlineData("Hello world")] + [LogLevel(LogLevel.Information)] + public void LoggedTheoryFilteredByLogLevel(string argument) + { + Logger.LogInformation("Information"); + Logger.LogDebug("Debug"); + + var message = Assert.Single(TestSink.Writes); + Assert.Equal(LogLevel.Information, message.LogLevel); + Assert.Equal("Information", message.Formatter(message.State, null)); + + // Use the test argument + Assert.NotNull(argument); + } + + [Fact] + public void AddTestLoggingUpdatedWhenLoggerFactoryIsSet() + { + var loggerFactory = new LoggerFactory(); + var serviceCollection = new ServiceCollection(); + + LoggerFactory = loggerFactory; + AddTestLogging(serviceCollection); + + Assert.Same(loggerFactory, serviceCollection.BuildServiceProvider().GetRequiredService()); + } + + [ConditionalTheory] + [EnvironmentVariableSkipCondition("ASPNETCORE_TEST_LOG_DIR", "")] // The test name is only generated when logging is enabled via the environment variable + [InlineData(null)] + public void LoggedTheoryNullArgumentsAreEscaped(string argument) + { + Assert.NotNull(LoggerFactory); + Assert.Equal($"{nameof(LoggedTheoryNullArgumentsAreEscaped)}_null", TestMethodTestName); + // Use the test argument + Assert.Null(argument); + } + } +} diff --git a/test/Microsoft.Extensions.Logging.Testing.Tests/Microsoft.Extensions.Logging.Testing.Tests.csproj b/test/Microsoft.Extensions.Logging.Testing.Tests/Microsoft.Extensions.Logging.Testing.Tests.csproj index e04e476c..3f9c6139 100644 --- a/test/Microsoft.Extensions.Logging.Testing.Tests/Microsoft.Extensions.Logging.Testing.Tests.csproj +++ b/test/Microsoft.Extensions.Logging.Testing.Tests/Microsoft.Extensions.Logging.Testing.Tests.csproj @@ -1,4 +1,5 @@  + $(StandardTestTfms) @@ -9,5 +10,4 @@ -