xamarin-macios/tests/xharness/AppRunner.cs

443 строки
16 KiB
C#
Исходник Обычный вид История

using System;
2016-05-26 16:06:52 +03:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xharness.Execution;
using Xharness.Execution.Mlaunch;
using Xharness.Hardware;
using Xharness.Jenkins.TestTasks;
using Xharness.Listeners;
using Xharness.Logging;
[Harness] Split out Targets, Utils and remove external links (#8061) * [Harness] Refactor process management to be testable. 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. * Apply suggestions from code review Co-Authored-By: Přemek Vysoký <premek.vysoky@microsoft.com> * Move out utils into a separate namespace * Move Cache to the test project * Fix namespaces after merge * Remove unneeded code * Move Target files * Sort csproj * Refactor Targets * Rename Cache to TempDirectory * Fix using * Move ProjectFileExtensions * Remove dead code * Move Extensions * Add empty StringUtilsTests * Add StringUtils tests * Revert refactorings * Update tests/xharness/Utilities/StringUtils.cs Co-Authored-By: Rolf Bjarne Kvinge <rolf@xamarin.com> * Update tests/xharness/Utilities/StringUtils.cs Co-Authored-By: Rolf Bjarne Kvinge <rolf@xamarin.com> * Add better formatarguments test * Update tests/xharness/Utilities/StringUtils.cs Co-Authored-By: Rolf Bjarne Kvinge <rolf@xamarin.com> Co-authored-by: Manuel de la Pena <mandel@microsoft.com> Co-authored-by: Premek Vysoky <prvysoky@microsoft.com> Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
2020-03-10 15:12:33 +03:00
using Xharness.Utilities;
2016-05-26 16:06:52 +03:00
namespace Xharness {
class AppRunner {
readonly IProcessManager processManager;
readonly ISimulatorsLoaderFactory simulatorsLoaderFactory;
readonly ISimpleListenerFactory listenerFactory;
readonly IDeviceLoaderFactory devicesLoaderFactory;
readonly ICrashSnapshotReporterFactory snapshotReporterFactory;
readonly ICaptureLogFactory captureLogFactory;
readonly IDeviceLogCapturerFactory deviceLogCapturerFactory;
readonly ITestReporterFactory testReporterFactory;
readonly RunMode runMode;
readonly bool isSimulator;
readonly TestTarget target;
readonly IHarness harness;
readonly double timeoutMultiplier;
readonly BuildToolTask buildTask;
string deviceName;
string companionDeviceName;
ISimulatorDevice [] simulators;
ISimulatorDevice simulator => simulators [0];
2016-05-26 16:06:52 +03:00
bool ensureCleanSimulatorState = true;
bool EnsureCleanSimulatorState {
get => ensureCleanSimulatorState && string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("SKIP_SIMULATOR_SETUP"));
set => ensureCleanSimulatorState = value;
}
public AppBundleInformation AppInformation { get; }
bool IsExtension => AppInformation.Extension.HasValue;
public TestExecutingResult Result { get; private set; }
public string FailureMessage { get; private set; }
public ILog MainLog { get; set; }
public ILogs Logs { get; }
public AppRunner (IProcessManager processManager,
IAppBundleInformationParser appBundleInformationParser,
ISimulatorsLoaderFactory simulatorsFactory,
ISimpleListenerFactory simpleListenerFactory,
IDeviceLoaderFactory devicesFactory,
ICrashSnapshotReporterFactory snapshotReporterFactory,
ICaptureLogFactory captureLogFactory,
IDeviceLogCapturerFactory deviceLogCapturerFactory,
ITestReporterFactory reporterFactory,
TestTarget target,
IHarness harness,
ILog mainLog,
ILogs logs,
string projectFilePath,
string buildConfiguration,
ISimulatorDevice [] simulators = null,
string deviceName = null,
string companionDeviceName = null,
bool ensureCleanSimulatorState = false,
double timeoutMultiplier = 1,
string variation = null,
BuildToolTask buildTask = null)
{
if (appBundleInformationParser is null)
throw new ArgumentNullException (nameof (appBundleInformationParser));
this.processManager = processManager ?? throw new ArgumentNullException (nameof (processManager));
this.simulatorsLoaderFactory = simulatorsFactory ?? throw new ArgumentNullException (nameof (simulatorsFactory));
this.listenerFactory = simpleListenerFactory ?? throw new ArgumentNullException (nameof (simpleListenerFactory));
this.devicesLoaderFactory = devicesFactory ?? throw new ArgumentNullException (nameof (devicesFactory));
this.snapshotReporterFactory = snapshotReporterFactory ?? throw new ArgumentNullException (nameof (snapshotReporterFactory));
this.captureLogFactory = captureLogFactory ?? throw new ArgumentNullException (nameof (captureLogFactory));
this.deviceLogCapturerFactory = deviceLogCapturerFactory ?? throw new ArgumentNullException (nameof (deviceLogCapturerFactory));
this.testReporterFactory = reporterFactory ?? throw new ArgumentNullException (nameof (testReporterFactory));
this.harness = harness ?? throw new ArgumentNullException (nameof (harness));
this.MainLog = mainLog ?? throw new ArgumentNullException (nameof (mainLog));
this.Logs = logs ?? throw new ArgumentNullException (nameof (logs));
this.timeoutMultiplier = timeoutMultiplier;
this.deviceName = deviceName;
this.companionDeviceName = companionDeviceName;
this.ensureCleanSimulatorState = ensureCleanSimulatorState;
this.simulators = simulators;
this.buildTask = buildTask;
this.target = target;
runMode = target.ToRunMode ();
isSimulator = target.IsSimulator ();
AppInformation = appBundleInformationParser.ParseFromProject (projectFilePath, target, buildConfiguration);
AppInformation.Variation = variation;
}
async Task<bool> FindSimulatorAsync ()
2016-05-26 16:06:52 +03:00
{
if (simulators != null)
return true;
var sims = simulatorsLoaderFactory.CreateLoader ();
await sims.LoadAsync (Logs.Create ($"simulator-list-{Helpers.Timestamp}.log", "Simulator list"), false, false);
simulators = await sims.FindAsync (target, MainLog);
2016-05-26 16:06:52 +03:00
return simulators != null;
2016-05-26 16:06:52 +03:00
}
void FindDevice ()
{
if (deviceName != null)
2016-05-26 16:06:52 +03:00
return;
deviceName = Environment.GetEnvironmentVariable ("DEVICE_NAME");
if (!string.IsNullOrEmpty (deviceName))
2016-05-26 16:06:52 +03:00
return;
var devs = devicesLoaderFactory.CreateLoader ();
Task.Run (async () => {
await devs.LoadAsync (MainLog, false, false);
}).Wait ();
2016-05-26 16:06:52 +03:00
DeviceClass [] deviceClasses;
switch (runMode) {
case RunMode.iOS:
deviceClasses = new [] { DeviceClass.iPhone, DeviceClass.iPad, DeviceClass.iPod };
break;
case RunMode.WatchOS:
deviceClasses = new [] { DeviceClass.Watch };
break;
case RunMode.TvOS:
deviceClasses = new [] { DeviceClass.AppleTV }; // Untested
break;
default:
throw new ArgumentException (nameof (runMode));
}
2016-05-26 16:06:52 +03:00
var selected = devs.ConnectedDevices.Where ((v) => deviceClasses.Contains (v.DeviceClass) && v.IsUsableForDebugging != false);
IHardwareDevice selected_data;
if (selected.Count () == 0) {
throw new NoDeviceFoundException ($"Could not find any applicable devices with device class(es): {string.Join (", ", deviceClasses)}");
} else if (selected.Count () > 1) {
selected_data = selected
.OrderBy ((dev) => {
Version v;
if (Version.TryParse (dev.ProductVersion, out v))
return v;
return new Version ();
})
.First ();
MainLog.WriteLine ("Found {0} devices for device class(es) '{1}': '{2}'. Selected: '{3}' (because it has the lowest version).", selected.Count (), string.Join ("', '", deviceClasses), string.Join ("', '", selected.Select ((v) => v.Name).ToArray ()), selected_data.Name);
} else {
selected_data = selected.First ();
}
deviceName = selected_data.Name;
2016-05-26 16:06:52 +03:00
if (runMode == RunMode.WatchOS)
companionDeviceName = devs.FindCompanionDevice (MainLog, selected_data).Name;
2016-05-26 16:06:52 +03:00
}
public async Task<ProcessExecutionResult> InstallAsync (CancellationToken cancellation_token)
2016-05-26 16:06:52 +03:00
{
if (isSimulator) {
// We reset the simulator when running, so a separate install step does not make much sense.
throw new InvalidOperationException ("Installing to a simulator is not supported.");
2016-05-26 16:06:52 +03:00
}
FindDevice ();
var args = new MlaunchArguments ();
for (int i = -1; i < harness.Verbosity; i++)
args.Add (new VerbosityArgument ());
args.Add (new InstallAppOnDeviceArgument (AppInformation.AppPath));
args.Add (new DeviceNameArgument (companionDeviceName ?? deviceName));
2016-05-26 16:06:52 +03:00
if (runMode == RunMode.WatchOS) {
args.Add (new DeviceArgument ("ios,watchos"));
Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. (#7177) * Implement a different escaping/quoting algorithm for arguments to System.Diagnostics.Process. mono changed how quotes should be escaped when passed to System.Diagnostic.Process, so we need to change accordingly. The main difference is that single quotes don't have to be escaped anymore. This solves problems like this: System.ComponentModel.Win32Exception : ApplicationName='nuget', CommandLine='restore '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable/CellCustomTable.sln' -Verbosity detailed -SolutionDir '/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories/ios-samples/WorkingWithTables/Part 3 - Customizing a Table\'s appearance/3 - CellCustomTable'', CurrentDirectory='/Users/vsts/agent/2.158.0/work/1/s/tests/sampletester/bin/Debug/repositories', Native error= Cannot find the specified file at System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) [0x0029f] in /Users/builder/jenkins/workspace/build-package-osx-mono/2019-08/external/bockbuild/builds/mono-x64/mcs/class/System/System.Diagnostics/Process.cs:778 ref: https://github.com/mono/mono/pull/15047 * Rework process arguments to pass arrays/lists around instead of quoted strings. And then only convert to a string at the very end when we create the Process instance. In the future there will be a ProcessStartInfo.ArgumentList property we can use to give the original array/list of arguments directly to the BCL so that we can avoid quoting at all. These changes gets us almost all the way there already (except that the ArgumentList property isn't available quite yet). We also have to bump to target framework version v4.7.2 from v4.5 in several places because of 'Array.Empty<T> ()' which is now used in more places. * Parse linker flags from LinkWith attributes. * [sampletester] Bump to v4.7.2 for Array.Empty<T> (). * Fix typo. * Rename GetVerbosity -> AddVerbosity. * Remove unnecessary string interpolation. * Remove unused variable. * [mtouch] Simplify code a bit. * Use implicitly typed arrays.
2019-10-14 17:18:46 +03:00
}
2016-05-26 16:06:52 +03:00
var totalSize = Directory.GetFiles (AppInformation.AppPath, "*", SearchOption.AllDirectories).Select ((v) => new FileInfo (v).Length).Sum ();
MainLog.WriteLine ($"Installing '{AppInformation.AppPath}' to '{companionDeviceName ?? deviceName}'. Size: {totalSize} bytes = {totalSize / 1024.0 / 1024.0:N2} MB");
return await processManager.ExecuteCommandAsync (args, MainLog, TimeSpan.FromHours (1), cancellation_token: cancellation_token);
2016-05-26 16:06:52 +03:00
}
public async Task<ProcessExecutionResult> UninstallAsync ()
{
if (isSimulator)
throw new InvalidOperationException ("Uninstalling from a simulator is not supported.");
FindDevice ();
var args = new MlaunchArguments ();
for (int i = -1; i < harness.Verbosity; i++)
args.Add (new VerbosityArgument ());
args.Add (new UninstallAppFromDeviceArgument (AppInformation.BundleIdentifier));
args.Add (new DeviceNameArgument (companionDeviceName ?? deviceName));
return await processManager.ExecuteCommandAsync (args, MainLog, TimeSpan.FromMinutes (1));
}
public async Task<int> RunAsync ()
2016-05-26 16:06:52 +03:00
{
if (!isSimulator)
FindDevice ();
var args = new MlaunchArguments ();
for (int i = -1; i < harness.Verbosity; i++)
args.Add (new VerbosityArgument ());
args.Add (new SetAppArgumentArgument ("-connection-mode"));
args.Add (new SetAppArgumentArgument ("none")); // This will prevent the app from trying to connect to any IDEs
args.Add (new SetAppArgumentArgument ("-autostart", true));
args.Add (new SetEnvVariableArgument ("NUNIT_AUTOSTART", true));
args.Add (new SetAppArgumentArgument ("-autoexit", true));
args.Add (new SetEnvVariableArgument ("NUNIT_AUTOEXIT", true));
args.Add (new SetAppArgumentArgument ("-enablenetwork", true));
args.Add (new SetEnvVariableArgument ("NUNIT_ENABLE_NETWORK", true));
// detect if we are using a jenkins bot.
var useXmlOutput = harness.InCI;
if (useXmlOutput) {
args.Add (new SetEnvVariableArgument ("NUNIT_ENABLE_XML_OUTPUT", true));
args.Add (new SetEnvVariableArgument ("NUNIT_ENABLE_XML_MODE", "wrapped"));
args.Add (new SetEnvVariableArgument ("NUNIT_XML_VERSION", "nunitv3"));
}
if (harness.InCI) {
// We use the 'BUILD_REVISION' variable to detect whether we're running CI or not.
args.Add (new SetEnvVariableArgument ("BUILD_REVISION", Environment.GetEnvironmentVariable ("BUILD_REVISION")));
}
if (!harness.GetIncludeSystemPermissionTests (TestPlatform.iOS, !isSimulator))
args.Add (new SetEnvVariableArgument ("DISABLE_SYSTEM_PERMISSION_TESTS", 1));
2016-05-26 16:06:52 +03:00
if (isSimulator) {
args.Add (new SetAppArgumentArgument ("-hostname:127.0.0.1", true));
args.Add (new SetEnvVariableArgument ("NUNIT_HOSTNAME", "127.0.0.1"));
2016-05-26 16:06:52 +03:00
} else {
var ips = new StringBuilder ();
var ipAddresses = System.Net.Dns.GetHostEntry (System.Net.Dns.GetHostName ()).AddressList;
for (int i = 0; i < ipAddresses.Length; i++) {
if (i > 0)
ips.Append (',');
ips.Append (ipAddresses [i].ToString ());
}
var ipArg = ips.ToString ();
args.Add (new SetAppArgumentArgument ($"-hostname:{ipArg}", true));
args.Add (new SetEnvVariableArgument ("NUNIT_HOSTNAME", ipArg));
2016-05-26 16:06:52 +03:00
}
var listener_log = Logs.Create ($"test-{runMode.ToString ().ToLowerInvariant ()}-{Helpers.Timestamp}.log", LogType.TestLog.ToString (), timestamp: !useXmlOutput);
var (transport, listener, listenerTmpFile) = listenerFactory.Create (runMode, MainLog, listener_log, isSimulator, true, useXmlOutput);
listener.Initialize ();
args.Add (new SetAppArgumentArgument ($"-transport:{transport}", true));
args.Add (new SetEnvVariableArgument ("NUNIT_TRANSPORT", transport.ToString ().ToUpper ()));
if (transport == ListenerTransport.File)
args.Add (new SetEnvVariableArgument ("NUNIT_LOG_FILE", listenerTmpFile));
2016-05-26 16:06:52 +03:00
args.Add (new SetAppArgumentArgument ($"-hostport:{listener.Port}", true));
args.Add (new SetEnvVariableArgument ("NUNIT_HOSTPORT", listener.Port));
2016-05-26 16:06:52 +03:00
listener.StartAsync ();
// object that will take care of capturing and parsing the results
ILog runLog = MainLog;
var crashLogs = new Logs (Logs.Directory);
ICrashSnapshotReporter crashReporter = snapshotReporterFactory.Create (MainLog, crashLogs, isDevice: !isSimulator, deviceName);
var testReporterTimeout = TimeSpan.FromMinutes (harness.Timeout * timeoutMultiplier);
var testReporter = testReporterFactory.Create (MainLog,
runLog,
Logs,
crashReporter,
listener,
new XmlResultParser (),
AppInformation,
runMode,
harness.XmlJargon,
deviceName,
testReporterTimeout,
harness.LaunchTimeout,
buildTask?.Logs?.Directory,
(level, message) => harness.Log (level, message));
listener.ConnectedTask
.TimeoutAfter (TimeSpan.FromMinutes (harness.LaunchTimeout))
.ContinueWith (testReporter.LaunchCallback)
.DoNotAwait ();
args.AddRange (harness.EnvironmentVariables.Select (kvp => new SetEnvVariableArgument (kvp.Key, kvp.Value)));
if (IsExtension) {
switch (AppInformation.Extension) {
case Extension.TodayExtension:
args.Add (isSimulator
? (MlaunchArgument) new LaunchSimulatorExtensionArgument (AppInformation.LaunchAppPath, AppInformation.BundleIdentifier)
: new LaunchDeviceExtensionArgument (AppInformation.LaunchAppPath, AppInformation.BundleIdentifier));
break;
case Extension.WatchKit2:
default:
throw new NotImplementedException ();
}
} else {
args.Add (isSimulator
? (MlaunchArgument) new LaunchSimulatorArgument (AppInformation.LaunchAppPath)
: new LaunchDeviceArgument (AppInformation.LaunchAppPath));
}
if (!isSimulator)
args.Add (new DisableMemoryLimitsArgument ());
2016-05-26 16:06:52 +03:00
if (isSimulator) {
if (!await FindSimulatorAsync ())
return 1;
if (runMode != RunMode.WatchOS) {
var stderr_tty = harness.GetStandardErrorTty ();
if (!string.IsNullOrEmpty (stderr_tty)) {
args.Add (new SetStdoutArgument (stderr_tty));
args.Add (new SetStderrArgument (stderr_tty));
} else {
var stdout_log = Logs.CreateFile ($"stdout-{Helpers.Timestamp}.log", "Standard output");
var stderr_log = Logs.CreateFile ($"stderr-{Helpers.Timestamp}.log", "Standard error");
args.Add (new SetStdoutArgument (stdout_log));
args.Add (new SetStderrArgument (stderr_log));
}
}
var systemLogs = new List<ICaptureLog> ();
foreach (var sim in simulators) {
// Upload the system log
MainLog.WriteLine ("System log for the '{1}' simulator is: {0}", sim.SystemLog, sim.Name);
bool isCompanion = sim != simulator;
var logDescription = isCompanion ? LogType.CompanionSystemLog.ToString () : LogType.SystemLog.ToString ();
var log = captureLogFactory.Create (
Path.Combine (Logs.Directory, sim.Name + ".log"),
sim.SystemLog,
harness.Action != HarnessAction.Jenkins,
logDescription);
log.StartCapture ();
Logs.Add (log);
systemLogs.Add (log);
WrenchLog.WriteLine ("AddFile: {0}", log.FullPath);
}
MainLog.WriteLine ("*** Executing {0}/{1} in the simulator ***", AppInformation.AppName, runMode);
if (EnsureCleanSimulatorState) {
foreach (var sim in simulators)
await sim.PrepareSimulatorAsync (MainLog, AppInformation.BundleIdentifier);
}
2016-05-26 16:06:52 +03:00
args.Add (new SimulatorUDIDArgument (simulator.UDID));
2016-05-26 16:06:52 +03:00
await crashReporter.StartCaptureAsync ();
2016-05-26 16:06:52 +03:00
MainLog.WriteLine ("Starting test run");
2016-05-26 16:06:52 +03:00
await testReporter.CollectSimulatorResult (
processManager.ExecuteCommandAsync (args, runLog, testReporterTimeout, cancellation_token: testReporter.CancellationToken));
2016-05-26 16:06:52 +03:00
// cleanup after us
if (EnsureCleanSimulatorState)
await simulator.KillEverythingAsync (MainLog);
foreach (var log in systemLogs)
log.StopCapture ();
2016-05-26 16:06:52 +03:00
} else {
MainLog.WriteLine ("*** Executing {0}/{1} on device '{2}' ***", AppInformation.AppName, runMode, deviceName);
2016-05-26 16:06:52 +03:00
if (runMode == RunMode.WatchOS) {
args.Add (new AttachNativeDebuggerArgument ()); // this prevents the watch from backgrounding the app.
} else {
args.Add (new WaitForExitArgument ());
}
args.Add (new DeviceNameArgument (deviceName));
2016-05-26 16:06:52 +03:00
var deviceSystemLog = Logs.Create ($"device-{deviceName}-{Helpers.Timestamp}.log", "Device log");
var deviceLogCapturer = deviceLogCapturerFactory.Create (harness.HarnessLog, deviceSystemLog, deviceName);
deviceLogCapturer.StartCapture ();
2016-05-26 16:06:52 +03:00
try {
await crashReporter.StartCaptureAsync ();
MainLog.WriteLine ("Starting test run");
// We need to check for MT1111 (which means that mlaunch won't wait for the app to exit).
var aggregatedLog = Log.CreateAggregatedLog (testReporter.CallbackLog, MainLog);
Task<ProcessExecutionResult> runTestTask = processManager.ExecuteCommandAsync (
args,
aggregatedLog,
testReporterTimeout,
cancellation_token: testReporter.CancellationToken);
await testReporter.CollectDeviceResult (runTestTask);
} finally {
deviceLogCapturer.StopCapture ();
deviceSystemLog.Dispose ();
2016-05-26 16:06:52 +03:00
}
// Upload the system log
if (File.Exists (deviceSystemLog.FullPath)) {
MainLog.WriteLine ("A capture of the device log is: {0}", deviceSystemLog.FullPath);
WrenchLog.WriteLine ("AddFile: {0}", deviceSystemLog.FullPath);
2016-05-26 16:06:52 +03:00
}
}
listener.Cancel ();
2016-05-26 16:06:52 +03:00
listener.Dispose ();
// check the final status, copy all the required data
(Result, FailureMessage) = await testReporter.ParseResult ();
return testReporter.Success.Value ? 0 : 1;
2016-05-26 16:06:52 +03:00
}
}
}