diff --git a/tests/ExchangeSharpTests/CryptoUtilityTests.cs b/tests/ExchangeSharpTests/CryptoUtilityTests.cs index d53577f6..a0db5809 100644 --- a/tests/ExchangeSharpTests/CryptoUtilityTests.cs +++ b/tests/ExchangeSharpTests/CryptoUtilityTests.cs @@ -226,32 +226,47 @@ public void ConvertInvariantTest() } } - [TestMethod] + [ConditionalTestMethod] + [PlatformSpecificTest( + ~TestPlatforms.OSX, + "Has an issue on MacOS. See https://github.com/dotnet/corefx/issues/42607" + )] public async Task RateGate() { - const int timesPerPeriod = 1; - const int ms = 100; - const int loops = 5; - double msMax = (double)ms * 1.5; - double msMin = (double)ms * (1.0 / 1.5); - RateGate gate = new RateGate(timesPerPeriod, TimeSpan.FromMilliseconds(ms)); - if (!(await gate.WaitToProceedAsync(0))) - { - throw new APIException("Rate gate should have allowed immediate access to first attempt"); - } - for (int i = 0; i < loops; i++) - { - Stopwatch timer = Stopwatch.StartNew(); - await gate.WaitToProceedAsync(); - timer.Stop(); - - if (i > 0) - { - // check for too much elapsed time with a little fudge - Assert.IsTrue(timer.Elapsed.TotalMilliseconds <= msMax, "Rate gate took too long to wait in between calls: " + timer.Elapsed.TotalMilliseconds + "ms"); - Assert.IsTrue(timer.Elapsed.TotalMilliseconds >= msMin, "Rate gate took too little to wait in between calls: " + timer.Elapsed.TotalMilliseconds + "ms"); - } - } + const int timesPerPeriod = 1; + const int ms = 100; + const int loops = 5; + const double msMax = (double) ms * 1.5; + const double msMin = (double) ms * (1.0 / 1.5); + var gate = new RateGate(timesPerPeriod, TimeSpan.FromMilliseconds(ms)); + + var entered = await gate.WaitToProceedAsync(0); + if (!entered) + { + throw new APIException("Rate gate should have allowed immediate access to first attempt"); + } + + for (var i = 0; i < loops; i++) + { + var timer = Stopwatch.StartNew(); + await gate.WaitToProceedAsync(); + timer.Stop(); + + if (i <= 0) + { + continue; + } + + // check for too much elapsed time with a little fudge + Assert.IsTrue( + timer.Elapsed.TotalMilliseconds <= msMax, + "Rate gate took too long to wait in between calls: " + timer.Elapsed.TotalMilliseconds + "ms" + ); + Assert.IsTrue( + timer.Elapsed.TotalMilliseconds >= msMin, + "Rate gate took too little to wait in between calls: " + timer.Elapsed.TotalMilliseconds + "ms" + ); + } } } -} \ No newline at end of file +} diff --git a/tests/ExchangeSharpTests/ExchangeSharpTests.csproj b/tests/ExchangeSharpTests/ExchangeSharpTests.csproj index d4a90fea..ef79bc19 100644 --- a/tests/ExchangeSharpTests/ExchangeSharpTests.csproj +++ b/tests/ExchangeSharpTests/ExchangeSharpTests.csproj @@ -25,7 +25,6 @@ - diff --git a/tests/ExchangeSharpTests/Utility/ConditionalTestMethod.cs b/tests/ExchangeSharpTests/Utility/ConditionalTestMethod.cs new file mode 100644 index 00000000..d240cd4d --- /dev/null +++ b/tests/ExchangeSharpTests/Utility/ConditionalTestMethod.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace Microsoft.VisualStudio.TestTools.UnitTesting +{ + /// + /// An extension to the [TestMethod] attribute. It walks the method hierarchy looking + /// for an [IgnoreIf] attribute. If one or more are found, they are each evaluated, if the attribute + /// returns `true`, evaluation is short-circuited, and the test method is skipped. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class ConditionalTestMethodAttribute : TestMethodAttribute + { + public override TestResult[] Execute(ITestMethod testMethod) + { + var ignoreAttributes = FindAttributes(testMethod); + + // Evaluate each attribute, and skip if one returns `true` + foreach (var ignoreAttribute in ignoreAttributes) + { + if (!ignoreAttribute.ShouldIgnore(testMethod)) + continue; + + var message = + "Test not executed. " + + (string.IsNullOrWhiteSpace(ignoreAttribute.Message) + ? $"Conditionally ignored by {ignoreAttribute.GetType().Name}." + : ignoreAttribute.Message); + + return new[] + { + new TestResult + { + Outcome = UnitTestOutcome.Inconclusive, + TestFailureException = new AssertInconclusiveException(message) + } + }; + } + + return base.Execute(testMethod); + } + + private IEnumerable FindAttributes(ITestMethod testMethod) + { + // Look for an [IgnoreIf] on the method, including any virtuals this method overrides + var ignoreAttributes = new List(); + + ignoreAttributes.AddRange(testMethod.GetAttributes(inherit: true)); + + return ignoreAttributes; + } + } +} diff --git a/tests/ExchangeSharpTests/Utility/IgnoreIfAttribute.cs b/tests/ExchangeSharpTests/Utility/IgnoreIfAttribute.cs new file mode 100644 index 00000000..e2abd462 --- /dev/null +++ b/tests/ExchangeSharpTests/Utility/IgnoreIfAttribute.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +// ReSharper disable once CheckNamespace +namespace Microsoft.VisualStudio.TestTools.UnitTesting +{ + /// + /// An extension to the [Ignore] attribute. Instead of using test lists / test categories to conditionally + /// skip tests, allow a [TestClass] or [TestMethod] to specify a method to run. If the member returns + /// `true` the test method will be skipped. The "ignore criteria" method or property must be `static`, return a single + /// `bool` value, and not accept any parameters. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class IgnoreIfAttribute : Attribute + { + public string IgnoreCriteriaMemberName { get; } + + public string Message { get; } + + public IgnoreIfAttribute(string ignoreCriteriaMemberName, string message = null) + { + IgnoreCriteriaMemberName = ignoreCriteriaMemberName; + Message = message; + } + + internal virtual bool ShouldIgnore(ITestMethod testMethod) + { + try + { + // Search for the method or prop specified by name in this class or any parent classes. + var searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | + BindingFlags.Static; + Debug.Assert(testMethod.MethodInfo.DeclaringType != null, + "testMethod.MethodInfo.DeclaringType != null"); + var member = testMethod.MethodInfo.DeclaringType.GetMember(IgnoreCriteriaMemberName, searchFlags) + .FirstOrDefault(); + + switch (member) + { + case MethodInfo method: + return (bool) method.Invoke(null, null); + case PropertyInfo prop: + return (bool) prop.GetValue(null); + default: + throw new ArgumentOutOfRangeException(nameof(member)); + } + } + catch (Exception e) + { + var message = + $"Conditional ignore bool returning method/prop {IgnoreCriteriaMemberName} not found. Ensure the method/prop is in the same class as the test method, marked as `static`, returns a `bool`, and doesn't accept any parameters."; + throw new ArgumentException(message, e); + } + } + } +} diff --git a/tests/ExchangeSharpTests/Utility/PlatformSpecificTest.cs b/tests/ExchangeSharpTests/Utility/PlatformSpecificTest.cs new file mode 100644 index 00000000..5e7460ca --- /dev/null +++ b/tests/ExchangeSharpTests/Utility/PlatformSpecificTest.cs @@ -0,0 +1,40 @@ +using System; +using System.Runtime.InteropServices; + +// ReSharper disable once CheckNamespace +namespace Microsoft.VisualStudio.TestTools.UnitTesting +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class PlatformSpecificTestAttribute : IgnoreIfAttribute + { + public static OSPlatform NetBSD { get; } = OSPlatform.Create("NETBSD"); + + public TestPlatforms FlagPlatform { get; } + + public PlatformSpecificTestAttribute(TestPlatforms flagPlatform, string message = null) + : base(null, message) + { + FlagPlatform = flagPlatform; + } + + internal override bool ShouldIgnore(ITestMethod testMethod) + { + var shouldRun = false; + + if (FlagPlatform.HasFlag(TestPlatforms.Any)) + return true; + if (FlagPlatform.HasFlag(TestPlatforms.Windows)) + shouldRun = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (FlagPlatform.HasFlag(TestPlatforms.Linux)) + shouldRun = shouldRun || RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + if (FlagPlatform.HasFlag(TestPlatforms.OSX)) + shouldRun = shouldRun || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + if (FlagPlatform.HasFlag(TestPlatforms.FreeBSD)) + shouldRun = shouldRun || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); + if (FlagPlatform.HasFlag(TestPlatforms.NetBSD)) + shouldRun = shouldRun || RuntimeInformation.IsOSPlatform(NetBSD); + + return !shouldRun; + } + } +} diff --git a/tests/ExchangeSharpTests/Utility/TestPlatforms.cs b/tests/ExchangeSharpTests/Utility/TestPlatforms.cs new file mode 100644 index 00000000..e573c8f4 --- /dev/null +++ b/tests/ExchangeSharpTests/Utility/TestPlatforms.cs @@ -0,0 +1,17 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Microsoft.VisualStudio.TestTools.UnitTesting +{ + [Flags] + public enum TestPlatforms + { + Windows = 1, + Linux = 2, + OSX = 4, + FreeBSD = 8, + NetBSD = 16, + AnyUnix = FreeBSD | Linux | NetBSD | OSX, + Any = ~0 + } +}