Merge pull request #295 from rolfbjarne/xharness-backport-from-xcode8

[xharness] Merge xcode8 changes back to master.
This commit is contained in:
Rolf Bjarne Kvinge 2016-06-28 05:53:00 -07:00 коммит произвёл GitHub
Родитель 101a1c5e6a 613fb2118c
Коммит 52af5a54d0
13 изменённых файлов: 1036 добавлений и 799 удалений

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

@ -45,39 +45,27 @@ namespace xharness
set { log_directory = value; }
}
public LogFiles Logs = new LogFiles ();
Log main_log;
public Logs Logs = new Logs ();
public Log MainLog {
get { return main_log; }
set { main_log = value; }
}
public SimDevice [] Simulators {
get { return simulators; }
set { simulators = value; }
}
string mode;
LogFile SymbolicateCrashReport (LogFile report)
{
var symbolicatecrash = Path.Combine (Harness.XcodeRoot, "Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash");
if (!File.Exists (symbolicatecrash))
symbolicatecrash = Path.Combine (Harness.XcodeRoot, "Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash");
if (!File.Exists (symbolicatecrash)) {
Harness.Log ("Can't symbolicate {0} because the symbolicatecrash script {1} does not exist", report.Path, symbolicatecrash);
return report;
public string BundleIdentifier {
get {
return bundle_identifier;
}
var output = new StringBuilder ();
if (ExecuteCommand (symbolicatecrash, "\"" + report.Path + "\"", true, captured_output: output, environment_variables: new Dictionary<string, string> { { "DEVELOPER_DIR", Path.Combine (Harness.XcodeRoot, "Contents", "Developer") }})) {
var rv = Logs.Create (LogDirectory, report.Path + ".symbolicated", "Symbolicated crash report: " + Path.GetFileName (report.Path));
File.WriteAllText (rv.Path, output.ToString ());
Harness.Log ("Symbolicated {0} successfully.", report.Path);
return rv;
}
Harness.Log ("Failed to symbolicate {0}:\n{1}", report.Path, output.ToString ());
return report;
}
string mode;
void FindSimulator ()
{
if (simulators != null)
@ -96,7 +84,7 @@ namespace xharness
simulator_runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + Xamarin.SdkVersions.iOS.Replace ('.', '-');
break;
case "ios-simulator":
simulator_devicetype = "com.apple.CoreSimulator.SimDeviceType.iPhone-4s";
simulator_devicetype = "com.apple.CoreSimulator.SimDeviceType.iPhone-5";
simulator_runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + Xamarin.SdkVersions.iOS.Replace ('.', '-');
break;
case "tvos-simulator":
@ -116,7 +104,7 @@ namespace xharness
};
Task.Run (async () =>
{
await sims.LoadAsync (Logs.Create (LogDirectory, "simulator-list.log", "Simulator list"));
await sims.LoadAsync (Logs.CreateStream (LogDirectory, "simulator-list.log", "Simulator list"));
}).Wait ();
var devices = sims.AvailableDevices.Where ((SimDevice v) => v.SimRuntime == simulator_runtime && v.SimDeviceType == simulator_devicetype);
@ -153,9 +141,9 @@ namespace xharness
if (simulators == null)
throw new Exception ("Could not find simulator");
Harness.Log (1, "Found simulator: {0} {1}", simulators [0].Name, simulators [0].UDID);
main_log.WriteLine ("Found simulator: {0} {1}", simulators [0].Name, simulators [0].UDID);
if (simulators.Length > 1)
Harness.Log (1, "Found companion simulator: {0} {1}", simulators [1].Name, simulators [1].UDID);
main_log.WriteLine ("Found companion simulator: {0} {1}", simulators [1].Name, simulators [1].UDID);
}
void FindDevice ()
@ -167,10 +155,12 @@ namespace xharness
if (!string.IsNullOrEmpty (device_name))
return;
var devs = new Devices ();
var devs = new Devices () {
Harness = Harness,
};
Task.Run (async () =>
{
await devs.LoadAsync ();
await devs.LoadAsync (main_log);
}).Wait ();
string [] deviceClasses;
@ -194,7 +184,7 @@ namespace xharness
throw new Exception ($"Could not find any applicable devices with device class(es): {string.Join (", ", deviceClasses)}");
} else if (selected.Count () > 1) {
selected_data = selected.First ();
Harness.Log ("Found {0} devices for device class(es) {1}: {2}. Selected: '{3}'", selected.Count (), string.Join (", ", deviceClasses), string.Join (", ", selected.Select ((v) => v.Name).ToArray ()), selected_data.Name);
main_log.WriteLine ("Found {0} devices for device class(es) {1}: {2}. Selected: '{3}'", selected.Count (), string.Join (", ", deviceClasses), string.Join (", ", selected.Select ((v) => v.Name).ToArray ()), selected_data.Name);
} else {
selected_data = selected.First ();
}
@ -205,119 +195,11 @@ namespace xharness
if (companion.Count () == 0)
throw new Exception ($"Could not find the companion device for '{selected_data.Name}'");
else if (companion.Count () > 1)
Harness.Log ("Found {0} companion devices for {1}?!?", companion.Count (), selected_data.Name);
main_log.WriteLine ("Found {0} companion devices for {1}?!?", companion.Count (), selected_data.Name);
companion_device_name = companion.First ().Name;
}
}
public void AgreeToPrompts (bool delete_first = true)
{
var TCC_db = Path.Combine (simulator.DataPath, "data", "Library", "TCC", "TCC.db");
var sim_services = new string [] {
"kTCCServiceAddressBook",
"kTCCServicePhotos",
"kTCCServiceUbiquity",
"kTCCServiceWillow"
};
var failure = false;
var tcc_edit_timeout = 5;
var watch = new Stopwatch ();
watch.Start ();
do {
failure = false;
foreach (var service in sim_services) {
if (delete_first && !ExecuteCommand ("sqlite3", string.Format ("{0} \"DELETE FROM access WHERE service = '{1}' and client ='{2}';\"", TCC_db, service, bundle_identifier), true, output_verbosity_level: 1)) {
failure = true;
}
if (!failure && !ExecuteCommand ("sqlite3", string.Format ("{0} \"INSERT INTO access VALUES('{1}','{2}',0,1,0,NULL,NULL);\"", TCC_db, service, bundle_identifier), true, output_verbosity_level: 1)) {
failure = true;
}
}
if (failure) {
if (watch.Elapsed.TotalSeconds > tcc_edit_timeout)
break;
Harness.Log ("Failed to edit TCC.db, trying again in 1 second... ", (int) (tcc_edit_timeout - watch.Elapsed.TotalSeconds));
Thread.Sleep (TimeSpan.FromSeconds (1));
}
} while (failure);
if (failure) {
Harness.Log ("Failed to edit TCC.db, the test run might hang due to permission request dialogs");
} else {
Harness.Log ("Successfully edited TCC.db");
}
}
bool simulator_prepared;
public void PrepareSimulator ()
{
if (simulator_prepared)
return;
simulator_prepared = true;
if (SkipSimulatorSetup) {
AgreeToPrompts (false);
Harness.Log (0, "Simulator setup skipped.");
return;
}
KillEverything ();
ShowSimulatorList ();
// We shutdown and erase all simulators.
// We only fixup TCC.db on the main simulator.
foreach (var sim in simulators) {
var udid = sim.UDID;
// erase the simulator (make sure the device isn't running first)
ExecuteXcodeCommand ("simctl", "shutdown " + udid, true, output_verbosity_level: 1, timeout: TimeSpan.FromMinutes (1));
ExecuteXcodeCommand ("simctl", "erase " + udid, true, output_verbosity_level: 1, timeout: TimeSpan.FromMinutes (1));
// boot & shutdown to make sure it actually works
ExecuteXcodeCommand ("simctl", "boot " + udid, true, output_verbosity_level: 1, timeout: TimeSpan.FromMinutes (1));
ExecuteXcodeCommand ("simctl", "shutdown " + udid, true, output_verbosity_level: 1, timeout: TimeSpan.FromMinutes (1));
}
// Edit the permissions to prevent dialog boxes in the test app
var TCC_db = Path.Combine (simulator.DataPath, "data", "Library", "TCC", "TCC.db");
if (!File.Exists (TCC_db)) {
Harness.Log ("Opening simulator to create TCC.db");
var simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "Simulator.app");
if (!Directory.Exists (simulator_app))
simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "iOS Simulator.app");
ExecuteCommand ("open", "-a \"" + simulator_app + "\" --args -CurrentDeviceUDID " + simulator.UDID, output_verbosity_level: 1);
var tcc_creation_timeout = 60;
var watch = new Stopwatch ();
watch.Start ();
while (!File.Exists (TCC_db) && watch.Elapsed.TotalSeconds < tcc_creation_timeout) {
Harness.Log ("Waiting for simulator to create TCC.db... {0}", (int) (tcc_creation_timeout - watch.Elapsed.TotalSeconds));
Thread.Sleep (TimeSpan.FromSeconds (1));
}
}
if (File.Exists (TCC_db)) {
AgreeToPrompts (true);
} else {
Harness.Log ("No TCC.db found for the simulator {0} (SimRuntime={1} and SimDeviceType={1})", simulator.UDID, simulator.SimRuntime, simulator.SimDeviceType);
}
KillEverything ();
foreach (var sim in simulators) {
ExecuteXcodeCommand ("simctl", "shutdown " + sim.UDID, true, output_verbosity_level: 1, timeout: TimeSpan.FromMinutes (1));
if (!File.Exists (sim.SystemLog)) {
Harness.Log ("No system log found for SimRuntime={0} and SimDeviceType={1}", sim.SimRuntime, sim.SimDeviceType);
} else {
File.WriteAllText (sim.SystemLog, string.Format (" *** This log file was cleared out by Xamarin.iOS's test run at {0} **** \n", DateTime.Now.ToString ()));
}
}
}
bool initialized;
public void Initialize ()
{
@ -389,7 +271,7 @@ namespace xharness
}
}
public int Install ()
public int Install (Log log)
{
Initialize ();
@ -413,35 +295,26 @@ namespace xharness
if (mode == "watchos")
args.Append (" --device ios,watchos");
var success = ExecuteCommand (Harness.MlaunchPath, args.ToString ());
return success ? 0 : 1;
var rv = ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args.ToString (), log, TimeSpan.FromHours (1)).Result;
return rv.Succeeded ? 0 : 1;
}
bool skip_simulator_setup;
public bool SkipSimulatorSetup {
bool ensure_clean_simulator_state = true;
public bool EnsureCleanSimulatorState {
get {
return skip_simulator_setup || !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("SKIP_SIMULATOR_SETUP"));
return ensure_clean_simulator_state || !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("SKIP_SIMULATOR_SETUP"));
}
set {
skip_simulator_setup = value;
ensure_clean_simulator_state = value;
}
}
bool skip_simulator_cleanup;
public bool SkipSimulatorCleanup {
get {
return skip_simulator_cleanup || !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("SKIP_SIMULATOR_CLEANUP"));
}
set {
skip_simulator_cleanup = value;
}
}
public int Run ()
public async Task<int> RunAsync ()
{
HashSet<string> start_crashes = null;
LogFile device_system_log = null;
LogFile listener_log = null;
LogStream device_system_log = null;
LogStream listener_log = null;
Log run_log = main_log;
Initialize ();
@ -487,8 +360,9 @@ namespace xharness
default:
throw new NotImplementedException ();
}
listener_log = Logs.Create (LogDirectory, string.Format ("test-{0:yyyyMMdd_HHmmss}.log", DateTime.Now), "Test log");
listener.LogPath = listener_log.Path;
listener_log = Logs.CreateStream (LogDirectory, string.Format ("test-{0:yyyyMMdd_HHmmss}.log", DateTime.Now), "Test log");
listener.TestLog = listener_log;
listener.Log = main_log;
listener.AutoExit = true;
listener.Address = System.Net.IPAddress.Any;
listener.Initialize ();
@ -502,130 +376,122 @@ namespace xharness
if (isSimulator) {
FindSimulator ();
Harness.Log ("*** Executing {0}/{1} in the simulator ***", appName, mode);
var systemLogs = new List<CaptureLog> ();
foreach (var sim in simulators) {
// Upload the system log
main_log.WriteLine ("System log for the '{1}' simulator is: {0}", sim.SystemLog, sim.Name);
bool isCompanion = sim != simulator;
PrepareSimulator ();
var log = new CaptureLog (sim.SystemLog) {
Path = Path.Combine (LogDirectory, sim.UDID + ".log"),
Description = isCompanion ? "System log (companion)" : "System log",
};
log.StartCapture ();
Logs.Add (log);
systemLogs.Add (log);
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", log.Path);
}
main_log.WriteLine ("*** Executing {0}/{1} in the simulator ***", appName, mode);
if (EnsureCleanSimulatorState) {
foreach (var sim in simulators)
await sim.PrepareSimulatorAsync (main_log, bundle_identifier);
}
args.Append (" --launchsim");
args.AppendFormat (" \"{0}\" ", launchAppPath);
args.Append (" --device=:v2:udid=").Append (simulator.UDID).Append (" ");
start_crashes = CreateCrashReportsSnapshot (true);
start_crashes = await Harness.CreateCrashReportsSnapshotAsync (main_log, true);
listener.StartAsync ();
Harness.Log ("Starting test run");
var proc = new XProcess () {
Harness = Harness,
FileName = Harness.MlaunchPath,
Arguments = args.ToString (),
VerbosityLevel = 0,
};
proc.Start ();
main_log.WriteLine ("Starting test run");
var launchState = 0; // 0: launching, 1: launch timed out, 2: run timed out, 3: completed
var launchMutex = new Mutex ();
var runCompleted = new ManualResetEvent (false);
var cancellation_source = new CancellationTokenSource ();
ThreadPool.QueueUserWorkItem ((v) => {
if (!listener.WaitForConnection (TimeSpan.FromMinutes (Harness.LaunchTimeout))) {
lock (launchMutex) {
if (launchState == 0) {
launchState = 1;
runCompleted.Set ();
}
}
Harness.Log ("Test launch timed out after {0} minute(s).", Harness.LaunchTimeout);
cancellation_source.Cancel ();
main_log.WriteLine ("Test launch timed out after {0} minute(s).", Harness.LaunchTimeout);
} else {
Harness.Log ("Test run started");
main_log.WriteLine ("Test run started");
}
});
ThreadPool.QueueUserWorkItem ((v) => {
var rv = proc.WaitForExit (TimeSpan.FromMinutes (Harness.Timeout));
lock (launchMutex) {
if (launchState == 0)
launchState = rv ? 3 : 2;
runCompleted.Set ();
}
if (rv) {
Harness.Log ("Test run completed");
} else {
Harness.Log ("Test run timed out after {0} minute(s).", Harness.Timeout);
}
});
runCompleted.WaitOne ();
switch (launchState) {
case 1:
case 2:
success = false;
var result = await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args.ToString (), run_log, TimeSpan.FromMinutes (Harness.Timeout), cancellation_token: cancellation_source.Token);
if (result.TimedOut) {
timed_out = true;
success = false;
main_log.WriteLine ("Test run timed out after {0} minute(s).", Harness.Timeout);
} else if (result.Succeeded) {
main_log.WriteLine ("Test run completed");
success = true;
} else {
main_log.WriteLine ("Test run failed");
success = false;
}
if (!success.Value) {
// find pid
var pid = -1;
var output = proc.ReadCurrentOutput ();
foreach (var line in output.ToString ().Split ('\n')) {
if (line.StartsWith ("Application launched. PID = ", StringComparison.Ordinal)) {
var pidstr = line.Substring ("Application launched. PID = ".Length);
if (!int.TryParse (pidstr, out pid))
Harness.Log ("Could not parse pid: {0}", pidstr);
} else if (line.Contains ("Xamarin.Hosting: Launched ") && line.Contains (" with pid ")) {
var pidstr = line.Substring (line.LastIndexOf (' '));
if (!int.TryParse (pidstr, out pid))
Harness.Log ("Could not parse pid: {0}", pidstr);
using (var reader = run_log.GetReader ()) {
while (!reader.EndOfStream) {
var line = reader.ReadLine ();
if (line.StartsWith ("Application launched. PID = ", StringComparison.Ordinal)) {
var pidstr = line.Substring ("Application launched. PID = ".Length);
if (!int.TryParse (pidstr, out pid))
main_log.WriteLine ("Could not parse pid: {0}", pidstr);
} else if (line.Contains ("Xamarin.Hosting: Launched ") && line.Contains (" with pid ")) {
var pidstr = line.Substring (line.LastIndexOf (' '));
if (!int.TryParse (pidstr, out pid))
main_log.WriteLine ("Could not parse pid: {0}", pidstr);
}
}
}
if (pid > 0) {
KillPid (proc, pid, TimeSpan.FromSeconds (5), TimeSpan.FromMinutes (launchState == 1 ? Harness.LaunchTimeout : Harness.Timeout), launchState == 1 ? "Launch" : "Completion");
var launchTimedout = cancellation_source.IsCancellationRequested;
await KillPidAsync (main_log, pid, TimeSpan.FromSeconds (5), TimeSpan.FromMinutes (launchTimedout ? Harness.LaunchTimeout : Harness.Timeout), launchTimedout ? "Launch" : "Completion");
} else {
Harness.Log ("Could not find pid in mtouch output.");
main_log.WriteLine ("Could not find pid in mtouch output.");
}
// kill mtouch too
kill (proc.Id, 9);
break;
case 3:
// Success!
break;
case 0: // shouldn't happen ever
default:
throw new Exception ($"Invalid launch state: {launchState}");
}
listener.Cancel ();
var run_log = Logs.Create (LogDirectory, string.Format ("launch-{0:yyyyMMdd_HHmmss}.log", DateTime.Now), "Launch log");
File.WriteAllText (run_log.Path, proc.ReadCurrentOutput ());
// cleanup after us
KillEverything ();
if (EnsureCleanSimulatorState)
await SimDevice.KillEverythingAsync (main_log);
foreach (var log in systemLogs)
log.StopCapture ();
} else {
FindDevice ();
Harness.Log ("*** Executing {0}/{1} on device ***", appName, mode);
main_log.WriteLine ("*** Executing {0}/{1} on device ***", appName, mode);
args.Append (" --launchdev");
args.AppendFormat (" \"{0}\" ", launchAppPath);
AddDeviceName (args);
device_system_log = Logs.Create (LogDirectory, "device.log", "Device log");
device_system_log = Logs.CreateStream (LogDirectory, "device.log", "Device log");
var logdev = new DeviceLogCapturer () {
Harness = Harness,
LogPath = device_system_log.Path,
Log = device_system_log,
DeviceName = device_name,
};
logdev.StartCapture ();
start_crashes = CreateCrashReportsSnapshot (false);
start_crashes = await Harness.CreateCrashReportsSnapshotAsync (main_log, false);
listener.StartAsync ();
Harness.Log ("Starting test run");
ExecuteCommand (Harness.MlaunchPath, args.ToString ());
main_log.WriteLine ("Starting test run");
// This will not wait for app completion
await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, args.ToString (), main_log, TimeSpan.FromMinutes (1));
if (listener.WaitForCompletion (TimeSpan.FromMinutes (Harness.Timeout))) {
Harness.Log ("Test run completed");
main_log.WriteLine ("Test run completed");
} else {
Harness.Log ("Test run did not complete in {0} minutes.", Harness.Timeout);
main_log.WriteLine ("Test run did not complete in {0} minutes.", Harness.Timeout);
listener.Cancel ();
success = false;
timed_out = true;
@ -634,10 +500,10 @@ namespace xharness
logdev.StopCapture ();
// Upload the system log
if (File.Exists (device_system_log.Path)) {
Harness.Log (1, "A capture of the device log is: {0}", device_system_log.Path);
if (File.Exists (device_system_log.FullPath)) {
main_log.WriteLine ("A capture of the device log is: {0}", device_system_log.FullPath);
if (Harness.InWrench)
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", device_system_log.Path);
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", device_system_log.FullPath);
}
}
@ -645,12 +511,14 @@ namespace xharness
// check the final status
var crashed = false;
if (File.Exists (listener_log.Path)) {
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", listener_log.Path);
var log = File.ReadAllText (listener_log.Path);
if (File.Exists (listener_log.FullPath)) {
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", listener_log.FullPath);
string log;
using (var reader = listener_log.GetReader ())
log = reader.ReadToEnd ();
if (log.Contains ("Tests run")) {
var tests_run = string.Empty;
var log_lines = File.ReadAllLines (listener_log.Path);
var log_lines = log.Split ('\n');
var failed = false;
foreach (var line in log_lines) {
if (line.Contains ("Tests run:")) {
@ -665,25 +533,26 @@ namespace xharness
if (failed) {
Harness.LogWrench ("@MonkeyWrench: AddSummary: <b>{0} failed: {1}</b><br/>", mode, tests_run);
Harness.Log ("Test run failed");
main_log.WriteLine ("Test run failed");
success = false;
} else {
Harness.LogWrench ("@MonkeyWrench: AddSummary: {0} succeeded: {1}<br/>", mode, tests_run);
Harness.Log ("Test run succeeded");
main_log.WriteLine ("Test run succeeded");
success = true;
}
} else if (timed_out) {
Harness.LogWrench ("@MonkeyWrench: AddSummary: <b><i>{0} timed out</i></b><br/>", mode);
} else {
Harness.LogWrench ("@MonkeyWrench: AddSummary: <b><i>{0} crashed</i></b><br/>", mode);
Harness.Log ("Test run crashed");
main_log.WriteLine ("Test run crashed");
crashed = true;
}
} else if (timed_out) {
Harness.LogWrench ("@MonkeyWrench: AddSummary: <b><i>{0} never launched</i></b><br/>", mode);
Harness.Log ("Test run never launched");
main_log.WriteLine ("Test run never launched");
} else {
Harness.LogWrench ("@MonkeyWrench: AddSummary: <b><i>{0} crashed at startup (no log)</i></b><br/>", mode);
Harness.Log ("Test run crashed before it started (no log file produced)");
main_log.WriteLine ("Test run crashed before it started (no log file produced)");
crashed = true;
}
@ -693,37 +562,39 @@ namespace xharness
var watch = new Stopwatch ();
watch.Start ();
do {
var end_crashes = CreateCrashReportsSnapshot (isSimulator);
var end_crashes = await Harness.CreateCrashReportsSnapshotAsync (main_log, isSimulator);
end_crashes.ExceptWith (start_crashes);
if (end_crashes.Count > 0) {
Harness.Log ("Found {0} new crash report(s)", end_crashes.Count);
main_log.WriteLine ("Found {0} new crash report(s)", end_crashes.Count);
List<LogFile> crash_reports;
if (isSimulator) {
crash_reports = new List<LogFile> (end_crashes.Count);
foreach (var path in end_crashes) {
var report = Logs.Create (LogDirectory, Path.GetFileName (path), "Crash report: " + Path.GetFileName (path));
File.Copy (path, report.Path, true);
crash_reports.Add (report);
var logPath = Path.Combine (LogDirectory, Path.GetFileName (path));
File.Copy (path, logPath, true);
crash_reports.Add (Logs.CreateFile ("Crash report: " + Path.GetFileName (path), logPath));
}
} else {
// Download crash reports from the device. We put them in the project directory so that they're automatically deleted on wrench
// (if we put them in /tmp, they'd never be deleted).
var downloaded_crash_reports = new List<LogFile> ();
foreach (var file in end_crashes) {
var crash_report_target = Logs.Create (LogDirectory, Path.GetFileName (file), "Crash report: " + Path.GetFileName (file));
if (ExecuteCommand (Harness.MlaunchPath, "--download-crash-report=" + file + " --download-crash-report-to=" + crash_report_target.Path + " --sdkroot " + Harness.XcodeRoot)) {
Harness.Log ("Downloaded crash report {0} to {1}", file, crash_report_target.Path);
crash_report_target = SymbolicateCrashReport (crash_report_target);
var crash_report_target = Logs.CreateFile ("Crash report: " + Path.GetFileName (file), Path.Combine (LogDirectory, Path.GetFileName (file)));
var result = await ProcessHelper.ExecuteCommandAsync (Harness.MlaunchPath, "--download-crash-report=" + file + " --download-crash-report-to=" + crash_report_target.Path + " --sdkroot " + Harness.XcodeRoot, main_log, TimeSpan.FromMinutes (1));
if (result.Succeeded) {
main_log.WriteLine ("Downloaded crash report {0} to {1}", file, crash_report_target.Path);
crash_report_target = await Harness.SymbolicateCrashReportAsync (main_log, crash_report_target);
Logs.Add (crash_report_target);
downloaded_crash_reports.Add (crash_report_target);
} else {
Harness.Log ("Could not download crash report {0}", file);
main_log.WriteLine ("Could not download crash report {0}", file);
}
}
crash_reports = downloaded_crash_reports;
}
foreach (var cp in crash_reports) {
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", cp.Path);
Harness.Log (" {0}", cp.Path);
main_log.WriteLine (" {0}", cp.Path);
}
crash_report_search_done = true;
} else if (!crashed && !timed_out) {
@ -732,7 +603,7 @@ namespace xharness
if (watch.Elapsed.TotalSeconds > crash_report_search_timeout) {
crash_report_search_done = true;
} else {
Harness.Log ("No crash reports, waiting a second to see if the crash report service just didn't complete in time ({0})", (int) (crash_report_search_timeout - watch.Elapsed.TotalSeconds));
main_log.WriteLine ("No crash reports, waiting a second to see if the crash report service just didn't complete in time ({0})", (int) (crash_report_search_timeout - watch.Elapsed.TotalSeconds));
Thread.Sleep (TimeSpan.FromSeconds (1));
}
}
@ -740,27 +611,13 @@ namespace xharness
if (!success.HasValue)
success = false;
if (isSimulator) {
foreach (var sim in simulators) {
// Upload the system log
if (File.Exists (sim.SystemLog)) {
Harness.Log (success.Value ? 1 : 0, "System log for the '{1}' simulator is: {0}", sim.SystemLog, sim.Name);
bool isCompanion = sim != simulator;
var log = Logs.Create (LogDirectory, sim.UDID + ".log", isCompanion ? "System log (companion)" : "System log");
File.Copy (sim.SystemLog, log.Path, true);
Harness.LogWrench ("@MonkeyWrench: AddFile: {0}", log.Path);
}
}
}
if (success.Value) {
Result = TestExecutingResult.Succeeded;
} else if (timed_out) {
if (timed_out) {
Result = TestExecutingResult.TimedOut;
} else if (crashed) {
Result = TestExecutingResult.Crashed;
} else if (success.Value) {
Result = TestExecutingResult.Succeeded;
} else {
Result = TestExecutingResult.Failed;
}
@ -784,132 +641,20 @@ namespace xharness
[DllImport ("/usr/lib/libc.dylib")]
static extern void kill (int pid, int sig);
void KillPid (XProcess proc, int pid, TimeSpan kill_separation, TimeSpan timeout, string type)
async Task KillPidAsync (Log log, int pid, TimeSpan kill_separation, TimeSpan timeout, string type)
{
Harness.Log ("{2} timeout ({1} s) reached, will now send SIGQUIT to the app (PID: {0})", pid, timeout.TotalSeconds, type);
log.WriteLine ("{2} timeout ({1} s) reached, will now send SIGQUIT to the app (PID: {0})", pid, timeout.TotalSeconds, type);
kill (pid, 3 /* SIGQUIT */); // print managed stack traces.
if (!proc.WaitForExit (kill_separation /* wait for at most 5 seconds to see if something happens */)) {
Harness.Log ("{2} timeout ({1} s) reached, will now send SIGABRT to the app (PID: {0})", pid, timeout.TotalSeconds, type);
kill (pid, 6 /* SIGABRT */); // print native stack traces.
if (!proc.WaitForExit (kill_separation /* wait another 5 seconds */)) {
Harness.Log ("{2} timeout ({1} s) reached, will now send SIGKILL to the app (PID: {0})", pid, timeout.TotalSeconds, type);
kill (pid, 9 /* SIGKILL */); // terminate unconditionally.
}
}
}
HashSet<string> CreateCrashReportsSnapshot (bool simulator)
{
HashSet<string> rv;
if (simulator) {
var dir = Path.Combine (Environment.GetEnvironmentVariable ("HOME"), "Library", "Logs", "DiagnosticReports");
if (Directory.Exists (dir)) {
rv = new HashSet<string> (Directory.EnumerateFiles (dir));
} else {
rv = new HashSet<string> ();
}
} else {
var tmp = Path.GetTempFileName ();
if (ExecuteCommand (Harness.MlaunchPath, "--list-crash-reports=" + tmp + " --sdkroot " + Harness.XcodeRoot, true)) {
rv = new HashSet<string> (File.ReadAllLines (tmp));
} else {
rv = new HashSet<string> ();
}
File.Delete (tmp);
}
return rv;
}
void KillEverything ()
{
if (SkipSimulatorCleanup)
if (await ProcessHelper.PollForExitAsync (pid, kill_separation /* wait for at most 5 seconds to see if something happens */))
return;
var to_kill = new string [] { "iPhone Simulator", "iOS Simulator", "Simulator", "Simulator (Watch)", "com.apple.CoreSimulator.CoreSimulatorService" };
foreach (var k in to_kill)
ExecuteCommand ("killall", "-9 \"" + k + "\"", true, output_verbosity_level: 1);
}
static bool shown_simulator_list;
void ShowSimulatorList ()
{
if (shown_simulator_list)
log.WriteLine ("{2} timeout ({1} s) reached, will now send SIGABRT to the app (PID: {0})", pid, timeout.TotalSeconds, type);
kill (pid, 6 /* SIGABRT */); // print native stack traces.
if (await ProcessHelper.PollForExitAsync (pid, kill_separation /* wait another 5 seconds */))
return;
shown_simulator_list = true;
if (Harness.Verbosity > 0)
ExecuteXcodeCommand ("simctl", "list", ignore_errors: true, timeout: TimeSpan.FromSeconds (10));
}
bool ExecuteXcodeCommand (string executable, string args, bool ignore_errors = false, int output_verbosity_level = 1, TimeSpan? timeout = null)
{
return ExecuteCommand (Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "usr", "bin", executable), args, ignore_errors, output_verbosity_level, timeout: timeout);
}
bool ExecuteCommand (string filename, string args, bool ignore_errors = false, int output_verbosity_level = 1, StringBuilder captured_output = null, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null)
{
int exitcode;
return ExecuteCommand (filename, args, out exitcode, ignore_errors, output_verbosity_level, captured_output, timeout, environment_variables);
}
bool ExecuteCommand (string filename, string args, out int exitcode, bool ignore_errors = false, int output_verbosity_level = 1, StringBuilder captured_output = null, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null)
{
if (captured_output == null)
captured_output = new StringBuilder ();
var streamEnds = new CountdownEvent (2);
using (var p = new Process ()) {
p.StartInfo.FileName = filename;
p.StartInfo.Arguments = args;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
if (environment_variables != null) {
foreach (var kvp in environment_variables)
p.StartInfo.EnvironmentVariables.Add (kvp.Key, kvp.Value);
}
p.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (e.Data == null) {
streamEnds.Signal ();
} else {
lock (captured_output) {
captured_output.AppendLine (e.Data);
Harness.Log (output_verbosity_level, e.Data);
}
}
};
p.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (e.Data == null) {
streamEnds.Signal ();
} else {
lock (captured_output) {
captured_output.AppendLine (e.Data);
Harness.Log (output_verbosity_level, e.Data);
}
}
};
Harness.Log (output_verbosity_level, "{0} {1}", p.StartInfo.FileName, p.StartInfo.Arguments);
p.Start ();
p.BeginOutputReadLine ();
p.BeginErrorReadLine ();
if (p.WaitForExit (!timeout.HasValue ? int.MaxValue : (int) timeout.Value.TotalMilliseconds )) {
streamEnds.Wait ();
exitcode = p.ExitCode;
if (p.ExitCode != 0 && !ignore_errors)
throw new Exception (string.Format ("Failed to execute {0}:\n{1}", filename, captured_output.ToString ()));
return p.ExitCode == 0;
} else {
if (!ignore_errors)
throw new Exception (string.Format ("Execution of {0} timed out after {2} minutes:\n{1}", filename, captured_output.ToString (), timeout.Value.TotalMinutes));
else
Harness.Log ("Execution of {0} timed out after {2} minutes:\n{1}", filename, captured_output.ToString (), timeout.Value.TotalMinutes);
exitcode = 0;
kill (p.Id, 9);
return false;
}
}
log.WriteLine ("{2} timeout ({1} s) reached, will now send SIGKILL to the app (PID: {0})", pid, timeout.TotalSeconds, type);
kill (pid, 9 /* SIGKILL */); // terminate unconditionally.
}
}
}

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

@ -9,16 +9,14 @@ namespace xharness
public class DeviceLogCapturer
{
public Harness Harness;
public string LogPath;
public Log Log;
public string DeviceName;
StreamWriter writer;
Process process;
CountdownEvent streamEnds;
public void StartCapture ()
{
writer = new StreamWriter (new FileStream (LogPath, FileMode.Create));
streamEnds = new CountdownEvent (2);
process = new Process ();
@ -35,8 +33,8 @@ namespace xharness
if (e.Data == null) {
streamEnds.Signal ();
} else {
lock (writer) {
writer.WriteLine (e.Data);
lock (Log) {
Log.WriteLine (e.Data);
}
}
};
@ -44,12 +42,12 @@ namespace xharness
if (e.Data == null) {
streamEnds.Signal ();
} else {
lock (writer) {
writer.WriteLine (e.Data);
lock (Log) {
Log.WriteLine (e.Data);
}
}
};
Harness.Log (1, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
Log.WriteLine ("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Start ();
process.BeginOutputReadLine ();
process.BeginErrorReadLine ();
@ -62,8 +60,6 @@ namespace xharness
Harness.Log ("Could not kill 'mtouch --logdev' process in 5 seconds.");
}
process.Dispose ();
writer.Flush ();
writer.Dispose ();
}
}
}

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

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
namespace xharness
@ -20,7 +23,7 @@ namespace xharness
{
public HarnessAction Action { get; set; }
public int Verbosity { get; set; }
public LogFile HarnessLog { get; set; }
public Log HarnessLog { get; set; }
// This is the maccore/tests directory.
string root_directory;
@ -66,7 +69,7 @@ namespace xharness
public Harness ()
{
LaunchTimeout = InWrench ? 1 : 120;
LaunchTimeout = InWrench ? 3 : 120;
}
public string XcodeRoot {
@ -83,19 +86,57 @@ namespace xharness
}
}
string DownloadMlaunch ()
{
// Just hardcode this for now. We should be able to switch to a shipped version of XS soon.
var mlaunch_url = "https://files.xamarin.com/~rolf/mlaunch-9d097ff4457cfc9943a91a4e17c07b09a7743625";
var mlaunch_path = Path.Combine (Path.GetTempPath (), Path.GetFileName (mlaunch_url), "mlaunch");
if (File.Exists (mlaunch_path))
return mlaunch_path;
try {
Log ("Downloading mlaunch...");
Directory.CreateDirectory (Path.GetDirectoryName (mlaunch_path));
var wc = new System.Net.WebClient ();
wc.DownloadFile (mlaunch_url, mlaunch_path + ".tmp");
new Mono.Unix.UnixFileInfo (mlaunch_path + ".tmp").FileAccessPermissions |= Mono.Unix.FileAccessPermissions.UserExecute;
File.Delete (mlaunch_path);
File.Move (mlaunch_path + ".tmp", mlaunch_path);
Log ("Downloaded mlaunch.");
} catch (Exception e) {
Log ("Could not download mlaunch: {0}", e);
}
return mlaunch_path;
}
string mlaunch;
public string MlaunchPath {
get {
if (mlaunch == null) {
var path = Path.GetFullPath (Path.Combine (Path.GetDirectoryName (Path.GetDirectoryName (RootDirectory)), "maccore", "tools", "mlaunch", "mlaunch"));
var dir = Path.GetFullPath (RootDirectory);
while (dir.Length > 3) {
var filename = Path.GetFullPath (Path.Combine (dir, "maccore", "tools", "mlaunch", "mlaunch"));
if (File.Exists (filename))
return mlaunch = filename;
dir = Path.GetDirectoryName (dir);
}
string path = string.Empty;
Log ("Could not find mlaunch locally, will try downloading it.");
try {
path = DownloadMlaunch ();
} catch (Exception e) {
Log ("Could not download mlaunch: {0}", e);
}
if (!File.Exists (path)) {
Log ("Could not find mlaunch locally ({0}), will try in Xamarin Studio.app.", path);
Log ("Will try in Xamarin Studio.app.", path);
path = "/Applications/Xamarin Studio.app/Contents/Resources/lib/monodevelop/AddIns/MonoDevelop.IPhone/mlaunch.app/Contents/MacOS/mlaunch";
}
if (!File.Exists (path))
throw new FileNotFoundException (string.Format ("Could not find mlaunch: {0}", path));
Log ("Found mlaunch: {0}", path);
mlaunch = path;
}
@ -366,12 +407,16 @@ namespace xharness
public int Install ()
{
if (HarnessLog == null)
HarnessLog = new ConsoleLog ();
foreach (var project in IOSTestProjects) {
var runner = new AppRunner () {
Harness = this,
ProjectFile = project.Path,
MainLog = HarnessLog,
};
var rv = runner.Install ();
var rv = runner.Install (HarnessLog);
if (rv != 0)
return rv;
}
@ -380,12 +425,16 @@ namespace xharness
public int Run ()
{
if (HarnessLog == null)
HarnessLog = new ConsoleLog ();
foreach (var project in IOSTestProjects) {
var runner = new AppRunner () {
Harness = this,
ProjectFile = project.Path,
MainLog = HarnessLog,
};
var rv = runner.Run ();
var rv = runner.RunAsync ().Result;
if (rv != 0)
return rv;
}
@ -547,5 +596,65 @@ namespace xharness
return disable_watchos_on_wrench.Value;
}
}
public Task<ProcessExecutionResult> ExecuteXcodeCommandAsync (string executable, string args, TextWriter output, TimeSpan timeout)
{
return ProcessHelper.ExecuteCommandAsync (Path.Combine (XcodeRoot, "Contents", "Developer", "usr", "bin", executable), args, output, timeout: timeout);
}
public Task<ProcessExecutionResult> ExecuteXcodeCommandAsync (string executable, string args, Log log, TimeSpan timeout)
{
return ProcessHelper.ExecuteCommandAsync (Path.Combine (XcodeRoot, "Contents", "Developer", "usr", "bin", executable), args, log.GetWriter () , timeout: timeout);
}
public async Task ShowSimulatorList (LogStream log)
{
await ExecuteXcodeCommandAsync ("simctl", "list", log.GetWriter (), TimeSpan.FromSeconds (10));
}
public async Task<LogFile> SymbolicateCrashReportAsync (Log log, LogFile report)
{
var symbolicatecrash = Path.Combine (XcodeRoot, "Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash");
if (!File.Exists (symbolicatecrash))
symbolicatecrash = Path.Combine (XcodeRoot, "Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash");
if (!File.Exists (symbolicatecrash)) {
log.WriteLine ("Can't symbolicate {0} because the symbolicatecrash script {1} does not exist", report.Path, symbolicatecrash);
return report;
}
var symbolicated = new LogFile ("Symbolicated crash report", report.Path + ".symbolicated");
var environment = new Dictionary<string, string> { { "DEVELOPER_DIR", Path.Combine (XcodeRoot, "Contents", "Developer") } };
var rv = await ProcessHelper.ExecuteCommandAsync (symbolicatecrash, Quote (report.Path), symbolicated, TimeSpan.FromMinutes (1), environment);
if (rv.Succeeded) {;
log.WriteLine ("Symbolicated {0} successfully.", report.Path);
return symbolicated;
} else {
log.WriteLine ("Failed to symbolicate {0}.", report.Path);
return report;
}
}
public async Task<HashSet<string>> CreateCrashReportsSnapshotAsync (Log log, bool simulator)
{
var rv = new HashSet<string> ();
if (simulator) {
var dir = Path.Combine (Environment.GetEnvironmentVariable ("HOME"), "Library", "Logs", "DiagnosticReports");
if (Directory.Exists (dir))
rv.UnionWith (Directory.EnumerateFiles (dir));
} else {
var tmp = Path.GetTempFileName ();
try {
var result = await ProcessHelper.ExecuteCommandAsync (MlaunchPath, "--list-crash-reports=" + tmp + " --sdkroot " + XcodeRoot, log, TimeSpan.FromMinutes (1));
if (result.Succeeded)
rv.UnionWith (File.ReadAllLines (tmp));
} finally {
File.Delete (tmp);
}
}
return rv;
}
}
}

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

@ -12,11 +12,18 @@ namespace xharness
public class Jenkins
{
public Harness Harness;
public bool IncludeClassic;
public bool IncludeClassiciOS;
public bool IncludeClassicMac = true;
public bool IncludeBcl;
public bool IncludeMac = true;
public bool IncludeiOS = true;
public bool IncludetvOS = true;
public bool IncludewatchOS = true;
public bool IncludeMmpTest;
public LogFiles Logs = new LogFiles ();
LogFile SimulatorLoadLog;
public Logs Logs = new Logs ();
public Log MainLog;
Log SimulatorLoadLog;
public string LogDirectory {
get {
@ -35,7 +42,7 @@ namespace xharness
Simulators.Harness = Harness;
if (SimulatorLoadLog == null)
SimulatorLoadLog = Logs.Create (LogDirectory, "simulator-list.log", "Simulator Listing");
SimulatorLoadLog = Logs.CreateStream (LogDirectory, "simulator-list.log", "Simulator Listing");
try {
await Simulators.LoadAsync (SimulatorLoadLog);
} catch (Exception e) {
@ -70,12 +77,18 @@ namespace xharness
Simulators.SupportedDeviceTypes.
Where ((SimDeviceType v) => v.ProductFamilyId == "Watch").
First ();
var devices =
Simulators.AvailableDevices.
Where ((SimDevice d) => d.SimRuntime == latestwatchOSRuntime.Identifier && d.SimDeviceType == watchOSDeviceType.Identifier);
var pair = Simulators.AvailableDevicePairs.
FirstOrDefault ((SimDevicePair v) => devices.Any ((SimDevice d) => d.UDID == v.Gizmo));
var device =
Simulators.AvailableDevices.
Where ((SimDevice v) => v.SimRuntime == latestwatchOSRuntime.Identifier && v.SimDeviceType == watchOSDeviceType.Identifier).
Where ((SimDevice v) => Simulators.AvailableDevicePairs.Any ((pair) => pair.Gizmo == v.UDID)). // filter to watch devices that exists in a device pair
First ();
runtasks.Add (new RunSimulatorTask (buildTask, device) { Platform = TestPlatform.watchOS, ExecutionResult = TestExecutingResult.Ignored });
FirstOrDefault ((SimDevice v) => pair.Gizmo == v.UDID); // select the device in the device pair.
var companion =
Simulators.AvailableDevices.
FirstOrDefault ((SimDevice v) => pair.Companion == v.UDID);
runtasks.Add (new RunSimulatorTask (buildTask, device, companion) { Platform = TestPlatform.watchOS, ExecutionResult = TestExecutingResult.Ignored });
} else {
var latestiOSRuntime =
Simulators.SupportedRuntimes.
@ -87,7 +100,7 @@ namespace xharness
runtasks.Add (new RunSimulatorTask (buildTask, Simulators.AvailableDevices.Where ((SimDevice v) => v.SimRuntime == latestiOSRuntime.Identifier && v.SimDeviceType == "com.apple.CoreSimulator.SimDeviceType.iPhone-5").First ()) { Platform = TestPlatform.iOS_Unified32 });
runtasks.Add (new RunSimulatorTask (buildTask, Simulators.AvailableDevices.Where ((SimDevice v) => v.SimRuntime == latestiOSRuntime.Identifier && v.SimDeviceType == "com.apple.CoreSimulator.SimDeviceType.iPhone-6s").First ()) { Platform = TestPlatform.iOS_Unified64 });
} else {
runtasks.Add (new RunSimulatorTask (buildTask, Simulators.AvailableDevices.Where ((SimDevice v) => v.SimRuntime == latestiOSRuntime.Identifier && v.SimDeviceType == "com.apple.CoreSimulator.SimDeviceType.iPhone-4s").First ()) { Platform = TestPlatform.iOS_Classic });
runtasks.Add (new RunSimulatorTask (buildTask, Simulators.AvailableDevices.Where ((SimDevice v) => v.SimRuntime == latestiOSRuntime.Identifier && v.SimDeviceType == "com.apple.CoreSimulator.SimDeviceType.iPhone-5").First ()) { Platform = TestPlatform.iOS_Classic });
}
}
@ -104,100 +117,98 @@ namespace xharness
// Missing:
// api-diff
// msbuild tests
var runSimulatorTasks = new List<RunSimulatorTask> ();
foreach (var project in Harness.IOSTestProjects) {
if (!project.IsExecutableProject)
continue;
if (IncludeiOS || IncludetvOS || IncludewatchOS) {
var runSimulatorTasks = new List<RunSimulatorTask> ();
if (!IncludeBcl && project.Path.Contains ("bcl-test"))
continue;
foreach (var project in Harness.IOSTestProjects) {
if (!project.IsExecutableProject)
continue;
var build = new XBuildTask ()
{
Jenkins = this,
ProjectFile = project.Path,
ProjectConfiguration = "Debug",
ProjectPlatform = "iPhoneSimulator",
Platform = TestPlatform.iOS_Classic,
};
if (IncludeClassic)
runSimulatorTasks.AddRange (await CreateRunSimulatorTaskAsync (build));
if (!IncludeBcl && project.Path.Contains ("bcl-test"))
continue;
var suffixes = new string [] { "-unified", "-tvos", "-watchos" };
foreach (var suffix in suffixes) {
var derived = new XBuildTask ()
{
var build = new XBuildTask () {
Jenkins = this,
ProjectFile = AddSuffixToPath (project.Path, suffix),
ProjectFile = project.Path,
ProjectConfiguration = "Debug",
ProjectPlatform = "iPhoneSimulator",
Platform = TestPlatform.iOS_Classic,
};
if (IncludeClassiciOS && IncludeiOS)
runSimulatorTasks.AddRange (await CreateRunSimulatorTaskAsync (build));
var suffixes = new List<Tuple<string, TestPlatform>> ();
if (IncludeiOS)
suffixes.Add (new Tuple<string, TestPlatform> ("-unified", TestPlatform.iOS_Unified));
if (IncludetvOS)
suffixes.Add (new Tuple<string, TestPlatform> ("-tvos", TestPlatform.tvOS));
if (IncludewatchOS)
suffixes.Add (new Tuple<string, TestPlatform> ("-watchos", TestPlatform.watchOS));
foreach (var pair in suffixes) {
var derived = new XBuildTask () {
Jenkins = this,
ProjectFile = AddSuffixToPath (project.Path, pair.Item1),
ProjectConfiguration = build.ProjectConfiguration,
ProjectPlatform = build.ProjectPlatform,
Platform = pair.Item2,
};
runSimulatorTasks.AddRange (await CreateRunSimulatorTaskAsync (derived));
}
}
foreach (var taskGroup in runSimulatorTasks.GroupBy ((RunSimulatorTask task) => task.Device)) {
Tasks.Add (new AggregatedRunSimulatorTask (taskGroup) {
Jenkins = this,
Devices = taskGroup.First ().Simulators,
});
}
foreach (var task in runSimulatorTasks) {
if (task.TestName == "framework-test")
task.ExecutionResult = TestExecutingResult.Ignored;
}
}
if (IncludeMac) {
foreach (var project in Harness.MacTestProjects) {
if (!project.IsExecutableProject)
continue;
if (!IncludeMmpTest && project.Path.Contains ("mmptest"))
continue;
BuildToolTask build;
if (project.GenerateVariations) {
build = new MdtoolTask ();
build.Platform = TestPlatform.Mac_Classic;
} else {
build = new XBuildTask ();
build.Platform = TestPlatform.Mac;
}
build.Jenkins = this;
build.ProjectFile = project.Path;
build.ProjectConfiguration = "Debug";
build.ProjectPlatform = "x86";
build.SpecifyPlatform = false;
build.SpecifyConfiguration = false;
var exec = new MacExecuteTask () {
Platform = build.Platform,
Jenkins = this,
BuildTask = build,
ProjectFile = build.ProjectFile,
ProjectConfiguration = build.ProjectConfiguration,
ProjectPlatform = build.ProjectPlatform,
};
switch (suffix) {
case "-unified":
derived.Platform = TestPlatform.iOS_Unified;
break;
case "-tvos":
derived.Platform = TestPlatform.tvOS;
break;
case "-watchos":
derived.Platform = TestPlatform.watchOS;
break;
default:
throw new NotImplementedException ();
if (IncludeClassicMac)
Tasks.Add (exec);
if (project.GenerateVariations) {
Tasks.Add (CloneExecuteTask (exec, TestPlatform.Mac_Unified, "-unified"));
Tasks.Add (CloneExecuteTask (exec, TestPlatform.Mac_UnifiedXM45, "-unifiedXM45"));
}
runSimulatorTasks.AddRange (await CreateRunSimulatorTaskAsync (derived));
}
}
foreach (var taskGroup in runSimulatorTasks.GroupBy ((RunSimulatorTask task) => task.Device)) {
Tasks.Add (new AggregatedRunSimulatorTask (taskGroup)
{
Jenkins = this,
Device = taskGroup.Key,
});
}
foreach (var project in Harness.MacTestProjects) {
if (!project.IsExecutableProject)
continue;
BuildToolTask build;
if (project.GenerateVariations) {
build = new MdtoolTask ();
build.Platform = TestPlatform.Mac_Classic;
} else {
build = new XBuildTask ();
build.Platform = TestPlatform.Mac;
}
build.Jenkins = this;
build.ProjectFile = project.Path;
build.ProjectConfiguration = "Debug";
build.ProjectPlatform = "x86";
build.SpecifyPlatform = false;
build.SpecifyConfiguration = false;
var exec = new MacExecuteTask ()
{
Platform = build.Platform,
Jenkins = this,
BuildTask = build,
ProjectFile = build.ProjectFile,
ProjectConfiguration = build.ProjectConfiguration,
ProjectPlatform = build.ProjectPlatform,
};
Tasks.Add (exec);
if (project.GenerateVariations) {
Tasks.Add (CloneExecuteTask (exec, TestPlatform.Mac_Unified, "-unified"));
Tasks.Add (CloneExecuteTask (exec, TestPlatform.Mac_UnifiedXM45, "-unifiedXM45"));
}
}
foreach (var task in runSimulatorTasks) {
if (task.TestName == "framework-test")
task.ExecutionResult = TestExecutingResult.Ignored;
}
}
static MacExecuteTask CloneExecuteTask (MacExecuteTask task, TestPlatform platform, string suffix)
@ -230,7 +241,9 @@ namespace xharness
{
try {
Directory.CreateDirectory (LogDirectory);
Harness.HarnessLog = Logs.Create (LogDirectory, "Harness.log", "Harness log");
Harness.HarnessLog = MainLog = Logs.CreateStream (LogDirectory, "Harness.log", "Harness log");
Harness.HarnessLog.Timestamp = true;
Task.Run (async () =>
{
await PopulateTasksAsync ();
@ -242,7 +255,7 @@ namespace xharness
GenerateReport ();
return Tasks.Any ((v) => v.ExecutionResult == TestExecutingResult.Failed || v.ExecutionResult == TestExecutingResult.Crashed) ? 1 : 0;
} catch (Exception ex) {
Harness.Log ("Unexpected exception: {0}", ex);
MainLog.WriteLine ("Unexpected exception: {0}", ex);
return 2;
}
}
@ -343,7 +356,7 @@ function toggleContainerVisibility (containerName)
writer.WriteLine ("<h1>Test results</h1>");
foreach (var log in Logs)
writer.WriteLine ("<a href='{0}' type='text/plain'>{1}</a><br />", log.Path.Substring (LogDirectory.Length + 1), log.Description);
writer.WriteLine ("<a href='{0}' type='text/plain'>{1}</a><br />", log.FullPath.Substring (LogDirectory.Length + 1), log.Description);
var allSimulatorTasks = new List<RunSimulatorTask> ();
var allExecuteTasks = new List<MacExecuteTask> ();
@ -384,26 +397,20 @@ function toggleContainerVisibility (containerName)
continue;
}
//var executionGroup = group as IEnumerable<MacExecuteTask>;
//if (executionGroup != null) {
// writer.WriteLine ("<a href='#test_{2}'>{0}</a> ({1})<br />", group.Key, string.Join (", ", executionGroup.Select ((v) => string.Format ("<span style='color: {0}'>{1}</span>", GetTestColor (v), v.Mode)).ToArray ()), group.Key.Replace (' ', '-'));
// continue;
//}
throw new NotImplementedException ();
}
}
//foreach (var group in allExecuteTasks.GroupBy ((MacExecuteTask v) => v.TestName)) {
//}
foreach (var group in allTasks.GroupBy ((TestTask v) => v.TestName)) {
var firstResult = group.First ().ExecutionResult;
var identicalResults = group.All ((v) => v.ExecutionResult == firstResult);
// Create a collection of all non-ignored tests in the group (unless all tests were ignored).
var relevantGroup = group.Where ((v) => v.ExecutionResult != TestExecutingResult.Ignored);
if (!relevantGroup.Any ())
relevantGroup = group;
var firstResult = relevantGroup.First ().ExecutionResult;
var identicalResults = relevantGroup.All ((v) => v.ExecutionResult == firstResult);
var defaultHide = !group.Any ((v) => v.Failed);
writer.WriteLine ("<h2 id='test_{1}'>{0} (<span style='color: {2}'>{4}</span>) <small><a id='button_container_{1}' href=\"javascript: toggleContainerVisibility ('{1}');\">{3}</a></small> </h2>",
group.Key, group.Key.Replace (' ', '-'), GetTestColor (group), defaultHide ? "Show" : "Hide", identicalResults ? firstResult.ToString () : "multiple results");
group.Key, group.Key.Replace (' ', '-'), GetTestColor (relevantGroup), defaultHide ? "Show" : "Hide", identicalResults ? firstResult.ToString () : "multiple results");
writer.WriteLine ("<div id='test_container_{0}' style='display: {1}'>", group.Key.Replace (' ', '-'), defaultHide ? "none" : "block");
foreach (var test in group) {
string state;
@ -415,27 +422,22 @@ function toggleContainerVisibility (containerName)
var logs = test.AggregatedLogs;
if (logs.Count () > 0) {
foreach (var log in logs) {
writer.WriteLine ("<a href='{0}' type='text/plain'>{1}</a><br />", log.Path.Substring (LogDirectory.Length + 1), log.Description);
log.Flush ();
writer.WriteLine ("<a href='{0}' type='text/plain'>{1}</a><br />", log.FullPath.Substring (LogDirectory.Length + 1), log.Description);
if (log.Description == "Test log") {
var summary = string.Empty;
try {
if (File.Exists (log.Path)) {
using (var fs = new FileStream (log.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
using (var reader = new StreamReader (fs)) {
while (!reader.EndOfStream) {
string line = reader.ReadLine ();
if (line.StartsWith ("Tests run:", StringComparison.Ordinal)) {
summary = line;
} else if (line.Trim ().StartsWith ("[FAIL]", StringComparison.Ordinal)) {
writer.WriteLine ("<span style='padding-left: 20px;'>{0}</span><br />", line.Trim ());
}
}
using (var reader = log.GetReader ()) {
while (!reader.EndOfStream) {
string line = reader.ReadLine ();
if (line.StartsWith ("Tests run:", StringComparison.Ordinal)) {
summary = line;
} else if (line.Trim ().StartsWith ("[FAIL]", StringComparison.Ordinal)) {
writer.WriteLine ("<span style='padding-left: 20px;'>{0}</span><br />", line.Trim ());
}
}
} else {
summary = "No test log (yet).";
}
if (summary != null)
if (!string.IsNullOrEmpty (summary))
writer.WriteLine ("<span style='padding-left: 15px;'>{0}</span><br />", summary);
} catch (Exception ex) {
writer.WriteLine ("<span style='padding-left: 15px;'>Could not parse log file: {0}</span><br />", System.Web.HttpUtility.HtmlEncode (ex.Message));
@ -443,18 +445,12 @@ function toggleContainerVisibility (containerName)
} else if (log.Description == "Build log") {
var errors = new HashSet<string> ();
try {
if (File.Exists (log.Path)) {
using (var fs = new FileStream (log.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
using (var reader = new StreamReader (fs)) {
while (!reader.EndOfStream) {
string line = reader.ReadLine ().Trim ();
if (line.Contains (": error"))
errors.Add (line);
}
}
using (var reader = log.GetReader ()) {
while (!reader.EndOfStream) {
string line = reader.ReadLine ().Trim ();
if (line.Contains (": error"))
errors.Add (line);
}
} else {
errors.Add ("Log file doesn't exist (yet)");
}
foreach (var error in errors)
writer.WriteLine ("<span style='padding-left: 15\tpx;'>{0}</span> <br />", error);
@ -478,7 +474,6 @@ function toggleContainerVisibility (containerName)
if (File.Exists (report))
File.Delete (report);
File.WriteAllBytes (report, stream.ToArray ());
Harness.Log (2, "Generated report: {0}", report);
}
}
}
@ -556,10 +551,10 @@ function toggleContainerVisibility (containerName)
public TestPlatform Platform { get; set; }
public LogFiles Logs = new LogFiles ();
public Logs Logs = new Logs ();
public List<Resource> Resources = new List<Resource> ();
public virtual IEnumerable<LogFile> AggregatedLogs {
public virtual IEnumerable<Log> AggregatedLogs {
get {
return Logs;
}
@ -663,27 +658,30 @@ function toggleContainerVisibility (containerName)
var sln = Path.ChangeExtension (ProjectFile, "sln");
args.Append (Harness.Quote (File.Exists (sln) ? sln : ProjectFile));
xbuild.StartInfo.Arguments = args.ToString ();
Harness.Log ("Building {0} ({1})", TestName, Mode);
Jenkins.MainLog.WriteLine ("Building {0} ({1})", TestName, Mode);
SetEnvironmentVariables (xbuild);
var log = Logs.Create (LogDirectory, "build-" + Platform + ".txt", "Build log");
var log = Logs.CreateStream (LogDirectory, "build-" + Platform + ".txt", "Build log");
foreach (string key in xbuild.StartInfo.EnvironmentVariables.Keys)
log.WriteLine ("{0}={1}", key, xbuild.StartInfo.EnvironmentVariables [key]);
log.WriteLine ("{0} {1}", xbuild.StartInfo.FileName, xbuild.StartInfo.Arguments);
if (Harness.DryRun) {
Harness.Log ("{0} {1}", xbuild.StartInfo.FileName, xbuild.StartInfo.Arguments);
} else {
if (!Harness.DryRun) {
try {
await xbuild.RunAsync (log.Path, true, TimeSpan.FromMinutes (5));
ExecutionResult = xbuild.ExitCode == 0 ? TestExecutingResult.Succeeded : TestExecutingResult.Failed;
} catch (TimeoutException e) {
log.WriteLine ("Build timed out after {0} seconds.", e.Timeout.TotalSeconds);
ExecutionResult = TestExecutingResult.TimedOut;
var timeout = TimeSpan.FromMinutes (5);
var result = await xbuild.RunAsync (log, true, timeout);
if (result.TimedOut) {
ExecutionResult = TestExecutingResult.TimedOut;
log.WriteLine ("Build timed out after {0} seconds.", timeout.TotalSeconds);
} else if (result.Succeeded) {
ExecutionResult = TestExecutingResult.Succeeded;
} else {
ExecutionResult = TestExecutingResult.Failed;
}
} catch (Exception e) {
log.WriteLine ("Harness exception: {0}", e);
ExecutionResult = TestExecutingResult.HarnessException;
}
}
Harness.Log ("Built {0} ({1})", TestName, Mode);
Jenkins.MainLog.WriteLine ("Built {0} ({1})", TestName, Mode);
}
}
}
@ -704,27 +702,30 @@ function toggleContainerVisibility (containerName)
args.Append ($"/p:Configuration={ProjectConfiguration} ");
args.Append (Harness.Quote (ProjectFile));
xbuild.StartInfo.Arguments = args.ToString ();
Harness.Log ("Building {0} ({1})", TestName, Mode);
Jenkins.MainLog.WriteLine ("Building {0} ({1})", TestName, Mode);
SetEnvironmentVariables (xbuild);
var log = Logs.Create (LogDirectory, "build-" + Platform + ".txt", "Build log");
var log = Logs.CreateStream (LogDirectory, "build-" + Platform + ".txt", "Build log");
foreach (string key in xbuild.StartInfo.EnvironmentVariables.Keys)
log.WriteLine ("{0}={1}", key, xbuild.StartInfo.EnvironmentVariables [key]);
log.WriteLine ("{0} {1}", xbuild.StartInfo.FileName, xbuild.StartInfo.Arguments);
if (Harness.DryRun) {
Harness.Log ("{0} {1}", xbuild.StartInfo.FileName, xbuild.StartInfo.Arguments);
} else {
if (!Harness.DryRun) {
try {
await xbuild.RunAsync (log.Path, true, TimeSpan.FromMinutes (5));
ExecutionResult = xbuild.ExitCode == 0 ? TestExecutingResult.Succeeded : TestExecutingResult.Failed;
} catch (TimeoutException e) {
log.WriteLine ("Build timed out after {0} seconds.", e.Timeout.TotalSeconds);
ExecutionResult = TestExecutingResult.TimedOut;
var timeout = TimeSpan.FromMinutes (5);
var result = await xbuild.RunAsync (log, true, timeout);
if (result.TimedOut) {
ExecutionResult = TestExecutingResult.TimedOut;
log.WriteLine ("Build timed out after {0} seconds.", timeout.TotalSeconds);
} else if (result.Succeeded) {
ExecutionResult = TestExecutingResult.Succeeded;
} else {
ExecutionResult = TestExecutingResult.Failed;
}
} catch (Exception e) {
log.WriteLine ("Harness exception: {0}", e);
ExecutionResult = TestExecutingResult.HarnessException;
}
}
Harness.Log ("Built {0} ({1})", TestName, Mode);
Jenkins.MainLog.WriteLine ("Built {0} ({1})", TestName, Mode);
}
}
}
@ -758,7 +759,7 @@ function toggleContainerVisibility (containerName)
public string Path;
public BuildToolTask BuildTask;
public override IEnumerable<LogFile> AggregatedLogs {
public override IEnumerable<Log> AggregatedLogs {
get {
return base.AggregatedLogs.Union (BuildTask.Logs);
}
@ -793,25 +794,28 @@ function toggleContainerVisibility (containerName)
using (var resource = await Jenkins.DesktopResource.AcquireConcurrentAsync ()) {
using (var proc = new Process ()) {
proc.StartInfo.FileName = Path;
Harness.Log ("Executing {0} ({1})", TestName, Mode);
var log = Logs.Create (LogDirectory, "execute-" + Platform + ".txt", "Execution log");
Jenkins.MainLog.WriteLine ("Executing {0} ({1})", TestName, Mode);
var log = Logs.CreateStream (LogDirectory, "execute-" + Platform + ".txt", "Execution log");
log.WriteLine ("{0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments);
if (Harness.DryRun) {
Harness.Log ("{0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments);
} else {
if (!Harness.DryRun) {
ExecutionResult = TestExecutingResult.Running;
try {
await proc.RunAsync (log.Path, true, TimeSpan.FromMinutes (5));
ExecutionResult = proc.ExitCode == 0 ? TestExecutingResult.Succeeded : TestExecutingResult.Failed;
} catch (TimeoutException e) {
log.WriteLine ("Execution timed out after {0} seconds.", e.Timeout.TotalSeconds);
ExecutionResult = TestExecutingResult.TimedOut;
var timeout = TimeSpan.FromMinutes (10);
var result = await proc.RunAsync (log, true, timeout);
if (result.TimedOut) {
log.WriteLine ("Execution timed out after {0} seconds.", timeout.TotalSeconds);
ExecutionResult = TestExecutingResult.TimedOut;
} else if (result.Succeeded) {
ExecutionResult = TestExecutingResult.Succeeded;
} else {
ExecutionResult = TestExecutingResult.Failed;
}
} catch (Exception e) {
log.WriteLine (e.ToString ());
ExecutionResult = TestExecutingResult.HarnessException;
}
}
Harness.Log ("Executed {0} ({1})", TestName, Mode);
Jenkins.MainLog.WriteLine ("Executed {0} ({1})", TestName, Mode);
}
}
}
@ -820,11 +824,28 @@ function toggleContainerVisibility (containerName)
class RunSimulatorTask : TestTask
{
public SimDevice Device;
public SimDevice CompanionDevice;
public XBuildTask BuildTask;
public string AppRunnerTarget;
AppRunner runner;
public SimDevice [] Simulators {
get {
if (CompanionDevice == null) {
return new SimDevice [] { Device };
} else {
return new SimDevice [] { Device, CompanionDevice };
}
}
}
public string BundleIdentifier {
get {
return runner.BundleIdentifier;
}
}
public async Task BuildAsync ()
{
if (Finished)
@ -838,7 +859,7 @@ function toggleContainerVisibility (containerName)
}
}
public override IEnumerable<LogFile> AggregatedLogs {
public override IEnumerable<Log> AggregatedLogs {
get {
return base.AggregatedLogs.Union (BuildTask.Logs);
}
@ -869,10 +890,11 @@ function toggleContainerVisibility (containerName)
set { throw new NotSupportedException (); }
}
public RunSimulatorTask (XBuildTask build_task, SimDevice device)
public RunSimulatorTask (XBuildTask build_task, SimDevice device, SimDevice companion_device = null)
{
BuildTask = build_task;
Device = device;
CompanionDevice = companion_device;
Jenkins = build_task.Jenkins;
ProjectFile = build_task.ProjectFile;
@ -886,7 +908,7 @@ function toggleContainerVisibility (containerName)
}
}
public Task PrepareSimulatorAsync (bool initialize)
public Task PrepareSimulatorAsync ()
{
if (Finished)
return Task.FromResult (true);
@ -895,49 +917,43 @@ function toggleContainerVisibility (containerName)
ExecutionResult = TestExecutingResult.BuildFailure;
return Task.FromResult (true);
}
var clean_state = false;//Platform == TestPlatform.tvOS;
runner = new AppRunner ()
{
Harness = Harness,
ProjectFile = ProjectFile,
SkipSimulatorSetup = !initialize,
SkipSimulatorCleanup = !initialize,
EnsureCleanSimulatorState = clean_state,
Target = AppRunnerTarget,
LogDirectory = LogDirectory,
MainLog = Logs.CreateStream (LogDirectory, "run-" + Device.UDID + ".log", "Run log"),
};
runner.Simulators = new SimDevice [] { Device };
runner.Simulators = Simulators;
runner.Initialize ();
runner.PrepareSimulator ();
return Task.FromResult (true);
}
protected override Task ExecuteAsync ()
protected override async Task ExecuteAsync ()
{
Harness.Log ("Running simulator '{0}' ({2}) for {1}", Device.Name, ProjectFile, Jenkins.Simulators.SupportedRuntimes.Where ((v) => v.Identifier == Device.SimRuntime).First ().Name);
Jenkins.MainLog.WriteLine ("Running simulator '{0}' ({2}) for {1}", Device.Name, ProjectFile, Jenkins.Simulators.SupportedRuntimes.Where ((v) => v.Identifier == Device.SimRuntime).First ().Name);
if (Finished)
return Task.FromResult (true);
return;
if (Harness.DryRun) {
Harness.Log ("<running app in simulator>");
Jenkins.MainLog.WriteLine ("<running app in simulator>");
} else {
try {
ExecutionResult = (ExecutionResult & ~TestExecutingResult.InProgressMask) | TestExecutingResult.Running;
Jenkins.GenerateReport ();
runner.Run ();
await runner.RunAsync ();
ExecutionResult = runner.Result;
} catch (Exception ex) {
Harness.Log ("Test {0} failed: {1}", Path.GetFileName (ProjectFile), ex);
Jenkins.MainLog.WriteLine ("Test {0} failed: {1}", Path.GetFileName (ProjectFile), ex);
ExecutionResult = TestExecutingResult.HarnessException;
}
Logs.AddRange (runner.Logs);
}
foreach (var log in Logs)
Console.WriteLine ("Log: {0}: {1}", log.Description, log.Path);
return Task.FromResult (true);
}
}
@ -946,7 +962,7 @@ function toggleContainerVisibility (containerName)
// between different simulators (which is slow).
class AggregatedRunSimulatorTask : TestTask
{
public SimDevice Device;
public SimDevice[] Devices;
public IEnumerable<RunSimulatorTask> Tasks;
@ -971,19 +987,26 @@ function toggleContainerVisibility (containerName)
build_timer.Stop ();
using (var desktop = await Jenkins.DesktopResource.AcquireExclusiveAsync ()) {
Harness.Log ("Preparing simulator: {0}", Device.Name);
run_timer.Start ();
Jenkins.MainLog.WriteLine ("Preparing simulator: {0}", Devices [0].Name);
// We need to set the dialog permissions for all the apps
// before launching the simulator, because once launched
// the simulator caches the values in-memory.
bool first = true;
foreach (var task in Tasks) {
await task.PrepareSimulatorAsync (first);
first = false;
}
foreach (var task in Tasks)
await task.PrepareSimulatorAsync ();
run_timer.Start ();
foreach (var dev in Devices)
await dev.PrepareSimulatorAsync (Jenkins.MainLog, Tasks.Where ((v) => !v.Ignored).Select ((v) => v.BundleIdentifier).ToArray ());
foreach (var task in Tasks)
await task.RunAsync ();
foreach (var dev in Devices)
await dev.ShutdownAsync (Jenkins.MainLog);
await SimDevice.KillEverythingAsync (Jenkins.MainLog);
run_timer.Stop ();
}
@ -1007,7 +1030,6 @@ function toggleContainerVisibility (containerName)
int max_concurrent_users = 1;
bool exclusive;
public Resource (string name, int max_concurrent_users = 1)
{
this.Name = name;
@ -1120,4 +1142,3 @@ function toggleContainerVisibility (containerName)
BuildFailure = 0x8000 + Failed,
}
}

306
tests/xharness/Log.cs Normal file
Просмотреть файл

@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
namespace xharness
{
public abstract class Log : IDisposable
{
public string Description;
public bool Timestamp;
protected Log ()
{
}
protected Log (string description)
{
Description = description;
}
public abstract string FullPath { get; }
protected abstract void WriteImpl (string value);
public virtual StreamReader GetReader ()
{
throw new NotSupportedException ();
}
public virtual TextWriter GetWriter ()
{
throw new NotSupportedException ();
}
public void Write (string value)
{
if (Timestamp)
value = DateTime.Now.ToString ("HH:mm:ss.fffffff") + " " + value;
WriteImpl (value);
}
public void WriteLine (string value)
{
Write (value + "\n");
}
public void WriteLine (string format, params object [] args)
{
Write (string.Format (format, args) + "\n");
}
public override string ToString ()
{
return Description;
}
public virtual void Flush ()
{
}
#region IDisposable Support
protected virtual void Dispose (bool disposing)
{
}
public void Dispose ()
{
Dispose (true);
}
#endregion
}
public class LogFile : Log
{
public string Path;
StreamWriter writer;
public LogFile (string description, string path)
: base (description)
{
Path = path;
}
protected override void WriteImpl (string value)
{
lock (this) {
using (var str = new FileStream (Path, FileMode.Append, FileAccess.Write, FileShare.Read)) {
using (var writer = new StreamWriter (str)) {
writer.Write (value);
writer.Flush ();
}
}
}
}
public override string FullPath {
get {
return Path;
}
}
public override StreamReader GetReader ()
{
return new StreamReader (new FileStream (Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
}
public override TextWriter GetWriter ()
{
return writer ?? (writer = new StreamWriter (new FileStream (Path, FileMode.Open, FileAccess.Write, FileShare.Read)));
}
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
if (writer != null) {
writer.Dispose ();
writer = null;
}
}
}
public class LogStream : Log
{
string path;
FileStream fs;
StreamWriter writer;
public FileStream FileStream {
get {
return fs;
}
}
public override StreamReader GetReader ()
{
return new StreamReader (new FileStream (path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
}
public override TextWriter GetWriter ()
{
return writer ?? (writer = new StreamWriter (fs));
}
public LogStream (string description, string path)
: base (description)
{
this.path = path;
fs = new FileStream (path, FileMode.Create, FileAccess.Write, FileShare.Read);
}
protected override void WriteImpl (string value)
{
var w = GetWriter ();
w.Write (value);
w.Flush ();
}
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
if (writer != null) {
writer.Dispose ();
writer = null;
}
if (fs != null) {
fs.Dispose ();
fs = null;
}
}
public override string FullPath {
get {
return path;
}
}
}
public class Logs : List<Log>
{
public LogStream CreateStream (string directory, string filename, string name)
{
Directory.CreateDirectory (directory);
var rv = new LogStream (name, Path.GetFullPath (Path.Combine (directory, filename)));
Add (rv);
return rv;
}
public LogFile CreateFile (string description, string path)
{
var rv = new LogFile (description, path);
Add (rv);
return rv;
}
}
public class ConsoleLog : Log
{
protected override void WriteImpl (string value)
{
Console.Write (value);
}
public override string FullPath {
get {
throw new NotSupportedException ();
}
}
public override TextWriter GetWriter ()
{
return Console.Out;
}
}
public class CaptureLog : Log
{
public string CapturePath { get; private set; }
public string Path { get; set; }
long startPosition;
long endPosition;
public CaptureLog (string capture_path)
{
CapturePath = capture_path;
}
public void StartCapture ()
{
if (File.Exists (CapturePath))
startPosition = new FileInfo (CapturePath).Length;
}
public void StopCapture ()
{
endPosition = new FileInfo (CapturePath).Length;
Capture ();
}
void Capture ()
{
if (startPosition == 0)
return;
var currentEndPosition = endPosition;
if (currentEndPosition == 0)
currentEndPosition = new FileInfo (CapturePath).Length;
var length = (int) (currentEndPosition - startPosition);
var currentLength = new FileInfo (CapturePath).Length;
var capturedLength = 0L;
if (File.Exists (Path))
capturedLength = new FileInfo (Path).Length;
// capture 1k more data than when we stopped, since the system log
// is cached in memory and flushed once in a while (so when the app
// requests the system log to be captured, it's usually not complete).
var availableLength = currentLength - startPosition;
if (availableLength <= capturedLength)
return; // We've captured before, and nothing new as added since last time.
// Capture at most 1k more
availableLength = Math.Min (availableLength, length + 1024);
using (var reader = new FileStream (CapturePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
using (var writer = new FileStream (Path, FileMode.Create, FileAccess.Write, FileShare.Read)) {
var buffer = new byte [4096];
reader.Position = startPosition;
while (availableLength > 0) {
int read = reader.Read (buffer, 0, Math.Min (buffer.Length, length));
if (read > 0) {
writer.Write (buffer, 0, read);
availableLength -= read;
}
}
}
}
}
public override void Flush ()
{
base.Flush ();
Capture ();
}
protected override void WriteImpl (string value)
{
throw new InvalidOperationException ();
}
public override string FullPath {
get {
return Path;
}
}
}
}

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

@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.IO;
namespace xharness
{
public class LogFile
{
public string Description;
public string Path;
void Write (string value)
{
lock (this) {
using (var str = new FileStream (Path, FileMode.Append, FileAccess.Write, FileShare.Read)) {
using (var writer = new StreamWriter (str))
writer.Write (value);
}
}
}
public void WriteLine (string value)
{
Write (value + "\n");
}
public void WriteLine (string format, params object [] args)
{
Write (string.Format (format, args) + "\n");
}
}
public class LogFiles : List<LogFile>
{
public LogFile Create (string directory, string filename, string name, bool overwrite = true)
{
var rv = new LogFile ()
{
Path = Path.GetFullPath (Path.Combine (directory, filename)),
Description = name,
};
Add (rv);
if (File.Exists (rv.Path)) {
if (overwrite)
File.Delete (rv.Path);
} else {
Directory.CreateDirectory (directory);
}
return rv;
}
}
}

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

@ -1,38 +1,109 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace xharness
{
public static class Process_Extensions
public class ProcessExecutionResult
{
public static async Task RunAsync (this Process process, LogFile log)
public bool TimedOut { get; set; }
public int ExitCode { get; set; }
public bool Succeeded { get { return !TimedOut && ExitCode == 0; } }
}
public static class ProcessHelper
{
public static Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, string args, Log log, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
using (var stream = new StreamWriter (log.Path, false))
await RunAsync (process, stream, stream);
return ExecuteCommandAsync (filename, args, log.GetWriter (), timeout, environment_variables, cancellation_token);
}
public static async Task RunAsync (this Process process, string outputFile, bool append, TimeSpan? timeout = null)
public static async Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, string args, string outputPath, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
Directory.CreateDirectory (Path.GetDirectoryName (outputPath));
using (var fs = new FileStream (outputPath, FileMode.Append, FileAccess.Write, FileShare.Read)) {
using (var stream = new StreamWriter (fs))
return await ExecuteCommandAsync (filename, args, stream, timeout, environment_variables, cancellation_token);
}
}
public static async Task<ProcessExecutionResult> ExecuteCommandAsync (string filename, string args, TextWriter output, TimeSpan timeout, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
using (var p = new Process ()) {
p.StartInfo.FileName = filename;
p.StartInfo.Arguments = args;
return await p.RunAsync (output, output, timeout, environment_variables, cancellation_token);
}
}
[DllImport ("/usr/lib/libc.dylib")]
internal static extern int kill (int pid, int sig);
public static Task<bool> PollForExitAsync (int pid, TimeSpan timeout)
{
var rv = new TaskCompletionSource<bool> ();
var watch = new Stopwatch ();
watch.Start ();
Task.Run (async () => {
while (watch.ElapsedMilliseconds < timeout.TotalMilliseconds) {
if (kill (pid, 0) != 0) {
// pid is not valid anymore, program exited
rv.SetResult (true);
return;
}
await Task.Delay (TimeSpan.FromMilliseconds (100));
}
rv.SetResult (false);
});
return rv.Task;
}
}
public static class Process_Extensions
{
public static async Task<ProcessExecutionResult> RunAsync (this Process process, Log log, CancellationToken? cancellation_token = null)
{
var stream = log.GetWriter ();
return await RunAsync (process, stream, stream, cancellation_token: cancellation_token);
}
public static async Task<ProcessExecutionResult> RunAsync (this Process process, string outputFile, bool append, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
Directory.CreateDirectory (Path.GetDirectoryName (outputFile));
using (var fs = new FileStream (outputFile, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read)) {
using (var stream = new StreamWriter (fs))
await RunAsync (process, stream, stream, timeout);
return await RunAsync (process, stream, stream, timeout, environment_variables, cancellation_token);
}
}
public static async Task RunAsync (this Process process, StreamWriter StdoutStream, StreamWriter StderrStream, TimeSpan? timeout = null)
public static Task<ProcessExecutionResult> RunAsync (this Process process, Log log, bool append, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
var writer = log.GetWriter ();
return RunAsync (process, writer, writer, timeout, environment_variables, cancellation_token);
}
public static async Task<ProcessExecutionResult> RunAsync (this Process process, TextWriter StdoutStream, TextWriter StderrStream, TimeSpan? timeout = null, Dictionary<string, string> environment_variables = null, CancellationToken? cancellation_token = null)
{
var stdout_completion = new TaskCompletionSource<bool> ();
var stderr_completion = new TaskCompletionSource<bool> ();
var exit_completion = new TaskCompletionSource<bool> ();
var rv = new ProcessExecutionResult ();
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
if (environment_variables != null) {
foreach (var kvp in environment_variables)
process.StartInfo.EnvironmentVariables [kvp.Key] = kvp.Value;
}
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (e.Data != null) {
@ -57,35 +128,42 @@ namespace xharness
}
};
StdoutStream.WriteLine ("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Start ();
process.BeginErrorReadLine ();
process.BeginOutputReadLine ();
cancellation_token?.Register (() => {
if (!exit_completion.Task.IsCompleted) {
StderrStream.WriteLine ($"Execution was cancelled.");
ProcessHelper.kill (process.Id, 9);
}
});
new Thread (() =>
{
if (timeout.HasValue) {
if (!process.WaitForExit ((int) timeout.Value.TotalMilliseconds)) {
process.Kill ();
process.WaitForExit ((int) 5); // Wait 5s for the kill to work, just
exit_completion.SetException (new TimeoutException { Timeout = timeout.Value });
} else {
exit_completion.SetResult (true);
ProcessHelper.kill (process.Id, 9);
process.WaitForExit ((int) TimeSpan.FromSeconds (5).TotalMilliseconds); // Wait 5s for the kill to work
rv.TimedOut = true;
lock (StderrStream)
StderrStream.WriteLine ($"Execution timed out after {timeout.Value.TotalSeconds} seconds and the process was killed.");
}
} else {
process.WaitForExit ();
exit_completion.SetResult (true);
}
exit_completion.TrySetResult (true);
}) {
IsBackground = true,
}.Start ();
await Task.WhenAll (stderr_completion.Task, stdout_completion.Task, exit_completion.Task);
rv.ExitCode = process.ExitCode;
return rv;
}
}
class TimeoutException : Exception
{
public TimeSpan Timeout;
}
}

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

@ -29,7 +29,7 @@ namespace xharness
Port = newPort;
break;
} catch (Exception ex) {
Console.WriteLine ("Failed to listen on port {0}: {1}", newPort, ex.Message);
Log.WriteLine ("Failed to listen on port {0}: {1}", newPort, ex.Message);
}
}
}
@ -44,7 +44,7 @@ namespace xharness
bool processed;
try {
Console.WriteLine ("Test log server listening on: {0}:{1}", Address, Port);
Log.WriteLine ("Test log server listening on: {0}:{1}", Address, Port);
do {
var context = server.GetContext ();
processed = Processing (context);
@ -87,7 +87,7 @@ namespace xharness
finished = true;
break;
default:
Console.WriteLine ("Unknown upload url: {0}", request.RawUrl);
Log.WriteLine ("Unknown upload url: {0}", request.RawUrl);
response = $"Unknown upload url: {request.RawUrl}";
break;
}

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

@ -15,7 +15,8 @@ namespace xharness
public IPAddress Address { get; set; }
public int Port { get; set; }
public string LogPath { get; set; }
public Log Log { get; set; }
public LogStream TestLog { get; set; }
public bool AutoExit { get; set; }
public abstract void Initialize ();
@ -30,7 +31,7 @@ namespace xharness
protected void Connected (string remote)
{
Console.WriteLine ("Connection from {0} saving logs to {1}", remote, LogPath);
Log.WriteLine ("Connection from {0} saving logs to {1}", remote, TestLog.FullPath);
connected.Set ();
if (output_stream != null) {
@ -38,9 +39,7 @@ namespace xharness
output_stream.Dispose ();
}
Directory.CreateDirectory (Path.GetDirectoryName (LogPath));
var fs = new FileStream (LogPath, FileMode.Create, FileAccess.Write, FileShare.Read);
var fs = TestLog.FileStream;
// a few extra bits of data only available from this side
string header = String.Format ("[Local Date/Time:\t{1}]{0}[Remote Address:\t{2}]{0}",
Environment.NewLine, DateTime.Now, remote);

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

@ -32,7 +32,7 @@ namespace xharness
try {
do {
Console.WriteLine ("Test log server listening on: {0}:{1}", Address, Port);
Log.WriteLine ("Test log server listening on: {0}:{1}", Address, Port);
using (TcpClient client = server.AcceptTcpClient ()) {
processed = Processing (client);
}

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
@ -16,7 +17,7 @@ namespace xharness
public List<SimDevice> AvailableDevices = new List<SimDevice> ();
public List<SimDevicePair> AvailableDevicePairs = new List<SimDevicePair> ();
public async Task LoadAsync (LogFile log)
public async Task LoadAsync (Log log)
{
if (SupportedRuntimes.Count > 0)
return;
@ -27,7 +28,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);
await process.RunAsync (log.Path, false);
await process.RunAsync (log, false);
log.WriteLine ("Result:");
log.WriteLine (File.ReadAllText (tmpfile));
var simulator_data = new XmlDocument ();
@ -54,6 +55,7 @@ namespace xharness
foreach (XmlNode sim in simulator_data.SelectNodes ("/MTouch/Simulator/AvailableDevices/SimDevice")) {
AvailableDevices.Add (new SimDevice ()
{
Harness = Harness,
Name = sim.Attributes ["Name"].Value,
UDID = sim.Attributes ["UDID"].Value,
SimRuntime = sim.SelectSingleNode ("SimRuntime").InnerText,
@ -105,6 +107,134 @@ namespace xharness
public string LogPath;
public string SystemLog { get { return Path.Combine (LogPath, "system.log"); } }
public Harness Harness;
public bool IsWatchSimulator { get { return SimRuntime.StartsWith ("com.apple.CoreSimulator.SimRuntime.watchOS", StringComparison.Ordinal); } }
public async Task EraseAsync (Log log)
{
// here we don't care if execution fails.
// erase the simulator (make sure the device isn't running first)
await Harness.ExecuteXcodeCommandAsync ("simctl", "shutdown " + UDID, log, TimeSpan.FromMinutes (1));
await Harness.ExecuteXcodeCommandAsync ("simctl", "erase " + UDID, log, TimeSpan.FromMinutes (1));
// boot & shutdown to make sure it actually works
await Harness.ExecuteXcodeCommandAsync ("simctl", "boot " + UDID, log, TimeSpan.FromMinutes (1));
await Harness.ExecuteXcodeCommandAsync ("simctl", "shutdown " + UDID, log, TimeSpan.FromMinutes (1));
}
public async Task ShutdownAsync (Log log)
{
await Harness.ExecuteXcodeCommandAsync ("simctl", "shutdown " + UDID, log, TimeSpan.FromMinutes (1));
}
public static Task KillEverythingAsync (Log log)
{
var to_kill = new string [] { "iPhone Simulator", "iOS Simulator", "Simulator", "Simulator (Watch)", "com.apple.CoreSimulator.CoreSimulatorService" };
return ProcessHelper.ExecuteCommandAsync ("killall", "-9 " + string.Join (" ", to_kill.Select ((v) => Harness.Quote (v)).ToArray ()), log, TimeSpan.FromSeconds (10));
}
public async Task AgreeToPromptsAsync (Log log, params string[] bundle_identifiers)
{
if (bundle_identifiers == null || bundle_identifiers.Length == 0) {
log.WriteLine ("No bundle identifiers given when requested permission editing.");
return;
}
var TCC_db = Path.Combine (DataPath, "data", "Library", "TCC", "TCC.db");
var sim_services = new string [] {
"kTCCServiceAddressBook",
"kTCCServicePhotos",
"kTCCServiceMediaLibrary",
"kTCCServiceUbiquity",
"kTCCServiceWillow"
};
var failure = false;
var tcc_edit_timeout = 5;
var watch = new Stopwatch ();
watch.Start ();
do {
failure = false;
foreach (var bundle_identifier in bundle_identifiers) {
foreach (var service in sim_services) {
var sql = string.Format ("{0} \"INSERT INTO access VALUES('{1}','{2}',0,1,0,NULL,NULL);\"", TCC_db, service, bundle_identifier);
var rv = await ProcessHelper.ExecuteCommandAsync ("sqlite3", sql, log, TimeSpan.FromSeconds (5));
if (!rv.Succeeded) {
failure = true;
break;
}
}
if (failure) {
if (watch.Elapsed.TotalSeconds > tcc_edit_timeout)
break;
log.WriteLine ("Failed to edit TCC.db, trying again in 1 second... ", (int) (tcc_edit_timeout - watch.Elapsed.TotalSeconds));
await Task.Delay (TimeSpan.FromSeconds (1));
}
}
} while (failure);
if (failure) {
log.WriteLine ("Failed to edit TCC.db, the test run might hang due to permission request dialogs");
} else {
log.WriteLine ("Successfully edited TCC.db");
}
}
async Task OpenSimulator (Log log)
{
string simulator_app;
if (IsWatchSimulator) {
simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "Simulator (Watch).app");
} else {
simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "Simulator.app");
if (!Directory.Exists (simulator_app))
simulator_app = Path.Combine (Harness.XcodeRoot, "Contents", "Developer", "Applications", "iOS Simulator.app");
}
await ProcessHelper.ExecuteCommandAsync ("open", "-a " + Harness.Quote (simulator_app) + " --args -CurrentDeviceUDID " + UDID, log, TimeSpan.FromSeconds (15));
}
public async Task PrepareSimulatorAsync (Log log, params string[] bundle_identifiers)
{
// Kill all existing processes
await KillEverythingAsync (log);
// We shutdown and erase all simulators.
await EraseAsync (log);
// Edit the permissions to prevent dialog boxes in the test app
var TCC_db = Path.Combine (DataPath, "data", "Library", "TCC", "TCC.db");
if (!File.Exists (TCC_db)) {
log.WriteLine ("Opening simulator to create TCC.db");
await OpenSimulator (log);
var tcc_creation_timeout = 60;
var watch = new Stopwatch ();
watch.Start ();
while (!File.Exists (TCC_db) && watch.Elapsed.TotalSeconds < tcc_creation_timeout) {
log.WriteLine ("Waiting for simulator to create TCC.db... {0}", (int)(tcc_creation_timeout - watch.Elapsed.TotalSeconds));
await Task.Delay (TimeSpan.FromSeconds (0.250));
}
}
if (File.Exists (TCC_db)) {
await AgreeToPromptsAsync (log, bundle_identifiers);
} else {
log.WriteLine ("No TCC.db found for the simulator {0} (SimRuntime={1} and SimDeviceType={1})", UDID, SimRuntime, SimDeviceType);
}
// Make sure we're in a clean state
await KillEverythingAsync (log);
// Make 100% sure we're shutdown
await ShutdownAsync (log);
}
}
public class SimDevicePair
@ -120,7 +250,7 @@ namespace xharness
public List<Device> ConnectedDevices = new List<Device> ();
public async Task LoadAsync ()
public async Task LoadAsync (Log log)
{
if (ConnectedDevices.Count > 0)
return;
@ -130,7 +260,7 @@ namespace xharness
using (var process = new Process ()) {
process.StartInfo.FileName = Harness.MlaunchPath;
process.StartInfo.Arguments = string.Format ("--sdkroot {0} --listdev={1} --output-format=xml", Harness.XcodeRoot, tmpfile);
await process.RunAsync (tmpfile, false);
await process.RunAsync (log, false);
var doc = new XmlDocument ();
doc.LoadWithoutNetworkAccess (tmpfile);

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

@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace xharness
{
public class XProcess
{
public Harness Harness;
public int VerbosityLevel = 1;
Process process = new Process ();
CountdownEvent output_completed = new CountdownEvent (2);
object output_lock = new object ();
StringBuilder output = new StringBuilder ();
public string FileName {
get { return process.StartInfo.FileName; }
set { process.StartInfo.FileName = value; }
}
public string Arguments {
get { return process.StartInfo.Arguments; }
set { process.StartInfo.Arguments = value; }
}
public Dictionary<string, string> EnvironmentVariables { get; set; }
public void Start ()
{
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
if (EnvironmentVariables != null) {
foreach (var kvp in EnvironmentVariables)
process.StartInfo.EnvironmentVariables.Add (kvp.Key, kvp.Value);
}
process.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (e.Data == null) {
output_completed.Signal ();
} else {
lock (output_lock) {
output.AppendLine (e.Data);
Harness.Log (VerbosityLevel, e.Data);
}
}
};
process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
{
if (e.Data == null) {
output_completed.Signal ();
} else {
lock (output_lock) {
output.AppendLine (e.Data);
Harness.Log (VerbosityLevel, e.Data);
}
}
};
Harness.Log ("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Start ();
process.BeginErrorReadLine ();
process.BeginOutputReadLine ();
}
public bool WaitForExit (TimeSpan timeout)
{
return process.WaitForExit ((int) timeout.TotalMilliseconds);
}
public int ExitCode {
get { return process.ExitCode; }
}
public string ReadCurrentOutput ()
{
lock (output_lock)
return output.ToString ();
}
public int Id {
get { return process.Id; }
}
}
}

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

@ -34,6 +34,7 @@
<Reference Include="System" />
<Reference Include="System.Xml" />
<Reference Include="System.Web" />
<Reference Include="Mono.Posix" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
@ -56,7 +57,6 @@
<Compile Include="SimpleTcpListener.cs" />
<Compile Include="BCLTarget.cs" />
<Compile Include="DeviceLogCapturer.cs" />
<Compile Include="XProcess.cs" />
<Compile Include="MakefileGenerator.cs" />
<Compile Include="SolutionGenerator.cs" />
<Compile Include="MacClassicTarget.cs" />
@ -68,7 +68,7 @@
<Compile Include="Process_Extensions.cs" />
<Compile Include="Simulators.cs" />
<Compile Include="TestProject.cs" />
<Compile Include="LogFile.cs" />
<Compile Include="Log.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>