using System; using System.Collections.Generic; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Text; namespace xharness { public class Jenkins { public Harness Harness; public bool IncludeClassic; public bool IncludeBcl; public LogFiles Logs = new LogFiles (); LogFile SimulatorLoadLog; public string LogDirectory { get { return Path.Combine (Harness.JENKINS_RESULTS_DIRECTORY, "tests"); } } public Simulators Simulators = new Simulators (); List Tasks = new List (); internal static Resource DesktopResource = new Resource ("Desktop", Environment.ProcessorCount); async Task> CreateRunSimulatorTaskAsync (XBuildTask buildTask) { var runtasks = new List (); Simulators.Harness = Harness; if (SimulatorLoadLog == null) SimulatorLoadLog = Logs.Create (LogDirectory, "simulator-list.log", "Simulator Listing"); try { await Simulators.LoadAsync (SimulatorLoadLog); } catch (Exception e) { SimulatorLoadLog.WriteLine ("Failed to load simulators:"); SimulatorLoadLog.WriteLine (e.ToString ()); return runtasks; } var fn = Path.GetFileNameWithoutExtension (buildTask.ProjectFile); if (fn.EndsWith ("-tvos", StringComparison.Ordinal)) { var latesttvOSRuntime = Simulators.SupportedRuntimes. Where ((SimRuntime v) => v.Identifier.StartsWith ("com.apple.CoreSimulator.SimRuntime.tvOS-", StringComparison.Ordinal)). OrderBy ((SimRuntime v) => v.Version). Last (); var tvOSDeviceType = Simulators.SupportedDeviceTypes. Where ((SimDeviceType v) => v.ProductFamilyId == "TV"). First (); var device = Simulators.AvailableDevices. Where ((SimDevice v) => v.SimRuntime == latesttvOSRuntime.Identifier && v.SimDeviceType == tvOSDeviceType.Identifier). First (); runtasks.Add (new RunSimulatorTask (buildTask, device) { Platform = TestPlatform.tvOS }); } else if (fn.EndsWith ("-watchos", StringComparison.Ordinal)) { var latestwatchOSRuntime = Simulators.SupportedRuntimes. Where ((SimRuntime v) => v.Identifier.StartsWith ("com.apple.CoreSimulator.SimRuntime.watchOS-", StringComparison.Ordinal)). OrderBy ((SimRuntime v) => v.Version). Last (); var watchOSDeviceType = Simulators.SupportedDeviceTypes. Where ((SimDeviceType v) => v.ProductFamilyId == "Watch"). First (); 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 }); } else { var latestiOSRuntime = Simulators.SupportedRuntimes. Where ((SimRuntime v) => v.Identifier.StartsWith ("com.apple.CoreSimulator.SimRuntime.iOS-", StringComparison.Ordinal)). OrderBy ((SimRuntime v) => v.Version). Last (); if (fn.EndsWith ("-unified", StringComparison.Ordinal)) { 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 }); } } return runtasks; } static string AddSuffixToPath (string path, string suffix) { return Path.Combine (Path.GetDirectoryName (path), Path.GetFileNameWithoutExtension (path) + suffix + Path.GetExtension (path)); } async Task PopulateTasksAsync () { // Missing: // api-diff // msbuild tests var runSimulatorTasks = new List (); foreach (var project in Harness.IOSTestProjects) { if (!project.IsExecutableProject) continue; if (!IncludeBcl && project.Path.Contains ("bcl-test")) 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)); var suffixes = new string [] { "-unified", "-tvos", "-watchos" }; foreach (var suffix in suffixes) { var derived = new XBuildTask () { Jenkins = this, ProjectFile = AddSuffixToPath (project.Path, suffix), 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 (); } 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) { var build = new XBuildTask () { Platform = platform, Jenkins = task.Jenkins, ProjectFile = AddSuffixToPath (task.ProjectFile, suffix), ProjectConfiguration = task.ProjectConfiguration, ProjectPlatform = task.ProjectPlatform, SpecifyPlatform = task.BuildTask.SpecifyPlatform, SpecifyConfiguration = task.BuildTask.SpecifyConfiguration, }; var execute = new MacExecuteTask () { Platform = build.Platform, Jenkins = build.Jenkins, ProjectFile = build.ProjectFile, ProjectConfiguration = build.ProjectConfiguration, ProjectPlatform = build.ProjectPlatform, BuildTask = build, }; return execute; } public int Run () { try { Directory.CreateDirectory (LogDirectory); Harness.HarnessLog = Logs.Create (LogDirectory, "Harness.log", "Harness log"); Task.Run (async () => { await PopulateTasksAsync (); }).Wait (); var tasks = new List (); foreach (var task in Tasks) tasks.Add (task.RunAsync ()); Task.WaitAll (tasks.ToArray ()); GenerateReport (); return Tasks.Any ((v) => v.ExecutionResult == TestExecutingResult.Failed || v.ExecutionResult == TestExecutingResult.Crashed) ? 1 : 0; } catch (Exception ex) { Harness.Log ("Unexpected exception: {0}", ex); return 2; } } string GetTestColor (IEnumerable tests) { if (tests.All ((v) => v.Succeeded)) return "green"; else if (tests.Any ((v) => v.Crashed)) return "maroon"; else if (tests.Any ((v) => v.TimedOut)) return "purple"; else if (tests.Any ((v) => v.BuildFailure)) return "darkred"; else if (tests.Any ((v) => v.Failed)) return "red"; else if (tests.All ((v) => v.Building)) return "darkblue"; else if (tests.All ((v) => v.InProgress)) return "blue"; else if (tests.Any ((v) => v.NotStarted)) return "black"; else if (tests.Any ((v) => v.Ignored)) return "gray"; else return "black"; } string GetTestColor (TestTask test) { if (test.NotStarted) { return "black"; } else if (test.InProgress) { if (test.Building) { return "darkblue"; } else if (test.Running) { return "lightblue"; } else { return "blue"; } } else { if (test.Crashed) { return "maroon"; } else if (test.HarnessException) { return "yellow"; } else if (test.TimedOut) { return "purple"; } else if (test.BuildFailure) { return "darkred"; } else if (test.Failed) { return "red"; } else if (test.Succeeded) { return "green"; } else if (test.Ignored) { return "gray"; } else { return "pink"; } } } object report_lock = new object (); public void GenerateReport () { var id_counter = 0; using (var stream = new MemoryStream ()) { using (var writer = new StreamWriter (stream)) { writer.WriteLine (""); writer.WriteLine (""); writer.WriteLine ("Test results"); writer.WriteLine (@""); writer.WriteLine (""); writer.WriteLine ("

Test results

"); foreach (var log in Logs) writer.WriteLine ("{1}
", log.Path.Substring (LogDirectory.Length + 1), log.Description); var allSimulatorTasks = new List (); var allExecuteTasks = new List (); foreach (var task in Tasks) { var aggregated = task as AggregatedRunSimulatorTask; if (aggregated != null) { allSimulatorTasks.AddRange (aggregated.Tasks); continue; } var execute = task as MacExecuteTask; if (execute != null) { allExecuteTasks.Add (execute); continue; } throw new NotImplementedException (); } var allTasks = new List (); allTasks.AddRange (allExecuteTasks); allTasks.AddRange (allSimulatorTasks); var failedTests = allTasks.Where ((v) => v.Failed); var stillInProgress = allTasks.Any ((v) => v.InProgress); if (failedTests.Count () == 0) { if (stillInProgress) { writer.WriteLine ("

All tests passed (but still tests in progress)

"); } else { writer.WriteLine ("

All tests passed

"); } } else { writer.WriteLine ("

{0} tests failed

", failedTests.Count ()); foreach (var group in failedTests.GroupBy ((v) => v.TestName)) { var enumerableGroup = group as IEnumerable; if (enumerableGroup != null) { writer.WriteLine ("{0} ({1})
", group.Key, string.Join (", ", enumerableGroup.Select ((v) => string.Format ("{1}", GetTestColor (v), string.IsNullOrEmpty (v.Mode) ? v.ExecutionResult.ToString () : v.Mode)).ToArray ()), group.Key.Replace (' ', '-')); continue; } //var executionGroup = group as IEnumerable; //if (executionGroup != null) { // writer.WriteLine ("{0} ({1})
", group.Key, string.Join (", ", executionGroup.Select ((v) => string.Format ("{1}", 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); var defaultHide = !group.Any ((v) => v.Failed); writer.WriteLine ("

{0} ({4}) {3}

", group.Key, group.Key.Replace (' ', '-'), GetTestColor (group), defaultHide ? "Show" : "Hide", identicalResults ? firstResult.ToString () : "multiple results"); writer.WriteLine ("
", group.Key.Replace (' ', '-'), defaultHide ? "none" : "block"); foreach (var test in group) { string state; state = test.ExecutionResult.ToString (); var log_id = id_counter++; writer.WriteLine ("{0} ({1}) Show details
", test.Mode, state, log_id, GetTestColor (test)); writer.WriteLine (""); } writer.WriteLine ("
"); } writer.WriteLine (""); writer.WriteLine (""); } lock (report_lock) { var report = Path.Combine (LogDirectory, "index.html"); if (File.Exists (report)) File.Delete (report); File.WriteAllBytes (report, stream.ToArray ()); Harness.Log (2, "Generated report: {0}", report); } } } } abstract class TestTask { public Jenkins Jenkins; public Harness Harness { get { return Jenkins.Harness; } } public string ProjectFile; public string ProjectConfiguration; public string ProjectPlatform; Stopwatch duration = new Stopwatch (); public TimeSpan Duration { get { return duration.Elapsed; } } TestExecutingResult execution_result; public TestExecutingResult ExecutionResult { get { return execution_result; } set { execution_result = value; Jenkins.GenerateReport (); } } public bool NotStarted { get { return (ExecutionResult & TestExecutingResult.StateMask) == TestExecutingResult.NotStarted; } } public bool InProgress { get { return (ExecutionResult & TestExecutingResult.StateMask) == TestExecutingResult.InProgress; } } public bool Finished { get { return (ExecutionResult & TestExecutingResult.StateMask) == TestExecutingResult.Finished; } } public bool Building { get { return (ExecutionResult & TestExecutingResult.InProgressMask) == TestExecutingResult.Building; } } public bool Built { get { return (ExecutionResult & TestExecutingResult.InProgressMask) == TestExecutingResult.Built; } } public bool Running { get { return (ExecutionResult & TestExecutingResult.InProgressMask) == TestExecutingResult.Running; } } public bool Succeeded { get { return (ExecutionResult & TestExecutingResult.Succeeded) == TestExecutingResult.Succeeded; } } public bool Failed { get { return (ExecutionResult & TestExecutingResult.Failed) == TestExecutingResult.Failed; } } public bool Ignored { get { return (ExecutionResult & TestExecutingResult.Ignored) == TestExecutingResult.Ignored; } } public bool Crashed { get { return (ExecutionResult & TestExecutingResult.Crashed) == TestExecutingResult.Crashed; } } public bool TimedOut { get { return (ExecutionResult & TestExecutingResult.TimedOut) == TestExecutingResult.TimedOut; } } public bool BuildFailure { get { return (ExecutionResult & TestExecutingResult.BuildFailure) == TestExecutingResult.BuildFailure; } } public bool HarnessException { get { return (ExecutionResult & TestExecutingResult.HarnessException) == TestExecutingResult.HarnessException; } } public virtual string Mode { get; set; } public virtual string TestName { get { var rv = Path.GetFileNameWithoutExtension (ProjectFile); switch (Platform) { case TestPlatform.Mac: case TestPlatform.Mac_Classic: return rv; case TestPlatform.Mac_Unified: return rv.Substring (0, rv.Length - "-unified".Length); case TestPlatform.Mac_UnifiedXM45: return rv.Substring (0, rv.Length - "-unifiedXM45".Length); default: if (rv.EndsWith ("-watchos", StringComparison.Ordinal)) { return rv.Substring (0, rv.Length - 8); } else if (rv.EndsWith ("-tvos", StringComparison.Ordinal)) { return rv.Substring (0, rv.Length - 5); } else if (rv.EndsWith ("-unified", StringComparison.Ordinal)) { return rv.Substring (0, rv.Length - 8); } else { return rv; } } } } public TestPlatform Platform { get; set; } public LogFiles Logs = new LogFiles (); public List Resources = new List (); public virtual IEnumerable AggregatedLogs { get { return Logs; } } public string LogDirectory { get { return Path.Combine (Jenkins.LogDirectory, TestName); } } Task build_task; async Task RunInternalAsync () { if (Finished) return; ExecutionResult = (ExecutionResult & ~TestExecutingResult.StateMask) | TestExecutingResult.InProgress; Jenkins.GenerateReport (); duration.Start (); build_task = ExecuteAsync (); await build_task; duration.Stop (); ExecutionResult = (ExecutionResult & ~TestExecutingResult.StateMask) | TestExecutingResult.Finished; if ((ExecutionResult & ~TestExecutingResult.StateMask) == 0) throw new Exception ("Result not set!"); Jenkins.GenerateReport (); } public Task RunAsync () { if (build_task == null) build_task = RunInternalAsync (); return build_task; } protected abstract Task ExecuteAsync (); public override string ToString () { return ExecutionResult.ToString (); } } abstract class BuildToolTask : TestTask { public bool SpecifyPlatform = true; public bool SpecifyConfiguration = true; public override string Mode { get { return Platform.ToString (); } set { throw new NotSupportedException (); } } protected void SetEnvironmentVariables (Process process) { switch (Platform) { case TestPlatform.iOS_Classic: case TestPlatform.iOS_Unified: case TestPlatform.iOS_Unified32: case TestPlatform.iOS_Unified64: case TestPlatform.tvOS: case TestPlatform.watchOS: process.StartInfo.EnvironmentVariables ["MD_APPLE_SDK_ROOT"] = Harness.XcodeRoot; process.StartInfo.EnvironmentVariables ["MD_MTOUCH_SDK_ROOT"] = Path.Combine (Harness.IOS_DESTDIR, "Library", "Frameworks", "Xamarin.iOS.framework", "Versions", "Current"); process.StartInfo.EnvironmentVariables ["XBUILD_FRAMEWORK_FOLDERS_PATH"] = Path.Combine (Harness.IOS_DESTDIR, "Library", "Frameworks", "Mono.framework", "External", "xbuild-frameworks"); process.StartInfo.EnvironmentVariables ["MSBuildExtensionsPath"] = Path.Combine (Harness.IOS_DESTDIR, "Library", "Frameworks", "Mono.framework", "External", "xbuild"); break; case TestPlatform.Mac: case TestPlatform.Mac_Classic: case TestPlatform.Mac_Unified: case TestPlatform.Mac_UnifiedXM45: process.StartInfo.EnvironmentVariables ["MD_APPLE_SDK_ROOT"] = Harness.XcodeRoot; process.StartInfo.EnvironmentVariables ["XBUILD_FRAMEWORK_FOLDERS_PATH"] = Path.Combine (Harness.MAC_DESTDIR, "Library", "Frameworks", "Mono.framework", "External", "xbuild-frameworks"); process.StartInfo.EnvironmentVariables ["MSBuildExtensionsPath"] = Path.Combine (Harness.MAC_DESTDIR, "Library", "Frameworks", "Mono.framework", "External", "xbuild"); process.StartInfo.EnvironmentVariables ["XamarinMacFrameworkRoot"] = Path.Combine (Harness.MAC_DESTDIR, "Library", "Frameworks", "Xamarin.Mac.framework", "Versions", "Current"); process.StartInfo.EnvironmentVariables ["XAMMAC_FRAMEWORK_PATH"] = Path.Combine (Harness.MAC_DESTDIR, "Library", "Frameworks", "Xamarin.Mac.framework", "Versions", "Current"); break; default: throw new NotImplementedException (); } } } class MdtoolTask : BuildToolTask { protected override async Task ExecuteAsync () { using (var resource = await Jenkins.DesktopResource.AcquireConcurrentAsync ()) { using (var xbuild = new Process ()) { xbuild.StartInfo.FileName = "/Applications/Xamarin Studio.app/Contents/MacOS/mdtool"; var args = new StringBuilder (); args.Append ("build "); 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); SetEnvironmentVariables (xbuild); var log = Logs.Create (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 { 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; } catch (Exception e) { log.WriteLine ("Harness exception: {0}", e); ExecutionResult = TestExecutingResult.HarnessException; } } Harness.Log ("Built {0} ({1})", TestName, Mode); } } } } class XBuildTask : BuildToolTask { protected override async Task ExecuteAsync () { using (var resource = await Jenkins.DesktopResource.AcquireConcurrentAsync ()) { using (var xbuild = new Process ()) { xbuild.StartInfo.FileName = "xbuild"; var args = new StringBuilder (); args.Append ("/verbosity:diagnostic "); if (SpecifyPlatform) args.Append ($"/p:Platform={ProjectPlatform} "); if (SpecifyConfiguration) args.Append ($"/p:Configuration={ProjectConfiguration} "); args.Append (Harness.Quote (ProjectFile)); xbuild.StartInfo.Arguments = args.ToString (); Harness.Log ("Building {0} ({1})", TestName, Mode); SetEnvironmentVariables (xbuild); var log = Logs.Create (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 { 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; } catch (Exception e) { log.WriteLine ("Harness exception: {0}", e); ExecutionResult = TestExecutingResult.HarnessException; } } Harness.Log ("Built {0} ({1})", TestName, Mode); } } } } abstract class MacTask : TestTask { public override string Mode { get { switch (Platform) { case TestPlatform.Mac: return TestName; case TestPlatform.Mac_Classic: return "Classic"; case TestPlatform.Mac_Unified: return "Unified"; case TestPlatform.Mac_UnifiedXM45: return "Unified XM45"; default: throw new NotImplementedException (); } } set { throw new NotSupportedException (); } } } class MacExecuteTask : MacTask { public string Path; public BuildToolTask BuildTask; public override IEnumerable AggregatedLogs { get { return base.AggregatedLogs.Union (BuildTask.Logs); } } protected override async Task ExecuteAsync () { ExecutionResult = TestExecutingResult.Building; await BuildTask.RunAsync (); if (!BuildTask.Succeeded) { ExecutionResult = TestExecutingResult.BuildFailure; return; } ExecutionResult = TestExecutingResult.Built; var projectDir = System.IO.Path.GetDirectoryName (ProjectFile); var name = System.IO.Path.GetFileName (projectDir); if (string.Equals ("mac", name, StringComparison.OrdinalIgnoreCase)) name = System.IO.Path.GetFileName (System.IO.Path.GetDirectoryName (projectDir)); var suffix = string.Empty; switch (Platform) { case TestPlatform.Mac_Unified: suffix = "-unified"; break; case TestPlatform.Mac_UnifiedXM45: suffix = "-unifiedXM45"; break; } Path = System.IO.Path.Combine (System.IO.Path.GetDirectoryName (ProjectFile), "bin", BuildTask.ProjectPlatform, BuildTask.ProjectConfiguration + suffix, name + ".app", "Contents", "MacOS", name); 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"); log.WriteLine ("{0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments); if (Harness.DryRun) { Harness.Log ("{0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments); } else { 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; } catch (Exception e) { log.WriteLine (e.ToString ()); ExecutionResult = TestExecutingResult.HarnessException; } } Harness.Log ("Executed {0} ({1})", TestName, Mode); } } } } class RunSimulatorTask : TestTask { public SimDevice Device; public XBuildTask BuildTask; public string AppRunnerTarget; AppRunner runner; public async Task BuildAsync () { if (Finished) return; ExecutionResult |= TestExecutingResult.Building; await BuildTask.RunAsync (); if (BuildTask.Succeeded) { ExecutionResult = (ExecutionResult & ~TestExecutingResult.InProgressMask) | TestExecutingResult.Built; } else { ExecutionResult = (ExecutionResult & ~(TestExecutingResult.InProgressMask | TestExecutingResult.StateMask)) | TestExecutingResult.BuildFailure; } } public override IEnumerable AggregatedLogs { get { return base.AggregatedLogs.Union (BuildTask.Logs); } } public override string Mode { get { switch (Platform) { case TestPlatform.tvOS: case TestPlatform.watchOS: return Platform.ToString (); case TestPlatform.iOS_Classic: return "iOS Classic"; case TestPlatform.iOS_Unified32: return "iOS Unified 32-bits"; case TestPlatform.iOS_Unified64: return "iOS Unified 64-bits"; case TestPlatform.iOS_Unified: if (Jenkins.Simulators.SupportedDeviceTypes.Find ((SimDeviceType v) => v.Identifier == Device.SimDeviceType).Supports64Bits) { return "iOS Unified 32-bits"; } else { return "iOS Unified 64-bits"; } default: throw new NotImplementedException (); } } set { throw new NotSupportedException (); } } public RunSimulatorTask (XBuildTask build_task, SimDevice device) { BuildTask = build_task; Device = device; Jenkins = build_task.Jenkins; ProjectFile = build_task.ProjectFile; var project = Path.GetFileNameWithoutExtension (ProjectFile); if (project.EndsWith ("-tvos", StringComparison.Ordinal)) { AppRunnerTarget = "tvos-simulator"; } else if (project.EndsWith ("-watchos", StringComparison.Ordinal)) { AppRunnerTarget = "watchos-simulator"; } else { AppRunnerTarget = "ios-simulator"; } } public Task PrepareSimulatorAsync (bool initialize) { if (Finished) return Task.FromResult (true); if (!BuildTask.Succeeded) { ExecutionResult = TestExecutingResult.BuildFailure; return Task.FromResult (true); } runner = new AppRunner () { Harness = Harness, ProjectFile = ProjectFile, SkipSimulatorSetup = !initialize, SkipSimulatorCleanup = !initialize, Target = AppRunnerTarget, LogDirectory = LogDirectory, }; runner.Simulators = new SimDevice [] { Device }; runner.Initialize (); runner.PrepareSimulator (); return Task.FromResult (true); } protected override Task ExecuteAsync () { Harness.Log ("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); if (Harness.DryRun) { Harness.Log (""); } else { try { ExecutionResult = (ExecutionResult & ~TestExecutingResult.InProgressMask) | TestExecutingResult.Running; Jenkins.GenerateReport (); runner.Run (); ExecutionResult = runner.Result; } catch (Exception ex) { Harness.Log ("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); } } // This class groups simulator run tasks according to the // simulator they'll run from, so that we minimize switching // between different simulators (which is slow). class AggregatedRunSimulatorTask : TestTask { public SimDevice Device; public IEnumerable Tasks; // Due to parallelization this isn't the same as the sum of the duration for all the build tasks. Stopwatch build_timer = new Stopwatch (); public TimeSpan BuildDuration { get { return build_timer.Elapsed; } } Stopwatch run_timer = new Stopwatch (); public TimeSpan RunDuration { get { return run_timer.Elapsed; } } public AggregatedRunSimulatorTask (IEnumerable tasks) { this.Tasks = tasks; } protected override async Task ExecuteAsync () { // First build everything. This is required for the run simulator // task to properly configure the simulator. build_timer.Start (); await Task.WhenAll (Tasks.Select ((v) => v.BuildAsync ()).Distinct ()); build_timer.Stop (); using (var desktop = await Jenkins.DesktopResource.AcquireExclusiveAsync ()) { Harness.Log ("Preparing simulator: {0}", Device.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; } run_timer.Start (); foreach (var task in Tasks) await task.RunAsync (); run_timer.Stop (); } ExecutionResult = Tasks.Any ((v) => !v.Succeeded) ? TestExecutingResult.Failed : TestExecutingResult.Succeeded; } } // This is a very simple class to manage the general concept of 'resource'. // Performance isn't important, so this is very simple. // Currently it's only used to make sure everything that happens on the desktop // is serialized (Jenkins.DesktopResource), but in the future the idea is to // make each connected device a separate resource, which will make it possible // to run tests in parallel across devices (and at the same time use the desktop // to build the next test project). class Resource { public string Name; ConcurrentQueue> queue = new ConcurrentQueue> (); ConcurrentQueue> exclusive_queue = new ConcurrentQueue> (); int users; int max_concurrent_users = 1; bool exclusive; public Resource (string name, int max_concurrent_users = 1) { this.Name = name; this.max_concurrent_users = max_concurrent_users; } public Task AcquireConcurrentAsync () { lock (queue) { if (!exclusive && users < max_concurrent_users) { users++; return Task.FromResult (new AcquiredResource (this)); } else { var tcs = new TaskCompletionSource (new AcquiredResource (this)); queue.Enqueue (tcs); return tcs.Task; } } } public Task AcquireExclusiveAsync () { lock (queue) { if (users == 0) { users++; exclusive = true; return Task.FromResult (new AcquiredResource (this)); } else { var tcs = new TaskCompletionSource (new AcquiredResource (this)); exclusive_queue.Enqueue (tcs); return tcs.Task; } } } void Release () { TaskCompletionSource tcs; lock (queue) { users--; exclusive = false; if (queue.TryDequeue (out tcs)) { users++; tcs.SetResult ((IDisposable) tcs.Task.AsyncState); } else if (users == 0 && exclusive_queue.TryDequeue (out tcs)) { users++; exclusive = true; tcs.SetResult ((IDisposable) tcs.Task.AsyncState); } } } class AcquiredResource : IDisposable { Resource resource; public AcquiredResource (Resource resource) { this.resource = resource; } void IDisposable.Dispose () { resource.Release (); } } } public enum TestPlatform { None, iOS_Classic, iOS_Unified, iOS_Unified32, iOS_Unified64, tvOS, watchOS, Mac, Mac_Classic, Mac_Unified, Mac_UnifiedXM45, } [Flags] public enum TestExecutingResult { NotStarted = 0, InProgress = 0x1, Finished = 0x2, StateMask = NotStarted + InProgress + Finished, // In progress state Building = 0x10 + InProgress, Built = 0x20 + InProgress, Running = 0x40 + InProgress, InProgressMask = 0x10 + 0x20 + 0x40, // Finished results Succeeded = 0x100 + Finished, Failed = 0x200 + Finished, Ignored = 0x400 + Finished, // Finished & Failed results Crashed = 0x1000 + Failed, TimedOut = 0x2000 + Failed, HarnessException = 0x4000 + Failed, BuildFailure = 0x8000 + Failed, } }