File: Discoverers\ConditionalTestDiscoverer.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitExtensions\src\Microsoft.DotNet.XUnitExtensions.csproj (Microsoft.DotNet.XUnitExtensions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
 
namespace Microsoft.DotNet.XUnitExtensions
{
    // Internal helper class for code common to conditional test discovery through
    // [ConditionalFact] and [ConditionalTheory]
    internal static class ConditionalTestDiscoverer
    {
        // This helper method evaluates the given condition member names for a given set of test cases.
        // If any condition member evaluates to 'false', the test cases are marked to be skipped.
        // The skip reason is the collection of all the condition members that evaluated to 'false'.
        internal static string EvaluateSkipConditions(ITestMethod testMethod, object[] conditionArguments)
        {
            Type calleeType = null;
            string[] conditionMemberNames = null;
 
            if (CheckInputToSkipExecution(conditionArguments, ref calleeType, ref conditionMemberNames, testMethod)) return null;
 
            MethodInfo testMethodInfo = testMethod.Method.ToRuntimeMethod();
            Type testMethodDeclaringType = testMethodInfo.DeclaringType;
            List<string> falseConditions = new List<string>(conditionMemberNames.Count());
 
            foreach (string entry in conditionMemberNames)
            {
                string conditionMemberName = entry;
 
                // Null condition member names are silently tolerated
                if (string.IsNullOrWhiteSpace(conditionMemberName))
                {
                    continue;
                }
 
                Type declaringType;
 
                if (calleeType != null)
                {
                    declaringType = calleeType;
                }
                else
                {
                    declaringType = testMethodDeclaringType;
 
                    string[] symbols = conditionMemberName.Split('.');
                    if (symbols.Length == 2)
                    {
                        conditionMemberName = symbols[1];
                        ITypeInfo type = testMethod.TestClass.Class.Assembly.GetTypes(false).Where(t => t.Name.Contains(symbols[0])).FirstOrDefault();
                        if (type != null)
                        {
                            declaringType = type.ToRuntimeType();
                        }
                    }
                }
 
                Func<bool> conditionFunc;
                if ((conditionFunc = LookupConditionalMember(declaringType, conditionMemberName)) == null)
                {
                    throw new ConditionalDiscovererException(GetFailedLookupString(conditionMemberName, declaringType));
                }
 
                // In the case of multiple conditions, collect the results of all
                // of them to produce a summary skip reason.
                try
                {
                    if (!conditionFunc())
                    {
                        falseConditions.Add(conditionMemberName);
                    }
                }
                catch (Exception exc)
                {
                    falseConditions.Add($"{conditionMemberName} ({exc.GetType().Name})");
                }
            }
 
            // Compose a summary of all conditions that returned false.
            if (falseConditions.Count > 0)
            {
                return string.Format("Condition(s) not met: \"{0}\"", string.Join("\", \"", falseConditions));
            }
 
            // No conditions returned false (including the absence of any conditions).
            return null;
        }
 
        internal static bool TryEvaluateSkipConditions(ITestFrameworkDiscoveryOptions discoveryOptions, IMessageSink diagnosticMessageSink, ITestMethod testMethod, object[] conditionArguments, out string skipReason, out ExecutionErrorTestCase errorTestCase)
        {
            skipReason = null;
            errorTestCase = null;
            try
            {
                skipReason = EvaluateSkipConditions(testMethod, conditionArguments);
                return true;
            }
            catch (ConditionalDiscovererException e)
            {
                errorTestCase = new ExecutionErrorTestCase(
                    diagnosticMessageSink,
                    discoveryOptions.MethodDisplayOrDefault(),
                    discoveryOptions.MethodDisplayOptionsOrDefault(),
                    testMethod,
                    e.Message);
                return false;
            }
        }
 
        internal static string GetFailedLookupString(string name, Type type)
        {
            return
                $"An appropriate member '{name}' could not be found. " +
                $"The conditional method needs to be a static method, property, or field on the type {type} or any ancestor, " +
                "of any visibility, accepting zero arguments, and having a return type of Boolean.";
        }
        
        internal static Func<bool> LookupConditionalMember(Type t, string name)
        {
            if (t == null || name == null)
                return null;
 
            TypeInfo ti = t.GetTypeInfo();
 
            MethodInfo mi = ti.GetDeclaredMethod(name);
            if (mi != null && mi.IsStatic && mi.GetParameters().Length == 0 && mi.ReturnType == typeof(bool))
                return () => (bool)mi.Invoke(null, null);
 
            PropertyInfo pi = ti.GetDeclaredProperty(name);
            if (pi != null && pi.PropertyType == typeof(bool) && pi.GetMethod != null && pi.GetMethod.IsStatic && pi.GetMethod.GetParameters().Length == 0)
                return () => (bool)pi.GetValue(null);
 
            FieldInfo fi = ti.GetDeclaredField(name);
            if (fi != null && fi.FieldType == typeof(bool) && fi.IsStatic)
                return () => (bool)fi.GetValue(null);
 
            return LookupConditionalMember(ti.BaseType, name);
        }
 
        internal static bool CheckInputToSkipExecution(object[] conditionArguments, ref Type calleeType, ref string[] conditionMemberNames, ITestMethod testMethod = null)
        {
            // A null or empty list of conditionArguments is treated as "no conditions".
            // and the test cases will be executed.
            // Example: [ConditionalClass()]
            if (conditionArguments == null || conditionArguments.Length == 0) return true;
 
            calleeType = conditionArguments[0] as Type;
            if (calleeType != null)
            {
                if (conditionArguments.Length < 2)
                {
                    // [ConditionalFact(typeof(x))] no provided methods.
                    return true;
                }
 
                // [ConditionalFact(typeof(x), "MethodName")]
                conditionMemberNames = conditionArguments[1] as string[];
            }
            else
            {
                // For [ConditionalClass], unable to get the Type info. All test cases will be executed.
                if (testMethod == null) return true;
 
                // [ConditionalFact("MethodName")]
                conditionMemberNames = conditionArguments[0] as string[];
            }
 
            // [ConditionalFact((string[]) null)]
            if (conditionMemberNames == null || conditionMemberNames.Count() == 0) return true;
 
            return false;
        }
    }
}