diff --git a/src/ObjectPool/src/DefaultObjectPool.cs b/src/ObjectPool/src/DefaultObjectPool.cs index 87825967ac..dcd7f1c715 100644 --- a/src/ObjectPool/src/DefaultObjectPool.cs +++ b/src/ObjectPool/src/DefaultObjectPool.cs @@ -42,8 +42,7 @@ namespace Microsoft.Extensions.ObjectPool public override T Get() { - T item = _firstItem; - + var item = _firstItem; if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item) { item = GetViaScan(); @@ -55,12 +54,10 @@ namespace Microsoft.Extensions.ObjectPool [MethodImpl(MethodImplOptions.AggressiveInlining)] private T GetViaScan() { - ObjectWrapper[] items = _items; - + var items = _items; for (var i = 0; i < items.Length; i++) { - T item = items[i]; - + var item = items[i].Element; if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) { return item; @@ -88,21 +85,17 @@ namespace Microsoft.Extensions.ObjectPool [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ReturnViaScan(T obj) { - ObjectWrapper[] items = _items; - + var items = _items; for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i) { } } + // PERF: the struct wrapper avoids array-covariance-checks from the runtime when assigning to elements of the array. [DebuggerDisplay("{Element}")] private struct ObjectWrapper { public T Element; - - public ObjectWrapper(T item) => Element = item; - - public static implicit operator T(ObjectWrapper wrapper) => wrapper.Element; } } } diff --git a/src/ObjectPool/test/DefaultObjectPoolTest.cs b/src/ObjectPool/test/DefaultObjectPoolTest.cs index ca14a3f53b..b44aa7e1c7 100644 --- a/src/ObjectPool/test/DefaultObjectPoolTest.cs +++ b/src/ObjectPool/test/DefaultObjectPoolTest.cs @@ -1,12 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; -using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.ObjectPool.Test +namespace Microsoft.Extensions.ObjectPool { public class DefaultObjectPoolTest { diff --git a/src/ObjectPool/test/ThreadingTest.cs b/src/ObjectPool/test/ThreadingTest.cs new file mode 100644 index 0000000000..541bc5ffd4 --- /dev/null +++ b/src/ObjectPool/test/ThreadingTest.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using Xunit; + +namespace Microsoft.Extensions.ObjectPool +{ + public class ThreadingTest + { + private CancellationTokenSource _cts; + private DefaultObjectPool _pool; + private bool _foundError; + + [Fact] + public void RunThreadingTest() + { + _cts = new CancellationTokenSource(); + _pool = new DefaultObjectPool(new DefaultPooledObjectPolicy(), 10); + + var threads = new Thread[8]; + for (var i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(Run); + } + + for (var i = 0; i < threads.Length; i++) + { + threads[i].Start(); + } + + // Run for 1000ms + _cts.CancelAfter(1000); + + // Wait for all threads to complete + for (var i = 0; i < threads.Length; i++) + { + threads[i].Join(); + } + + Assert.False(_foundError, "Race condition found. An item was shared across threads."); + } + + private void Run() + { + while (!_cts.IsCancellationRequested) + { + var obj = _pool.Get(); + if (obj.i != 0) + { + _foundError = true; + } + obj.i = 123; + + var obj2 = _pool.Get(); + if (obj2.i != 0) + { + _foundError = true; + } + obj2.i = 321; + + obj.Reset(); + _pool.Return(obj); + + obj2.Reset(); + _pool.Return(obj2); + } + } + + private class Item + { + public int i = 0; + + public void Reset() + { + i = 0; + } + } + } +} diff --git a/src/Shared/BenchmarkRunner/AspNetCoreBenchmarkAttribute.cs b/src/Shared/BenchmarkRunner/AspNetCoreBenchmarkAttribute.cs index a4044d1b5e..d16493a738 100644 --- a/src/Shared/BenchmarkRunner/AspNetCoreBenchmarkAttribute.cs +++ b/src/Shared/BenchmarkRunner/AspNetCoreBenchmarkAttribute.cs @@ -2,38 +2,72 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; -using System.Reflection; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; +using System.Collections.Generic; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.InProcess; namespace BenchmarkDotNet.Attributes { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] internal class AspNetCoreBenchmarkAttribute : Attribute, IConfigSource { - public static bool UseValidationConfig { get; set; } - - public Type ConfigType { get; } - public Type ValidationConfigType { get; } - - public AspNetCoreBenchmarkAttribute() : this(typeof(DefaultCoreConfig)) + public AspNetCoreBenchmarkAttribute() + : this(typeof(DefaultCoreConfig)) { } - public AspNetCoreBenchmarkAttribute(Type configType) : this(configType, typeof(DefaultCoreValidationConfig)) + public AspNetCoreBenchmarkAttribute(Type configType) + : this(configType, typeof(DefaultCoreValidationConfig)) { } public AspNetCoreBenchmarkAttribute(Type configType, Type validationConfigType) { - ConfigType = configType; - ValidationConfigType = validationConfigType; + ConfigTypes = new Dictionary() + { + { NamedConfiguration.Default, typeof(DefaultCoreConfig) }, + { NamedConfiguration.Validation, typeof(DefaultCoreValidationConfig) }, + { NamedConfiguration.Profile, typeof(DefaultCoreProfileConfig) }, + { NamedConfiguration.Debug, typeof(DefaultCoreDebugConfig) }, + { NamedConfiguration.PerfLab, typeof(DefaultCorePerfLabConfig) }, + }; + + if (configType != null) + { + ConfigTypes[NamedConfiguration.Default] = configType; + } + + if (validationConfigType != null) + { + ConfigTypes[NamedConfiguration.Validation] = validationConfigType; + } } - public IConfig Config => (IConfig) Activator.CreateInstance(UseValidationConfig ? ValidationConfigType : ConfigType, Array.Empty()); + public IConfig Config + { + get + { + if (!ConfigTypes.TryGetValue(ConfigName ?? NamedConfiguration.Default, out var configType)) + { + var message = $"Could not find a configuration matching {ConfigName}. " + + $"Known configurations: {string.Join(", ", ConfigTypes.Keys)}"; + throw new InvalidOperationException(message); + } + + return (IConfig)Activator.CreateInstance(configType, Array.Empty()); + } + } + + public Dictionary ConfigTypes { get; } + + public static string ConfigName { get; set; } = NamedConfiguration.Default; + + public static class NamedConfiguration + { + public static readonly string Default = "default"; + public static readonly string Validation = "validation"; + public static readonly string Profile = "profile"; + public static readonly string Debug = "debug"; + public static readonly string PerfLab = "perflab"; + } } } diff --git a/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs b/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs index a8d9d60536..5e2bafd506 100644 --- a/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs +++ b/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs @@ -28,7 +28,11 @@ namespace BenchmarkDotNet.Attributes Add(JitOptimizationsValidator.FailOnError); Add(Job.Core +#if NETCOREAPP2_1 .With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21)) +#else + .With(CsProjCoreToolchain.From(new NetCoreAppSettings("netcoreapp2.2", null, ".NET Core 2.2"))) +#endif .With(new GcMode { Server = true }) .With(RunStrategy.Throughput)); } diff --git a/src/Shared/BenchmarkRunner/DefaultCoreDebugConfig.cs b/src/Shared/BenchmarkRunner/DefaultCoreDebugConfig.cs new file mode 100644 index 0000000000..f51bed2db9 --- /dev/null +++ b/src/Shared/BenchmarkRunner/DefaultCoreDebugConfig.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 BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Attributes +{ + internal class DefaultCoreDebugConfig : ManualConfig + { + public DefaultCoreDebugConfig() + { + Add(ConsoleLogger.Default); + Add(JitOptimizationsValidator.DontFailOnError); + + Add(Job.InProcess + .With(RunStrategy.Throughput)); + } + } +} diff --git a/src/Shared/BenchmarkRunner/DefaultCorePerfLabConfig.cs b/src/Shared/BenchmarkRunner/DefaultCorePerfLabConfig.cs new file mode 100644 index 0000000000..5cc809e166 --- /dev/null +++ b/src/Shared/BenchmarkRunner/DefaultCorePerfLabConfig.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Attributes +{ + internal class DefaultCorePerfLabConfig : ManualConfig + { + public DefaultCorePerfLabConfig() + { + Add(ConsoleLogger.Default); + + Add(MemoryDiagnoser.Default); + Add(StatisticColumn.OperationsPerSecond); + Add(new ParamsSummaryColumn()); + Add(DefaultColumnProviders.Statistics, DefaultColumnProviders.Diagnosers, DefaultColumnProviders.Target); + + // TODO: When upgrading to BDN 0.11.1, use Add(DefaultColumnProviders.Descriptor); + // DefaultColumnProviders.Target is deprecated + + Add(JitOptimizationsValidator.FailOnError); + + Add(Job.InProcess + .With(RunStrategy.Throughput)); + + Add(MarkdownExporter.GitHub); + + Add(new CsvExporter( + CsvSeparator.Comma, + new Reports.SummaryStyle + { + PrintUnitsInHeader = true, + PrintUnitsInContent = false, + TimeUnit = Horology.TimeUnit.Microsecond, + SizeUnit = SizeUnit.KB + })); + } + } +} diff --git a/src/Shared/BenchmarkRunner/DefaultCoreProfileConfig.cs b/src/Shared/BenchmarkRunner/DefaultCoreProfileConfig.cs new file mode 100644 index 0000000000..1b59cb89c5 --- /dev/null +++ b/src/Shared/BenchmarkRunner/DefaultCoreProfileConfig.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Attributes +{ + internal class DefaultCoreProfileConfig : ManualConfig + { + public DefaultCoreProfileConfig() + { + Add(ConsoleLogger.Default); + Add(MarkdownExporter.GitHub); + + Add(MemoryDiagnoser.Default); + Add(StatisticColumn.OperationsPerSecond); + Add(DefaultColumnProviders.Instance); + + Add(JitOptimizationsValidator.FailOnError); + + Add(Job.InProcess + .With(RunStrategy.Throughput)); + } + } +} diff --git a/src/Shared/BenchmarkRunner/ParamsDisplayInfoColumn.cs b/src/Shared/BenchmarkRunner/ParamsDisplayInfoColumn.cs new file mode 100644 index 0000000000..b246e21c2e --- /dev/null +++ b/src/Shared/BenchmarkRunner/ParamsDisplayInfoColumn.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace BenchmarkDotNet.Attributes +{ + public class ParamsSummaryColumn : IColumn + { + public string Id => nameof(ParamsSummaryColumn); + public string ColumnName { get; } = "Params"; + public bool IsDefault(Summary summary, Benchmark benchmark) => false; + public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Parameters.DisplayInfo; + public bool IsAvailable(Summary summary) => true; + public bool AlwaysShow => true; + public ColumnCategory Category => ColumnCategory.Params; + public int PriorityInCategory => 0; + public override string ToString() => ColumnName; + public bool IsNumeric => false; + public UnitType UnitType => UnitType.Dimensionless; + public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => GetValue(summary, benchmark); + public string Legend => $"Summary of all parameter values"; + } +} \ No newline at end of file diff --git a/src/Shared/BenchmarkRunner/Program.cs b/src/Shared/BenchmarkRunner/Program.cs index 3297d5dae9..a1db1a50e8 100644 --- a/src/Shared/BenchmarkRunner/Program.cs +++ b/src/Shared/BenchmarkRunner/Program.cs @@ -2,15 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.InProcess; +using BenchmarkDotNet.Running; namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner { @@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner { BeforeMain(args); - CheckValidate(ref args); + AssignConfiguration(ref args); var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly) .Run(args, ManualConfig.CreateEmpty()); @@ -66,16 +65,35 @@ namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner return 1; } - private static void CheckValidate(ref string[] args) + private static void AssignConfiguration(ref string[] args) { var argsList = args.ToList(); if (argsList.Remove("--validate") || argsList.Remove("--validate-fast")) { + // Compat: support the old style of passing a config that is used by our build system. SuppressConsole(); - AspNetCoreBenchmarkAttribute.UseValidationConfig = true; + AspNetCoreBenchmarkAttribute.ConfigName = AspNetCoreBenchmarkAttribute.NamedConfiguration.Validation; + args = argsList.ToArray(); + return; + } + + var index = argsList.IndexOf("--config"); + if (index >= 0 && index < argsList.Count -1) + { + AspNetCoreBenchmarkAttribute.ConfigName = argsList[index + 1]; + argsList.RemoveAt(index + 1); + argsList.RemoveAt(index); + args = argsList.ToArray(); + return; } - args = argsList.ToArray(); + if (Debugger.IsAttached) + { + Console.WriteLine("Using the debug config since you are debugging. I hope that's OK!"); + Console.WriteLine("Specify a configuration with --config to override"); + AspNetCoreBenchmarkAttribute.ConfigName = AspNetCoreBenchmarkAttribute.NamedConfiguration.Debug; + return; + } } private static void SuppressConsole() diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 4e2a0a9964..952cf7c36d 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -19,17 +19,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation public const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1"; public const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate"; - public const string AspNetIdentityOid = "1.3.6.1.4.1.311.84.1.2"; - public const string AspNetIdentityOidFriendlyName = "ASP.NET Core Identity Json Web Token signing development certificate"; - private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1"; private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication"; private const string LocalhostHttpsDnsName = "localhost"; private const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName; - private const string IdentityDistinguishedName = "CN=Microsoft.AspNetCore.Identity.Signing"; - public const int RSAMinimumKeySizeInBits = 2048; private static readonly TimeSpan MaxRegexTimeout = TimeSpan.FromMinutes(1); @@ -37,7 +32,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation private const string MacOSSystemKeyChain = "/Library/Keychains/System.keychain"; private static readonly string MacOSUserKeyChain = Environment.GetEnvironmentVariable("HOME") + "/Library/Keychains/login.keychain-db"; private const string MacOSFindCertificateCommandLine = "security"; -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 private static readonly string MacOSFindCertificateCommandLineArgumentsFormat = "find-certificate -c {0} -a -Z -p " + MacOSSystemKeyChain; #endif private const string MacOSFindCertificateOutputRegex = "SHA-1 hash: ([0-9A-Z]+)"; @@ -46,7 +41,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation private const string MacOSDeleteCertificateCommandLine = "sudo"; private const string MacOSDeleteCertificateCommandLineArgumentsFormat = "security delete-certificate -Z {0} {1}"; private const string MacOSTrustCertificateCommandLine = "sudo"; -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " "; #endif private const int UserCancelledErrorCode = 1223; @@ -56,8 +51,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation StoreName storeName, StoreLocation location, bool isValid, - bool requireExportable = true) + bool requireExportable = true, + DiagnosticInformation diagnostics = null) { + diagnostics?.Debug($"Listing '{purpose.ToString()}' certificates on '{location}\\{storeName}'."); var certificates = new List(); try { @@ -70,28 +67,37 @@ namespace Microsoft.AspNetCore.Certificates.Generation { case CertificatePurpose.All: matchingCertificates = matchingCertificates - .Where(c => HasOid(c, AspNetHttpsOid) || HasOid(c, AspNetIdentityOid)); + .Where(c => HasOid(c, AspNetHttpsOid)); break; case CertificatePurpose.HTTPS: matchingCertificates = matchingCertificates .Where(c => HasOid(c, AspNetHttpsOid)); break; - case CertificatePurpose.Signing: - matchingCertificates = matchingCertificates - .Where(c => HasOid(c, AspNetIdentityOid)); - break; default: break; } + + diagnostics?.Debug(diagnostics.DescribeCertificates(matchingCertificates)); if (isValid) { // Ensure the certificate hasn't expired, has a private key and its exportable // (for container/unix scenarios). + diagnostics?.Debug("Checking certificates for validity."); var now = DateTimeOffset.Now; - matchingCertificates = matchingCertificates + var validCertificates = matchingCertificates .Where(c => c.NotBefore <= now && now <= c.NotAfter && - (!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c))); + (!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c))) + .ToArray(); + + var invalidCertificates = matchingCertificates.Except(validCertificates); + + diagnostics?.Debug("Listing valid certificates"); + diagnostics?.Debug(diagnostics.DescribeCertificates(validCertificates)); + diagnostics?.Debug("Listing invalid certificates"); + diagnostics?.Debug(diagnostics.DescribeCertificates(invalidCertificates)); + + matchingCertificates = validCertificates; } // We need to enumerate the certificates early to prevent dispoisng issues. @@ -123,7 +129,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport)); #else // Only check for RSA CryptoServiceProvider and do not fail in XPlat tooling as - // System.Security.Cryptography.Cng is not pat of the shared framework and we don't + // System.Security.Cryptography.Cng is not part of the shared framework and we don't // want to bring the dependency in on CLI scenarios. This functionality will be used // on CLI scenarios as part of the first run experience, so checking the exportability // of the certificate is not important. @@ -133,7 +139,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation #endif } - private void DisposeCertificates(IEnumerable disposables) + private static void DisposeCertificates(IEnumerable disposables) { foreach (var disposable in disposables) { @@ -147,9 +153,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation } } -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 - public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) + public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride, DiagnosticInformation diagnostics = null) { var subject = new X500DistinguishedName(subjectOverride ?? LocalhostHttpsDistinguishedName); var extensions = new List(); @@ -192,46 +198,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation return certificate; } - public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) - { - var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName); - var extensions = new List(); - - var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true); - var enhancedKeyUsage = new X509EnhancedKeyUsageExtension( - new OidCollection() { - new Oid( - ServerAuthenticationEnhancedKeyUsageOid, - ServerAuthenticationEnhancedKeyUsageOidFriendlyName) - }, - critical: true); - - var basicConstraints = new X509BasicConstraintsExtension( - certificateAuthority: false, - hasPathLengthConstraint: false, - pathLengthConstraint: 0, - critical: true); - - var aspNetIdentityExtension = new X509Extension( - new AsnEncodedData( - new Oid(AspNetIdentityOid, AspNetIdentityOidFriendlyName), - Encoding.ASCII.GetBytes(AspNetIdentityOidFriendlyName)), - critical: false); - - extensions.Add(basicConstraints); - extensions.Add(keyUsage); - extensions.Add(enhancedKeyUsage); - extensions.Add(aspNetIdentityExtension); - - var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - certificate.FriendlyName = AspNetIdentityOidFriendlyName; - } - - return certificate; - } - public X509Certificate2 CreateSelfSignedCertificate( X500DistinguishedName subject, IEnumerable extensions, @@ -260,8 +226,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation } } - public X509Certificate2 SaveCertificateInStore(X509Certificate2 certificate, StoreName name, StoreLocation location) + public X509Certificate2 SaveCertificateInStore(X509Certificate2 certificate, StoreName name, StoreLocation location, DiagnosticInformation diagnostics = null) { + diagnostics?.Debug("Saving the certificate into the certificate store."); var imported = certificate; if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { @@ -287,33 +254,67 @@ namespace Microsoft.AspNetCore.Certificates.Generation return imported; } - public void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password) + public void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password, DiagnosticInformation diagnostics = null) { - if (Path.GetDirectoryName(path) != "") + diagnostics?.Debug( + $"Exporting certificate to '{path}'", + includePrivateKey ? "The certificate will contain the private key" : "The certificate will not contain the private key"); + if (includePrivateKey && password == null) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + diagnostics?.Debug("No password was provided for the certificate."); } + var targetDirectoryPath = Path.GetDirectoryName(path); + if (targetDirectoryPath != "") + { + diagnostics?.Debug($"Ensuring that the directory for the target exported certificate path exists '{targetDirectoryPath}'"); + Directory.CreateDirectory(targetDirectoryPath); + } + + byte[] bytes; if (includePrivateKey) { - var bytes = certificate.Export(X509ContentType.Pkcs12, password); try { - File.WriteAllBytes(path, bytes); + diagnostics?.Debug($"Exporting the certificate including the private key."); + bytes = certificate.Export(X509ContentType.Pkcs12, password); } - finally + catch (Exception e) { - Array.Clear(bytes, 0, bytes.Length); + diagnostics?.Error($"Failed to export the certificate with the private key", e); + throw; } } else { - var bytes = certificate.Export(X509ContentType.Cert); + try + { + diagnostics?.Debug($"Exporting the certificate without the private key."); + bytes = certificate.Export(X509ContentType.Cert); + } + catch (Exception ex) + { + diagnostics?.Error($"Failed to export the certificate without the private key", ex); + throw; + } + } + try + { + diagnostics?.Debug($"Writing exported certificate to path '{path}'."); File.WriteAllBytes(path, bytes); } + catch (Exception ex) + { + diagnostics?.Error("Failed writing the certificate to the target path", ex); + throw; + } + finally + { + Array.Clear(bytes, 0, bytes.Length); + } } - public void TrustCertificate(X509Certificate2 certificate) + public void TrustCertificate(X509Certificate2 certificate, DiagnosticInformation diagnostics = null) { // Strip certificate of the private key if any. var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert)); @@ -322,21 +323,24 @@ namespace Microsoft.AspNetCore.Certificates.Generation { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - TrustCertificateOnWindows(certificate, publicCertificate); + diagnostics?.Debug("Trusting the certificate on Windows."); + TrustCertificateOnWindows(certificate, publicCertificate, diagnostics); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - TrustCertificateOnMac(publicCertificate); + diagnostics?.Debug("Trusting the certificate on MAC."); + TrustCertificateOnMac(publicCertificate, diagnostics); } } } - private void TrustCertificateOnMac(X509Certificate2 publicCertificate) + private void TrustCertificateOnMac(X509Certificate2 publicCertificate, DiagnosticInformation diagnostics) { var tmpFile = Path.GetTempFileName(); try { ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null); + diagnostics?.Debug("Running the trust command on Mac OS"); using (var process = Process.Start(MacOSTrustCertificateCommandLine, MacOSTrustCertificateCommandLineArguments + tmpFile)) { process.WaitForExit(); @@ -362,19 +366,29 @@ namespace Microsoft.AspNetCore.Certificates.Generation } } - private static void TrustCertificateOnWindows(X509Certificate2 certificate, X509Certificate2 publicCertificate) + private static void TrustCertificateOnWindows(X509Certificate2 certificate, X509Certificate2 publicCertificate, DiagnosticInformation diagnostics = null) { publicCertificate.FriendlyName = certificate.FriendlyName; using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); + var existing = store.Certificates.Find(X509FindType.FindByThumbprint, publicCertificate.Thumbprint, validOnly: false); + if (existing.Count > 0) + { + diagnostics?.Debug("Certificate already trusted. Skipping trust step."); + DisposeCertificates(existing.OfType()); + return; + } + try { + diagnostics?.Debug("Adding certificate to the store."); store.Add(publicCertificate); } catch (CryptographicException exception) when (exception.HResult == UserCancelledErrorCode) { + diagnostics?.Debug("User cancelled the trust prompt."); throw new UserCancelledTrustException(); } store.Close(); @@ -437,6 +451,30 @@ namespace Microsoft.AspNetCore.Certificates.Generation } } + public DiagnosticInformation CleanupHttpsCertificates2(string subject = LocalhostHttpsDistinguishedName) + { + return CleanupCertificates2(CertificatePurpose.HTTPS, subject); + } + + public DiagnosticInformation CleanupCertificates2(CertificatePurpose purpose, string subject) + { + var diagnostics = new DiagnosticInformation(); + // On OS X we don't have a good way to manage trusted certificates in the system keychain + // so we do everything by invoking the native toolchain. + // This has some limitations, like for example not being able to identify our custom OID extension. For that + // matter, when we are cleaning up certificates on the machine, we start by removing the trusted certificates. + // To do this, we list the certificates that we can identify on the current user personal store and we invoke + // the native toolchain to remove them from the sytem keychain. Once we have removed the trusted certificates, + // we remove the certificates from the local user store to finish up the cleanup. + var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: false, requireExportable: true, diagnostics); + foreach (var certificate in certificates) + { + RemoveCertificate(certificate, RemoveLocations.All, diagnostics); + } + + return diagnostics; + } + public void RemoveAllCertificates(CertificatePurpose purpose, StoreName storeName, StoreLocation storeLocation, string subject = null) { var certificates = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? @@ -454,32 +492,33 @@ namespace Microsoft.AspNetCore.Certificates.Generation DisposeCertificates(certificates); } - private void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations) + private void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations, DiagnosticInformation diagnostics = null) { switch (locations) { case RemoveLocations.Undefined: throw new InvalidOperationException($"'{nameof(RemoveLocations.Undefined)}' is not a valid location."); case RemoveLocations.Local: - RemoveCertificateFromUserStore(certificate); + RemoveCertificateFromUserStore(certificate, diagnostics); break; case RemoveLocations.Trusted when !RuntimeInformation.IsOSPlatform(OSPlatform.Linux): - RemoveCertificateFromTrustedRoots(certificate); + RemoveCertificateFromTrustedRoots(certificate, diagnostics); break; case RemoveLocations.All: if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - RemoveCertificateFromTrustedRoots(certificate); + RemoveCertificateFromTrustedRoots(certificate, diagnostics); } - RemoveCertificateFromUserStore(certificate); + RemoveCertificateFromUserStore(certificate, diagnostics); break; default: throw new InvalidOperationException("Invalid location."); } } - private static void RemoveCertificateFromUserStore(X509Certificate2 certificate) + private static void RemoveCertificateFromUserStore(X509Certificate2 certificate, DiagnosticInformation diagnostics) { + diagnostics?.Debug($"Trying to remove certificate with thumbprint '{certificate.Thumbprint}' from certificate store '{StoreLocation.CurrentUser}\\{StoreName.My}'."); using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); @@ -492,8 +531,9 @@ namespace Microsoft.AspNetCore.Certificates.Generation } } - private void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) + private void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate, DiagnosticInformation diagnostics) { + diagnostics?.Debug($"Trying to remove certificate with thumbprint '{certificate.Thumbprint}' from certificate store '{StoreLocation.CurrentUser}\\{StoreName.Root}'."); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser)) @@ -501,9 +541,13 @@ namespace Microsoft.AspNetCore.Certificates.Generation store.Open(OpenFlags.ReadWrite); var matching = store.Certificates .OfType() - .Single(c => c.SerialNumber == certificate.SerialNumber); + .SingleOrDefault(c => c.SerialNumber == certificate.SerialNumber); + + if (matching != null) + { + store.Remove(matching); + } - store.Remove(matching); store.Close(); } } @@ -513,10 +557,12 @@ namespace Microsoft.AspNetCore.Certificates.Generation { try { + diagnostics?.Debug("Trying to remove the certificate trust rule."); RemoveCertificateTrustRule(certificate); } catch { + diagnostics?.Debug("Failed to remove the certificate trust rule."); // We don't care if we fail to remove the trust rule if // for some reason the certificate became untrusted. // The delete command will fail if the certificate is @@ -524,6 +570,10 @@ namespace Microsoft.AspNetCore.Certificates.Generation } RemoveCertificateFromKeyChain(MacOSSystemKeyChain, certificate); } + else + { + diagnostics?.Debug("The certificate was not trusted."); + } } } @@ -601,18 +651,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject); } - public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate( - DateTimeOffset notBefore, - DateTimeOffset notAfter, - string path = null, - bool trust = false, - bool includePrivateKey = false, - string password = null, - string subject = IdentityDistinguishedName) - { - return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.Signing, path, trust, includePrivateKey, password, subject); - } - public EnsureCertificateResult EnsureValidCertificateExists( DateTimeOffset notBefore, DateTimeOffset notAfter, @@ -652,9 +690,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation case CertificatePurpose.HTTPS: certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter, subjectOverride); break; - case CertificatePurpose.Signing: - certificate = CreateApplicationTokenSigningDevelopmentCertificate(notBefore, notAfter, subjectOverride); - break; default: throw new InvalidOperationException("The certificate must have a purpose."); } @@ -704,6 +739,143 @@ namespace Microsoft.AspNetCore.Certificates.Generation return result; } + // This is just to avoid breaking changes across repos. + // Will be renamed back to EnsureAspNetCoreHttpsDevelopmentCertificate once updates are made elsewhere. + public DetailedEnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate2( + DateTimeOffset notBefore, + DateTimeOffset notAfter, + string path = null, + bool trust = false, + bool includePrivateKey = false, + string password = null, + string subject = LocalhostHttpsDistinguishedName) + { + return EnsureValidCertificateExists2(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject); + } + + public DetailedEnsureCertificateResult EnsureValidCertificateExists2( + DateTimeOffset notBefore, + DateTimeOffset notAfter, + CertificatePurpose purpose, + string path, + bool trust, + bool includePrivateKey, + string password, + string subject) + { + if (purpose == CertificatePurpose.All) + { + throw new ArgumentException("The certificate must have a specific purpose."); + } + + var result = new DetailedEnsureCertificateResult(); + + var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true, result.Diagnostics).Concat( + ListCertificates(purpose, StoreName.My, StoreLocation.LocalMachine, isValid: true, requireExportable: true, result.Diagnostics)); + + var filteredCertificates = subject == null ? certificates : certificates.Where(c => c.Subject == subject); + if (subject != null) + { + var excludedCertificates = certificates.Except(filteredCertificates); + + result.Diagnostics.Debug($"Filtering found certificates to those with a subject equal to '{subject}'"); + result.Diagnostics.Debug(result.Diagnostics.DescribeCertificates(filteredCertificates)); + result.Diagnostics.Debug($"Listing certificates excluded from consideration."); + result.Diagnostics.Debug(result.Diagnostics.DescribeCertificates(excludedCertificates)); + } + else + { + result.Diagnostics.Debug("Skipped filtering certificates by subject."); + } + + certificates = filteredCertificates; + + result.ResultCode = EnsureCertificateResult.Succeeded; + + X509Certificate2 certificate = null; + if (certificates.Count() > 0) + { + result.Diagnostics.Debug("Found valid certificates present on the machine."); + result.Diagnostics.Debug(result.Diagnostics.DescribeCertificates(certificates)); + certificate = certificates.First(); + result.Diagnostics.Debug("Selected certificate"); + result.Diagnostics.Debug(result.Diagnostics.DescribeCertificates(certificate)); + result.ResultCode = EnsureCertificateResult.ValidCertificatePresent; + } + else + { + result.Diagnostics.Debug("No valid certificates present on this machine. Trying to create one."); + try + { + switch (purpose) + { + case CertificatePurpose.All: + throw new InvalidOperationException("The certificate must have a specific purpose."); + case CertificatePurpose.HTTPS: + certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter, subject, result.Diagnostics); + break; + default: + throw new InvalidOperationException("The certificate must have a purpose."); + } + } + catch (Exception e) + { + result.Diagnostics.Error("Error creating the certificate.", e); + result.ResultCode = EnsureCertificateResult.ErrorCreatingTheCertificate; + return result; + } + + try + { + certificate = SaveCertificateInStore(certificate, StoreName.My, StoreLocation.CurrentUser, result.Diagnostics); + } + catch (Exception e) + { + result.Diagnostics.Error($"Error saving the certificate in the certificate store '{StoreLocation.CurrentUser}\\{StoreName.My}'.", e); + result.ResultCode = EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore; + return result; + } + } + if (path != null) + { + result.Diagnostics.Debug("Trying to export the certificate."); + result.Diagnostics.Debug(result.Diagnostics.DescribeCertificates(certificate)); + try + { + ExportCertificate(certificate, path, includePrivateKey, password, result.Diagnostics); + } + catch (Exception e) + { + result.Diagnostics.Error("An error ocurred exporting the certificate.", e); + result.ResultCode = EnsureCertificateResult.ErrorExportingTheCertificate; + return result; + } + } + + if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && trust) + { + try + { + result.Diagnostics.Debug("Trying to export the certificate."); + TrustCertificate(certificate, result.Diagnostics); + } + catch (UserCancelledTrustException) + { + result.Diagnostics.Error("The user cancelled trusting the certificate.", null); + result.ResultCode = EnsureCertificateResult.UserCancelledTrustStep; + return result; + } + catch (Exception e) + { + result.Diagnostics.Error("There was an error trusting the certificate.", e); + result.ResultCode = EnsureCertificateResult.FailedToTrustTheCertificate; + return result; + } + } + + return result; + } + private class UserCancelledTrustException : Exception { } @@ -715,6 +887,65 @@ namespace Microsoft.AspNetCore.Certificates.Generation Trusted, All } + + internal class DetailedEnsureCertificateResult + { + public EnsureCertificateResult ResultCode { get; set; } + public DiagnosticInformation Diagnostics { get; set; } = new DiagnosticInformation(); + } #endif + + internal class DiagnosticInformation + { + public IList Messages { get; } = new List(); + + public IList Exceptions { get; } = new List(); + + internal void Debug(params string[] messages) + { + foreach (var message in messages) + { + Messages.Add(message); + } + } + + internal string[] DescribeCertificates(params X509Certificate2[] certificates) + { + return DescribeCertificates(certificates.AsEnumerable()); + } + + internal string[] DescribeCertificates(IEnumerable certificates) + { + var result = new List(); + result.Add($"'{certificates.Count()}' found matching the criteria."); + result.Add($"SUBJECT - THUMBPRINT - NOT BEFORE - EXPIRES - HAS PRIVATE KEY"); + foreach (var certificate in certificates) + { + result.Add(DescribeCertificate(certificate)); + } + + return result.ToArray(); + } + + private static string DescribeCertificate(X509Certificate2 certificate) => + $"{certificate.Subject} - {certificate.Thumbprint} - {certificate.NotBefore} - {certificate.NotAfter} - {certificate.HasPrivateKey}"; + + internal void Error(string preamble, Exception e) + { + Messages.Add(preamble); + if (Exceptions.Count > 0 && Exceptions[Exceptions.Count - 1] == e) + { + return; + } + + var ex = e; + while (ex != null) + { + Messages.Add("Exception message: " + ex.Message); + ex = ex.InnerException; + } + + } + } } } \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/CertificatePurpose.cs b/src/Shared/CertificateGeneration/CertificatePurpose.cs index 1ad1a6d79b..7b3231f80d 100644 --- a/src/Shared/CertificateGeneration/CertificatePurpose.cs +++ b/src/Shared/CertificateGeneration/CertificatePurpose.cs @@ -6,7 +6,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation internal enum CertificatePurpose { All, - HTTPS, - Signing + HTTPS } } \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/EnsureCertificateResult.cs b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs index d3c86ce05d..84c495249d 100644 --- a/src/Shared/CertificateGeneration/EnsureCertificateResult.cs +++ b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 namespace Microsoft.AspNetCore.Certificates.Generation { diff --git a/src/Shared/NonCapturingTimer/NonCapturingTimer.cs b/src/Shared/NonCapturingTimer/NonCapturingTimer.cs new file mode 100644 index 0000000000..6f54b2db47 --- /dev/null +++ b/src/Shared/NonCapturingTimer/NonCapturingTimer.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.Internal +{ + // A convenience API for interacting with System.Threading.Timer in a way + // that doesn't capture the ExecutionContext. We should be using this (or equivalent) + // everywhere we use timers to avoid rooting any values stored in asynclocals. + internal static class NonCapturingTimer + { + public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + bool restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } +} diff --git a/src/Shared/Process/ProcessHelper.cs b/src/Shared/Process/ProcessHelper.cs index cf42a7e3a7..c6cbd1f970 100644 --- a/src/Shared/Process/ProcessHelper.cs +++ b/src/Shared/Process/ProcessHelper.cs @@ -14,44 +14,40 @@ namespace Microsoft.Extensions.Internal private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); - public static void KillTree(this Process process) - { - process.KillTree(_defaultTimeout); - } + public static void KillTree(this Process process) => process.KillTree(_defaultTimeout); public static void KillTree(this Process process, TimeSpan timeout) { - string stdout; + var pid = process.Id; if (_isWindows) { RunProcessAndWaitForExit( "taskkill", - $"/T /F /PID {process.Id}", + $"/T /F /PID {pid}", timeout, - out stdout); + out var _); } else { var children = new HashSet(); - GetAllChildIdsUnix(process.Id, children, timeout); + GetAllChildIdsUnix(pid, children, timeout); foreach (var childId in children) { KillProcessUnix(childId, timeout); } - KillProcessUnix(process.Id, timeout); + KillProcessUnix(pid, timeout); } } private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) { - string stdout; - var exitCode = RunProcessAndWaitForExit( + RunProcessAndWaitForExit( "pgrep", $"-P {parentId}", timeout, - out stdout); + out var stdout); - if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + if (!string.IsNullOrEmpty(stdout)) { using (var reader = new StringReader(stdout)) { @@ -63,8 +59,7 @@ namespace Microsoft.Extensions.Internal return; } - int id; - if (int.TryParse(text, out id)) + if (int.TryParse(text, out var id)) { children.Add(id); // Recursively get the children @@ -77,22 +72,22 @@ namespace Microsoft.Extensions.Internal private static void KillProcessUnix(int processId, TimeSpan timeout) { - string stdout; RunProcessAndWaitForExit( "kill", $"-TERM {processId}", timeout, - out stdout); + out var stdout); } - private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) { var startInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, RedirectStandardOutput = true, - UseShellExecute = false + RedirectStandardError = true, + UseShellExecute = false, }; var process = Process.Start(startInfo); @@ -106,8 +101,6 @@ namespace Microsoft.Extensions.Internal { process.Kill(); } - - return process.ExitCode; } } } diff --git a/src/Shared/PropertyHelper/PropertyHelper.cs b/src/Shared/PropertyHelper/PropertyHelper.cs index 27ba5661a4..f6aad151e5 100644 --- a/src/Shared/PropertyHelper/PropertyHelper.cs +++ b/src/Shared/PropertyHelper/PropertyHelper.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace Microsoft.Extensions.Internal { @@ -37,6 +38,12 @@ namespace Microsoft.Extensions.Internal private static readonly ConcurrentDictionary VisiblePropertiesCache = new ConcurrentDictionary(); + // We need to be able to check if a type is a 'ref struct' - but we need to be able to compile + // for platforms where the attribute is not defined, like net46. So we can fetch the attribute + // by late binding. If the attribute isn't defined, then we assume we won't encounter any + // 'ref struct' types. + private static readonly Type IsByRefLikeAttribute = Type.GetType("System.Runtime.CompilerServices.IsByRefLikeAttribute", throwOnError: false); + private Action _valueSetter; private Func _valueGetter; @@ -511,16 +518,34 @@ namespace Microsoft.Extensions.Internal return helpers; } - // Indexed properties are not useful (or valid) for grabbing properties off an object. + private static bool IsInterestingProperty(PropertyInfo property) { // For improving application startup time, do not use GetIndexParameters() api early in this check as it // creates a copy of parameter array and also we would like to check for the presence of a get method // and short circuit asap. - return property.GetMethod != null && + return + property.GetMethod != null && property.GetMethod.IsPublic && !property.GetMethod.IsStatic && + + // PropertyHelper can't work with ref structs. + !IsRefStructProperty(property) && + + // Indexed properties are not useful (or valid) for grabbing properties off an object. property.GetMethod.GetParameters().Length == 0; } + + // PropertyHelper can't really interact with ref-struct properties since they can't be + // boxed and can't be used as generic types. We just ignore them. + // + // see: https://github.com/aspnet/Mvc/issues/8545 + private static bool IsRefStructProperty(PropertyInfo property) + { + return + IsByRefLikeAttribute != null && + property.PropertyType.IsValueType && + property.PropertyType.IsDefined(IsByRefLikeAttribute); + } } } diff --git a/src/Shared/test/Shared.Tests/CertificateManagerTests.cs b/src/Shared/test/Shared.Tests/CertificateManagerTests.cs index 613f5c966f..cd314383c9 100644 --- a/src/Shared/test/Shared.Tests/CertificateManagerTests.cs +++ b/src/Shared/test/Shared.Tests/CertificateManagerTests.cs @@ -1,7 +1,7 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_2 using System; using System.IO; @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests public ITestOutputHelper Output { get; } - [Fact] + [Fact(Skip = "True")] public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() { try @@ -109,6 +109,92 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } } + [Fact] + public void EnsureCreateHttpsCertificate2_CreatesACertificate_WhenThereAreNoHttpsCertificates() + { + try + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer"; + var manager = new CertificateManager(); + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + // Act + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate2(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject); + + // Assert + Assert.Equal(EnsureCertificateResult.Succeeded, result.ResultCode); + Assert.NotNull(result.Diagnostics); + Assert.NotEmpty(result.Diagnostics.Messages); + Assert.Empty(result.Diagnostics.Exceptions); + + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName)); + Assert.NotNull(exportedCertificate); + Assert.False(exportedCertificate.HasPrivateKey); + + var httpsCertificates = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false); + var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject); + Assert.True(httpsCertificate.HasPrivateKey); + Assert.Equal(TestCertificateSubject, httpsCertificate.Subject); + Assert.Equal(TestCertificateSubject, httpsCertificate.Issuer); + Assert.Equal("sha256RSA", httpsCertificate.SignatureAlgorithm.FriendlyName); + Assert.Equal("1.2.840.113549.1.1.11", httpsCertificate.SignatureAlgorithm.Value); + + Assert.Equal(now.LocalDateTime, httpsCertificate.NotBefore); + Assert.Equal(now.AddYears(1).LocalDateTime, httpsCertificate.NotAfter); + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509BasicConstraintsExtension basicConstraints && + basicConstraints.Critical == true && + basicConstraints.CertificateAuthority == false && + basicConstraints.HasPathLengthConstraint == false && + basicConstraints.PathLengthConstraint == 0); + + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509KeyUsageExtension keyUsage && + keyUsage.Critical == true && + keyUsage.KeyUsages == X509KeyUsageFlags.KeyEncipherment); + + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage && + enhancedKeyUsage.Critical == true && + enhancedKeyUsage.EnhancedKeyUsages.OfType().Single() is Oid keyUsage && + keyUsage.Value == "1.3.6.1.5.5.7.3.1"); + + // Subject alternative name + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e.Critical == true && + e.Oid.Value == "2.5.29.17"); + + // ASP.NET HTTPS Development certificate extension + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e.Critical == false && + e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" && + Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core HTTPS development certificate"); + + Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); + + } + catch (Exception e) + { + Output.WriteLine(e.Message); + ListCertificates(Output); + throw; + } + } + private void ListCertificates(ITestOutputHelper output) { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) @@ -125,7 +211,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } } - [Fact] + [Fact(Skip = "true")] public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() { // Arrange @@ -196,108 +282,6 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); } } - - [Fact] - public void EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() - { - // Arrange - const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer"; - var manager = new CertificateManager(); - - manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); - } - - // Act - DateTimeOffset now = DateTimeOffset.UtcNow; - now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); - var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject); - - // Assert - Assert.Equal(EnsureCertificateResult.Succeeded, result); - Assert.True(File.Exists(CertificateName)); - - var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName)); - Assert.NotNull(exportedCertificate); - Assert.False(exportedCertificate.HasPrivateKey); - - var identityCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false); - var identityCertificate = Assert.Single(identityCertificates, i => i.Subject == TestCertificateSubject); - Assert.True(identityCertificate.HasPrivateKey); - Assert.Equal(TestCertificateSubject, identityCertificate.Subject); - Assert.Equal(TestCertificateSubject, identityCertificate.Issuer); - Assert.Equal("sha256RSA", identityCertificate.SignatureAlgorithm.FriendlyName); - Assert.Equal("1.2.840.113549.1.1.11", identityCertificate.SignatureAlgorithm.Value); - - Assert.Equal(now.LocalDateTime, identityCertificate.NotBefore); - Assert.Equal(now.AddYears(1).LocalDateTime, identityCertificate.NotAfter); - Assert.Contains( - identityCertificate.Extensions.OfType(), - e => e is X509BasicConstraintsExtension basicConstraints && - basicConstraints.Critical == true && - basicConstraints.CertificateAuthority == false && - basicConstraints.HasPathLengthConstraint == false && - basicConstraints.PathLengthConstraint == 0); - - Assert.Contains( - identityCertificate.Extensions.OfType(), - e => e is X509KeyUsageExtension keyUsage && - keyUsage.Critical == true && - keyUsage.KeyUsages == X509KeyUsageFlags.DigitalSignature); - - Assert.Contains( - identityCertificate.Extensions.OfType(), - e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage && - enhancedKeyUsage.Critical == true && - enhancedKeyUsage.EnhancedKeyUsages.OfType().Single() is Oid keyUsage && - keyUsage.Value == "1.3.6.1.5.5.7.3.1"); - - // ASP.NET Core Identity Json Web Token signing development certificate - Assert.Contains( - identityCertificate.Extensions.OfType(), - e => e.Critical == false && - e.Oid.Value == "1.3.6.1.4.1.311.84.1.2" && - Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core Identity Json Web Token signing development certificate"); - - Assert.Equal(identityCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); - } - - [Fact] - public void EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() - { - // Arrange - const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx"; - var certificatePassword = Guid.NewGuid().ToString(); - - var manager = new CertificateManager(); - - manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); - } - - DateTimeOffset now = DateTimeOffset.UtcNow; - now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); - manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); - - var identityTokenSigningCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); - - // Act - var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject); - - // Assert - Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result); - Assert.True(File.Exists(CertificateName)); - - var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword); - Assert.NotNull(exportedCertificate); - Assert.True(exportedCertificate.HasPrivateKey); - - Assert.Equal(identityTokenSigningCertificates.GetCertHashString(), exportedCertificate.GetCertHashString()); - } } } diff --git a/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs b/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs index ba1dd06511..92e06a8f70 100644 --- a/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs +++ b/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if NETCOREAPP2_0 || NETCOREAPP2_1 +#if NETCOREAPP2_2 using System.IO; using System.Runtime.InteropServices; using Xunit; diff --git a/src/Shared/test/Shared.Tests/NonCapturingTimerTest.cs b/src/Shared/test/Shared.Tests/NonCapturingTimerTest.cs new file mode 100644 index 0000000000..ef21ce5f3b --- /dev/null +++ b/src/Shared/test/Shared.Tests/NonCapturingTimerTest.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 System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class NonCapturingTimerTest + { + [Fact] + public async Task NonCapturingTimer_DoesntCaptureExecutionContext() + { + // Arrange + var message = new AsyncLocal(); + message.Value = "Hey, this is a value stored in the execuion context"; + + var tcs = new TaskCompletionSource(); + + // Act + var timer = NonCapturingTimer.Create((_) => + { + // Observe the value based on the current execution context + tcs.SetResult(message.Value); + }, state: null, dueTime: TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan); + + // Assert + var messageFromTimer = await tcs.Task; + timer.Dispose(); + + // ExecutionContext didn't flow to timer callback + Assert.Null(messageFromTimer); + + // ExecutionContext was restored + Assert.NotNull(await Task.Run(() => message.Value)); + } + } +} diff --git a/src/Shared/test/Shared.Tests/PropertyHelperTest.cs b/src/Shared/test/Shared.Tests/PropertyHelperTest.cs index 19cf08b370..1c43dc880b 100644 --- a/src/Shared/test/Shared.Tests/PropertyHelperTest.cs +++ b/src/Shared/test/Shared.Tests/PropertyHelperTest.cs @@ -153,6 +153,22 @@ namespace Microsoft.Extensions.Internal Assert.Equal("Prop5", helper.Name); } +#if NETSTANDARD || NETCOREAPP + [Fact] + public void PropertyHelper_RefStructProperties() + { + // Arrange + var obj = new RefStructProperties(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(obj.GetType().GetTypeInfo())); + Assert.Equal("Prop5", helper.Name); + } +#elif NET46 || NET461 +#else +#error Unknown TFM - update the set of TFMs where we test for ref structs +#endif + [Fact] public void PropertyHelper_DoesNotFindSetOnlyProperties() { @@ -718,6 +734,22 @@ namespace Microsoft.Extensions.Internal public int Prop5 { get; set; } } +#if NETSTANDARD || NETCOREAPP + private class RefStructProperties + { + public Span Span => throw new NotImplementedException(); + public MyRefStruct UserDefined => throw new NotImplementedException(); + + public int Prop5 { get; set; } + } + + private readonly ref struct MyRefStruct + { + } +#elif NET46 || NET461 +#else +#error Unknown TFM - update the set of TFMs where we test for ref structs +#endif private struct MyProperties { public int IntProp { get; set; } diff --git a/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj b/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj index d77d392873..2b2900911a 100644 --- a/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj +++ b/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj @@ -3,6 +3,7 @@ netstandard2.0 portable + false diff --git a/src/Testing/src/CultureReplacer.cs b/src/Testing/src/CultureReplacer.cs new file mode 100644 index 0000000000..51e35e8354 --- /dev/null +++ b/src/Testing/src/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/Testing/src/ExceptionAssertions.cs b/src/Testing/src/ExceptionAssertions.cs new file mode 100644 index 0000000000..244cad5a37 --- /dev/null +++ b/src/Testing/src/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/Testing/src/HttpClientSlim.cs b/src/Testing/src/HttpClientSlim.cs new file mode 100644 index 0000000000..6214ffefc1 --- /dev/null +++ b/src/Testing/src/HttpClientSlim.cs @@ -0,0 +1,158 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Testing +{ + /// + /// 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: {GetHost(requestUri)}\r\n").ConfigureAwait(false); + await writer.WriteAsync("\r\n").ConfigureAwait(false); + } + + return await ReadResponse(stream).ConfigureAwait(false); + } + } + + internal static string GetHost(Uri requestUri) + { + var authority = requestUri.Authority; + if (requestUri.HostNameType == UriHostNameType.IPv6) + { + // Make sure there's no % scope id. https://github.com/aspnet/KestrelHttpServer/issues/2637 + var address = IPAddress.Parse(requestUri.Host); + address = new IPAddress(address.GetAddressBytes()); // Drop scope Id. + if (requestUri.IsDefaultPort) + { + authority = $"[{address}]"; + } + else + { + authority = $"[{address}]:{requestUri.Port.ToString(CultureInfo.InvariantCulture)}"; + } + } + return authority; + } + + public static async Task 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/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj new file mode 100644 index 0000000000..d9d9008dd2 --- /dev/null +++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj @@ -0,0 +1,33 @@ + + + + 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/Testing/src/Properties/AssemblyInfo.cs b/src/Testing/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0212e111ee --- /dev/null +++ b/src/Testing/src/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/Testing/src/ReplaceCulture.cs b/src/Testing/src/ReplaceCulture.cs new file mode 100644 index 0000000000..9580bfd0da --- /dev/null +++ b/src/Testing/src/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/Testing/src/TaskExtensions.cs b/src/Testing/src/TaskExtensions.cs new file mode 100644 index 0000000000..83130aeae4 --- /dev/null +++ b/src/Testing/src/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/Testing/src/TestPathUtilities.cs b/src/Testing/src/TestPathUtilities.cs new file mode 100644 index 0000000000..ebd10897c3 --- /dev/null +++ b/src/Testing/src/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/Testing/src/TestPlatformHelper.cs b/src/Testing/src/TestPlatformHelper.cs new file mode 100644 index 0000000000..1a3f275c7e --- /dev/null +++ b/src/Testing/src/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/Testing/src/Tracing/CollectingEventListener.cs b/src/Testing/src/Tracing/CollectingEventListener.cs new file mode 100644 index 0000000000..d22a4996af --- /dev/null +++ b/src/Testing/src/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/Testing/src/Tracing/EventAssert.cs b/src/Testing/src/Tracing/EventAssert.cs new file mode 100644 index 0000000000..b32fb36dad --- /dev/null +++ b/src/Testing/src/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/Testing/src/Tracing/EventSourceTestBase.cs b/src/Testing/src/Tracing/EventSourceTestBase.cs new file mode 100644 index 0000000000..721966d6c5 --- /dev/null +++ b/src/Testing/src/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/Testing/src/contentFiles/cs/netstandard2.0/EventSourceTestCollection.cs b/src/Testing/src/contentFiles/cs/netstandard2.0/EventSourceTestCollection.cs new file mode 100644 index 0000000000..0ed9e1a9a9 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/ConditionalFactAttribute.cs b/src/Testing/src/xunit/ConditionalFactAttribute.cs new file mode 100644 index 0000000000..7448b48d8c --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/ConditionalFactDiscoverer.cs b/src/Testing/src/xunit/ConditionalFactDiscoverer.cs new file mode 100644 index 0000000000..819373fa31 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/ConditionalTheoryAttribute.cs b/src/Testing/src/xunit/ConditionalTheoryAttribute.cs new file mode 100644 index 0000000000..9249078cc5 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/ConditionalTheoryDiscoverer.cs b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs new file mode 100644 index 0000000000..d24421f5cd --- /dev/null +++ b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing.xunit +{ + internal class ConditionalTheoryDiscoverer : TheoryDiscoverer + { + public ConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + } + + protected override IEnumerable 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(); + if (skipReason == null && dataRow?.Length > 0) + { + var obj = dataRow[0]; + if (obj != null) + { + var type = obj.GetType(); + var property = type.GetProperty("Skip"); + if (property != null && property.PropertyType.Equals(typeof(string))) + { + skipReason = property.GetValue(obj) as string; + } + } + } + + return skipReason != null ? + base.CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, skipReason) + : base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow); + } + } +} \ No newline at end of file diff --git a/src/Testing/src/xunit/DockerOnlyAttribute.cs b/src/Testing/src/xunit/DockerOnlyAttribute.cs new file mode 100644 index 0000000000..d67a35a672 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs b/src/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs new file mode 100644 index 0000000000..8bf1bfd15e --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/FrameworkSkipConditionAttribute.cs b/src/Testing/src/xunit/FrameworkSkipConditionAttribute.cs new file mode 100644 index 0000000000..168076a434 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/IEnvironmentVariable.cs b/src/Testing/src/xunit/IEnvironmentVariable.cs new file mode 100644 index 0000000000..068c210611 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/ITestCondition.cs b/src/Testing/src/xunit/ITestCondition.cs new file mode 100644 index 0000000000..bb6ff1f031 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/MinimumOsVersionAttribute.cs b/src/Testing/src/xunit/MinimumOsVersionAttribute.cs new file mode 100644 index 0000000000..89e3b19556 --- /dev/null +++ b/src/Testing/src/xunit/MinimumOsVersionAttribute.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace Microsoft.AspNetCore.Testing.xunit +{ + /// + /// Skips a test if the OS is the given type (Windows) and the OS version is less than specified. + /// E.g. Specifying Window 10.0 skips on Win 8, but not on Linux. Combine with OSSkipConditionAttribute as needed. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] + public class MinimumOSVersionAttribute : Attribute, ITestCondition + { + private readonly OperatingSystems _excludedOperatingSystem; + private readonly Version _minVersion; + private readonly OperatingSystems _osPlatform; + private readonly Version _osVersion; + + public MinimumOSVersionAttribute(OperatingSystems operatingSystem, string minVersion) : + this( + operatingSystem, + GetCurrentOS(), + GetCurrentOSVersion(), + Version.Parse(minVersion)) + { + } + + // to enable unit testing + internal MinimumOSVersionAttribute( + OperatingSystems operatingSystem, OperatingSystems osPlatform, Version osVersion, Version minVersion) + { + if (operatingSystem != OperatingSystems.Windows) + { + throw new NotImplementedException("Min version support is only implemented for Windows."); + } + _excludedOperatingSystem = operatingSystem; + _minVersion = minVersion; + _osPlatform = osPlatform; + _osVersion = osVersion; + + SkipReason = $"This test requires {_excludedOperatingSystem} {_minVersion} or later."; + } + + public bool IsMet + { + get + { + // Do not skip other OS's, Use OSSkipConditionAttribute or a separate MinimumOSVersionAttribute for that. + if (_osPlatform != _excludedOperatingSystem) + { + return true; + } + + return _osVersion >= _minVersion; + } + } + + public string SkipReason { get; set; } + + private static OperatingSystems GetCurrentOS() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OperatingSystems.Windows; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OperatingSystems.Linux; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OperatingSystems.MacOSX; + } + throw new PlatformNotSupportedException(); + } + + private static Version GetCurrentOSVersion() + { + // currently not used on other OS's + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Win10+ + var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + var major = key.GetValue("CurrentMajorVersionNumber") as int?; + var minor = key.GetValue("CurrentMinorVersionNumber") as int?; + + if (major.HasValue && minor.HasValue) + { + return new Version(major.Value, minor.Value); + } + + // CurrentVersion doesn't work past Win8.1 + var current = key.GetValue("CurrentVersion") as string; + if (!string.IsNullOrEmpty(current) && Version.TryParse(current, out var currentVersion)) + { + return currentVersion; + } + + // Environment.OSVersion doesn't work past Win8. + return Environment.OSVersion.Version; + } + else + { + return new Version(); + } + } + } +} diff --git a/src/Testing/src/xunit/OSSkipConditionAttribute.cs b/src/Testing/src/xunit/OSSkipConditionAttribute.cs new file mode 100644 index 0000000000..9996510718 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/OperatingSystems.cs b/src/Testing/src/xunit/OperatingSystems.cs new file mode 100644 index 0000000000..c575d3e197 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/RuntimeFrameworks.cs b/src/Testing/src/xunit/RuntimeFrameworks.cs new file mode 100644 index 0000000000..2ec5ea7ec1 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/SkippedTestCase.cs b/src/Testing/src/xunit/SkippedTestCase.cs new file mode 100644 index 0000000000..c2e15fa640 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/TestMethodExtensions.cs b/src/Testing/src/xunit/TestMethodExtensions.cs new file mode 100644 index 0000000000..5ec3bb4ec3 --- /dev/null +++ b/src/Testing/src/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/Testing/src/xunit/WindowsVersions.cs b/src/Testing/src/xunit/WindowsVersions.cs new file mode 100644 index 0000000000..ff8312b363 --- /dev/null +++ b/src/Testing/src/xunit/WindowsVersions.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. + +namespace Microsoft.AspNetCore.Testing.xunit +{ + public static class WindowsVersions + { + public const string Win7 = "6.1"; + + public const string Win2008R2 = Win7; + + public const string Win8 = "6.2"; + + public const string Win81 = "6.3"; + + public const string Win10 = "10.0"; + } +} diff --git a/src/Testing/test/CollectingEventListenerTest.cs b/src/Testing/test/CollectingEventListenerTest.cs new file mode 100644 index 0000000000..8f131982f0 --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/ConditionalFactTest.cs b/src/Testing/test/ConditionalFactTest.cs new file mode 100644 index 0000000000..a04eb1731d --- /dev/null +++ b/src/Testing/test/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_2 + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.CLR)] + public void ThisTestMustRunOnCoreCLR() + { + Asserter.TestRan = true; + } +#elif NET461 || NET46 + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)] + public void ThisTestMustRunOnCLR() + { + Asserter.TestRan = true; + } +#else +#error Target frameworks need to be updated. +#endif + + public class ConditionalFactAsserter : IDisposable + { + public bool TestRan { get; set; } + + public void Dispose() + { + Assert.True(TestRan, "If this assertion fails, a conditional fact wasn't discovered."); + } + } + } +} \ No newline at end of file diff --git a/src/Testing/test/ConditionalTheoryTest.cs b/src/Testing/test/ConditionalTheoryTest.cs new file mode 100644 index 0000000000..1181f1365a --- /dev/null +++ b/src/Testing/test/ConditionalTheoryTest.cs @@ -0,0 +1,156 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Testing +{ + public class ConditionalTheoryTest : IClassFixture + { + 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_2 + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.CLR)] + [MemberData(nameof(GetInts))] + public void ThisTestMustRunOnCoreCLR(int value) + { + Asserter.TestRan = true; + } +#elif NET461 || NET46 + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)] + [MemberData(nameof(GetInts))] + public void ThisTestMustRunOnCLR(int value) + { + Asserter.TestRan = true; + } +#else +#error Target frameworks need to be updated. +#endif + + public static TheoryData> 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."); + } + } + + [ConditionalTheory] + [MemberData(nameof(SkippableData))] + public void WithSkipableData(Skippable skippable) + { + Assert.Null(skippable.Skip); + Assert.Equal(1, skippable.Data); + } + + public static TheoryData SkippableData => new TheoryData + { + new Skippable() { Data = 1 }, + new Skippable() { Data = 2, Skip = "This row should be skipped." } + }; + + public class Skippable : IXunitSerializable + { + public Skippable() { } + public int Data { get; set; } + public string Skip { get; set; } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(Data), Data, typeof(int)); + } + + public void Deserialize(IXunitSerializationInfo info) + { + Data = info.GetValue(nameof(Data)); + } + + public override string ToString() + { + return Data.ToString(); + } + } + } +} diff --git a/src/Testing/test/DockerTests.cs b/src/Testing/test/DockerTests.cs new file mode 100644 index 0000000000..c66fdd679c --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/EnvironmentVariableSkipConditionTest.cs b/src/Testing/test/EnvironmentVariableSkipConditionTest.cs new file mode 100644 index 0000000000..b536ae56f7 --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/ExceptionAssertTest.cs b/src/Testing/test/ExceptionAssertTest.cs new file mode 100644 index 0000000000..aa7354dca8 --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/HttpClientSlimTest.cs b/src/Testing/test/HttpClientSlimTest.cs new file mode 100644 index 0000000000..42b19ece08 --- /dev/null +++ b/src/Testing/test/HttpClientSlimTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Testing +{ + public class HttpClientSlimTest + { + private static byte[] _defaultResponse = Encoding.ASCII.GetBytes("test"); + + [Fact] + public async Task GetStringAsyncHttp() + { + using (var host = StartHost(out var address)) + { + Assert.Equal("test", await HttpClientSlim.GetStringAsync(address)); + } + } + + [Fact] + public async Task GetStringAsyncThrowsForErrorResponse() + { + using (var host = StartHost(out var address, statusCode: 500)) + { + await Assert.ThrowsAnyAsync(() => 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(""))); + } + } + + [Fact] + public void Ipv6ScopeIdsFilteredOut() + { + var requestUri = new Uri("http://[fe80::5d2a:d070:6fd6:1bac%7]:5003/"); + Assert.Equal("[fe80::5d2a:d070:6fd6:1bac]:5003", HttpClientSlim.GetHost(requestUri)); + } + + [Fact] + public void GetHostExcludesDefaultPort() + { + var requestUri = new Uri("http://[fe80::5d2a:d070:6fd6:1bac%7]:80/"); + Assert.Equal("[fe80::5d2a:d070:6fd6:1bac]", HttpClientSlim.GetHost(requestUri)); + } + + private HttpListener StartHost(out string address, int statusCode = 200, Func handler = null) + { + var listener = new HttpListener(); + var random = new Random(); + address = null; + + for (var i = 0; i < 10; i++) + { + try + { + // HttpListener doesn't support requesting port 0 (dynamic). + // Requesting port 0 from Sockets and then passing that to HttpListener is racy. + // Just keep trying until we find a free one. + address = $"http://127.0.0.1:{random.Next(1024, ushort.MaxValue)}/"; + listener.Prefixes.Add(address); + listener.Start(); + break; + } + catch (HttpListenerException) + { + // Address in use + listener.Close(); + listener = new HttpListener(); + } + } + + Assert.True(listener.IsListening, "IsListening"); + + _ = listener.GetContextAsync().ContinueWith(async task => + { + var context = task.Result; + context.Response.StatusCode = statusCode; + + if (handler == null) + { + await context.Response.OutputStream.WriteAsync(_defaultResponse, 0, _defaultResponse.Length); + } + else + { + await handler(context); + } + + context.Response.Close(); + }); + + return listener; + } + } +} diff --git a/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj b/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj new file mode 100644 index 0000000000..3fdd9ff379 --- /dev/null +++ b/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj @@ -0,0 +1,28 @@ + + + + $(StandardTestTfms) + + + $(NoWarn);xUnit1004 + + $(NoWarn);xUnit1026 + + + + + + + + + + + + + + + + + + + diff --git a/src/Testing/test/OSSkipConditionAttributeTest.cs b/src/Testing/test/OSSkipConditionAttributeTest.cs new file mode 100644 index 0000000000..0120eb7a4c --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/OSSkipConditionTest.cs b/src/Testing/test/OSSkipConditionTest.cs new file mode 100644 index 0000000000..2d76f2c2cd --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/ReplaceCultureAttributeTest.cs b/src/Testing/test/ReplaceCultureAttributeTest.cs new file mode 100644 index 0000000000..6b8df346c9 --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/TaskExtensionsTest.cs b/src/Testing/test/TaskExtensionsTest.cs new file mode 100644 index 0000000000..f7ad603df5 --- /dev/null +++ b/src/Testing/test/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/src/Testing/test/TestPathUtilitiesTest.cs b/src/Testing/test/TestPathUtilitiesTest.cs new file mode 100644 index 0000000000..0c9a7c5ee4 --- /dev/null +++ b/src/Testing/test/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("Extensions")); + } + + [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/src/Testing/test/TestPlatformHelperTest.cs b/src/Testing/test/TestPlatformHelperTest.cs new file mode 100644 index 0000000000..8e35e164d5 --- /dev/null +++ b/src/Testing/test/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); + } + } +}