[Harness] Refactor process management to be testable. (#8053)

Move all the extension methods to a class. After this refactor, we will
be able to DI the manager in the other classes and assert that the
processes are called with the correct parameters without the need of
launching them.

Also added tests for the manager. We create a dummy console app that
will be executed by the tests. The console app has a number of
parameters that will be used to ensure that the new process behaves as
we want:

- Use the passed exit code.
- Create child proecesses if needed.
- Sleep to force a timeout.
- Writer messages to stdout and stderr.

Our tests call the dummy app and ensures that the results match the
behaviour expected by the dummy app.


Co-authored-by: Přemek Vysoký <premek.vysoky@microsoft.com>
This commit is contained in:
Manuel de la Pena 2020-03-06 09:11:33 -05:00 коммит произвёл GitHub
Родитель 65a7168c66
Коммит 597b7d7b0b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 455 добавлений и 61 удалений

Просмотреть файл

@ -9,6 +9,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Xamarin;
using Xamarin.Utils;
using Xharness.Execution;
using Xharness.Jenkins.TestTasks;
using Xharness.Listeners;
using Xharness.Logging;
@ -124,6 +126,8 @@ namespace Xharness
}
}
public IProcessManager ProcessManager { get; set; } = new ProcessManager ();
string mode;
async Task<bool> FindSimulatorAsync ()
@ -302,7 +306,7 @@ namespace Xharness
var totalSize = Directory.GetFiles (appPath, "*", SearchOption.AllDirectories).Select ((v) => new FileInfo (v).Length).Sum ();
main_log.WriteLine ($"Installing '{appPath}' to '{companion_device_name ?? device_name}'. Size: {totalSize} bytes = {totalSize / 1024.0 / 1024.0:N2} MB");
return await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args, main_log, TimeSpan.FromHours (1), cancellation_token: cancellation_token);
return await ProcessManager.ExecuteCommandAsync (Harness.MlaunchPath, args, main_log, TimeSpan.FromHours (1), cancellation_token: cancellation_token);
}
public async Task<ProcessExecutionResult> UninstallAsync ()
@ -326,7 +330,7 @@ namespace Xharness
args.Add (bundle_identifier);
AddDeviceName (args, companion_device_name ?? device_name);
return await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args, main_log, TimeSpan.FromMinutes (1));
return await ProcessManager.ExecuteCommandAsync (Harness.MlaunchPath, args, main_log, TimeSpan.FromMinutes (1));
}
bool ensure_clean_simulator_state = true;
@ -655,7 +659,7 @@ namespace Xharness
main_log.WriteLine ("Starting test run");
var result = await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args, run_log, timeout, cancellation_token: cancellation_source.Token);
var result = await ProcessManager.ExecuteCommandAsync (Harness.MlaunchPath, args, run_log, timeout, cancellation_token: cancellation_source.Token);
if (result.TimedOut) {
timed_out = true;
success = false;
@ -692,7 +696,7 @@ namespace Xharness
var timeoutType = launchTimedout ? "Launch" : "Completion";
var timeoutValue = launchTimedout ? Harness.LaunchTimeout : timeout.TotalSeconds;
main_log.WriteLine ($"{timeoutType} timed out after {timeoutValue} seconds");
await Process_Extensions.KillTreeAsync (pid, main_log, true);
await ProcessManager.KillTreeAsync (pid, main_log, true);
} else {
main_log.WriteLine ("Could not find pid in mtouch output.");
}
@ -740,7 +744,7 @@ namespace Xharness
});
var runLog = Log.CreateAggregatedLog (callbackLog, main_log);
var timeoutWatch = Stopwatch.StartNew ();
var result = await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args, runLog, timeout, cancellation_token: cancellation_source.Token);
var result = await ProcessManager.ExecuteCommandAsync (Harness.MlaunchPath, args, runLog, timeout, cancellation_token: cancellation_source.Token);
if (!waitedForExit && !result.TimedOut) {
// mlaunch couldn't wait for exit for some reason. Let's assume the app exits when the test listener completes.

Просмотреть файл

@ -67,7 +67,7 @@ namespace Xharness
if (process.WaitForExit ((int) TimeSpan.FromSeconds (5).TotalMilliseconds))
return;
process.KillTreeAsync (Harness.HarnessLog, diagnostics: false).Wait ();
Harness.ProcessManager.KillTreeAsync (process, Harness.HarnessLog, diagnostics: false).Wait ();
process.Dispose ();
}
}

Просмотреть файл

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2318BC20-40F1-4A12-B695-10B0433CFB35}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>DummyTestProcess</RootNamespace>
<AssemblyName>DummyTestProcess</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Xharness.Tests\bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ExternalConsole>true</ExternalConsole>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>.\Xharness.Tests\bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ExternalConsole>true</ExternalConsole>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="Mono.Options">
<HintPath>..\..\..\packages\Mono.Options.6.6.0.161\lib\net40\Mono.Options.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>

Просмотреть файл

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Mono.Options;
namespace DummyTestProcess {
// The following is a dummy console application that we are going to be using
// for testing purposes. The application takes a number of commands that will
// help to test diff situations:
//
// * Exit codes.
// * Timeouts
// * Child processes.
// * Provided parameters
//
// The console worjs as following:
//
// -exit-code: Provide the exit code that will be returned after execution
// -timeout: The time the application will be idle. This will be used to
// fake a timeout in the xharness ide.
// -children: Create a number of childre processes.
//
// By default, the application will alsways print the arguments that have been
// provided and will run for the given timeout time.
class MainClass {
public static void Main (string [] args)
{
var timeout = TimeSpan.FromMinutes(1);
var childrenCount = 0;
var exitCode = 0;
var showHelp = false;
string stdout = "";
string stderr = "";
List<Process> children;
var os = new OptionSet () {
{ "h|?|help", "Displays the help", (v) => showHelp = true },
{ "exit-code=", "Exit code to be used by the application", (v) => int.TryParse (v, out exitCode) },
{ "stdout=", "Message to print in stdout. Default is null", (v) => stdout = v },
{ "stderr=", "Message to print in stderr. Default is null", (v) => stderr = v },
{ "timeout=", "Timeout for a test run (in seconds). Default is 1 minute.", (v) =>
{
if (int.TryParse (v, out var newTimeout)) {
timeout = TimeSpan.FromSeconds(newTimeout);
}
}
},
{ "children=", "The number of children processes that will be created by the application", (v) => int.TryParse (v, out childrenCount) },
};
_ = os.Parse (args); // dont care about any extra args, ignore them
if (showHelp) {
os.WriteOptionDescriptions (Console.Out);
Environment.Exit (0);
}
// spawn the number of children needed, we spawn the same process with the same args BUT no children
if (childrenCount > 0) {
Console.WriteLine (childrenCount);
children = new List<Process> (childrenCount);
for (var i = 0; i < childrenCount; i++) {
Console.WriteLine (i);
var p = new Process ();
p.StartInfo.FileName = "mono";
p.StartInfo.Arguments = $"DummyTestProcess.exe -exit-code={exitCode} -timeout={timeout.Seconds}";
p.Start ();
children.Add (p);
}
}
// write something to the stdout and stderr to test the output
if (!string.IsNullOrEmpty (stdout))
Console.WriteLine (stdout);
if (!string.IsNullOrEmpty (stderr))
Console.Error.WriteLine (stderr);
// sleep for the required timeout
Thread.Sleep (timeout);
Environment.Exit (exitCode);
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System.Reflection;
using System.Runtime.CompilerServices;
// Information about this assembly is defined by the following attributes.
// Change them to the values specific to your project.
[assembly: AssemblyTitle ("DummyTestProcess")]
[assembly: AssemblyDescription ("")]
[assembly: AssemblyConfiguration ("")]
[assembly: AssemblyCompany ("")]
[assembly: AssemblyProduct ("")]
[assembly: AssemblyCopyright ("${AuthorCopyright}")]
[assembly: AssemblyTrademark ("")]
[assembly: AssemblyCulture ("")]
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
[assembly: AssemblyVersion ("1.0.*")]
// The following attributes are used to specify the signing key for the assembly,
// if desired. See the Mono documentation for more information about signing.
//[assembly: AssemblyDelaySign(false)]
//[assembly: AssemblyKeyFile("")]

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.Options" version="6.6.0.161" targetFramework="net472" />
</packages>

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xharness.Logging;
namespace Xharness.Execution {
public class ProcessExecutionResult {
public bool TimedOut { get; set; }
public int ExitCode { get; set; }
public bool Succeeded => !TimedOut && ExitCode == 0;
}
// interface that helps to manage the different processes in the app.
public interface IProcessManager {
Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, IList<string> args, ILog log, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null);
Task<ProcessExecutionResult> RunAsync (Process process, ILog log, CancellationToken? cancellationToken = null, bool? diagnostics = null);
Task<ProcessExecutionResult> RunAsync (Process process, ILog log, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null);
Task<ProcessExecutionResult> RunAsync (Process process, ILog log, ILog stdoutLog, ILog stderrLog, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null);
Task<ProcessExecutionResult> RunAsync (Process process, ILog log, TextWriter StdoutStream, TextWriter StderrStream, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null);
Task<bool> WaitForExitAsync (Process process, TimeSpan? timeout = null);
Task KillTreeAsync (Process process, ILog log, bool? diagnostics = true);
Task KillTreeAsync (int pid, ILog log, bool? diagnostics = true);
void GetChildrenPS (ILog log, List<int> list, int pid);
}
}

Просмотреть файл

@ -8,26 +8,21 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Utils;
using Xharness;
using Xharness.Logging;
namespace Xharness
{
public class ProcessExecutionResult
{
public bool TimedOut { get; set; }
public int ExitCode { get; set; }
namespace Xharness.Execution {
public class ProcessManager : IProcessManager {
public ProcessManager ()
{
}
public bool Succeeded { get { return !TimedOut && ExitCode == 0; } }
}
public static class ProcessHelper
{
public static async Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, IList<string> args, ILog log, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
public async Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, IList<string> args, ILog log, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
using (var p = new Process ()) {
p.StartInfo.FileName = filename;
p.StartInfo.Arguments = StringUtils.FormatArguments (args);
return await p.RunAsync (log, true, timeout, environment_variables, cancellation_token);
return await RunAsync (p, log, timeout, environment_variables, cancellation_token);
}
}
@ -53,21 +48,18 @@ namespace Xharness
});
return rv.Task;
}
}
public static class Process_Extensions
{
public static async Task<ProcessExecutionResult> RunAsync (this Process process, ILog log, CancellationToken? cancellation_token = null, bool? diagnostics = null)
public async Task<ProcessExecutionResult> RunAsync (Process process, ILog log, CancellationToken? cancellation_token = null, bool? diagnostics = null)
{
return await RunAsync (process, log, log, log, cancellation_token: cancellation_token, diagnostics: diagnostics);
}
public static Task<ProcessExecutionResult> RunAsync (this Process process, ILog log, bool append = true, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
public Task<ProcessExecutionResult> RunAsync (Process process, ILog log, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
{
return RunAsync (process, log, log, log, timeout, environment_variables, cancellation_token, diagnostics);
}
public static Task<ProcessExecutionResult> RunAsync (this Process process, ILog log, ILog stdoutLog, ILog stderrLog, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
public Task<ProcessExecutionResult> RunAsync (Process process, ILog log, ILog stdoutLog, ILog stderrLog, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
{
if (stdoutLog is TextWriter StdoutStream && stderrLog is TextWriter StderrStream) {
return RunAsync (process, log, StdoutStream, StderrStream, timeout, environment_variables, cancellation_token, diagnostics);
@ -76,7 +68,7 @@ namespace Xharness
}
}
public static async Task<ProcessExecutionResult> RunAsync (this Process process, ILog log, TextWriter StdoutStream, TextWriter StderrStream, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
public async Task<ProcessExecutionResult> RunAsync (Process process, ILog log, TextWriter StdoutStream, TextWriter StderrStream, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null)
{
var stdout_completion = new TaskCompletionSource<bool> ();
var stderr_completion = new TaskCompletionSource<bool> ();
@ -149,19 +141,19 @@ namespace Xharness
}
if (!hasExited) {
StderrStream.WriteLine ($"Execution of {pid} was cancelled.");
ProcessHelper.kill (pid, 9);
kill (pid, 9);
}
});
if (timeout.HasValue) {
if (!await process.WaitForExitAsync (timeout.Value)) {
await process.KillTreeAsync (log, diagnostics ?? true);
if (!await WaitForExitAsync (process, timeout.Value)) {
await KillTreeAsync (process, log, diagnostics ?? true);
rv.TimedOut = true;
lock (StderrStream)
log.WriteLine ($"{pid} Execution timed out after {timeout.Value.TotalSeconds} seconds and the process was killed.");
}
}
await process.WaitForExitAsync ();
await WaitForExitAsync (process);
Task.WaitAll (new Task [] { stderr_completion.Task, stdout_completion.Task }, TimeSpan.FromSeconds (1));
try {
@ -173,7 +165,7 @@ namespace Xharness
return rv;
}
public async static Task<bool> WaitForExitAsync (this Process process, TimeSpan? timeout = null)
public async Task<bool> WaitForExitAsync (Process process, TimeSpan? timeout = null)
{
if (process.HasExited)
return true;
@ -205,12 +197,12 @@ namespace Xharness
}
}
public static Task KillTreeAsync (this Process @this, ILog log, bool? diagnostics = true)
public Task KillTreeAsync (Process process, ILog log, bool? diagnostics = true)
{
return KillTreeAsync (@this.Id, log, diagnostics);
return KillTreeAsync (process.Id, log, diagnostics);
}
public static async Task KillTreeAsync (int pid, ILog log, bool? diagnostics = true)
public async Task KillTreeAsync (int pid, ILog log, bool? diagnostics = true)
{
var pids = new List<int> ();
GetChildrenPS (log, pids, pid);
@ -220,7 +212,7 @@ namespace Xharness
log.WriteLine ("Writing process list:");
ps.StartInfo.FileName = "ps";
ps.StartInfo.Arguments = "-A -o pid,ruser,ppid,pgid,%cpu=%CPU,%mem=%MEM,flags=FLAGS,lstart,rss,vsz,tty,state,time,command";
await ps.RunAsync (log, true, TimeSpan.FromSeconds (5), diagnostics: false);
await RunAsync (ps, log, TimeSpan.FromSeconds (5), diagnostics: false);
}
foreach (var diagnose_pid in pids) {
@ -238,7 +230,7 @@ namespace Xharness
File.WriteAllText (template, commands.ToString ());
log.WriteLine ($"Printing backtrace for pid={pid}");
await dbg.RunAsync (log, true, TimeSpan.FromSeconds (30), diagnostics: false);
await RunAsync (dbg, log, TimeSpan.FromSeconds (30), diagnostics: false);
}
} finally {
try {
@ -253,14 +245,14 @@ namespace Xharness
// Send SIGABRT since that produces a crash report
// lldb may fail to attach to system processes, but crash reports will still be produced with potentially helpful stack traces.
for (int i = 0; i < pids.Count; i++)
ProcessHelper.kill (pids [i], 6);
kill (pids [i], 6);
// send kill -9 anyway as a last resort
for (int i = 0; i < pids.Count; i++)
ProcessHelper.kill (pids [i], 9);
kill (pids [i], 9);
}
static void GetChildrenPS (ILog log, List<int> list, int pid)
public void GetChildrenPS (ILog log, List<int> list, int pid)
{
string stdout;

Просмотреть файл

@ -8,11 +8,13 @@ using System.Runtime.Serialization.Json;
using System.Xml;
using System.Text;
using Xamarin;
using Xharness.Execution;
namespace Xharness
{
public static class GitHub
{
static IProcessManager ProcessManager { get; set; } = new ProcessManager ();
static WebClient CreateClient ()
{
var client = new WebClient ();
@ -152,7 +154,7 @@ namespace Xharness
git.StartInfo.FileName = "git";
git.StartInfo.Arguments = $"diff-tree --no-commit-id --name-only -r {base_commit}..{head_commit}";
var output = new StringWriter ();
var rv = git.RunAsync (harness.HarnessLog, output, output).Result;
var rv = ProcessManager.RunAsync (git, harness.HarnessLog, output, output).Result;
if (rv.Succeeded)
return output.ToString ().Split (new char [] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

Просмотреть файл

@ -11,6 +11,7 @@ using System.Xml;
using Xamarin.Utils;
using Xharness.BCLTestImporter;
using Xharness.Logging;
using Xharness.Execution;
namespace Xharness
{
@ -32,6 +33,7 @@ namespace Xharness
public bool UseSystem { get; set; } // if the system XI/XM should be used, or the locally build XI/XM.
public HashSet<string> Labels { get; } = new HashSet<string> ();
public XmlResultJargon XmlJargon { get; set; } = XmlResultJargon.NUnitV3;
public IProcessManager ProcessManager { get; set; } = new ProcessManager ();
public string XIBuildPath {
get { return Path.GetFullPath (Path.Combine (RootDirectory, "..", "tools", "xibuild", "xibuild")); }
@ -779,7 +781,7 @@ namespace Xharness
public Task<ProcessExecutionResult> ExecuteXcodeCommandAsync (string executable, IList<string> args, ILog log, TimeSpan timeout)
{
return ProcessHelper.ExecuteCommandAsync (Path.Combine (XcodeRoot, "Contents", "Developer", "usr", "bin", executable), args, log, timeout: timeout);
return ProcessManager.ExecuteCommandAsync (Path.Combine (XcodeRoot, "Contents", "Developer", "usr", "bin", executable), args, log, timeout: timeout);
}
public async Task ShowSimulatorList (Log log)
@ -801,7 +803,7 @@ namespace Xharness
var name = Path.GetFileName (report.Path);
var symbolicated = logs.Create (Path.ChangeExtension (name, ".symbolicated.log"), $"Symbolicated crash report: {name}", timestamp: false);
var environment = new Dictionary<string, string> { { "DEVELOPER_DIR", Path.Combine (XcodeRoot, "Contents", "Developer") } };
var rv = await ProcessHelper.ExecuteCommandAsync (symbolicatecrash, new [] { report.Path }, symbolicated, TimeSpan.FromMinutes (1), environment);
var rv = await ProcessManager.ExecuteCommandAsync (symbolicatecrash, new [] { report.Path }, symbolicated, TimeSpan.FromMinutes (1), environment);
if (rv.Succeeded) {;
log.WriteLine ("Symbolicated {0} successfully.", report.Path);
return symbolicated;
@ -830,7 +832,7 @@ namespace Xharness
sb.Add ("--devname");
sb.Add (device);
}
var result = await ProcessHelper.ExecuteCommandAsync (MlaunchPath, sb, log, TimeSpan.FromMinutes (1));
var result = await ProcessManager.ExecuteCommandAsync (MlaunchPath, sb, log, TimeSpan.FromMinutes (1));
if (result.Succeeded)
rv.UnionWith (File.ReadAllLines (tmp));
} finally {
@ -895,7 +897,7 @@ namespace Xharness
sb.Add ("--devname");
sb.Add (DeviceName);
}
var result = await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, sb, Log, TimeSpan.FromMinutes (1));
var result = await Harness.ProcessManager.ExecuteCommandAsync (Harness.MlaunchPath, sb, Log, TimeSpan.FromMinutes (1));
if (result.Succeeded) {
Log.WriteLine ("Downloaded crash report {0} to {1}", file, crash_report_target.Path);
crash_report_target = await Harness.SymbolicateCrashReportAsync (Logs, Log, crash_report_target);

Просмотреть файл

@ -7,6 +7,7 @@ using System.Net;
using System.Threading.Tasks;
using System.Text;
using Xharness.Logging;
using Xharness.Execution;
using Xharness.Jenkins.TestTasks;
namespace Xharness.Jenkins
@ -1212,7 +1213,7 @@ namespace Xharness.Jenkins
using (var process = new Process ()) {
process.StartInfo.FileName = Harness.PeriodicCommand;
process.StartInfo.Arguments = Harness.PeriodicCommandArguments;
var rv = await process.RunAsync (periodic_loc, timeout: Harness.PeriodicCommandInterval);
var rv = await Harness.ProcessManager.RunAsync (process, periodic_loc, timeout: Harness.PeriodicCommandInterval);
if (!rv.Succeeded)
periodic_loc.WriteLine ($"Periodic command failed with exit code {rv.ExitCode} (Timed out: {rv.TimedOut})");
}
@ -1278,7 +1279,7 @@ namespace Xharness.Jenkins
void BuildTestLibraries ()
{
ProcessHelper.ExecuteCommandAsync ("make", new [] { "all", $"-j{Environment.ProcessorCount}", "-C", Path.Combine (Harness.RootDirectory, "test-libraries") }, MainLog, TimeSpan.FromMinutes (10)).Wait ();
Harness.ProcessManager.ExecuteCommandAsync ("make", new [] { "all", $"-j{Environment.ProcessorCount}", "-C", Path.Combine (Harness.RootDirectory, "test-libraries") }, MainLog, TimeSpan.FromMinutes (10)).Wait ();
}
Task RunTestServer ()

Просмотреть файл

@ -50,7 +50,7 @@ namespace Xharness.Jenkins.TestTasks
LogEvent (log, "Restoring nugets for {0} ({1}) on path {2}", TestName, Mode, projectPath);
var timeout = TimeSpan.FromMinutes (15);
var result = await nuget.RunAsync (log, true, timeout);
var result = await ProcessManager.RunAsync (nuget, log, timeout);
if (result.TimedOut) {
log.WriteLine ("Nuget restore timed out after {0} seconds.", timeout.TotalSeconds);
return TestExecutingResult.TimedOut;

Просмотреть файл

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Xharness.Execution;
namespace Xharness.Jenkins.TestTasks
{
@ -7,6 +8,7 @@ namespace Xharness.Jenkins.TestTasks
{
public bool SpecifyPlatform = true;
public bool SpecifyConfiguration = true;
public IProcessManager ProcessManager { get; set; } = new ProcessManager ();
public override string Mode {
get { return Platform.ToString (); }

Просмотреть файл

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Xml;
using Xamarin;
using Xamarin.Utils;
using Xharness.Execution;
using Xharness.Logging;
namespace Xharness.Jenkins.TestTasks
@ -15,6 +16,7 @@ namespace Xharness.Jenkins.TestTasks
public string Path;
public bool BCLTest;
public bool IsUnitTest;
public IProcessManager ProcessManager { get; set; } = new ProcessManager ();
public MacExecuteTask (BuildToolTask build_task)
: base (build_task)
@ -97,7 +99,7 @@ namespace Xharness.Jenkins.TestTasks
try {
var timeout = TimeSpan.FromMinutes (20);
result = await proc.RunAsync (log, true, timeout);
result = await ProcessManager.RunAsync (proc, log, timeout);
if (result.TimedOut) {
FailureMessage = $"Execution timed out after {timeout.TotalSeconds} seconds.";
log.WriteLine (FailureMessage);

Просмотреть файл

@ -23,7 +23,7 @@ namespace Xharness.Jenkins.TestTasks
LogEvent (log, "Making {0} in {1}", Target, WorkingDirectory);
if (!Harness.DryRun) {
var timeout = Timeout;
var result = await make.RunAsync (log, true, timeout);
var result = await ProcessManager.RunAsync (make, log, timeout);
if (result.TimedOut) {
ExecutionResult = TestExecutingResult.TimedOut;
log.WriteLine ("Make timed out after {0} seconds.", timeout.TotalSeconds);

Просмотреть файл

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using System.Xml;
using Xamarin;
using Xamarin.Utils;
using Xharness.Execution;
using Xharness.Logging;
namespace Xharness.Jenkins.TestTasks
@ -20,6 +21,7 @@ namespace Xharness.Jenkins.TestTasks
public bool ProduceHtmlReport = true;
public bool InProcess;
public TimeSpan Timeout = TimeSpan.FromMinutes (10);
IProcessManager ProcessManager { get; set; } = new ProcessManager ();
public NUnitExecuteTask (BuildToolTask build_task)
: base (build_task)
@ -143,7 +145,7 @@ namespace Xharness.Jenkins.TestTasks
Jenkins.MainLog.WriteLine ("Executing {0} ({1})", TestName, Mode);
if (!Harness.DryRun) {
ExecutionResult = TestExecutingResult.Running;
var result = await proc.RunAsync (log, true, Timeout);
var result = await ProcessManager.RunAsync (proc, log, Timeout);
if (result.TimedOut) {
FailureMessage = $"Execution timed out after {Timeout.TotalMinutes} minutes.";
log.WriteLine (FailureMessage);

Просмотреть файл

@ -38,7 +38,7 @@ namespace Xharness.Jenkins.TestTasks
try {
var timeout = TimeSpan.FromMinutes (20);
var result = await proc.RunAsync (log, true, timeout);
var result = await ProcessManager.RunAsync (proc, log, timeout);
if (result.TimedOut) {
FailureMessage = $"Execution timed out after {timeout.TotalSeconds} seconds.";
log.WriteLine (FailureMessage);

Просмотреть файл

@ -40,7 +40,7 @@ namespace Xharness.Jenkins.TestTasks
LogEvent (BuildLog, "Building {0} ({1})", TestName, Mode);
if (!Harness.DryRun) {
var timeout = TimeSpan.FromMinutes (60);
var result = await xbuild.RunAsync (BuildLog, true, timeout);
var result = await ProcessManager.RunAsync (xbuild, BuildLog, timeout);
if (result.TimedOut) {
ExecutionResult = TestExecutingResult.TimedOut;
BuildLog.WriteLine ("Build timed out after {0} seconds.", timeout.TotalSeconds);
@ -75,7 +75,7 @@ namespace Xharness.Jenkins.TestTasks
SetEnvironmentVariables (xbuild);
LogEvent (log, "Cleaning {0} ({1}) - {2}", TestName, Mode, project_file);
var timeout = TimeSpan.FromMinutes (1);
await xbuild.RunAsync (log, true, timeout);
await ProcessManager.RunAsync (xbuild, log, timeout);
log.WriteLine ("Clean timed out after {0} seconds.", timeout.TotalSeconds);
Jenkins.MainLog.WriteLine ("Cleaned {0} ({1})", TestName, Mode);
}

Просмотреть файл

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using System.Xml;
using Xamarin;
using Xamarin.Utils;
using Xharness.Execution;
using Xharness.Logging;
namespace Xharness
@ -59,7 +60,7 @@ namespace Xharness
process.StartInfo.FileName = Harness.MlaunchPath;
process.StartInfo.Arguments = string.Format ("--sdkroot {0} --listsim {1}", Harness.XcodeRoot, tmpfile);
log.WriteLine ("Launching {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
var rv = await process.RunAsync (log, false, timeout: TimeSpan.FromSeconds (30));
var rv = await Harness.ProcessManager.RunAsync (process, log, timeout: TimeSpan.FromSeconds (30));
if (!rv.Succeeded)
throw new Exception ("Failed to list simulators.");
log.WriteLine ("Result:");
@ -450,6 +451,7 @@ namespace Xharness
{
public string UDID { get; set; }
public string Name { get; set; }
static IProcessManager ProcessManager { get; set; } = new ProcessManager ();
public string SimRuntime;
public string SimDeviceType;
public string DataPath;
@ -488,14 +490,14 @@ namespace Xharness
public static async Task KillEverythingAsync (ILog log)
{
await ProcessHelper.ExecuteCommandAsync ("launchctl", new [] { "remove", "com.apple.CoreSimulator.CoreSimulatorService" }, log, TimeSpan.FromSeconds (10));
await ProcessManager.ExecuteCommandAsync ("launchctl", new [] { "remove", "com.apple.CoreSimulator.CoreSimulatorService" }, log, TimeSpan.FromSeconds (10));
var to_kill = new string [] { "iPhone Simulator", "iOS Simulator", "Simulator", "Simulator (Watch)", "com.apple.CoreSimulator.CoreSimulatorService", "ibtoold" };
var args = new List<string> ();
args.Add ("-9");
args.AddRange (to_kill);
await ProcessHelper.ExecuteCommandAsync ("killall", args, log, TimeSpan.FromSeconds (10));
await ProcessManager.ExecuteCommandAsync ("killall", args, log, TimeSpan.FromSeconds (10));
foreach (var dir in new string [] {
Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), "Library", "Saved Application State", "com.apple.watchsimulator.savedState"),
@ -599,7 +601,7 @@ namespace Xharness
}
}
args.Add (sql.ToString ());
var rv = await ProcessHelper.ExecuteCommandAsync ("sqlite3", args, log, TimeSpan.FromSeconds (5));
var rv = await ProcessManager.ExecuteCommandAsync ("sqlite3", args, log, TimeSpan.FromSeconds (5));
if (!rv.Succeeded) {
failure = true;
break;
@ -614,7 +616,7 @@ namespace Xharness
}
log.WriteLine ("Current TCC database contents:");
await ProcessHelper.ExecuteCommandAsync ("sqlite3", new [] { TCC_db, ".dump" }, log, TimeSpan.FromSeconds (5));
await ProcessManager.ExecuteCommandAsync ("sqlite3", new [] { TCC_db, ".dump" }, log, TimeSpan.FromSeconds (5));
}
async Task OpenSimulator (ILog log)
@ -629,7 +631,7 @@ namespace Xharness
simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "iOS Simulator.app");
}
await ProcessHelper.ExecuteCommandAsync ("open", new [] { "-a", simulator_app, "--args", "-CurrentDeviceUDID", UDID }, log, TimeSpan.FromSeconds (15));
await ProcessManager.ExecuteCommandAsync ("open", new [] { "-a", simulator_app, "--args", "-CurrentDeviceUDID", UDID }, log, TimeSpan.FromSeconds (15));
}
public async Task PrepareSimulatorAsync (ILog log, params string[] bundle_identifiers)
@ -727,7 +729,7 @@ namespace Xharness
process.StartInfo.FileName = Harness.MlaunchPath;
process.StartInfo.Arguments = string.Format ("--sdkroot {0} --listdev={1} {2} --output-format=xml", Harness.XcodeRoot, tmpfile, extra_data ? "--list-extra-data" : string.Empty);
log.WriteLine ("Launching {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
var rv = await process.RunAsync (log, false, timeout: TimeSpan.FromSeconds (120));
var rv = await Harness.ProcessManager.RunAsync (process, log, timeout: TimeSpan.FromSeconds (120));
if (!rv.Succeeded)
throw new Exception ("Failed to list devices.");
log.WriteLine ("Result:");

Просмотреть файл

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Xharness.Execution;
using Xharness.Logging;
namespace Xharness.Tests.Execution.Tests {
[TestFixture]
public class ProcessManagerTests {
// not very portable, but we want to make sure that the class does the right thing
// maybe in the futue we could create a dummy process that will help us
string logPath;
string stdoutLogPath;
string stderrLogPath;
string stdoutMessage;
string stderrMessage;
Mock<ILogs> logs;
ILog executionLog;
ILog stdoutLog;
ILog stderrLog;
string dummyProcess;
Process testProcess;
ProcessManager manager;
Dictionary<string, string> environmentVariables = new Dictionary<string, string> ();
[SetUp]
public void SetUp ()
{
logPath = Path.GetTempFileName ();
stderrLogPath = Path.GetTempFileName ();
stdoutLogPath = Path.GetTempFileName ();
stdoutMessage = "Hola mundo!!!";
stderrMessage = "Adios mundo cruel";
logs = new Mock<ILogs> ();
executionLog = new LogFile (logs.Object, "my execution log", logPath);
stdoutLog = new LogFile (logs.Object, "my stdout log", stdoutLogPath);
stderrLog = new LogFile (logs.Object, "my stderr log", stderrLogPath);
dummyProcess = Path.Combine (Path.GetDirectoryName (GetType ().Assembly.Location), "DummyTestProcess.exe");
manager = new ProcessManager ();
testProcess = new Process ();
testProcess.StartInfo.FileName = "mono";
}
[TearDown]
public void TearDown ()
{
executionLog?.Dispose ();
executionLog = null;
stdoutLog?.Dispose ();
stdoutLog = null;
stderrLog?.Dispose ();
stderrLog = null;
testProcess?.Dispose ();
testProcess = null;
manager = null;
if (File.Exists (logPath))
File.Delete (logPath);
if (File.Exists (stderrLogPath))
File.Delete (stderrLogPath);
if(File.Exists (stdoutLogPath))
File.Delete (stdoutLogPath);
}
void AssertStdoutAndStderr ()
{
bool stdoutFound = false;
bool stderrFound = false;
using (var reader = new StreamReader (logPath)) {
string line;
while ((line = reader.ReadLine ()) != null) {
if (line.Contains (stdoutMessage))
stdoutFound = true;
if (line.Contains (stderrMessage))
stderrFound = true;
}
}
Assert.IsTrue (stdoutFound, "stdout was not captured");
Assert.IsTrue (stderrFound, "stderr was not captured");
}
[TestCase (0, 1, true, false, Description = "Success")] // 0, short timeout, success, no timeout
[TestCase (1, 1, false, false, Description = "Failure")] // 1, short timeout, failure, no timeout
[TestCase (0, 60, false, true, Description = "Timeout" )] // 0, long timeout, failure, timeout
public async Task ExecuteCommandAsyncTest (int resultCode, int timeoutCount, bool success, bool timeout)
{
var args = new List<string> ();
args.Add (dummyProcess);
args.Add ($"--exit-code={resultCode}");
args.Add ($"--timeout={timeoutCount}");
args.Add ($"--stdout=\"{stdoutMessage}\"");
args.Add ($"--stderr=\"{stderrMessage}\"");
var result = await manager.ExecuteCommandAsync ("mono", args, executionLog, new TimeSpan (0, 0, 5));
if (!timeout)
Assert.AreEqual (resultCode, result.ExitCode, "exit code");
Assert.AreEqual (success, result.Succeeded, "success");
Assert.AreEqual (timeout, result.TimedOut, "timeout");
AssertStdoutAndStderr ();
}
[TestCase (0, 1, true, false, Description = "Success")] // 0, short timeout, success, no timeout
[TestCase (1, 1, false, false, Description = "Failure")] // 1, short timeout, failure, no timeout
public async Task RunAsyncProcessNoArgsTest (int resultCode, int timeoutCount, bool success, bool timeout)
{
var source = new CancellationTokenSource ();
testProcess.StartInfo.Arguments = $"{dummyProcess} --exit-code={resultCode} --timeout={timeoutCount} --stdout=\"{stdoutMessage}\" --stderr=\"{stderrMessage}\"";
var result = await manager.RunAsync (testProcess, executionLog, source.Token);
if (!timeout)
Assert.AreEqual (resultCode, result.ExitCode, "exit code");
Assert.AreEqual (success, result.Succeeded, "success");
Assert.AreEqual (timeout, result.TimedOut, "timeout");
AssertStdoutAndStderr ();
}
[TestCase (0, 1, true, false, Description = "Success")] // 0, short timeout, success, no timeout
[TestCase (1, 1, false, false, Description = "Failure")] // 1, short timeout, failure, no timeout
[TestCase (0, 60, false, true, Description = "Timeout")] // 0, long timeout, failure, timeout
public async Task RunAsycnProcessAppendSuccessTest (int resultCode, int timeoutCounter, bool success, bool timeout)
{
// write some trash in the test log so that we ensure that we did append
string oldMessage = "Hello hello!";
using (var writer = new StreamWriter (logPath)) {
writer.WriteLine (oldMessage);
}
testProcess.StartInfo.Arguments = $"{dummyProcess} --exit-code={resultCode} --timeout={timeoutCounter} --stdout=\"{stdoutMessage}\" --stderr=\"{stderrMessage}\"";
var result = await manager.RunAsync (testProcess, executionLog, new TimeSpan (0, 0, 5));
if (!timeout)
Assert.AreEqual (resultCode, result.ExitCode, "exit code");
Assert.AreEqual (timeout, result.TimedOut, "timeout");
Assert.AreEqual (success, result.Succeeded, "success");
AssertStdoutAndStderr ();
}
[TestCase (0, 1, true, false, Description = "Success")] // 0, short timeout, success, no timeout
[TestCase (1, 1, false, false, Description = "Failure")] // 1, short timeout, failure, no timeout
[TestCase (0, 60, false, true, Description = "Timeout")] // 0, long timeout, failure, timeout
public async Task RunAsyncProcessTextWritersTest (int resultCode, int timeoutCounter, bool success, bool timeout)
{
//Task<ProcessExecutionResult> RunAsync (Process process, ILog log, ILog stdoutLog, ILog stderrLog, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null, bool? diagnostics = null);
var source = new CancellationTokenSource ();
testProcess.StartInfo.Arguments = $"{dummyProcess} --exit-code={resultCode} --timeout={timeoutCounter} --stdout=\"{stdoutMessage}\" --stderr=\"{stderrMessage}\"";
var result = await manager.RunAsync (testProcess, executionLog, stdoutLog, stderrLog, new TimeSpan (0, 0, 5), environmentVariables, source.Token);
if (!timeout)
Assert.AreEqual (resultCode, result.ExitCode, "exit code");
Assert.AreEqual (timeout, result.TimedOut, "timeout");
Assert.AreEqual (success, result.Succeeded, "success");
// assert the diff logs have the correct line
bool stdoutFound = false;
bool stderrFound = false;
using (var reader = new StreamReader (stdoutLogPath)) {
string line;
while ((line = reader.ReadLine ()) != null) {
if (line.Contains (stdoutMessage))
stdoutFound = true;
}
}
Assert.IsTrue (stdoutFound, "stdout was not captured");
using (var reader = new StreamReader (stderrLogPath)) {
string line;
while ((line = reader.ReadLine ()) != null) {
if (line.Contains (stderrMessage))
stderrFound = true;
}
}
Assert.IsTrue (stderrFound, "stderr was not captured");
}
}
}

Просмотреть файл

@ -62,6 +62,13 @@
<Compile Include="Listeners\Tests\SimpleListenerFactoryTest.cs" />
<Compile Include="Listeners\Tests\SimpleFileListenerTest.cs" />
<Compile Include="Listeners\Tests\SimpleTcpListenerTest.cs" />
<Compile Include="..\Execution\ProcessManager.cs">
<Link>Execution\ProcessManager.cs</Link>
</Compile>
<Compile Include="..\Execution\IProcessManager.cs">
<Link>Execution\IProcessManager.cs</Link>
</Compile>
<Compile Include="Execution\Tests\ProcessManagerTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -73,6 +80,9 @@
<EmbeddedResource Include="Samples\xUnitSample.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../DummyTestProcess/DummyTestProcess.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\xharness.csproj">
<Project>{e1f53f80-8399-499b-8017-c414b9cd263b}</Project>
<Name>xharness</Name>

Просмотреть файл

@ -108,7 +108,6 @@
<Compile Include="SolutionGenerator.cs" />
<Compile Include="MacTarget.cs" />
<Compile Include="Jenkins\Jenkins.cs" />
<Compile Include="Process_Extensions.cs" />
<Compile Include="Simulators.cs" />
<Compile Include="TestProject.cs" />
<Compile Include="GitHub.cs" />
@ -144,11 +143,14 @@
<Compile Include="Listeners\SimpleListener.cs" />
<Compile Include="Listeners\SimpleTcpListener.cs" />
<Compile Include="Listeners\SimpleListenerFactory.cs" />
<Compile Include="Execution\IProcessManager.cs" />
<Compile Include="Execution\ProcessManager.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="BCLTestImporter\" />
<Folder Include="Logging\" />
<Folder Include="Listeners\" />
<Folder Include="Execution\" />
</ItemGroup>
<ItemGroup>
<Content Include="xharness.js">

Просмотреть файл

@ -5,6 +5,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xharness", "xharness.csproj
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xharness.Tests", "Xharness.Tests\Xharness.Tests.csproj", "{AD1B78C3-6A36-40D0-B45D-246504A39D64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DummyTestProcess", "DummyTestProcess\DummyTestProcess.csproj", "{2318BC20-40F1-4A12-B695-10B0433CFB35}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -19,5 +21,9 @@ Global
{AD1B78C3-6A36-40D0-B45D-246504A39D64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD1B78C3-6A36-40D0-B45D-246504A39D64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD1B78C3-6A36-40D0-B45D-246504A39D64}.Release|Any CPU.Build.0 = Release|Any CPU
{2318BC20-40F1-4A12-B695-10B0433CFB35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2318BC20-40F1-4A12-B695-10B0433CFB35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2318BC20-40F1-4A12-B695-10B0433CFB35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2318BC20-40F1-4A12-B695-10B0433CFB35}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal