diff --git a/.gitignore b/.gitignore index e604b2473fd..4711d4cf18c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ project.lock.json *.docstates _ReSharper.* *.exe +*net45.csproj +*net451.csproj +*k10.csproj *.psess *.vsp *.pidb @@ -33,5 +36,6 @@ _ReSharper.* node_modules/ **/[Cc]ompiler/[Rr]esources/**/*.js .vscode/ +.testPublish/ global.json BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/Common.sln b/Common.sln index 245c1936bc8..169c77a1b4f 100644 --- a/Common.sln +++ b/Common.sln @@ -58,8 +58,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Object EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Primitives.Performance", "benchmarks\Microsoft.Extensions.Primitives.Performance\Microsoft.Extensions.Primitives.Performance.csproj", "{E180A42D-2CB3-4810-8605-1AC30250EFED}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Testing", "src\Microsoft.AspNetCore.Testing\Microsoft.AspNetCore.Testing.csproj", "{FBFB870A-39D1-4DA3-B002-C21E5EA247D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Tests", "test\Sample.Tests\Sample.Tests.csproj", "{225CFC67-B1CB-46E6-A4EE-E42B532F4986}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Analyzer.Testing", "src\Microsoft.AspNetCore.Analyzer.Testing\Microsoft.AspNetCore.Analyzer.Testing.csproj", "{50EC2676-229C-4305-AEA8-EE14AD3C3CFC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Testing.Tests", "test\Microsoft.AspNetCore.Testing.Tests\Microsoft.AspNetCore.Testing.Tests.csproj", "{B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,10 +108,22 @@ Global {E180A42D-2CB3-4810-8605-1AC30250EFED}.Debug|Any CPU.Build.0 = Debug|Any CPU {E180A42D-2CB3-4810-8605-1AC30250EFED}.Release|Any CPU.ActiveCfg = Release|Any CPU {E180A42D-2CB3-4810-8605-1AC30250EFED}.Release|Any CPU.Build.0 = Release|Any CPU + {FBFB870A-39D1-4DA3-B002-C21E5EA247D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBFB870A-39D1-4DA3-B002-C21E5EA247D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBFB870A-39D1-4DA3-B002-C21E5EA247D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBFB870A-39D1-4DA3-B002-C21E5EA247D9}.Release|Any CPU.Build.0 = Release|Any CPU + {225CFC67-B1CB-46E6-A4EE-E42B532F4986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {225CFC67-B1CB-46E6-A4EE-E42B532F4986}.Debug|Any CPU.Build.0 = Debug|Any CPU + {225CFC67-B1CB-46E6-A4EE-E42B532F4986}.Release|Any CPU.ActiveCfg = Release|Any CPU + {225CFC67-B1CB-46E6-A4EE-E42B532F4986}.Release|Any CPU.Build.0 = Release|Any CPU {50EC2676-229C-4305-AEA8-EE14AD3C3CFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {50EC2676-229C-4305-AEA8-EE14AD3C3CFC}.Debug|Any CPU.Build.0 = Debug|Any CPU {50EC2676-229C-4305-AEA8-EE14AD3C3CFC}.Release|Any CPU.ActiveCfg = Release|Any CPU {50EC2676-229C-4305-AEA8-EE14AD3C3CFC}.Release|Any CPU.Build.0 = Release|Any CPU + {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -120,7 +138,10 @@ Global {E1586801-D345-43C7-BC4B-9D4A83101B6C} = {8668B1D5-9C54-49CA-8446-18040B4C7D15} {7486AB7B-C22F-4CA0-91EA-D31D9443DBFB} = {A9A93AF9-2113-4321-AD20-51F60FF8B2BD} {E180A42D-2CB3-4810-8605-1AC30250EFED} = {A9A93AF9-2113-4321-AD20-51F60FF8B2BD} + {FBFB870A-39D1-4DA3-B002-C21E5EA247D9} = {FEAA3936-5906-4383-B750-F07FE1B156C5} + {225CFC67-B1CB-46E6-A4EE-E42B532F4986} = {6878D8F1-6DCE-4677-AA1A-4D14BA6D2D60} {50EC2676-229C-4305-AEA8-EE14AD3C3CFC} = {FEAA3936-5906-4383-B750-F07FE1B156C5} + {B439E0C8-F892-4AC5-BBF7-63BCDAACA7A9} = {6878D8F1-6DCE-4677-AA1A-4D14BA6D2D60} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {371030CF-B541-4BA9-9F54-3C7563415CF1} diff --git a/build/dependencies.props b/build/dependencies.props index ad1bf786aa1..2d8471688f0 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -12,13 +12,16 @@ 2.2.0-preview1-26424-04 15.6.1 4.7.49 - 2.0.3 + 2.0.1 11.0.2 4.5.0-preview3-26423-04 + 4.3.2 4.5.0-preview3-26423-04 1.6.0-preview3-26423-04 4.5.0-preview3-26423-04 + 4.3.0 4.5.0-preview3-26423-04 + 4.5.0-preview3-26413-02 4.5.0-preview3-26423-04 0.8.0 2.3.1 diff --git a/src/Microsoft.AspNetCore.Testing/CultureReplacer.cs b/src/Microsoft.AspNetCore.Testing/CultureReplacer.cs new file mode 100644 index 00000000000..51e35e83544 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/CultureReplacer.cs @@ -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; + } + + /// + /// The name of the culture that is used as the default value for CultureInfo.DefaultThreadCurrentCulture when CultureReplacer is used. + /// + public static string DefaultCultureName + { + get { return _defaultCultureName; } + } + + /// + /// The name of the culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentUICulture when CultureReplacer is used. + /// + public static string DefaultUICultureName + { + get { return _defaultUICultureName; } + } + + /// + /// The culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentCulture when CultureReplacer is used. + /// + 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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/ExceptionAssertions.cs b/src/Microsoft.AspNetCore.Testing/ExceptionAssertions.cs new file mode 100644 index 00000000000..244cad5a37d --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/ExceptionAssertions.cs @@ -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 + { + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + public static TException Throws(Action testCode) + where TException : Exception + { + return VerifyException(RecordException(testCode)); + } + + /// + /// Verifies that an exception of the given type is thrown. + /// Also verifies that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// The exception that was thrown, when successful + public static TException Throws(Action testCode, string exceptionMessage) + where TException : Exception + { + var ex = Throws(testCode); + VerifyExceptionMessage(ex, exceptionMessage); + return ex; + } + + /// + /// Verifies that an exception of the given type is thrown. + /// Also verifies that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// The exception that was thrown, when successful + public static async Task ThrowsAsync(Func 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(testCode); + VerifyExceptionMessage(ex, exceptionMessage); + return ex; + } + + /// + /// Verifies that an exception of the given type is thrown. + /// Also verified that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// The exception that was thrown, when successful + public static TException Throws(Func testCode, string exceptionMessage) + where TException : Exception + { + return Throws(() => { testCode(); }, exceptionMessage); + } + + /// + /// Verifies that the code throws an . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception message to verify + /// The exception that was thrown, when successful + public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage) + { + return ThrowsArgumentInternal(testCode, paramName, exceptionMessage); + } + + private static TException ThrowsArgumentInternal( + Action testCode, + string paramName, + string exceptionMessage) + where TException : ArgumentException + { + var ex = Throws(testCode); + if (paramName != null) + { + Assert.Equal(paramName, ex.ParamName); + } + VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true); + return ex; + } + + /// + /// Verifies that the code throws an . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception message to verify + /// The exception that was thrown, when successful + public static Task ThrowsArgumentAsync(Func testCode, string paramName, string exceptionMessage) + { + return ThrowsArgumentAsyncInternal(testCode, paramName, exceptionMessage); + } + + private static async Task ThrowsArgumentAsyncInternal( + Func testCode, + string paramName, + string exceptionMessage) + where TException : ArgumentException + { + var ex = await Assert.ThrowsAsync(testCode); + if (paramName != null) + { + Assert.Equal(paramName, ex.ParamName); + } + VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true); + return ex; + } + + /// + /// Verifies that the code throws an . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName) + { + var ex = Throws(testCode); + if (paramName != null) + { + Assert.Equal(paramName, ex.ParamName); + } + return ex; + } + + /// + /// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot + /// be null or empty. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName) + { + return ThrowsArgumentInternal(testCode, paramName, "Value cannot be null or empty."); + } + + /// + /// Verifies that the code throws an ArgumentException with the expected message that indicates that the value cannot + /// be null or empty. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + public static Task ThrowsArgumentNullOrEmptyAsync(Func testCode, string paramName) + { + return ThrowsArgumentAsyncInternal(testCode, paramName, "Value cannot be null or empty."); + } + + /// + /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot + /// be null or empty string. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName) + { + return ThrowsArgumentInternal(testCode, paramName, "Value cannot be null or an empty string."); + } + + /// + /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot + /// be null or empty string. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + public static Task ThrowsArgumentNullOrEmptyStringAsync(Func testCode, string paramName) + { + return ThrowsArgumentAsyncInternal(testCode, paramName, "Value cannot be null or an empty string."); + } + + /// + /// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception message to verify + /// The actual value provided + /// The exception that was thrown, when successful + public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, object actualValue = null) + { + var ex = ThrowsArgumentInternal(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(Exception exception) + { + var tie = exception as TargetInvocationException; + if (tie != null) + { + exception = tie.InnerException; + } + Assert.NotNull(exception); + return Assert.IsAssignableFrom(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); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/HttpClientSlim.cs b/src/Microsoft.AspNetCore.Testing/HttpClientSlim.cs new file mode 100644 index 00000000000..0bca4ec1915 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/HttpClientSlim.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Testing +{ + /// + /// Lightweight version of HttpClient implemented using Socket and SslStream. + /// + public static class HttpClientSlim + { + public static async Task GetStringAsync(string requestUri, bool validateCertificate = true) + => await GetStringAsync(new Uri(requestUri), validateCertificate).ConfigureAwait(false); + + public static async Task 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: {requestUri.Authority}\r\n").ConfigureAwait(false); + await writer.WriteAsync("\r\n").ConfigureAwait(false); + } + + return await ReadResponse(stream).ConfigureAwait(false); + } + } + + public static async Task PostAsync(string requestUri, HttpContent content, bool validateCertificate = true) + => await PostAsync(new Uri(requestUri), content, validateCertificate).ConfigureAwait(false); + + public static async Task 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 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 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 GetSocket(Uri requestUri) + { + var tcs = new TaskCompletionSource(); + + 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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj b/src/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj new file mode 100644 index 00000000000..bf356e99734 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj @@ -0,0 +1,30 @@ + + + + Various helpers for writing tests that use ASP.NET Core. + netstandard2.0;net46 + $(NoWarn);CS1591 + true + aspnetcore + false + true + + + + + + + + + + + + + + + True + contentFiles\cs\netstandard2.0\ + + + + diff --git a/src/Microsoft.AspNetCore.Testing/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Testing/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..0212e111ee0 --- /dev/null +++ b/src/Microsoft.AspNetCore.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.AspNetCore.Testing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Testing/ReplaceCulture.cs b/src/Microsoft.AspNetCore.Testing/ReplaceCulture.cs new file mode 100644 index 00000000000..9580bfd0da7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/ReplaceCulture.cs @@ -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 +{ + /// + /// Replaces the current culture and UI culture for the test. + /// + [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; + + /// + /// Replaces the current culture and UI culture to en-GB and en-US respectively. + /// + public ReplaceCultureAttribute() : + this(_defaultCultureName, _defaultUICultureName) + { + } + + /// + /// Replaces the current culture and UI culture based on specified values. + /// + public ReplaceCultureAttribute(string currentCulture, string currentUICulture) + { + Culture = new CultureInfo(currentCulture); + UICulture = new CultureInfo(currentUICulture); + } + + /// + /// The for the test. Defaults to en-GB. + /// + /// + /// 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. + /// + public CultureInfo Culture { get; } + + /// + /// The for the test. Defaults to en-US. + /// + 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; + } + } +} + diff --git a/src/Microsoft.AspNetCore.Testing/TaskExtensions.cs b/src/Microsoft.AspNetCore.Testing/TaskExtensions.cs new file mode 100644 index 00000000000..83130aeae40 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/TaskExtensions.cs @@ -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 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) + { + 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."; + } +} diff --git a/src/Microsoft.AspNetCore.Testing/TestPathUtilities.cs b/src/Microsoft.AspNetCore.Testing/TestPathUtilities.cs new file mode 100644 index 00000000000..ebd10897c38 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/TestPathUtilities.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; +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."); + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/TestPlatformHelper.cs b/src/Microsoft.AspNetCore.Testing/TestPlatformHelper.cs new file mode 100644 index 00000000000..1a3f275c7e1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/TestPlatformHelper.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/Tracing/CollectingEventListener.cs b/src/Microsoft.AspNetCore.Testing/Tracing/CollectingEventListener.cs new file mode 100644 index 00000000000..d22a4996afb --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/Tracing/CollectingEventListener.cs @@ -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 _events = new ConcurrentQueue(); + + private object _lock = new object(); + + private Dictionary _existingSources = new Dictionary(StringComparer.OrdinalIgnoreCase); + private HashSet _requestedEventSources = new HashSet(); + + 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 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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/Tracing/EventAssert.cs b/src/Microsoft.AspNetCore.Testing/Tracing/EventAssert.cs new file mode 100644 index 00000000000..b32fb36dad9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/Tracing/EventAssert.cs @@ -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 asserter)> _payloadAsserters = new List<(string, Action)>(); + + public EventAssert(int expectedId, string expectedName, EventLevel expectedLevel) + { + _expectedId = expectedId; + _expectedName = expectedName; + _expectedLevel = expectedLevel; + } + + public static void Collection(IEnumerable 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 asserter) + { + _payloadAsserters.Add((name, asserter)); + return this; + } + + private Action CreateAsserter() => Execute; + + private void Execute(EventWrittenEventArgs evt) + { + Assert.Equal(_expectedId, evt.EventId); + Assert.Equal(_expectedName, evt.EventName); + Assert.Equal(_expectedLevel, evt.Level); + + Action CreateNameAsserter((string name, Action 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()); + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/Tracing/EventSourceTestBase.cs b/src/Microsoft.AspNetCore.Testing/Tracing/EventSourceTestBase.cs new file mode 100644 index 00000000000..721966d6c5c --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/Tracing/EventSourceTestBase.cs @@ -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 GetEvents() => _listener.GetEventsWritten(); + + public void Dispose() + { + _listener.Dispose(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/contentFiles/cs/netstandard2.0/EventSourceTestCollection.cs b/src/Microsoft.AspNetCore.Testing/contentFiles/cs/netstandard2.0/EventSourceTestCollection.cs new file mode 100644 index 00000000000..0ed9e1a9a9b --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/contentFiles/cs/netstandard2.0/EventSourceTestCollection.cs @@ -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 + { + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactAttribute.cs new file mode 100644 index 00000000000..7448b48d8cf --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactAttribute.cs @@ -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 + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactDiscoverer.cs b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactDiscoverer.cs new file mode 100644 index 00000000000..819373fa313 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalFactDiscoverer.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryAttribute.cs new file mode 100644 index 00000000000..9249078cc5c --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryAttribute.cs @@ -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 + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryDiscoverer.cs b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryDiscoverer.cs new file mode 100644 index 00000000000..ed5b3dd2932 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/ConditionalTheoryDiscoverer.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 Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing.xunit +{ + internal class ConditionalTheoryDiscoverer : TheoryDiscoverer + { + public ConditionalTheoryDiscoverer(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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/DockerOnlyAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/DockerOnlyAttribute.cs new file mode 100644 index 00000000000..d67a35a672a --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/DockerOnlyAttribute.cs @@ -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)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/EnvironmentVariableSkipConditionAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/EnvironmentVariableSkipConditionAttribute.cs new file mode 100644 index 00000000000..8bf1bfd15ee --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/EnvironmentVariableSkipConditionAttribute.cs @@ -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 +{ + /// + /// Skips a test when the value of an environment variable matches any of the supplied values. + /// + [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; + + /// + /// Creates a new instance of . + /// + /// Name of the environment variable. + /// Value(s) of the environment variable to match for the test to be skipped + 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; + } + + /// + /// Skips the test only if the value of the variable matches any of the supplied values. Default is True. + /// + 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); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/FrameworkSkipConditionAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/FrameworkSkipConditionAttribute.cs new file mode 100644 index 00000000000..168076a4347 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/FrameworkSkipConditionAttribute.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/IEnvironmentVariable.cs b/src/Microsoft.AspNetCore.Testing/xunit/IEnvironmentVariable.cs new file mode 100644 index 00000000000..068c210611c --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/IEnvironmentVariable.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/ITestCondition.cs b/src/Microsoft.AspNetCore.Testing/xunit/ITestCondition.cs new file mode 100644 index 00000000000..bb6ff1f0312 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/ITestCondition.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/OSSkipConditionAttribute.cs b/src/Microsoft.AspNetCore.Testing/xunit/OSSkipConditionAttribute.cs new file mode 100644 index 00000000000..99965107182 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/OSSkipConditionAttribute.cs @@ -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 _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(); + _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; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/OperatingSystems.cs b/src/Microsoft.AspNetCore.Testing/xunit/OperatingSystems.cs new file mode 100644 index 00000000000..c575d3e1977 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/OperatingSystems.cs @@ -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, + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/RuntimeFrameworks.cs b/src/Microsoft.AspNetCore.Testing/xunit/RuntimeFrameworks.cs new file mode 100644 index 00000000000..2ec5ea7ec1a --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/RuntimeFrameworks.cs @@ -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 + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/SkippedTestCase.cs b/src/Microsoft.AspNetCore.Testing/xunit/SkippedTestCase.cs new file mode 100644 index 00000000000..c2e15fa640c --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/SkippedTestCase.cs @@ -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(nameof(_skipReason)); + } + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + data.AddValue(nameof(_skipReason), _skipReason); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Testing/xunit/TestMethodExtensions.cs b/src/Microsoft.AspNetCore.Testing/xunit/TestMethodExtensions.cs new file mode 100644 index 00000000000..5ec3bb4ec37 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/TestMethodExtensions.cs @@ -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() + .Select(attributeInfo => attributeInfo.Attribute); + + foreach (ITestCondition condition in conditionAttributes) + { + if (!condition.IsMet) + { + return condition.SkipReason; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Testing/xunit/WindowsVersions.cs b/src/Microsoft.AspNetCore.Testing/xunit/WindowsVersions.cs new file mode 100644 index 00000000000..7a0b422dee3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Testing/xunit/WindowsVersions.cs @@ -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 static class WindowsVersions + { + public const string Win7 = "6.1"; + + public const string Win2008R2 = Win7; + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/CollectingEventListenerTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/CollectingEventListenerTest.cs new file mode 100644 index 00000000000..8f131982f0f --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/CollectingEventListenerTest.cs @@ -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); + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/ConditionalFactTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/ConditionalFactTest.cs new file mode 100644 index 00000000000..29b3bb583d3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/ConditionalFactTest.cs @@ -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 + { + 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_0 || NETCOREAPP2_1 + [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."); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Testing.Tests/ConditionalTheoryTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/ConditionalTheoryTest.cs new file mode 100644 index 00000000000..0c911efcd7b --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/ConditionalTheoryTest.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 System; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Testing +{ + public class ConditionalTheoryTest : IClassFixture + { + 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 GetInts + => new TheoryData { 0, 1 }; + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Windows)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [OSSkipCondition(OperatingSystems.Linux)] + [MemberData(nameof(GetActionTestData))] + public void ConditionalTheoryWithFuncs(Func 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_0 || NETCOREAPP2_1 + [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> GetActionTestData + => new TheoryData> + { + (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."); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/DockerTests.cs b/test/Microsoft.AspNetCore.Testing.Tests/DockerTests.cs new file mode 100644 index 00000000000..c66fdd679c2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/DockerTests.cs @@ -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)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/EnvironmentVariableSkipConditionTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/EnvironmentVariableSkipConditionTest.cs new file mode 100644 index 00000000000..b536ae56f70 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/EnvironmentVariableSkipConditionTest.cs @@ -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; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/ExceptionAssertTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/ExceptionAssertTest.cs new file mode 100644 index 00000000000..aa7354dca88 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/ExceptionAssertTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Testing.Tests/HttpClientSlimTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/HttpClientSlimTest.cs new file mode 100644 index 00000000000..fc86176cfb6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/HttpClientSlimTest.cs @@ -0,0 +1,92 @@ +// 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(() => 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( + () => HttpClientSlim.PostAsync(address, new StringContent(""))); + } + } + + private HttpListener StartHost(out string address, int statusCode = 200, Func handler = null) + { + var listener = new HttpListener(); + + address = $"http://127.0.0.1:{FindFreePort()}/"; + listener.Prefixes.Add(address); + listener.Start(); + + _ = 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; + } + + private int FindFreePort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj b/test/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj new file mode 100644 index 00000000000..908a713d4d3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj @@ -0,0 +1,31 @@ + + + + $(StandardTestTfms) + + + $(NoWarn);xUnit1004 + + $(NoWarn);xUnit1026 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionAttributeTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionAttributeTest.cs new file mode 100644 index 00000000000..0120eb7a4c1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionAttributeTest.cs @@ -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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionTest.cs new file mode 100644 index 00000000000..2d76f2c2cd8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/OSSkipConditionTest.cs @@ -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."); + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/ReplaceCultureAttributeTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/ReplaceCultureAttributeTest.cs new file mode 100644 index 00000000000..6b8df346c93 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/ReplaceCultureAttributeTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Testing.Tests/TaskExtensionsTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/TaskExtensionsTest.cs new file mode 100644 index 00000000000..f7ad603df55 --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/TaskExtensionsTest.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; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Testing +{ + public class TaskExtensionsTest + { + [Fact] + public async Task TimeoutAfterTest() + { + await Assert.ThrowsAsync(async () => await Task.Delay(1000).TimeoutAfter(TimeSpan.FromMilliseconds(50))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/TestPathUtilitiesTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/TestPathUtilitiesTest.cs new file mode 100644 index 00000000000..eca4d98145d --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/TestPathUtilitiesTest.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; +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("Common")); + } + + [Fact] + public void GetSolutionRootDirectory_Throws_IfNotFound() + { + var exception = Assert.Throws(() => TestPathUtilities.GetSolutionRootDirectory("NotTesting")); + Assert.Equal($"Solution file NotTesting.sln could not be found in {AppContext.BaseDirectory} or its parent directories.", exception.Message); + } + } +} diff --git a/test/Microsoft.AspNetCore.Testing.Tests/TestPlatformHelperTest.cs b/test/Microsoft.AspNetCore.Testing.Tests/TestPlatformHelperTest.cs new file mode 100644 index 00000000000..8e35e164d5c --- /dev/null +++ b/test/Microsoft.AspNetCore.Testing.Tests/TestPlatformHelperTest.cs @@ -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); + } + } +} diff --git a/test/Sample.Tests/BaseTest.cs b/test/Sample.Tests/BaseTest.cs new file mode 100644 index 00000000000..75219891fc7 --- /dev/null +++ b/test/Sample.Tests/BaseTest.cs @@ -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 Xunit; + +namespace Sample.Tests +{ + public abstract class BaseTest + { + [Fact] + public void ThisGetsInherited() + { + Assert.False(false); + } + } +} diff --git a/test/Sample.Tests/DerivedTest.cs b/test/Sample.Tests/DerivedTest.cs new file mode 100644 index 00000000000..4a11e462a9f --- /dev/null +++ b/test/Sample.Tests/DerivedTest.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Sample.Tests +{ + public class DerivedTest : BaseTest + { + } +} diff --git a/test/Sample.Tests/Sample.Tests.csproj b/test/Sample.Tests/Sample.Tests.csproj new file mode 100644 index 00000000000..95bdfbb9477 --- /dev/null +++ b/test/Sample.Tests/Sample.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + + + + + + + + + diff --git a/test/Sample.Tests/SampleTest.cs b/test/Sample.Tests/SampleTest.cs new file mode 100644 index 00000000000..7e133524c19 --- /dev/null +++ b/test/Sample.Tests/SampleTest.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.Threading.Tasks; +using Xunit; + +namespace Sample.Tests +{ + public class SampleTest + { + [Fact] + public void True_is_true() + { + Assert.True(true); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void TheoryTest1(int x) + { + Assert.InRange(x, 1, 3); + } + + [Theory] + [InlineData(1, "Hi")] + [InlineData(2, "Hi")] + [InlineData(3, "Hi")] + public void TheoryTest2(int x, string s) + { + Assert.InRange(x, 1, 3); + Assert.Equal("Hi", s); + } + + [Fact] + public async Task SampleAsyncTest() + { + await Task.Delay(01); + } + } +} \ No newline at end of file