// 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.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.Services; using Microsoft.Testing.Platform.Telemetry; using Moq; namespace Microsoft.Testing.Platform.UnitTests; [TestClass] public sealed class TestApplicationResultTests : IDisposable { private readonly TestApplicationResult _testApplicationResult = new(new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, null); public void Dispose() => _testApplicationResult.Dispose(); [TestMethod] public async Task ConsumeAsync_ExecutionCompleted_ClosesOnlyOldestActivityWithoutOutcome() { var firstActivity = new Mock(); var secondActivity = new Mock(); var otelService = new Mock(); otelService .Setup(service => service.CreateCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateUpDownCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateHistogram(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .SetupSequence(service => service.StartActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())) .Returns(firstActivity.Object) .Returns(secondActivity.Object); using TestApplicationResult testApplicationResult = new( Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), otelService.Object); static TestNode CreateNode(IProperty property) => new() { Uid = "shared-uid", DisplayName = "Test", Properties = new PropertyBag(property), }; await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(InProgressTestNodeStateProperty.CachedInstance)), CancellationToken.None); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(InProgressTestNodeStateProperty.CachedInstance)), CancellationToken.None); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(TestNodeExecutionCompletedProperty.CachedInstance)), CancellationToken.None); firstActivity.Verify(activity => activity.Dispose(), Times.Once); firstActivity.Verify(activity => activity.SetTag("test.result", It.IsAny()), Times.Never); secondActivity.Verify(activity => activity.Dispose(), Times.Never); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(PassedTestNodeStateProperty.CachedInstance)), CancellationToken.None); secondActivity.Verify(activity => activity.SetTag("test.result", "passed"), Times.Once); secondActivity.Verify(activity => activity.Dispose(), Times.Once); } [TestMethod] public async Task ConsumeAsync_SupersededRetryAttempt_CompletesAttemptSpanWithoutChangingRunStatistics() { var activity = new Mock(); activity.Setup(a => a.SetTag(It.IsAny(), It.IsAny())).Returns(activity.Object); var otelService = new Mock(); otelService .Setup(service => service.CreateCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateUpDownCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateHistogram(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.StartActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())) .Returns(activity.Object); using TestApplicationResult testApplicationResult = new( Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), otelService.Object); static TestNode CreateNode(TestNodeStateProperty state) => new() { Uid = "retry-test", DisplayName = "Retry test", Properties = new PropertyBag( state, new RetryAttemptProperty(attemptNumber: 1, isSuperseded: true)), }; await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(InProgressTestNodeStateProperty.CachedInstance)), CancellationToken.None); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(new FailedTestNodeStateProperty())), CancellationToken.None); activity.Verify(a => a.SetTag("test.case.result.status", "fail"), Times.Once); activity.Verify(a => a.Dispose(), Times.Once); Statistics statistics = testApplicationResult.GetStatistics(); Assert.AreEqual(0, statistics.TotalRanTests); Assert.AreEqual(0, statistics.TotalFailedTests); } [TestMethod] public async Task ConsumeAsync_SupersededRetryAttempt_PreservesUnnumberedSpanForFinalResult() { var activity = new Mock(); activity.Setup(a => a.SetTag(It.IsAny(), It.IsAny())).Returns(activity.Object); var otelService = new Mock(); otelService .Setup(service => service.CreateCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateUpDownCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateHistogram(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.StartActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())) .Returns(activity.Object); using TestApplicationResult testApplicationResult = new( Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), otelService.Object); static TestNode CreateNode(TestNodeStateProperty state, RetryAttemptProperty? retryAttempt = null) => new() { Uid = "retry-test", DisplayName = "Retry test", Properties = retryAttempt is null ? new PropertyBag(state) : new PropertyBag(state, retryAttempt), }; await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(InProgressTestNodeStateProperty.CachedInstance)), CancellationToken.None); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage( default, CreateNode( new FailedTestNodeStateProperty(), new RetryAttemptProperty(attemptNumber: 1, isSuperseded: true))), CancellationToken.None); activity.Verify(a => a.SetTag("test.case.result.status", "fail"), Times.Never); activity.Verify(a => a.Dispose(), Times.Never); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage( default, CreateNode( PassedTestNodeStateProperty.CachedInstance, new RetryAttemptProperty(attemptNumber: 2, isSuperseded: false))), CancellationToken.None); activity.Verify(a => a.SetTag("test.case.result.status", "pass"), Times.Once); activity.Verify(a => a.Dispose(), Times.Once); Statistics statistics = testApplicationResult.GetStatistics(); Assert.AreEqual(1, statistics.TotalRanTests); Assert.AreEqual(0, statistics.TotalFailedTests); } [TestMethod] public async Task GetProcessExitCode_WithCoverageThresholdFailure_ReturnsCoverageThresholdFailed() { Mock coverageResult = new(); coverageResult.SetupGet(result => result.HasThresholdFailure).Returns(true); using TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.ZeroTestsPolicyOptionKey, [PlatformCommandLineProvider.ZeroTestsPolicyAllowSkippedArgument]), new Mock().Object, new Mock().Object, null, coverageResult.Object); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.CoverageThresholdFailed, testApplicationResult.GetProcessExitCode()); } [TestMethod] public void GetProcessExitCode_WithNoTestsAndCoverageThresholdFailure_ReturnsZeroTests() { Mock coverageResult = new(); coverageResult.SetupGet(result => result.HasThresholdFailure).Returns(true); using TestApplicationResult testApplicationResult = new( new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, null, coverageResult.Object); Assert.AreEqual((int)ExitCode.ZeroTests, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCode_WithFailedTestAndDeadline_ReturnsTestFailure() { Mock policiesService = new(); policiesService.SetupGet(service => service.IsDeadlineTriggered).Returns(true); using TestApplicationResult testApplicationResult = CreateTestApplicationResult(policiesService.Object); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = "failed-test", DisplayName = "FailedTest", Properties = new PropertyBag(new FailedTestNodeStateProperty("failure")), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.AtLeastOneTestFailed, testApplicationResult.GetProcessExitCode()); } [TestMethod] public void GetProcessExitCode_WithAbortAndDeadline_ReturnsAbort() { Mock policiesService = new(); policiesService.SetupGet(service => service.IsAbortTriggered).Returns(true); policiesService.SetupGet(service => service.IsDeadlineTriggered).Returns(true); using TestApplicationResult testApplicationResult = CreateTestApplicationResult(policiesService.Object); Assert.AreEqual((int)ExitCode.TestSessionAborted, testApplicationResult.GetProcessExitCode()); } [TestMethod] public void GetProcessExitCode_WithNoTestsAndDeadline_ReturnsDeadline() { Mock policiesService = new(); policiesService.SetupGet(service => service.IsDeadlineTriggered).Returns(true); using TestApplicationResult testApplicationResult = CreateTestApplicationResult(policiesService.Object); Assert.AreEqual((int)ExitCode.TestExecutionStoppedAtDeadline, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCode_WithOverlappingRequestDeadline_IsolatedPerRequest() { Mock cancellationTokenSource = new(); cancellationTokenSource.SetupGet(x => x.CancellationToken).Returns(CancellationToken.None); StopPoliciesService firstRequestPolicies = new(cancellationTokenSource.Object); StopPoliciesService secondRequestPolicies = new(cancellationTokenSource.Object); using TestApplicationResult firstRequestResult = CreateTestApplicationResult(firstRequestPolicies); using TestApplicationResult secondRequestResult = CreateTestApplicationResult(secondRequestPolicies); await firstRequestPolicies.ExecuteDeadlineCallbacksAsync(); Assert.AreEqual((int)ExitCode.TestExecutionStoppedAtDeadline, firstRequestResult.GetProcessExitCode()); Assert.AreEqual((int)ExitCode.ZeroTests, secondRequestResult.GetProcessExitCode()); } [TestMethod] public void GetProcessExitCode_WithMinimumExpectedTestsViolationAndDeadline_ReturnsDeadline() { Mock policiesService = new(); policiesService.SetupGet(service => service.IsDeadlineTriggered).Returns(true); using TestApplicationResult testApplicationResult = CreateTestApplicationResult( policiesService.Object, new CommandLineOption(PlatformCommandLineProvider.MinimumExpectedTestsOptionKey, ["1"])); Assert.AreEqual((int)ExitCode.TestExecutionStoppedAtDeadline, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCode_WithCoverageThresholdFailureAndDeadline_ReturnsDeadline() { Mock policiesService = new(); policiesService.SetupGet(service => service.IsDeadlineTriggered).Returns(true); Mock coverageResult = new(); coverageResult.SetupGet(result => result.HasThresholdFailure).Returns(true); using TestApplicationResult testApplicationResult = CreateTestApplicationResult(policiesService.Object, coverageResult: coverageResult.Object); await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = "passed-test", DisplayName = "PassedTest", Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.TestExecutionStoppedAtDeadline, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task ConsumeAsync_SupersededRetryAttempt_DoesNotCloseTheTestActivity() { // A test framework that retries in-process reports one in-progress update (which opens the OpenTelemetry // activity) followed by one result per attempt. Only the final attempt is the test's outcome, so only it // may close the activity - otherwise the first superseded result would close it and the real outcome would // find nothing in flight, reporting the retried test as failed in traces even though it passed. var activity = new Mock(); var otelService = new Mock(); otelService .Setup(service => service.CreateCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateUpDownCounter(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.CreateHistogram(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>?>())) .Returns(Mock.Of>()); otelService .Setup(service => service.StartActivity( It.IsAny(), It.IsAny>?>(), It.IsAny(), It.IsAny())) .Returns(activity.Object); using TestApplicationResult testApplicationResult = new( Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), otelService.Object); static TestNode CreateNode(params IProperty[] properties) => new() { Uid = "flaky-uid", DisplayName = "FlakyTest", Properties = new PropertyBag(properties), }; // The in-progress update is reported once per test (not once per attempt) and opens the activity. await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(InProgressTestNodeStateProperty.CachedInstance)), CancellationToken.None); // Attempt 1 failed but was superseded: it must neither close the activity nor mark it failed. await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(new FailedTestNodeStateProperty("boom"), new RetryAttemptProperty(1, isSuperseded: true))), CancellationToken.None); activity.Verify(a => a.Dispose(), Times.Never); // Attempt 2 passed and is the final outcome: it closes the activity exactly once. await testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(PassedTestNodeStateProperty.CachedInstance, new RetryAttemptProperty(2, isSuperseded: false))), CancellationToken.None); activity.Verify(a => a.Dispose(), Times.Once); Assert.AreEqual((int)ExitCode.Success, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task ConsumeAsync_SupersededRetryAttempt_IsNotCountedAsFailure() { static TestNode CreateNode(IProperty state, IProperty retryAttempt) => new() { Uid = new TestNodeUid("flaky-test"), DisplayName = "FlakyTest", Properties = new PropertyBag(state, retryAttempt), }; // Attempt 1 failed but was retried, attempt 2 passed: the test's outcome is "passed". await _testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(new FailedTestNodeStateProperty("boom"), new RetryAttemptProperty(1, isSuperseded: true))), CancellationToken.None); await _testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(PassedTestNodeStateProperty.CachedInstance, new RetryAttemptProperty(2, isSuperseded: false))), CancellationToken.None); Assert.AreEqual((int)ExitCode.Success, _testApplicationResult.GetProcessExitCode()); Assert.AreEqual(0, _testApplicationResult.GetStatistics().TotalFailedTests); } [TestMethod] public async Task ConsumeAsync_FinalRetryAttemptFailed_IsCountedAsFailure() { static TestNode CreateNode(IProperty state, IProperty retryAttempt) => new() { Uid = new TestNodeUid("always-failing-test"), DisplayName = "AlwaysFailingTest", Properties = new PropertyBag(state, retryAttempt), }; await _testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(new FailedTestNodeStateProperty("boom"), new RetryAttemptProperty(1, isSuperseded: true))), CancellationToken.None); await _testApplicationResult.ConsumeAsync( new DummyProducer(), new TestNodeUpdateMessage(default, CreateNode(new FailedTestNodeStateProperty("boom"), new RetryAttemptProperty(2, isSuperseded: false))), CancellationToken.None); Assert.AreEqual((int)ExitCode.AtLeastOneTestFailed, _testApplicationResult.GetProcessExitCode()); Assert.AreEqual(1, _testApplicationResult.GetStatistics().TotalFailedTests); } [TestMethod] public async Task GetProcessExitCodeAsync_If_All_Skipped_ByDefault_Returns_Success() { await _testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(SkippedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); // Default policy is 'allow-skipped', so an all-skipped run is treated as successful. Assert.AreEqual((int)ExitCode.Success, _testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_All_Skipped_With_StrictPolicy_Returns_ZeroTestsRan() { using TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.ZeroTestsPolicyOptionKey, [PlatformCommandLineProvider.ZeroTestsPolicyStrictArgument]), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(SkippedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.ZeroTests, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_No_Tests_Ran_Returns_ZeroTestsRan() { await _testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.ZeroTests, _testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_All_Skipped_With_AllowSkippedPolicy_Returns_Success() { using TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.ZeroTestsPolicyOptionKey, [PlatformCommandLineProvider.ZeroTestsPolicyAllowSkippedArgument]), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(SkippedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.Success, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_No_Tests_With_AllowSkippedPolicy_Returns_ZeroTestsRan() { using TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.ZeroTestsPolicyOptionKey, [PlatformCommandLineProvider.ZeroTestsPolicyAllowSkippedArgument]), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.ZeroTests, testApplicationResult.GetProcessExitCode()); } [TestMethod] [DynamicData(nameof(FailedState))] public async Task GetProcessExitCodeAsync_If_Failed_Tests_Returns_AtLeastOneTestFailed(TestNodeStateProperty testNodeStateProperty) { await _testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(testNodeStateProperty), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.AtLeastOneTestFailed, _testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_Canceled_Returns_TestSessionAborted() { Mock testApplicationCancellationTokenSource = new(); // CTS should not be created in SetupGet so that the mocked ITestApplicationCancellationTokenSource returns the same instance on every access // which is the case in the real production implementation. CancellationTokenSource cancellationTokenSource = new(); testApplicationCancellationTokenSource.SetupGet(x => x.CancellationToken).Returns(() => { cancellationTokenSource.Cancel(); return cancellationTokenSource.Token; }); TestApplicationResult testApplicationResult = new(new Mock().Object, new Mock().Object, new Mock().Object, new StopPoliciesService(testApplicationCancellationTokenSource.Object), null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.TestSessionAborted, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_TestAdapter_Returns_TestAdapterTestSessionFailure() { await _testApplicationResult.SetTestAdapterTestSessionFailureAsync("Adapter error", CancellationToken.None); await _testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.TestAdapterTestSessionFailure, _testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_If_MinimumExpectedTests_Violated_Returns_MinimumExpectedTestsPolicyViolation() { TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.MinimumExpectedTestsOptionKey, ["2"]), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), }), CancellationToken.None); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(InProgressTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.MinimumExpectedTestsPolicyViolation, testApplicationResult.GetProcessExitCode()); } [TestMethod] public void GetProcessExitCodeAsync_If_MinimumExpectedTests_Set_And_No_Tests_Ran_Returns_MinimumExpectedTestsPolicyViolation() { // When an explicit minimum is set, a zero-test run is reported as a minimum-expected violation (9) // rather than ZeroTests (8), so an orchestrator can tell a stricter-local-minimum violation apart // from a plain "ran nothing" module. See issue #7457. TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.MinimumExpectedTestsOptionKey, ["2"]), new Mock().Object, new Mock().Object, null); Assert.AreEqual((int)ExitCode.MinimumExpectedTestsPolicyViolation, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_OnDiscovery_No_Tests_Discovered_Returns_ZeroTests() { TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.DiscoverTestsOptionKey, []), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", }), CancellationToken.None); Assert.AreEqual((int)ExitCode.ZeroTests, testApplicationResult.GetProcessExitCode()); } [TestMethod] public async Task GetProcessExitCodeAsync_OnDiscovery_Some_Tests_Discovered_Returns_Success() { TestApplicationResult testApplicationResult = new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.DiscoverTestsOptionKey, []), new Mock().Object, new Mock().Object, null); await testApplicationResult.ConsumeAsync(new DummyProducer(), new TestNodeUpdateMessage( default, new TestNode { Uid = new TestNodeUid("id"), DisplayName = "DisplayName", Properties = new PropertyBag(DiscoveredTestNodeStateProperty.CachedInstance), }), CancellationToken.None); Assert.AreEqual((int)ExitCode.Success, testApplicationResult.GetProcessExitCode()); } [DataRow("8", (int)ExitCode.Success)] [DataRow("8;2", (int)ExitCode.Success)] [DataRow("8;", (int)ExitCode.Success)] [DataRow("8;2;", (int)ExitCode.Success)] [DataRow(" 8 ", (int)ExitCode.Success)] [DataRow("+8", (int)ExitCode.Success)] [DataRow("2;8", (int)ExitCode.Success)] [DataRow("5", (int)ExitCode.ZeroTests)] [DataRow("5;7", (int)ExitCode.ZeroTests)] [DataRow("5;", (int)ExitCode.ZeroTests)] [DataRow("5;7;", (int)ExitCode.ZeroTests)] [DataRow(";", (int)ExitCode.ZeroTests)] [DataRow(null, (int)ExitCode.ZeroTests)] [DataRow("", (int)ExitCode.ZeroTests)] [DataRow("8abc", (int)ExitCode.ZeroTests)] [DataRow("-8", (int)ExitCode.ZeroTests)] [DataRow("2147483648", (int)ExitCode.ZeroTests)] [DataRow("18446744073709551624", (int)ExitCode.ZeroTests)] [TestMethod] public void GetProcessExitCodeAsync_IgnoreExitCodes(string? argument, int expectedExitCode) { Mock environment = new(); environment.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.TESTINGPLATFORM_EXITCODE_IGNORE)).Returns(argument); foreach (TestApplicationResult testApplicationResult in new TestApplicationResult[] { new( new Mock().Object, new CommandLineOption(PlatformCommandLineProvider.IgnoreExitCodeOptionKey, argument is null ? [] : [argument]), new Mock().Object, new Mock().Object, null), new( new Mock().Object, new Mock().Object, environment.Object, new Mock().Object, null), }) { Assert.AreEqual(expectedExitCode, testApplicationResult.GetProcessExitCode()); } } private static TestApplicationResult CreateTestApplicationResult( IStopPoliciesService policiesService, ICommandLineOptions? commandLineOptions = null, ITestCoverageResult? coverageResult = null) => new( Mock.Of(), commandLineOptions ?? Mock.Of(), Mock.Of(), policiesService, null, coverageResult); internal static IEnumerable FailedState() { yield return [new FailedTestNodeStateProperty()]; yield return [new ErrorTestNodeStateProperty()]; #pragma warning disable CS0618, MTP0001 // Type or member is obsolete yield return [new CancelledTestNodeStateProperty()]; #pragma warning restore CS0618, MTP0001 // Type or member is obsolete yield return [new TimeoutTestNodeStateProperty()]; } private sealed class CommandLineOption : ICommandLineOptions { private readonly string _optionName; private readonly string[] _arguments; public CommandLineOption(string optionName, string[] arguments) { _optionName = optionName; _arguments = arguments; } public bool IsOptionSet(string optionName) => _optionName == optionName; #pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). public bool TryGetOptionArgumentList(string optionName, out string[]? arguments) #pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). { arguments = _arguments; return IsOptionSet(optionName); } } private sealed class DummyProducer : IDataProducer { public Type[] DataTypesProduced => throw new NotImplementedException(); public string Uid => nameof(DummyProducer); public string Version => "1.0.0"; public string DisplayName => string.Empty; public string Description => string.Empty; public Task IsEnabledAsync() => throw new NotImplementedException(); } }