diff --git a/eng/helix/content/RunTests/Program.cs b/eng/helix/content/RunTests/Program.cs index 1535d6663e..f9d3ec2752 100644 --- a/eng/helix/content/RunTests/Program.cs +++ b/eng/helix/content/RunTests/Program.cs @@ -14,222 +14,41 @@ namespace RunTests { static async Task Main(string[] args) { - var command = new RootCommand() + try { - new Option( - aliases: new string[] { "--target", "-t" }, - description: "The test dll to run") - { Argument = new Argument(), Required = true }, + var runner = new TestRunner(RunTestsOptions.Parse(args)); - new Option( - aliases: new string[] { "--sdk" }, - description: "The version of the sdk being used") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--runtime" }, - description: "The version of the runtime being used") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--queue" }, - description: "The name of the Helix queue being run on") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--arch" }, - description: "The architecture being run on") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--quarantined" }, - description: "Whether quarantined tests should run or not") - { Argument = new Argument(), Required = true }, - - new Option( - aliases: new string[] { "--ef" }, - description: "The version of the EF tool to use") - { Argument = new Argument(), Required = true }, - }; - - var parseResult = command.Parse(args); - var target = parseResult.ValueForOption("--target"); - var sdkVersion = parseResult.ValueForOption("--sdk"); - var runtimeVersion = parseResult.ValueForOption("--runtime"); - var helixQueue = parseResult.ValueForOption("--queue"); - var architecture = parseResult.ValueForOption("--arch"); - var quarantined = parseResult.ValueForOption("--quarantined"); - var efVersion = parseResult.ValueForOption("--ef"); - - var HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"); - - var path = Environment.GetEnvironmentVariable("PATH"); - var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - - // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project - // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state - if (!File.Exists("NuGet.config")) - { - File.Copy("default.NuGet.config", "NuGet.config"); - } - - var environmentVariables = new Dictionary(); - environmentVariables.Add("PATH", path); - environmentVariables.Add("DOTNET_ROOT", dotnetRoot); - environmentVariables.Add("helix", helixQueue); - - Console.WriteLine($"Current Directory: {HELIX_WORKITEM_ROOT}"); - var helixDir = HELIX_WORKITEM_ROOT; - Console.WriteLine($"Setting HELIX_DIR: {helixDir}"); - environmentVariables.Add("HELIX_DIR", helixDir); - environmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir); - var nugetRestore = Path.Combine(helixDir, "nugetRestore"); - Console.WriteLine($"Creating nuget restore directory: {nugetRestore}"); - environmentVariables.Add("NUGET_RESTORE", nugetRestore); - var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{efVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe"); - Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}"); - environmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath); - - Console.WriteLine("Checking for Microsoft.AspNetCore.App/"); - if (Directory.Exists("Microsoft.AspNetCore.App")) - { - Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}"); - foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories)) + var keepGoing = runner.SetupEnvironment(); + if (keepGoing) { - File.Copy(file, $"{dotnetRoot}/shared/Microsoft.AspNetCore.App/{runtimeVersion}", overwrite: true); + keepGoing = await runner.InstallAspNetAppIfNeededAsync(); } - Console.WriteLine($"Adding current directory to nuget sources: {HELIX_WORKITEM_ROOT}"); + runner.DisplayContents(); - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"nuget add source {HELIX_WORKITEM_ROOT} --configfile NuGet.config", - environmentVariables: environmentVariables); + if (keepGoing) + { + if (!await runner.CheckTestDiscoveryAsync()) + { + Console.WriteLine("RunTest stopping due to test discovery failure."); + Environment.Exit(1); + return; + } - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config", - environmentVariables: environmentVariables); + var exitCode = await runner.RunTestsAsync(); + runner.UploadResults(); + Console.WriteLine($"Completed Helix job with exit code '{exitCode}'"); + Environment.Exit(exitCode); + } - // Write nuget sources to console, useful for debugging purposes - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - "nuget list source", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.WriteLine); - - await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"tool install dotnet-ef --global --version {efVersion}", - environmentVariables: environmentVariables); - - // ';' is the path separator on Windows, and ':' on Unix - path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; - path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools"; - environmentVariables["PATH"] = path; - } - - Directory.CreateDirectory(nugetRestore); - - // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project - if (!File.Exists("xunit.runner.json")) - { - File.Copy("default.runner.json", "xunit.runner.json"); - } - - Console.WriteLine(); - Console.WriteLine("Displaying directory contents:"); - foreach (var file in Directory.EnumerateFiles("./")) - { - Console.WriteLine(Path.GetFileName(file)); - } - foreach (var file in Directory.EnumerateDirectories("./")) - { - Console.WriteLine(Path.GetFileName(file)); - } - Console.WriteLine(); - - // Run test discovery so we know if there are tests to run - var discoveryResult = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - $"vstest {target} -lt", - environmentVariables: environmentVariables); - - if (discoveryResult.StandardOutput.Contains("Exception thrown")) - { - Console.WriteLine("Exception thrown during test discovery."); - Console.WriteLine(discoveryResult.StandardOutput); + Console.WriteLine("Tests were not run due to previous failures. Exit code=1"); Environment.Exit(1); - return; } - - var exitCode = 0; - var commonTestArgs = $"vstest {target} --logger:xunit --logger:\"console;verbosity=normal\" --blame"; - if (quarantined) + catch (Exception e) { - Console.WriteLine("Running quarantined tests."); - - // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md - var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.WriteLine, - throwOnError: false); - - if (result.ExitCode != 0) - { - Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}."); - } + Console.WriteLine($"RunTests uncaught exception: {e.ToString()}"); + Environment.Exit(1); } - else - { - Console.WriteLine("Running non-quarantined tests."); - - // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md - var result = await ProcessUtil.RunAsync($"{dotnetRoot}/dotnet", - commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"", - environmentVariables: environmentVariables, - outputDataReceived: Console.WriteLine, - errorDataReceived: Console.Error.WriteLine, - throwOnError: false); - - if (result.ExitCode != 0) - { - Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}."); - exitCode = result.ExitCode; - } - } - - // 'testResults.xml' is the file Helix looks for when processing test results - Console.WriteLine(); - if (File.Exists("TestResults/TestResults.xml")) - { - Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml"); - File.Copy("TestResults/TestResults.xml", "testResults.xml"); - } - else - { - Console.WriteLine("No test results found."); - } - - var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); - Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/"); - if (Directory.Exists("artifacts/log")) - { - foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories)) - { - // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects - var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}"; - Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}"); - // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs - File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)); - File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName)); - } - } - else - { - Console.WriteLine("No logs found in artifacts/log"); - } - - Console.WriteLine("Completed Helix job."); - Environment.Exit(exitCode); } } } diff --git a/eng/helix/content/RunTests/RunTestsOptions.cs b/eng/helix/content/RunTests/RunTestsOptions.cs new file mode 100644 index 0000000000..fcfdc84e42 --- /dev/null +++ b/eng/helix/content/RunTests/RunTestsOptions.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class RunTestsOptions + { + public static RunTestsOptions Parse(string[] args) + { + var command = new RootCommand() + { + new Option( + aliases: new string[] { "--target", "-t" }, + description: "The test dll to run") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--sdk" }, + description: "The version of the sdk being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--runtime" }, + description: "The version of the runtime being used") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--queue" }, + description: "The name of the Helix queue being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--arch" }, + description: "The architecture being run on") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--quarantined" }, + description: "Whether quarantined tests should run or not") + { Argument = new Argument(), Required = true }, + + new Option( + aliases: new string[] { "--ef" }, + description: "The version of the EF tool to use") + { Argument = new Argument(), Required = true }, + }; + + var parseResult = command.Parse(args); + var options = new RunTestsOptions(); + options.Target = parseResult.ValueForOption("--target"); + options.SdkVersion = parseResult.ValueForOption("--sdk"); + options.RuntimeVersion = parseResult.ValueForOption("--runtime"); + options.HelixQueue = parseResult.ValueForOption("--queue"); + options.Architecture = parseResult.ValueForOption("--arch"); + options.Quarantined = parseResult.ValueForOption("--quarantined"); + options.EfVersion = parseResult.ValueForOption("--ef"); + options.HELIX_WORKITEM_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"); + options.Path = Environment.GetEnvironmentVariable("PATH"); + options.DotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + return options; + } + + public string Target { get; set;} + public string SdkVersion { get; set;} + public string RuntimeVersion { get; set;} + public string HelixQueue { get; set;} + public string Architecture { get; set;} + public bool Quarantined { get; set;} + public string EfVersion { get; set;} + public string HELIX_WORKITEM_ROOT { get; set;} + public string DotnetRoot { get; set; } + public string Path { get; set; } + } +} diff --git a/eng/helix/content/RunTests/TestRunner.cs b/eng/helix/content/RunTests/TestRunner.cs new file mode 100644 index 0000000000..850d0b629d --- /dev/null +++ b/eng/helix/content/RunTests/TestRunner.cs @@ -0,0 +1,251 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace RunTests +{ + public class TestRunner + { + public TestRunner(RunTestsOptions options) + { + Options = options; + EnvironmentVariables = new Dictionary(); + } + + public RunTestsOptions Options { get; set; } + public Dictionary EnvironmentVariables { get; set; } + + public bool SetupEnvironment() + { + try + { + // Rename default.NuGet.config to NuGet.config if there is not a custom one from the project + // We use a local NuGet.config file to avoid polluting global machine state and avoid relying on global machine state + if (!File.Exists("NuGet.config")) + { + File.Copy("default.NuGet.config", "NuGet.config"); + } + + EnvironmentVariables.Add("PATH", Options.Path); + EnvironmentVariables.Add("DOTNET_ROOT", Options.DotnetRoot); + EnvironmentVariables.Add("helix", Options.HelixQueue); + + Console.WriteLine($"Current Directory: {Options.HELIX_WORKITEM_ROOT}"); + var helixDir = Options.HELIX_WORKITEM_ROOT; + Console.WriteLine($"Setting HELIX_DIR: {helixDir}"); + EnvironmentVariables.Add("HELIX_DIR", helixDir); + EnvironmentVariables.Add("NUGET_FALLBACK_PACKAGES", helixDir); + var nugetRestore = Path.Combine(helixDir, "nugetRestore"); + EnvironmentVariables.Add("NUGET_RESTORE", nugetRestore); + var dotnetEFFullPath = Path.Combine(nugetRestore, $"dotnet-ef/{Options.EfVersion}/tools/netcoreapp3.1/any/dotnet-ef.exe"); + Console.WriteLine($"Set DotNetEfFullPath: {dotnetEFFullPath}"); + EnvironmentVariables.Add("DotNetEfFullPath", dotnetEFFullPath); + + Console.WriteLine($"Creating nuget restore directory: {nugetRestore}"); + Directory.CreateDirectory(nugetRestore); + + // Rename default.runner.json to xunit.runner.json if there is not a custom one from the project + if (!File.Exists("xunit.runner.json")) + { + File.Copy("default.runner.json", "xunit.runner.json"); + } + + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in SetupEnvironment: {e.ToString()}"); + return false; + } + } + + public void DisplayContents() + { + try + { + Console.WriteLine(); + Console.WriteLine("Displaying directory contents:"); + foreach (var file in Directory.EnumerateFiles("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + foreach (var file in Directory.EnumerateDirectories("./")) + { + Console.WriteLine(Path.GetFileName(file)); + } + Console.WriteLine(); + } + catch (Exception e) + { + Console.WriteLine($"Exception in DisplayInitialState: {e.ToString()}"); + } + } + + public async Task InstallAspNetAppIfNeededAsync() + { + try + { + Console.WriteLine("Checking for Microsoft.AspNetCore.App/"); + if (Directory.Exists("Microsoft.AspNetCore.App")) + { + var appRuntimePath = $"{Options.DotnetRoot}/shared/Microsoft.AspNetCore.App/{Options.RuntimeVersion}"; + Console.WriteLine($"Found Microsoft.AspNetCore.App/, copying to {appRuntimePath}"); + foreach (var file in Directory.EnumerateFiles("Microsoft.AspNetCore.App", "*.*", SearchOption.AllDirectories)) + { + File.Copy(file, Path.Combine(appRuntimePath, file), overwrite: true); + } + + Console.WriteLine($"Adding current directory to nuget sources: {Options.HELIX_WORKITEM_ROOT}"); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"nuget add source {Options.HELIX_WORKITEM_ROOT} --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget add source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json --configfile NuGet.config", + environmentVariables: EnvironmentVariables); + + // Write nuget sources to console, useful for debugging purposes + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + "nuget list source", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine); + + await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"tool install dotnet-ef --global --version {Options.EfVersion}", + environmentVariables: EnvironmentVariables); + + // ';' is the path separator on Windows, and ':' on Unix + Options.Path += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + Options.Path += $"{Environment.GetEnvironmentVariable("DOTNET_CLI_HOME")}/.dotnet/tools"; + EnvironmentVariables["PATH"] = Options.Path; + } + else + { + Console.WriteLine($"No app runtime found, skipping..."); + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in InstallAspNetAppIfNeeded: {e.ToString()}"); + return false; + } + } + + public async Task CheckTestDiscoveryAsync() + { + try + { + // Run test discovery so we know if there are tests to run + var discoveryResult = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + $"vstest {Options.Target} -lt", + environmentVariables: EnvironmentVariables); + + if (discoveryResult.StandardOutput.Contains("Exception thrown")) + { + Console.WriteLine("Exception thrown during test discovery."); + Console.WriteLine(discoveryResult.StandardOutput); + return false; + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"Exception in CheckTestDiscovery: {e.ToString()}"); + return false; + } + } + + public async Task RunTestsAsync() + { + var exitCode = 0; + try + { + var commonTestArgs = $"vstest {Options.Target} --logger:xunit --logger:\"console;verbosity=normal\" --blame"; + if (Options.Quarantined) + { + Console.WriteLine("Running quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in quarantined tests. Exit code: {result.ExitCode}."); + } + } + else + { + Console.WriteLine("Running non-quarantined tests."); + + // Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md + var result = await ProcessUtil.RunAsync($"{Options.DotnetRoot}/dotnet", + commonTestArgs + " --TestCaseFilter:\"Quarantined!=true\"", + environmentVariables: EnvironmentVariables, + outputDataReceived: Console.WriteLine, + errorDataReceived: Console.Error.WriteLine, + throwOnError: false); + + if (result.ExitCode != 0) + { + Console.WriteLine($"Failure in non-quarantined tests. Exit code: {result.ExitCode}."); + exitCode = result.ExitCode; + } + } + } + catch (Exception e) + { + Console.WriteLine($"Exception in RunTests: {e.ToString()}"); + exitCode = 1; + } + return exitCode; + } + + public void UploadResults() + { + // 'testResults.xml' is the file Helix looks for when processing test results + Console.WriteLine("Trying to upload results..."); + if (File.Exists("TestResults/TestResults.xml")) + { + Console.WriteLine("Copying TestResults/TestResults.xml to ./testResults.xml"); + File.Copy("TestResults/TestResults.xml", "testResults.xml"); + } + else + { + Console.WriteLine("No test results found."); + } + + var HELIX_WORKITEM_UPLOAD_ROOT = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + Console.WriteLine($"Copying artifacts/log/ to {HELIX_WORKITEM_UPLOAD_ROOT}/"); + if (Directory.Exists("artifacts/log")) + { + foreach (var file in Directory.EnumerateFiles("artifacts/log", "*.log", SearchOption.AllDirectories)) + { + // Combine the directory name + log name for the copied log file name to avoid overwriting duplicate test names in different test projects + var logName = $"{Path.GetFileName(Path.GetDirectoryName(file))}_{Path.GetFileName(file)}"; + Console.WriteLine($"Copying: {file} to {Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)}"); + // Need to copy to HELIX_WORKITEM_UPLOAD_ROOT and HELIX_WORKITEM_UPLOAD_ROOT/../ in order for Azure Devops attachments to link properly and for Helix to store the logs + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, logName)); + File.Copy(file, Path.Combine(HELIX_WORKITEM_UPLOAD_ROOT, "..", logName)); + } + } + else + { + Console.WriteLine("No logs found in artifacts/log"); + } + } + } +} diff --git a/eng/helix/content/runtests.cmd b/eng/helix/content/runtests.cmd index d71ff1b6ce..1078def35b 100644 --- a/eng/helix/content/runtests.cmd +++ b/eng/helix/content/runtests.cmd @@ -21,10 +21,12 @@ echo "Installing Runtime" powershell.exe -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; &([scriptblock]::Create((Invoke-WebRequest -useb 'https://dot.net/v1/dotnet-install.ps1'))) -Architecture %$arch% -Runtime dotnet -Version %$runtimeVersion% -InstallDir %DOTNET_ROOT%" set exit_code=0 +echo "Restore for RunTests..." dotnet restore RunTests\RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." dotnet run --project RunTests\RunTests.csproj -- --target %1 --sdk %2 --runtime %3 --queue %4 --arch %5 --quarantined %6 --ef %7 if errorlevel 1 ( set exit_code=1 ) - +echo "Finished running tests: exit_code=%exit_code%" exit /b %exit_code% diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh index a8665ab51c..2ce725910c 100755 --- a/eng/helix/content/runtests.sh +++ b/eng/helix/content/runtests.sh @@ -86,7 +86,10 @@ fi sync exit_code=0 +echo "Restore for RunTests..." $DOTNET_ROOT/dotnet restore RunTests/RunTests.csproj --source https://api.nuget.org/v3/index.json --ignore-failed-sources +echo "Running tests..." $DOTNET_ROOT/dotnet run --project RunTests/RunTests.csproj -- --target $1 --sdk $2 --runtime $3 --queue $4 --arch $5 --quarantined $6 --ef $7 - -exit $? +exit_code = $? +echo "Finished tests...exit_code=$exit_code" +exit $exit_code