This C# program should run on a DTM/WHQL server. It's used by the whql_submission test to schedule and monitor device submission jobs. Note: the binary is copied to the server at run time, so it doesn't need to be packaged in winutils.iso. Changes from v1: - Take a job filter regex from the user, and run only the jobs whose names match the regex. This is useful for running individual tests instead of whole submissions which can take many hours to run. - When test execution completes set the client machine's state to Unsafe and then Reset. - Instead of looking for the requested test device and failing immediately if it isn't found, allow up to 2 minutes for it to show up. - Add the job ID of each job to the result list printed to stdout. Signed-off-by: Michael Goldish <mgoldish@xxxxxxxxxx> --- client/tests/kvm/deps/whql_submission_15.cs | 289 ++++++++++++++++++++++++++ client/tests/kvm/deps/whql_submission_15.exe | Bin 0 -> 10240 bytes 2 files changed, 289 insertions(+), 0 deletions(-) create mode 100644 client/tests/kvm/deps/whql_submission_15.cs create mode 100644 client/tests/kvm/deps/whql_submission_15.exe diff --git a/client/tests/kvm/deps/whql_submission_15.cs b/client/tests/kvm/deps/whql_submission_15.cs new file mode 100644 index 0000000..bf6e136 --- /dev/null +++ b/client/tests/kvm/deps/whql_submission_15.cs @@ -0,0 +1,289 @@ +// DTM submission automation program +// Author: Michael Goldish <mgoldish@xxxxxxxxxx> +// Based on sample code by Microsoft. + +// Note: this program has only been tested with DTM version 1.5. +// It might fail to work with other versions, specifically because it uses +// a few undocumented methods/attributes. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.DistributedAutomation.DeviceSelection; +using Microsoft.DistributedAutomation.SqlDataStore; + +namespace automate0 +{ + class AutoJob + { + static int Main(string[] args) + { + if (args.Length != 5) + { + Console.WriteLine("Error: incorrect number of command line arguments"); + Console.WriteLine("Usage: {0} serverName clientName machinePoolName submissionName timeout", + System.Environment.GetCommandLineArgs()[0]); + return 1; + } + string serverName = args[0]; + string clientName = args[1]; + string machinePoolName = args[2]; + string submissionName = args[3]; + double timeout = Convert.ToDouble(args[4]); + + try + { + // Initialize DeviceScript and connect to data store + Console.WriteLine("Initializing DeviceScript object"); + DeviceScript script = new DeviceScript(); + Console.WriteLine("Connecting to data store"); + + script.ConnectToNamedDataStore(serverName); + + // Find client machine + IResourcePool rootPool = script.GetResourcePoolByName("$"); + Console.WriteLine("Looking for client machine '{0}'", clientName); + IResource machine = null; + while (true) + { + try + { + machine = rootPool.GetResourceByName(clientName); + } + catch (Exception e) + { + Console.WriteLine("Warning: " + e.Message); + } + // Make sure the machine is valid + if (machine != null && + machine.OperatingSystem != null && + machine.OperatingSystem.Length > 0 && + machine.ProcessorArchitecture != null && + machine.ProcessorArchitecture.Length > 0 && + machine.GetDevices().Length > 0) + break; + System.Threading.Thread.Sleep(1000); + } + Console.WriteLine("Client machine '{0}' found ({1}, {2})", + clientName, machine.OperatingSystem, machine.ProcessorArchitecture); + + // Create machine pool and add client machine to it + // (this must be done because jobs cannot be scheduled for machines in the + // default pool) + try + { + script.CreateResourcePool(machinePoolName, rootPool.ResourcePoolId); + } + catch (Exception e) + { + Console.WriteLine("Warning: " + e.Message); + } + IResourcePool newPool = script.GetResourcePoolByName(machinePoolName); + Console.WriteLine("Moving the client machine to pool '{0}'", machinePoolName); + machine.ChangeResourcePool(newPool); + + // Reset client machine + if (machine.Status != "Ready") + { + Console.WriteLine("Changing the client machine's status to 'Reset'"); + while (true) + { + try + { + machine = rootPool.GetResourceByName(clientName); + machine.ChangeResourceStatus("Unsafe"); + System.Threading.Thread.Sleep(5000); + machine.ChangeResourceStatus("Reset"); + break; + } + catch (Exception e) + { + Console.WriteLine("Warning: " + e.Message); + } + System.Threading.Thread.Sleep(5000); + } + Console.WriteLine("Waiting for client machine to be ready"); + while (machine.Status != "Ready") + { + try + { + machine = rootPool.GetResourceByName(clientName); + } + catch (Exception e) + { + Console.WriteLine("Warning: " + e.Message); + } + System.Threading.Thread.Sleep(1000); + } + } + Console.WriteLine("Client machine is ready"); + + // Get requested device regex and look for a matching device + Console.WriteLine("Device to test: "); + Regex deviceRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase); + Console.WriteLine("Looking for device '{0}'", deviceRegex); + IDevice device; + DateTime endTime = DateTime.Now.AddSeconds(120); + while (DateTime.Now < endTime) + { + machine = rootPool.GetResourceByName(clientName); + Console.WriteLine("(Client machine has {0} devices)", machine.GetDevices().Length); + foreach (IDevice d in machine.GetDevices()) + { + if (deviceRegex.IsMatch(d.FriendlyName)) + { + device = d; + goto deviceFound; + } + } + System.Threading.Thread.Sleep(5000); + } + Console.WriteLine("Error: device '{0}' not found", deviceRegex); + return 1; + + deviceFound: + Console.WriteLine("Found device '{0}'", device.FriendlyName); + + // Get requested jobs regex + Console.WriteLine("Jobs to run: "); + Regex jobRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase); + + // Create submission + Object[] existingSubmissions = script.GetSubmissionByName(submissionName); + if (existingSubmissions.Length > 0) + { + Console.WriteLine("Submission '{0}' already exists -- removing it", + submissionName); + script.DeleteSubmission(((ISubmission)existingSubmissions[0]).Id); + } + Console.WriteLine("Creating submission '{0}'", submissionName); + ISubmission submission = script.CreateHardwareSubmission(submissionName, + newPool.ResourcePoolId, device.InstanceId); + + // Get DeviceData objects from the user + List<Object> deviceDataList = new List<Object>(); + while (true) + { + ISubmissionDeviceData dd = script.CreateNewSubmissionDeviceData(); + Console.WriteLine("DeviceData name: "); + dd.Name = Console.ReadLine(); + if (dd.Name.Length == 0) + break; + Console.WriteLine("DeviceData data: "); + dd.Data = Console.ReadLine(); + deviceDataList.Add(dd); + } + + // Set the submission's DeviceData + submission.SetDeviceData(deviceDataList.ToArray()); + + // Get descriptors from the user + List<Object> descriptorList = new List<Object>(); + while (true) + { + Console.WriteLine("Descriptor path: "); + string descriptorPath = Console.ReadLine(); + if (descriptorPath.Length == 0) + break; + descriptorList.Add(script.GetDescriptorByPath(descriptorPath)); + } + + // Set the submission's descriptors + submission.SetLogoDescriptors(descriptorList.ToArray()); + + // Create a schedule + ISchedule schedule = script.CreateNewSchedule(); + Console.WriteLine("Scheduling jobs:"); + int jobCount = 0; + foreach (IJob j in submission.GetJobs()) + { + if (jobRegex.IsMatch(j.Name)) + { + Console.WriteLine(" " + j.Name); + schedule.AddDeviceJob(device, j); + jobCount++; + } + } + if (jobCount == 0) + { + Console.WriteLine("Error: no submission jobs match pattern '{0}'", jobRegex); + return 1; + } + schedule.AddSubmission(submission); + schedule.SetResourcePool(newPool); + script.RunSchedule(schedule); + + // Wait for jobs to complete + Console.WriteLine("Waiting for all jobs to complete (timeout={0})", timeout); + endTime = DateTime.Now.AddSeconds(timeout); + int numCompleted = 0, numFailed = 0; + while (numCompleted < submission.GetResults().Length && DateTime.Now < endTime) + { + // Sleep for 30 seconds + System.Threading.Thread.Sleep(30000); + // Count completed submission jobs + numCompleted = 0; + foreach (IResult r in submission.GetResults()) + if (r.ResultStatus != "InProgress") + numCompleted++; + // Report results in a Python readable format and count failed schedule jobs + // (submission jobs are a subset of schedule jobs) + Console.WriteLine(); + Console.WriteLine("---- ["); + numFailed = 0; + foreach (IResult r in schedule.GetResults()) + { + Console.WriteLine(" {"); + Console.WriteLine(" 'id': {0}, 'job': r'''{1}''',", r.Job.Id, r.Job.Name); + Console.WriteLine(" 'logs': r'''{0}''',", r.LogLocation); + if (r.ResultStatus != "InProgress") + Console.WriteLine(" 'report': r'''{0}''',", + submission.GetSubmissionResultReport(r)); + Console.WriteLine(" 'status': '{0}',", r.ResultStatus); + Console.WriteLine(" 'pass': {0}, 'fail': {1}, 'notrun': {2}, 'notapplicable': {3}", + r.Pass, r.Fail, r.NotRun, r.NotApplicable); + Console.WriteLine(" },"); + numFailed += r.Fail; + } + Console.WriteLine("] ----"); + } + Console.WriteLine(); + + // Cancel incomplete jobs + foreach (IResult r in schedule.GetResults()) + if (r.ResultStatus == "InProgress") + r.Cancel(); + + // Set the machine's status to Unsafe and then Reset + try + { + machine = rootPool.GetResourceByName(clientName); + machine.ChangeResourceStatus("Unsafe"); + System.Threading.Thread.Sleep(5000); + machine.ChangeResourceStatus("Reset"); + } + catch (Exception e) + { + Console.WriteLine("Warning: " + e.Message); + } + + // Report failures + if (numCompleted < submission.GetResults().Length) + Console.WriteLine("Some jobs did not complete on time."); + if (numFailed > 0) + Console.WriteLine("Some jobs failed."); + + if (numFailed > 0 || numCompleted < submission.GetResults().Length) + return 1; + + Console.WriteLine("All jobs completed."); + return 0; + } + catch (Exception e) + { + Console.WriteLine("Error: " + e.Message); + return 1; + } + } + } +} diff --git a/client/tests/kvm/deps/whql_submission_15.exe b/client/tests/kvm/deps/whql_submission_15.exe new file mode 100644 index 0000000000000000000000000000000000000000..4f30aa801c8f200bd96b0de6c10d2b59c2bad268 GIT binary patch literal 10240 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 A_y7O^ literal 0 HcmV?d00001 -- 1.5.5.6 -- To unsubscribe from this list: send the line "unsubscribe kvm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html