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