diff --git a/src/Microsoft.DotNet.XHarness.Apple/AppOperations/AppTester.cs b/src/Microsoft.DotNet.XHarness.Apple/AppOperations/AppTester.cs index d4b1f48ac..7399757ab 100644 --- a/src/Microsoft.DotNet.XHarness.Apple/AppOperations/AppTester.cs +++ b/src/Microsoft.DotNet.XHarness.Apple/AppOperations/AppTester.cs @@ -583,6 +583,13 @@ private Dictionary GetEnvVariables( variables.Add(EnviromentVariables.AppEndTag, appEndTag); } + // Propagate DOTNET_CI environment variable if it's set on the host + var dotnetCI = Environment.GetEnvironmentVariable(EnviromentVariables.DotnetCI); + if (!string.IsNullOrEmpty(dotnetCI)) + { + variables.Add(EnviromentVariables.DotnetCI, dotnetCI); + } + AddExtraEnvVars(variables, extraEnvVariables); return variables; diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidTestCommand.cs index 8a42e3d32..57686c426 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/Android/AndroidTestCommand.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.DotNet.XHarness.Android; using Microsoft.DotNet.XHarness.CLI.Android; @@ -61,11 +63,19 @@ protected override ExitCode InvokeCommand(ILogger logger) { runner.ClearAdbLog(); + // Propagate DOTNET_CI environment variable if it's set on the host + var instrumentationArguments = new Dictionary(Arguments.InstrumentationArguments.Value); + var dotnetCI = System.Environment.GetEnvironmentVariable("DOTNET_CI"); + if (!string.IsNullOrEmpty(dotnetCI) && !instrumentationArguments.ContainsKey("DOTNET_CI")) + { + instrumentationArguments.Add("DOTNET_CI", dotnetCI); + } + var instrumentationRunner = new InstrumentationRunner(logger, runner); exitCode = instrumentationRunner.RunApkInstrumentation( Arguments.PackageName, Arguments.InstrumentationName, - Arguments.InstrumentationArguments, + instrumentationArguments, Arguments.OutputDirectory, Arguments.DeviceOutputFolder, Arguments.Timeout, diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs index 5adf7dddc..bb97e41be 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASI/Engine/WasiTestCommand.cs @@ -94,6 +94,14 @@ protected override async Task InvokeInternal(ILogger logger) } } + // Propagate DOTNET_CI environment variable if it's set on the host + var dotnetCI = System.Environment.GetEnvironmentVariable("DOTNET_CI"); + if (!string.IsNullOrEmpty(dotnetCI)) + { + engineArgs.Add("--env"); + engineArgs.Add($"DOTNET_CI={dotnetCI}"); + } + engineArgs.AddRange(PassThroughArguments); var xmlResultsFilePath = Path.Combine(Arguments.OutputDirectory, "testResults.xml"); diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs index fce046a6b..d3c6462a4 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs @@ -285,6 +285,16 @@ private string BuildUrl(ServerURLs serverURLs) sb.Append($"arg={HttpUtility.UrlEncode(arg)}"); } + // Propagate DOTNET_CI environment variable if it's set on the host + var dotnetCI = System.Environment.GetEnvironmentVariable("DOTNET_CI"); + if (!string.IsNullOrEmpty(dotnetCI)) + { + if (sb.Length > 0) + sb.Append('&'); + + sb.Append($"arg={HttpUtility.UrlEncode($"--setenv=DOTNET_CI={dotnetCI}")}"); + } + if (sb.Length > 0) sb.Append('&'); diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs index 7c52e26a8..977e4ee86 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs @@ -135,6 +135,25 @@ protected override async Task InvokeInternal(ILogger logger) symbolicator); var logProcessorTask = Task.Run(() => logProcessor.RunAsync(cts.Token)); + // Prepare environment variables + Dictionary? environmentVariables = null; + if (Arguments.Engine.Value == JavaScriptEngine.NodeJS) + { + // Node respects LANG only, ignores LANGUAGE + environmentVariables = new Dictionary() { {"LANG", Arguments.Locale} }; + } + + // Propagate DOTNET_CI environment variable if it's set on the host + var dotnetCI = System.Environment.GetEnvironmentVariable("DOTNET_CI"); + if (!string.IsNullOrEmpty(dotnetCI)) + { + environmentVariables ??= new Dictionary(); + if (!environmentVariables.ContainsKey("DOTNET_CI")) + { + environmentVariables.Add("DOTNET_CI", dotnetCI); + } + } + var processTask = processManager.ExecuteCommandAsync( engineBinary, engineArgs, @@ -142,10 +161,7 @@ protected override async Task InvokeInternal(ILogger logger) stdoutLog: new CallbackLog(msg => logProcessor.Invoke(msg)), stderrLog: new CallbackLog(logProcessor.ProcessErrorMessage), Arguments.Timeout, - // Node respects LANG only, ignores LANGUAGE - environmentVariables: Arguments.Engine.Value == JavaScriptEngine.NodeJS ? - new Dictionary() { {"LANG", Arguments.Locale} } : - null); + environmentVariables); TaskCompletionSource wasmExitReceivedTcs = logProcessor.WasmExitReceivedTcs; var tasks = new Task[] diff --git a/src/Microsoft.DotNet.XHarness.iOS.Shared/Execution/EnviromentVariables.cs b/src/Microsoft.DotNet.XHarness.iOS.Shared/Execution/EnviromentVariables.cs index 32f7e6f46..676258852 100644 --- a/src/Microsoft.DotNet.XHarness.iOS.Shared/Execution/EnviromentVariables.cs +++ b/src/Microsoft.DotNet.XHarness.iOS.Shared/Execution/EnviromentVariables.cs @@ -89,4 +89,10 @@ public static class EnviromentVariables /// Env var uses to notify the test application which test classes will be excluded. /// public const string SkippedClasses = "NUNIT_SKIPPED_CLASSES"; + + /// + /// Env var that indicates the test is running in CI environment. + /// This is used by SkipOnCIAttribute and other test attributes to skip tests that shouldn't run in CI. + /// + public const string DotnetCI = "DOTNET_CI"; } diff --git a/tests/Microsoft.DotNet.XHarness.Apple.Tests/AppOperations/AppTesterTests.cs b/tests/Microsoft.DotNet.XHarness.Apple.Tests/AppOperations/AppTesterTests.cs index 31990ae00..92dcc6ad3 100644 --- a/tests/Microsoft.DotNet.XHarness.Apple.Tests/AppOperations/AppTesterTests.cs +++ b/tests/Microsoft.DotNet.XHarness.Apple.Tests/AppOperations/AppTesterTests.cs @@ -620,6 +620,156 @@ public async Task TestOnDeviceWithAppEndSignalTest() deviceSystemLog.Verify(x => x.Dispose(), Times.AtLeastOnce); } + [Fact] + public async Task TestOnSimulator_PropagatesDotnetCIEnvVar() + { + // Setup DOTNET_CI environment variable + var originalValue = Environment.GetEnvironmentVariable("DOTNET_CI"); + Environment.SetEnvironmentVariable("DOTNET_CI", "true"); + + try + { + var testResultFilePath = Path.GetTempFileName(); + var listenerLogFile = Mock.Of(x => x.FullPath == testResultFilePath); + File.WriteAllLines(testResultFilePath, new[] { "Some result here", "Tests run: 124", "Some result there" }); + + _logs + .Setup(x => x.Create("test-ios-simulator-64-mocked_timestamp.log", "TestLog", It.IsAny())) + .Returns(listenerLogFile); + + var captureLog = new Mock(); + captureLog.SetupGet(x => x.FullPath).Returns(_simulatorLogPath); + + var captureLogFactory = new Mock(); + captureLogFactory + .Setup(x => x.Create( + Path.Combine(_logs.Object.Directory, _mockSimulator.Name + ".log"), + _mockSimulator.SystemLog, + false, + It.IsAny())) + .Returns(captureLog.Object); + + var appTester = new AppTester( + _processManager.Object, + _listenerFactory.Object, + _snapshotReporterFactory, + captureLogFactory.Object, + Mock.Of(), + _testReporterFactory, + new XmlResultParser(), + _mainLog.Object, + _logs.Object, + _helpers.Object); + + // Act + var (result, resultMessage) = await appTester.TestApp( + _appBundleInfo, + new TestTargetOs(TestTarget.Simulator_tvOS, null), + _mockSimulator, + null, + TimeSpan.FromSeconds(30), + TimeSpan.FromSeconds(30), + signalAppEnd: false, + extraAppArguments: Array.Empty(), + extraEnvVariables: Array.Empty<(string, string)>()); + + // Verify + Assert.Equal(TestExecutingResult.Succeeded, result); + + // Verify DOTNET_CI was passed as an environment variable + _processManager + .Verify( + x => x.ExecuteCommandAsync( + It.Is(args => args.AsCommandLine().Contains("-setenv=DOTNET_CI=true")), + _mainLog.Object, + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + finally + { + // Restore original value + Environment.SetEnvironmentVariable("DOTNET_CI", originalValue); + } + } + + [Fact] + public async Task TestOnSimulator_DoesNotPropagateDotnetCIWhenNotSet() + { + // Ensure DOTNET_CI environment variable is not set + var originalValue = Environment.GetEnvironmentVariable("DOTNET_CI"); + Environment.SetEnvironmentVariable("DOTNET_CI", null); + + try + { + var testResultFilePath = Path.GetTempFileName(); + var listenerLogFile = Mock.Of(x => x.FullPath == testResultFilePath); + File.WriteAllLines(testResultFilePath, new[] { "Some result here", "Tests run: 124", "Some result there" }); + + _logs + .Setup(x => x.Create("test-ios-simulator-64-mocked_timestamp.log", "TestLog", It.IsAny())) + .Returns(listenerLogFile); + + var captureLog = new Mock(); + captureLog.SetupGet(x => x.FullPath).Returns(_simulatorLogPath); + + var captureLogFactory = new Mock(); + captureLogFactory + .Setup(x => x.Create( + Path.Combine(_logs.Object.Directory, _mockSimulator.Name + ".log"), + _mockSimulator.SystemLog, + false, + It.IsAny())) + .Returns(captureLog.Object); + + var appTester = new AppTester( + _processManager.Object, + _listenerFactory.Object, + _snapshotReporterFactory, + captureLogFactory.Object, + Mock.Of(), + _testReporterFactory, + new XmlResultParser(), + _mainLog.Object, + _logs.Object, + _helpers.Object); + + // Act + var (result, resultMessage) = await appTester.TestApp( + _appBundleInfo, + new TestTargetOs(TestTarget.Simulator_tvOS, null), + _mockSimulator, + null, + TimeSpan.FromSeconds(30), + TimeSpan.FromSeconds(30), + signalAppEnd: false, + extraAppArguments: Array.Empty(), + extraEnvVariables: Array.Empty<(string, string)>()); + + // Verify + Assert.Equal(TestExecutingResult.Succeeded, result); + + // Verify DOTNET_CI was NOT passed as an environment variable + _processManager + .Verify( + x => x.ExecuteCommandAsync( + It.Is(args => !args.AsCommandLine().Contains("-setenv=DOTNET_CI")), + _mainLog.Object, + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + finally + { + // Restore original value + Environment.SetEnvironmentVariable("DOTNET_CI", originalValue); + } + } + private string GetExpectedDeviceMlaunchArgs(string? skippedTests = null, bool useTunnel = false, string? extraArgs = null) => "-setenv=NUNIT_AUTOEXIT=true " + $"-setenv=NUNIT_HOSTPORT={Port} " +