зеркало из https://github.com/microsoft/testfx.git
Display fixture (Assembly/Class Initialize/Cleanup) methods as "test" entries (#2904)
Co-authored-by: Amaury Levé <amauryleve@microsoft.com>
This commit is contained in:
Родитель
a1e4196629
Коммит
86489a221a
|
@ -201,6 +201,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "samples\Playg
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTest.Acceptance.IntegrationTests", "test\IntegrationTests\MSTest.Acceptance.IntegrationTests\MSTest.Acceptance.IntegrationTests.csproj", "{BCB42780-C559-40B6-8C4A-85EBC464AAA8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FixturesTestProject", "test\IntegrationTests\TestAssets\FixturesTestProject\FixturesTestProject.csproj", "{A7D0995D-0516-4975-ABBD-EB93E1B79292}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -427,6 +429,10 @@ Global
|
|||
{BCB42780-C559-40B6-8C4A-85EBC464AAA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BCB42780-C559-40B6-8C4A-85EBC464AAA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BCB42780-C559-40B6-8C4A-85EBC464AAA8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A7D0995D-0516-4975-ABBD-EB93E1B79292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7D0995D-0516-4975-ABBD-EB93E1B79292}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7D0995D-0516-4975-ABBD-EB93E1B79292}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7D0995D-0516-4975-ABBD-EB93E1B79292}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -503,6 +509,7 @@ Global
|
|||
{7E9D98E7-733C-4D6B-A5AC-087D588A40ED} = {CB0CC552-2017-40C0-934A-C8A3B00EF650}
|
||||
{8A41B37E-0732-4F28-B214-A44233B447FE} = {92F8E9A2-903E-4025-99BC-7DC478D5466D}
|
||||
{BCB42780-C559-40B6-8C4A-85EBC464AAA8} = {FF69998C-C661-4EF0-804B-845675B3602E}
|
||||
{A7D0995D-0516-4975-ABBD-EB93E1B79292} = {C9F82701-0E0F-4E61-B05B-AE387E7631F6}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {31E0F4D5-975A-41CC-933E-545B2201FAF9}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||
|
@ -10,6 +10,36 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
|
|||
/// </summary>
|
||||
internal static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// The 3rd level entry (class) name in the hierarchy array.
|
||||
/// </summary>
|
||||
internal const string AssemblyFixturesHierarchyClassName = "[Assembly]";
|
||||
|
||||
/// <summary>
|
||||
/// Discover fixtures or not.
|
||||
/// </summary>
|
||||
internal const string FixturesTestTrait = "FixturesTrait";
|
||||
|
||||
/// <summary>
|
||||
/// Assembly initialize.
|
||||
/// </summary>
|
||||
internal const string AssemblyInitializeFixtureTrait = "AssemblyInitialize";
|
||||
|
||||
/// <summary>
|
||||
/// Assembly cleanup.
|
||||
/// </summary>
|
||||
internal const string AssemblyCleanupFixtureTrait = "AssemblyCleanup";
|
||||
|
||||
/// <summary>
|
||||
/// Class initialize.
|
||||
/// </summary>
|
||||
internal const string ClassInitializeFixtureTrait = "ClassInitialize";
|
||||
|
||||
/// <summary>
|
||||
/// Class cleanup.
|
||||
/// </summary>
|
||||
internal const string ClassCleanupFixtureTrait = "ClassCleanup";
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the MSTest executor.
|
||||
/// </summary>
|
||||
|
|
|
@ -11,6 +11,7 @@ using System.Text;
|
|||
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
|
||||
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
|
||||
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using FrameworkITestDataSource = Microsoft.VisualStudio.TestTools.UnitTesting.ITestDataSource;
|
||||
|
@ -44,12 +45,10 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
/// </summary>
|
||||
/// <param name="settings">The settings for the session.</param>
|
||||
/// <remarks>Use this constructor when creating this object in a new app domain so the settings for this app domain are set.</remarks>
|
||||
public AssemblyEnumerator(MSTestSettings settings)
|
||||
{
|
||||
public AssemblyEnumerator(MSTestSettings settings) =>
|
||||
// Populate the settings into the domain(Desktop workflow) performing discovery.
|
||||
// This would just be resetting the settings to itself in non desktop workflows.
|
||||
MSTestSettings.PopulateSettings(settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the run settings to use for current discovery session.
|
||||
|
@ -77,9 +76,10 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
internal ICollection<UnitTestElement> EnumerateAssembly(string assemblyFileName, out ICollection<string> warnings)
|
||||
{
|
||||
DebugEx.Assert(!StringEx.IsNullOrWhiteSpace(assemblyFileName), "Invalid assembly file name.");
|
||||
|
||||
var warningMessages = new List<string>();
|
||||
var tests = new List<UnitTestElement>();
|
||||
// Contains list of assembly/class names for which we have already added fixture tests.
|
||||
var fixturesTests = new HashSet<string>();
|
||||
|
||||
Assembly assembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(assemblyFileName, isReflectionOnly: false);
|
||||
|
||||
|
@ -109,7 +109,7 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
}
|
||||
|
||||
List<UnitTestElement> testsInType = DiscoverTestsInType(assemblyFileName, RunSettingsXml, type, warningMessages, discoverInternals,
|
||||
testDataSourceDiscovery, testIdGenerationStrategy);
|
||||
testDataSourceDiscovery, testIdGenerationStrategy, fixturesTests);
|
||||
tests.AddRange(testsInType);
|
||||
}
|
||||
|
||||
|
@ -211,7 +211,8 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
List<string> warningMessages,
|
||||
bool discoverInternals,
|
||||
TestDataSourceDiscoveryOption discoveryOption,
|
||||
TestIdGenerationStrategy testIdGenerationStrategy)
|
||||
TestIdGenerationStrategy testIdGenerationStrategy,
|
||||
HashSet<string> fixturesTests)
|
||||
{
|
||||
IDictionary<string, object> tempSourceLevelParameters = PlatformServiceProvider.Instance.SettingsProvider.GetProperties(assemblyFileName);
|
||||
tempSourceLevelParameters = RunSettingsUtilities.GetTestRunParameters(runSettingsXml)?.ConcatWithOverwrites(tempSourceLevelParameters)
|
||||
|
@ -235,7 +236,15 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
{
|
||||
if (discoveryOption == TestDataSourceDiscoveryOption.DuringDiscovery)
|
||||
{
|
||||
if (DynamicDataAttached(sourceLevelParameters, test, tests))
|
||||
Lazy<TestMethodInfo?> testMethodInfo = GetTestMethodInfo(sourceLevelParameters, test);
|
||||
|
||||
// Add fixture tests like AssemblyInitialize, AssemblyCleanup, ClassInitialize, ClassCleanup.
|
||||
if (MSTestSettings.CurrentSettings.ConsiderFixturesAsSpecialTests && testMethodInfo.Value is not null)
|
||||
{
|
||||
AddFixtureTests(testMethodInfo.Value, tests, fixturesTests);
|
||||
}
|
||||
|
||||
if (DynamicDataAttached(test, testMethodInfo, tests))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -257,7 +266,18 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
return tests;
|
||||
}
|
||||
|
||||
private bool DynamicDataAttached(IDictionary<string, object?> sourceLevelParameters, UnitTestElement test, List<UnitTestElement> tests)
|
||||
private Lazy<TestMethodInfo?> GetTestMethodInfo(IDictionary<string, object?> sourceLevelParameters, UnitTestElement test) =>
|
||||
new(() =>
|
||||
{
|
||||
// NOTE: From this place we don't have any path that would let the user write a message on the TestContext and we don't do
|
||||
// anything with what would be printed anyway so we can simply use a simple StringWriter.
|
||||
using var writer = new StringWriter();
|
||||
TestMethod testMethod = test.TestMethod;
|
||||
MSTestAdapter.PlatformServices.Interface.ITestContext testContext = PlatformServiceProvider.Instance.GetTestContext(testMethod, writer, sourceLevelParameters);
|
||||
return _typeCache.GetTestMethodInfo(testMethod, testContext, MSTestSettings.CurrentSettings.CaptureDebugTraces);
|
||||
});
|
||||
|
||||
private static bool DynamicDataAttached(UnitTestElement test, Lazy<TestMethodInfo?> testMethodInfo, List<UnitTestElement> tests)
|
||||
{
|
||||
// It should always be `true`, but if any part of the chain is obsolete; it might not contain those.
|
||||
// Since we depend on those properties, if they don't exist, we bail out early.
|
||||
|
@ -278,13 +298,90 @@ internal class AssemblyEnumerator : MarshalByRefObject
|
|||
// If you remove this line and acceptance tests still pass you are okay.
|
||||
test.TestMethod.DataType = DynamicDataType.None;
|
||||
|
||||
// NOTE: From this place we don't have any path that would let the user write a message on the TestContext and we don't do
|
||||
// anything with what would be printed anyway so we can simply use a simple StringWriter.
|
||||
using var writer = new StringWriter();
|
||||
TestMethod testMethod = test.TestMethod;
|
||||
MSTestAdapter.PlatformServices.Interface.ITestContext testContext = PlatformServiceProvider.Instance.GetTestContext(testMethod, writer, sourceLevelParameters);
|
||||
TestMethodInfo? testMethodInfo = _typeCache.GetTestMethodInfo(testMethod, testContext, MSTestSettings.CurrentSettings.CaptureDebugTraces);
|
||||
return testMethodInfo != null && TryProcessTestDataSourceTests(test, testMethodInfo, tests);
|
||||
return testMethodInfo.Value != null && TryProcessTestDataSourceTests(test, testMethodInfo.Value, tests);
|
||||
}
|
||||
|
||||
private static void AddFixtureTests(TestMethodInfo testMethodInfo, List<UnitTestElement> tests, HashSet<string> fixtureTests)
|
||||
{
|
||||
string assemblyName = testMethodInfo.Parent.Parent.Assembly.GetName().Name!;
|
||||
string assemblyLocation = testMethodInfo.Parent.Parent.Assembly.Location;
|
||||
string className = testMethodInfo.Parent.ClassType.Name;
|
||||
string classFullName = testMethodInfo.Parent.ClassType.FullName!;
|
||||
|
||||
// Check if fixtures for this assembly has already been added.
|
||||
if (!fixtureTests.Contains(assemblyLocation))
|
||||
{
|
||||
_ = fixtureTests.Add(assemblyLocation);
|
||||
|
||||
// Add AssemblyInitialize and AssemblyCleanup fixture tests if they exist.
|
||||
if (testMethodInfo.Parent.Parent.AssemblyInitializeMethod is not null)
|
||||
{
|
||||
tests.Add(GetAssemblyFixtureTest(testMethodInfo.Parent.Parent.AssemblyInitializeMethod, assemblyName, className,
|
||||
classFullName, assemblyLocation, Constants.AssemblyInitializeFixtureTrait));
|
||||
}
|
||||
|
||||
if (testMethodInfo.Parent.Parent.AssemblyCleanupMethod is not null)
|
||||
{
|
||||
tests.Add(GetAssemblyFixtureTest(testMethodInfo.Parent.Parent.AssemblyCleanupMethod, assemblyName, className,
|
||||
classFullName, assemblyLocation, Constants.AssemblyCleanupFixtureTrait));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if fixtures for this class has already been added.
|
||||
if (!fixtureTests.Contains(assemblyLocation + classFullName))
|
||||
{
|
||||
_ = fixtureTests.Add(assemblyLocation + classFullName);
|
||||
|
||||
// Add ClassInitialize and ClassCleanup fixture tests if they exist.
|
||||
if (testMethodInfo.Parent.ClassInitializeMethod is not null)
|
||||
{
|
||||
tests.Add(GetClassFixtureTest(testMethodInfo.Parent.ClassInitializeMethod, assemblyName, className, classFullName,
|
||||
assemblyLocation, Constants.ClassInitializeFixtureTrait));
|
||||
}
|
||||
|
||||
if (testMethodInfo.Parent.ClassCleanupMethod is not null)
|
||||
{
|
||||
tests.Add(GetClassFixtureTest(testMethodInfo.Parent.ClassCleanupMethod, assemblyName, className, classFullName,
|
||||
assemblyLocation, Constants.ClassCleanupFixtureTrait));
|
||||
}
|
||||
}
|
||||
|
||||
static UnitTestElement GetAssemblyFixtureTest(MethodInfo methodInfo, string assemblyName, string className, string classFullName,
|
||||
string assemblyLocation, string fixtureType)
|
||||
{
|
||||
string methodName = GetMethodName(methodInfo);
|
||||
string[] hierarchy = [null!, assemblyName, Constants.AssemblyFixturesHierarchyClassName, methodName];
|
||||
return GetFixtureTest(classFullName, assemblyLocation, fixtureType, methodName, hierarchy);
|
||||
}
|
||||
|
||||
static UnitTestElement GetClassFixtureTest(MethodInfo methodInfo, string assemblyName, string className, string classFullName,
|
||||
string assemblyLocation, string fixtureType)
|
||||
{
|
||||
string methodName = GetMethodName(methodInfo);
|
||||
string[] hierarchy = [null!, classFullName, methodName];
|
||||
return GetFixtureTest(classFullName, assemblyLocation, fixtureType, methodName, hierarchy);
|
||||
}
|
||||
|
||||
static string GetMethodName(MethodInfo methodInfo)
|
||||
{
|
||||
ParameterInfo[] args = methodInfo.GetParameters();
|
||||
return args.Length > 0
|
||||
? $"{methodInfo.Name}({string.Join(",", args.Select(a => a.ParameterType.FullName))})"
|
||||
: methodInfo.Name;
|
||||
}
|
||||
|
||||
static UnitTestElement GetFixtureTest(string classFullName, string assemblyLocation, string fixtureType, string methodName, string[] hierarchy)
|
||||
{
|
||||
var method = new TestMethod(classFullName, methodName,
|
||||
hierarchy, methodName, classFullName, assemblyLocation, false,
|
||||
TestIdGenerationStrategy.FullyQualified);
|
||||
return new UnitTestElement(method)
|
||||
{
|
||||
DisplayName = $"[{fixtureType}] {methodName}",
|
||||
Ignored = true,
|
||||
Traits = [new Trait(Constants.FixturesTestTrait, fixtureType)],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryProcessTestDataSourceTests(UnitTestElement test, TestMethodInfo testMethodInfo, List<UnitTestElement> tests)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
|
||||
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
@ -90,6 +91,8 @@ internal class UnitTestDiscoverer
|
|||
internal void SendTestCases(string source, IEnumerable<UnitTestElement> testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IMessageLogger logger)
|
||||
{
|
||||
bool shouldCollectSourceInformation = MSTestSettings.RunConfigurationSettings.CollectSourceInformation;
|
||||
bool hasAnyRunnableTests = false;
|
||||
var fixtureTests = new List<TestCase>();
|
||||
|
||||
var navigationSessions = new Dictionary<string, object?>();
|
||||
try
|
||||
|
@ -109,13 +112,25 @@ internal class UnitTestDiscoverer
|
|||
foreach (UnitTestElement testElement in testElements)
|
||||
{
|
||||
var testCase = testElement.ToTestCase();
|
||||
bool hasFixtureTraits = testCase.Traits.Any(t => t.Name == Constants.FixturesTestTrait);
|
||||
|
||||
// Filter tests based on test case filters
|
||||
if (filterExpression != null && !filterExpression.MatchTestCase(testCase, (p) => TestMethodFilter.PropertyValueProvider(testCase, p)))
|
||||
{
|
||||
// If test is a fixture test, add it to the list of fixture tests.
|
||||
if (hasFixtureTraits)
|
||||
{
|
||||
fixtureTests.Add(testCase);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasAnyRunnableTests)
|
||||
{
|
||||
hasAnyRunnableTests = !hasFixtureTraits;
|
||||
}
|
||||
|
||||
if (!shouldCollectSourceInformation)
|
||||
{
|
||||
discoverySink.SendTestCase(testCase);
|
||||
|
@ -165,6 +180,18 @@ internal class UnitTestDiscoverer
|
|||
|
||||
discoverySink.SendTestCase(testCase);
|
||||
}
|
||||
|
||||
// If there are runnable tests, then add all fixture tests to the discovery sink.
|
||||
// Scenarios:
|
||||
// 1. Execute only a fixture test => In this case, we do not need to track any other fixture tests. Selected fixture test will be tracked as will be marked as skipped.
|
||||
// 2. Execute a runnable test => In this case, case add all fixture tests. We will update status of only those fixtures which are triggered by the selected test.
|
||||
if (hasAnyRunnableTests)
|
||||
{
|
||||
foreach (TestCase testCase in fixtureTests)
|
||||
{
|
||||
discoverySink.SendTestCase(testCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -95,6 +95,11 @@ public class TestAssemblyInfo
|
|||
/// </summary>
|
||||
public Exception? AssemblyInitializationException { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly cleanup exception.
|
||||
/// </summary>
|
||||
internal Exception? AssemblyCleanupException { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this assembly has an executable <c>AssemblyCleanup</c> method.
|
||||
/// </summary>
|
||||
|
@ -210,12 +215,11 @@ public class TestAssemblyInfo
|
|||
return null;
|
||||
}
|
||||
|
||||
Exception? assemblyCleanupException;
|
||||
lock (_assemblyInfoExecuteSyncObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
assemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
|
||||
AssemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
|
||||
() => AssemblyCleanupMethod.InvokeAsSynchronousTask(null),
|
||||
new CancellationTokenSource(),
|
||||
AssemblyCleanupMethodTimeoutMilliseconds,
|
||||
|
@ -226,17 +230,17 @@ public class TestAssemblyInfo
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
assemblyCleanupException = ex;
|
||||
AssemblyCleanupException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
// If assemblyCleanup was successful, then don't do anything
|
||||
if (assemblyCleanupException is null)
|
||||
if (AssemblyCleanupException is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Exception realException = assemblyCleanupException.GetRealException();
|
||||
Exception realException = AssemblyCleanupException.GetRealException();
|
||||
|
||||
// special case AssertFailedException to trim off part of the stack trace
|
||||
string errorMessage = realException is AssertFailedException or AssertInconclusiveException
|
||||
|
@ -267,12 +271,11 @@ public class TestAssemblyInfo
|
|||
return;
|
||||
}
|
||||
|
||||
Exception? assemblyCleanupException;
|
||||
lock (_assemblyInfoExecuteSyncObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
assemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
|
||||
AssemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation(
|
||||
() => AssemblyCleanupMethod.InvokeAsSynchronousTask(null),
|
||||
new CancellationTokenSource(),
|
||||
AssemblyCleanupMethodTimeoutMilliseconds,
|
||||
|
@ -283,23 +286,23 @@ public class TestAssemblyInfo
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
assemblyCleanupException = ex;
|
||||
AssemblyCleanupException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
// If assemblyCleanup was successful, then don't do anything
|
||||
if (assemblyCleanupException is null)
|
||||
if (AssemblyCleanupException is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the exception is already a `TestFailedException` we throw it as-is
|
||||
if (assemblyCleanupException is TestFailedException)
|
||||
if (AssemblyCleanupException is TestFailedException)
|
||||
{
|
||||
throw assemblyCleanupException;
|
||||
throw AssemblyCleanupException;
|
||||
}
|
||||
|
||||
Exception realException = assemblyCleanupException.GetRealException();
|
||||
Exception realException = AssemblyCleanupException.GetRealException();
|
||||
|
||||
// special case AssertFailedException to trim off part of the stack trace
|
||||
string errorMessage = realException is AssertFailedException or AssertInconclusiveException
|
||||
|
|
|
@ -196,6 +196,12 @@ public class TestExecutionManager
|
|||
if (filterExpression != null
|
||||
&& !filterExpression.MatchTestCase(test, p => testMethodFilter.PropertyValueProvider(test, p)))
|
||||
{
|
||||
// If this is a fixture test, return true. Fixture tests are not filtered out and are always available for the status.
|
||||
if (test.Traits.Any(t => t.Name == Constants.FixturesTestTrait))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip test if not fitting filter criteria.
|
||||
return false;
|
||||
}
|
||||
|
@ -375,6 +381,9 @@ public class TestExecutionManager
|
|||
IDictionary<string, object> sourceLevelParameters,
|
||||
UnitTestRunner testRunner)
|
||||
{
|
||||
bool hasAnyRunnableTests = false;
|
||||
var fixtureTests = new List<TestCase>();
|
||||
|
||||
foreach (TestCase currentTest in tests)
|
||||
{
|
||||
if (_cancellationToken is { Canceled: true })
|
||||
|
@ -382,6 +391,15 @@ public class TestExecutionManager
|
|||
break;
|
||||
}
|
||||
|
||||
// If it is a fixture test, add it to the list of fixture tests and do not execute it.
|
||||
// It is executed by test itself.
|
||||
if (currentTest.Traits.Any(t => t.Name == Constants.FixturesTestTrait))
|
||||
{
|
||||
fixtureTests.Add(currentTest);
|
||||
continue;
|
||||
}
|
||||
|
||||
hasAnyRunnableTests = true;
|
||||
var unitTestElement = currentTest.ToUnitTestElement(source);
|
||||
|
||||
testExecutionRecorder.RecordStart(currentTest);
|
||||
|
@ -401,6 +419,30 @@ public class TestExecutionManager
|
|||
|
||||
SendTestResults(currentTest, unitTestResult, startTime, endTime, testExecutionRecorder);
|
||||
}
|
||||
|
||||
// Once all tests have been executed, update the status of fixture tests.
|
||||
foreach (TestCase currentTest in fixtureTests)
|
||||
{
|
||||
testExecutionRecorder.RecordStart(currentTest);
|
||||
|
||||
// If there were only fixture tests, send an inconclusive result.
|
||||
if (!hasAnyRunnableTests)
|
||||
{
|
||||
var result = new UnitTestResult(ObjectModel.UnitTestOutcome.Inconclusive, null);
|
||||
SendTestResults(currentTest, [result], DateTimeOffset.Now, DateTimeOffset.Now, testExecutionRecorder);
|
||||
continue;
|
||||
}
|
||||
|
||||
Trait trait = currentTest.Traits.First(t => t.Name == Constants.FixturesTestTrait);
|
||||
var unitTestElement = currentTest.ToUnitTestElement(source);
|
||||
FixtureTestResult fixtureTestResult = testRunner.GetFixtureTestResult(unitTestElement.TestMethod, trait.Value);
|
||||
|
||||
if (fixtureTestResult.IsExecuted)
|
||||
{
|
||||
var result = new UnitTestResult(fixtureTestResult.Outcome, null);
|
||||
SendTestResults(currentTest, [result], DateTimeOffset.Now, DateTimeOffset.Now, testExecutionRecorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -23,6 +23,8 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
|
|||
/// </summary>
|
||||
internal class UnitTestRunner : MarshalByRefObject
|
||||
{
|
||||
private readonly Dictionary<string, TestMethodInfo> _fixtureTests = new();
|
||||
|
||||
/// <summary>
|
||||
/// Type cache.
|
||||
/// </summary>
|
||||
|
@ -97,6 +99,44 @@ internal class UnitTestRunner : MarshalByRefObject
|
|||
_reflectHelper);
|
||||
}
|
||||
|
||||
internal FixtureTestResult GetFixtureTestResult(TestMethod testMethod, string fixtureType)
|
||||
{
|
||||
// For the fixture methods, we need to return the appropriate result.
|
||||
// Get matching testMethodInfo from the cache and return UnitTestOutcome for the fixture test.
|
||||
if (_fixtureTests.TryGetValue(testMethod.AssemblyName + testMethod.FullClassName, out TestMethodInfo? testMethodInfo))
|
||||
{
|
||||
if (fixtureType == Constants.ClassInitializeFixtureTrait)
|
||||
{
|
||||
return testMethodInfo.Parent.IsClassInitializeExecuted
|
||||
? new(true, GetOutcome(testMethodInfo.Parent.ClassInitializationException), testMethodInfo.Parent.ClassInitializationException?.Message)
|
||||
: new(true, ObjectModel.UnitTestOutcome.Inconclusive, null);
|
||||
}
|
||||
|
||||
if (fixtureType == Constants.ClassCleanupFixtureTrait)
|
||||
{
|
||||
return testMethodInfo.Parent.IsClassInitializeExecuted
|
||||
? new(testMethodInfo.Parent.IsClassInitializeExecuted, GetOutcome(testMethodInfo.Parent.ClassCleanupException), testMethodInfo.Parent.ClassCleanupException?.Message)
|
||||
: new(true, ObjectModel.UnitTestOutcome.Inconclusive, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (_fixtureTests.TryGetValue(testMethod.AssemblyName, out testMethodInfo))
|
||||
{
|
||||
if (fixtureType == Constants.AssemblyInitializeFixtureTrait)
|
||||
{
|
||||
return new(true, GetOutcome(testMethodInfo.Parent.Parent.AssemblyInitializationException), testMethodInfo.Parent.Parent.AssemblyInitializationException?.Message);
|
||||
}
|
||||
else if (fixtureType == Constants.AssemblyCleanupFixtureTrait)
|
||||
{
|
||||
return new(true, GetOutcome(testMethodInfo.Parent.Parent.AssemblyCleanupException), testMethodInfo.Parent.Parent.AssemblyInitializationException?.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return new(false, ObjectModel.UnitTestOutcome.Inconclusive, null);
|
||||
|
||||
static ObjectModel.UnitTestOutcome GetOutcome(Exception? exception) => exception == null ? ObjectModel.UnitTestOutcome.Passed : ObjectModel.UnitTestOutcome.Failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a single test.
|
||||
/// </summary>
|
||||
|
@ -141,6 +181,11 @@ internal class UnitTestRunner : MarshalByRefObject
|
|||
}
|
||||
|
||||
DebugEx.Assert(testMethodInfo is not null, "testMethodInfo should not be null.");
|
||||
|
||||
// Keep track of all non-runnable methods so that we can return the appropriate result at the end.
|
||||
_fixtureTests[testMethod.AssemblyName] = testMethodInfo;
|
||||
_fixtureTests[testMethod.AssemblyName + testMethod.FullClassName] = testMethodInfo;
|
||||
|
||||
var testMethodRunner = new TestMethodRunner(testMethodInfo, testMethod, testContext, MSTestSettings.CurrentSettings.CaptureDebugTraces);
|
||||
UnitTestResult[] result = testMethodRunner.Execute();
|
||||
RunRequiredCleanups(testContext, testMethodInfo, testMethod, result);
|
||||
|
@ -293,8 +338,9 @@ internal class UnitTestRunner : MarshalByRefObject
|
|||
ClassCleanupBehavior lifecycleFromAssembly,
|
||||
ReflectHelper reflectHelper)
|
||||
{
|
||||
IEnumerable<UnitTestElement> runnableTests = testsToRun.Where(t => t.Traits is null || !t.Traits.Any(t => t.Name == Constants.FixturesTestTrait));
|
||||
_remainingTestsByClass =
|
||||
new(testsToRun.GroupBy(t => t.TestMethod.FullClassName)
|
||||
new(runnableTests.GroupBy(t => t.TestMethod.FullClassName)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => new HashSet<string>(g.Select(t => t.TestMethod.UniqueName))));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<Project>
|
||||
<Project>
|
||||
|
||||
<!-- SDK top import -->
|
||||
<Import Project="Sdk.props" Sdk="MSBuild.Sdk.Extras" Condition=" '$(OS)' == 'Windows_NT' " />
|
||||
|
@ -97,6 +97,9 @@
|
|||
<Compile Include="$(Compile);$(IntermediateOutputPath)/MSTestVersion.cs" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Discovery\FixturesVisibility.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Version templating -->
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
@ -191,6 +191,13 @@ public class MSTestSettings
|
|||
/// </summary>
|
||||
public bool TreatClassAndAssemblyCleanupWarningsAsErrors { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether AssemblyInitialize, AssemblyCleanup, ClassInitialize and ClassCleanup methods are
|
||||
/// reported as special tests (cannot be executed). When this feature is enabled, these methods will be reported as
|
||||
/// separate entries in the TRX reports, in Test Explorer or in CLI.
|
||||
/// </summary>
|
||||
internal bool ConsiderFixturesAsSpecialTests { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Populate settings based on existing settings object.
|
||||
/// </summary>
|
||||
|
@ -222,6 +229,7 @@ public class MSTestSettings
|
|||
CurrentSettings.ClassCleanupTimeout = settings.ClassCleanupTimeout;
|
||||
CurrentSettings.TestInitializeTimeout = settings.TestInitializeTimeout;
|
||||
CurrentSettings.TestCleanupTimeout = settings.TestCleanupTimeout;
|
||||
CurrentSettings.ConsiderFixturesAsSpecialTests = settings.ConsiderFixturesAsSpecialTests;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -553,6 +561,16 @@ public class MSTestSettings
|
|||
break;
|
||||
}
|
||||
|
||||
case "CONSIDERFIXTURESASSPECIALTESTS":
|
||||
{
|
||||
if (bool.TryParse(reader.ReadInnerXml(), out result))
|
||||
{
|
||||
settings.ConsiderFixturesAsSpecialTests = result;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
PlatformServiceProvider.Instance.SettingsProvider.Load(reader.ReadSubtree());
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
|
||||
|
||||
[Serializable]
|
||||
internal sealed class FixtureTestResult
|
||||
{
|
||||
internal FixtureTestResult(bool isExecuted, UnitTestOutcome outcome, string? exceptionMessage)
|
||||
{
|
||||
IsExecuted = isExecuted;
|
||||
Outcome = outcome;
|
||||
ExceptionMessage = exceptionMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the test is executed or not.
|
||||
/// </summary>
|
||||
public bool IsExecuted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the outcome of the test.
|
||||
/// </summary>
|
||||
public UnitTestOutcome Outcome { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exception message if any.
|
||||
/// </summary>
|
||||
public string? ExceptionMessage { get; }
|
||||
}
|
|
@ -58,6 +58,7 @@ public class MSTestAdapterSettings
|
|||
// <MSTestV2>
|
||||
// <DeploymentEnabled>true</DeploymentEnabled>
|
||||
// <DeployTestSourceDependencies>true</DeployTestSourceDependencies>
|
||||
// <ConsiderFixturesAsSpecialTests>true</ConsiderFixturesAsSpecialTests>
|
||||
// <DeleteDeploymentDirectoryAfterTestRunIsComplete>true</DeleteDeploymentDirectoryAfterTestRunIsComplete>
|
||||
// <AssemblyResolution>
|
||||
// <Directory path= "% HOMEDRIVE %\directory "includeSubDirectories = "true" />
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.MSTestV2.CLIAutomation;
|
||||
|
||||
namespace MSTest.VstestConsoleWrapper.IntegrationTests;
|
||||
|
||||
public class FixturesTests : CLITestBase
|
||||
{
|
||||
private const string AssetName = "FixturesTestProject";
|
||||
|
||||
private static readonly string AssemblyInitialize = "FixturesTestProject1.UnitTest1.AssemblyInitialize(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext)";
|
||||
private static readonly string AssemblyCleanup = "FixturesTestProject1.UnitTest1.AssemblyCleanup";
|
||||
private static readonly string ClassInitialize = "FixturesTestProject1.UnitTest1.ClassInitialize(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext)";
|
||||
private static readonly string ClassCleanup = "FixturesTestProject1.UnitTest1.ClassCleanup";
|
||||
private static readonly string TestMethod = "FixturesTestProject1.UnitTest1.Test";
|
||||
private static readonly string PassingTest = "FixturesTestProject1.UnitTest1.PassingTest";
|
||||
|
||||
private readonly string[] _tests = new[]
|
||||
{
|
||||
AssemblyInitialize,
|
||||
AssemblyCleanup,
|
||||
ClassInitialize,
|
||||
ClassCleanup,
|
||||
TestMethod,
|
||||
PassingTest,
|
||||
};
|
||||
|
||||
public void FixturesDisabled_DoesNotReport_FixtureTests()
|
||||
{
|
||||
string runSettings = GetRunSettings(false, true, true, true, true, true);
|
||||
|
||||
// Discover tests,
|
||||
InvokeVsTestForDiscovery([AssetName], runSettings);
|
||||
ValidateDiscoveredTests([TestMethod, PassingTest]);
|
||||
|
||||
// Tests,
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
ValidatePassedTests([TestMethod, PassingTest]);
|
||||
}
|
||||
|
||||
public void FixturesEnabled_DoesReport_FixtureTests()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, true, true, true);
|
||||
|
||||
// Discover tests,
|
||||
InvokeVsTestForDiscovery([AssetName], runSettings);
|
||||
|
||||
ValidateDiscoveredTests(_tests);
|
||||
|
||||
// Run tests,
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
|
||||
ValidatePassedTests(_tests);
|
||||
}
|
||||
|
||||
public void AssemblyInitialize_Fails_TestMethod_Class_Skipped()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, false, true, true, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
|
||||
ValidatePassedTests([AssemblyCleanup]);
|
||||
ValidateFailedTests(false, [AssemblyInitialize, TestMethod, PassingTest]);
|
||||
ValidateSkippedTests([ClassInitialize, ClassCleanup]);
|
||||
}
|
||||
|
||||
public void AssemblyCleanup_OnlyFails_AssemblyCleanup()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, false, true, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
ValidatePassedTests([AssemblyInitialize, ClassInitialize, ClassCleanup, PassingTest]);
|
||||
// TestMethod fails because AssemblyCleanup is executed after it and hence it fails.
|
||||
ValidateFailedTests(false, [AssemblyCleanup, TestMethod]);
|
||||
}
|
||||
|
||||
public void ClassInitialize_OnlyFails_ClassInitialize()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, false, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
ValidateFailedTests(false, [ClassInitialize, TestMethod, PassingTest]);
|
||||
ValidatePassedTests([AssemblyInitialize, AssemblyCleanup, ClassCleanup]);
|
||||
}
|
||||
|
||||
public void ClassCleanup_OnlyFails_ClassCleanup()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, true, false, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings);
|
||||
ValidatePassedTests([AssemblyInitialize, AssemblyCleanup, ClassInitialize, PassingTest]);
|
||||
// TestMethod fails because ClassCleanup is executed after it and hence it fails.
|
||||
ValidateFailedTests(false, [ClassCleanup, TestMethod]);
|
||||
}
|
||||
|
||||
public void RunOnlyFixtures_DoesNot_Run_Fixtures()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, true, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(AssemblyInitialize));
|
||||
ValidateSkippedTests(AssemblyInitialize);
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(AssemblyCleanup));
|
||||
ValidateSkippedTests(AssemblyCleanup);
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(ClassInitialize));
|
||||
ValidateSkippedTests(ClassInitialize);
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(ClassCleanup));
|
||||
ValidateSkippedTests(ClassCleanup);
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: "ClassCleanup|AssemblyCleanup");
|
||||
ValidateSkippedTests([AssemblyCleanup, ClassCleanup]);
|
||||
}
|
||||
|
||||
public void RunSingleTest_Runs_Assembly_And_Class_Fixtures()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, true, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(PassingTest));
|
||||
ValidatePassedTests([AssemblyInitialize, AssemblyCleanup, ClassInitialize, ClassCleanup, PassingTest]);
|
||||
}
|
||||
|
||||
public void RunSingleTest_AssemblyInitialize_Failure_Skips_ClassFixtures()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, false, true, true, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(PassingTest));
|
||||
ValidateFailedTests(false, [AssemblyInitialize, PassingTest]);
|
||||
ValidatePassedTests([AssemblyCleanup]);
|
||||
// Class fixtures are not executed if AssemblyInitialize fails.
|
||||
ValidateSkippedTests([ClassInitialize, ClassCleanup]);
|
||||
}
|
||||
|
||||
public void RunSingleTest_ClassInitialize_Failure_Runs_AssemblyFixtures()
|
||||
{
|
||||
string runSettings = GetRunSettings(true, true, true, false, true, true);
|
||||
|
||||
InvokeVsTestForExecution([AssetName], runSettings, testCaseFilter: nameof(PassingTest));
|
||||
ValidateFailedTests(false, [ClassInitialize, PassingTest]);
|
||||
ValidatePassedTests([AssemblyInitialize, AssemblyCleanup, ClassCleanup]);
|
||||
}
|
||||
|
||||
private string GetRunSettings(bool fixturesEnabled, bool assemblyInitialize, bool assemblyCleanup, bool classInitialize, bool classCleanup, bool test)
|
||||
=> $@"<?xml version=""1.0"" encoding=""utf-8""?>
|
||||
<RunSettings>
|
||||
<RunConfiguration>
|
||||
<EnvironmentVariables>
|
||||
<AssemblyInitialize>{assemblyInitialize}</AssemblyInitialize>
|
||||
<AssemblyCleanup>{assemblyCleanup}</AssemblyCleanup>
|
||||
<ClassInitialize>{classInitialize}</ClassInitialize>
|
||||
<ClassCleanup>{classCleanup}</ClassCleanup>
|
||||
<Test>{test}</Test>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
<MSTest>
|
||||
<ConsiderFixturesAsSpecialTests>{fixturesEnabled}</ConsiderFixturesAsSpecialTests>
|
||||
</MSTest>
|
||||
</RunSettings>
|
||||
";
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net462</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(RepoRoot)src\Adapter\MSTest.TestAdapter\MSTest.TestAdapter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FixturesTestProject1;
|
||||
|
||||
[TestClass]
|
||||
public class UnitTest1
|
||||
{
|
||||
[AssemblyInitialize]
|
||||
public static void AssemblyInitialize(TestContext context)
|
||||
{
|
||||
bool? condition = GetCondition("AssemblyInitialize");
|
||||
Assert.IsNotNull(condition);
|
||||
Assert.IsTrue(condition.Value);
|
||||
}
|
||||
|
||||
[AssemblyCleanup]
|
||||
public static void AssemblyCleanup()
|
||||
{
|
||||
bool? condition = GetCondition("AssemblyCleanup");
|
||||
Assert.IsNotNull(condition);
|
||||
Assert.IsTrue(condition.Value);
|
||||
}
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassInitialize(TestContext testContext)
|
||||
{
|
||||
bool? condition = GetCondition("ClassInitialize");
|
||||
Assert.IsNotNull(condition);
|
||||
Assert.IsTrue(condition.Value);
|
||||
}
|
||||
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup()
|
||||
{
|
||||
bool? condition = GetCondition("ClassCleanup");
|
||||
Assert.IsNotNull(condition);
|
||||
Assert.IsTrue(condition.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PassingTest() => Assert.IsTrue(true);
|
||||
|
||||
[TestMethod]
|
||||
public void Test()
|
||||
{
|
||||
bool? condition = GetCondition("Test");
|
||||
Assert.IsNotNull(condition);
|
||||
Assert.IsTrue(condition.Value);
|
||||
}
|
||||
|
||||
private static bool? GetCondition(string environmentVariable)
|
||||
=> bool.TryParse(Environment.GetEnvironmentVariable(environmentVariable), out bool result)
|
||||
? result
|
||||
: null;
|
||||
}
|
Загрузка…
Ссылка в новой задаче