diff mbox

[KVM-AUTOTEST,21/28] KVM test: refactor whql_submission_15.cs and whql_submission.py

Message ID 1293465715-16599-21-git-send-email-mgoldish@redhat.com (mailing list archive)
State New, archived
Headers show

Commit Message

Michael Goldish Dec. 27, 2010, 4:01 p.m. UTC
None
diff mbox

Patch

diff --git a/client/tests/kvm/deps/whql_submission_15.cs b/client/tests/kvm/deps/whql_submission_15.cs
index 8fa6856..0928548 100644
--- a/client/tests/kvm/deps/whql_submission_15.cs
+++ b/client/tests/kvm/deps/whql_submission_15.cs
@@ -1,289 +1,373 @@ 
-// DTM submission automation program
-// Author: Michael Goldish <mgoldish@redhat.com>
-// 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;
-            }
-        }
-    }
-}
+// DTM submission automation program
+// Author: Michael Goldish <mgoldish@redhat.com>
+// 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
+    {
+        // Wait for a machine to show up in the data store
+        static void FindMachine(IResourcePool rootPool, string machineName)
+        {
+            Console.WriteLine("Looking for machine '{0}'", machineName);
+            IResource machine = null;
+            while (true)
+            {
+                try
+                {
+                    machine = rootPool.GetResourceByName(machineName);
+                }
+                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})",
+                machineName, machine.OperatingSystem, machine.ProcessorArchitecture);
+        }
+
+        // Delete a machine pool if it exists
+        static void DeleteResourcePool(IDeviceScript script, string poolName)
+        {
+            while (true)
+            {
+                try
+                {
+                    IResourcePool pool = script.GetResourcePoolByName(poolName);
+                    if (pool != null)
+                        script.DeleteResourcePool(pool);
+                    break;
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                    System.Threading.Thread.Sleep(1000);
+                }
+            }
+        }
+
+        // Set the machine's status to 'Reset' and optionally wait for it to become ready
+        static void ResetMachine(IResourcePool rootPool, string machineName, bool wait)
+        {
+            Console.WriteLine("Resetting machine '{0}'", machineName);
+            IResource machine;
+            while (true)
+            {
+                try
+                {
+                    machine = rootPool.GetResourceByName(machineName);
+                    machine.ChangeResourceStatus("Reset");
+                    break;
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                    System.Threading.Thread.Sleep(5000);
+                }
+            }
+            if (wait)
+            {
+                Console.WriteLine("Waiting for machine '{0}' to be ready", machineName);
+                while (machine.Status != "Ready")
+                {
+                    try
+                    {
+                        machine = rootPool.GetResourceByName(machineName);
+                    }
+                    catch (Exception e)
+                    {
+                        Console.WriteLine("Warning: " + e.Message);
+                    }
+                    System.Threading.Thread.Sleep(1000);
+                }
+                Console.WriteLine("Machine '{0}' is ready", machineName);
+            }
+        }
+
+        // Look for a device in a machine, and if not found, keep trying for 3 minutes
+        static IDevice GetDevice(IResourcePool rootPool, string machineName, string regexStr)
+        {
+            Regex deviceRegex = new Regex(regexStr, RegexOptions.IgnoreCase);
+            int numAttempts = 1;
+            DateTime endTime = DateTime.Now.AddSeconds(180);
+            while (DateTime.Now < endTime)
+            {
+                IResource machine = rootPool.GetResourceByName(machineName);
+                Console.WriteLine("Looking for device '{0}' in machine '{1}' (machine has {2} devices)",
+                    regexStr, machineName, machine.GetDevices().Length);
+                foreach (IDevice d in machine.GetDevices())
+                {
+                    if (deviceRegex.IsMatch(d.FriendlyName))
+                    {
+                        Console.WriteLine("Found device '{0}'", d.FriendlyName);
+                        return d;
+                    }
+                }
+                Console.WriteLine("Device not found");
+                if (numAttempts % 5 == 0)
+                    ResetMachine(rootPool, machineName, true);
+                else
+                    System.Threading.Thread.Sleep(5000);
+                numAttempts++;
+            }
+            Console.WriteLine("Error: device '{0}' not found", deviceRegex);
+            return null;
+        }
+
+        static int Main(string[] args)
+        {
+            if (args.Length < 5)
+            {
+                Console.WriteLine("Error: incorrect number of command line arguments");
+                Console.WriteLine("Usage: {0} serverName machinePoolName submissionName timeout machineName0 machineName1 ...",
+                    System.Environment.GetCommandLineArgs()[0]);
+                return 1;
+            }
+            string serverName = args[0];
+            string machinePoolName = args[1];
+            string submissionName = args[2];
+            double timeout = Convert.ToDouble(args[3]);
+
+            List<string> machines = new List<string>();
+            for (int i = 4; i < args.Length; i++)
+                machines.Add(args[i]);
+
+            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);
+
+                // Wait for client machines to become available
+                IResourcePool rootPool = script.GetResourcePoolByName("$");
+                foreach (string machineName in machines)
+                    FindMachine(rootPool, machineName);
+
+                // Delete the machine pool if it already exists
+                DeleteResourcePool(script, machinePoolName);
+
+                // Create the machine pool and add the client machines 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);
+                foreach (string machineName in machines)
+                {
+                    Console.WriteLine("Moving machine '{0}' to pool '{1}'", machineName, machinePoolName);
+                    rootPool.GetResourceByName(machineName).ChangeResourcePool(newPool);
+                }
+
+                // Reset client machine
+                foreach (string machineName in machines)
+                    ResetMachine(rootPool, machineName, true);
+
+                // Get requested device regex and look for a matching device in the first machine
+                Console.WriteLine("Device to test:");
+                IDevice device = GetDevice(rootPool, machines[0], Console.ReadLine());
+                if (device == null)
+                    return 1;
+
+                // Get requested jobs regex
+                Console.WriteLine("Jobs to run:");
+                Regex jobRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase);
+
+                // Create a 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);
+                }
+                string hardwareId = device.InstanceId.Remove(device.InstanceId.LastIndexOf("\\"));
+                Console.WriteLine("Creating submission '{0}' (hardware ID: {1})", submissionName, hardwareId);
+                ISubmission submission = script.CreateHardwareSubmission(submissionName, newPool.ResourcePoolId, hardwareId);
+
+                // Set submission DeviceData
+                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);
+                }
+                submission.SetDeviceData(deviceDataList.ToArray());
+
+                // Set submission descriptors
+                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));
+                }
+                submission.SetLogoDescriptors(descriptorList.ToArray());
+
+                // Set machine dimensions
+                foreach (string machineName in machines)
+                {
+                    IResource machine = rootPool.GetResourceByName(machineName);
+                    while (true)
+                    {
+                        Console.WriteLine("Dimension name ({0}):", machineName);
+                        string dimName = Console.ReadLine();
+                        if (dimName.Length == 0)
+                            break;
+                        Console.WriteLine("Dimension value ({0}):", machineName);
+                        machine.SetDimension(dimName, Console.ReadLine());
+                    }
+                    // Set the WDKSubmissionId dimension for all machines
+                    machine.SetDimension("WDKSubmissionId", submission.Id.ToString() + "_" + submission.Name);
+                }
+
+                // Get job parameters
+                List<string> paramNames = new List<string>();
+                List<string> paramValues = new List<string>();
+                foreach (string machineName in machines)
+                {
+                    while (true)
+                    {
+                        Console.WriteLine("Parameter name ({0}):", machineName);
+                        string paramName = Console.ReadLine();
+                        if (paramName.Length == 0)
+                            break;
+                        Console.WriteLine("Device regex ({0}):", machineName);
+                        IDevice d = GetDevice(rootPool, machineName, Console.ReadLine());
+                        if (d == null)
+                            return 1;
+                        string deviceName = d.GetAttribute("name")[0].ToString();
+                        Console.WriteLine("Setting parameter value to '{0}'", deviceName);
+                        paramNames.Add(paramName);
+                        paramValues.Add(deviceName);
+                    }
+                }
+
+                // Find jobs that match the requested pattern
+                Console.WriteLine("Scheduling jobs:");
+                List<IJob> jobs = new List<IJob>();
+                foreach (IJob j in submission.GetJobs())
+                {
+                    if (jobRegex.IsMatch(j.Name))
+                    {
+                        Console.WriteLine("    " + j.Name);
+                        // Set job parameters
+                        for (int i = 0; i < paramNames.Count; i++)
+                        {
+                            IParameter p = j.GetParameterByName(paramNames[i]);
+                            if (p != null)
+                                p.ScheduleValue = paramValues[i];
+                        }
+                        jobs.Add(j);
+                    }
+                }
+                if (jobs.Count == 0)
+                {
+                    Console.WriteLine("Error: no submission jobs match pattern '{0}'", jobRegex);
+                    return 1;
+                }
+
+                // Create a schedule, add jobs to it and run it
+                ISchedule schedule = script.CreateNewSchedule();
+                foreach (IScheduleItem item in submission.ProcessJobs(jobs.ToArray()))
+                {
+                    item.Device = device;
+                    schedule.AddScheduleItem(item);
+                }
+                schedule.AddSubmission(submission);
+                schedule.SetResourcePool(newPool);
+                script.RunSchedule(schedule);
+
+                // Wait for jobs to complete
+                Console.WriteLine("Waiting for all jobs to complete (timeout={0}s)", timeout);
+                DateTime endTime = DateTime.Now.AddSeconds(timeout);
+                int numCompleted, numFailed;
+                do
+                {
+                    System.Threading.Thread.Sleep(30000);
+                    // Report results in a Python readable format and count completed and failed schedule jobs
+                    numCompleted = numFailed = 0;
+                    Console.WriteLine();
+                    Console.WriteLine("---- [");
+                    foreach (IResult r in schedule.GetResults())
+                    {
+                        if (r.ResultStatus != "InProgress") numCompleted++;
+                        if (r.ResultStatus == "Investigate") numFailed++;
+                        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("  },");
+                    }
+                    Console.WriteLine("] ----");
+                } while (numCompleted < schedule.GetResults().Length && DateTime.Now < endTime);
+
+                Console.WriteLine();
+
+                // Cancel incomplete jobs
+                foreach (IResult r in schedule.GetResults())
+                    if (r.ResultStatus == "InProgress")
+                        r.Cancel();
+
+                // Reset the machines
+                foreach (string machineName in machines)
+                    ResetMachine(rootPool, machineName, false);
+
+                // Report failures
+                if (numCompleted < schedule.GetResults().Length)
+                    Console.WriteLine("Some jobs did not complete on time.");
+                if (numFailed > 0)
+                    Console.WriteLine("Some jobs failed.");
+                if (numFailed > 0 || numCompleted < schedule.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
index 4f30aa801c8f200bd96b0de6c10d2b59c2bad268..605e2e3aa1f0761155f4aac5191e3ad0f0ddecc6 100644
GIT binary patch
literal 12288
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

delta 4268
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

diff --git a/client/tests/kvm/tests/whql_submission.py b/client/tests/kvm/tests/whql_submission.py
index 8dd48b8..a0ff87c 100644
--- a/client/tests/kvm/tests/whql_submission.py
+++ b/client/tests/kvm/tests/whql_submission.py
@@ -6,20 +6,24 @@  import kvm_subprocess, kvm_test_utils, kvm_utils, rss_file_transfer
 def run_whql_submission(test, params, env):
     """
     WHQL submission test:
-    1) Log into the guest (the client machine) and into a DTM server machine
+    1) Log into the client machines and into a DTM server machine
     2) Copy the automation program binary (dsso_test_binary) to the server machine
     3) Run the automation program
     4) Pass the program all relevant parameters (e.g. device_data)
     5) Wait for the program to terminate
     6) Parse and report job results
-    (logs and HTML reports are placed in test.bindir)
+    (logs and HTML reports are placed in test.debugdir)
 
     @param test: kvm test object
     @param params: Dictionary with the test parameters
     @param env: Dictionary with test environment.
     """
-    vm = kvm_test_utils.get_living_vm(env, params.get("main_vm"))
-    session = kvm_test_utils.wait_for_login(vm, 0, 240)
+    # Log into all client VMs
+    vms = []
+    sessions = []
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vms.append(kvm_test_utils.get_living_vm(env, vm_name))
+        sessions.append(kvm_test_utils.wait_for_login(vms[-1], 0, 240))
 
     # Collect parameters
     server_address = params.get("server_address")
@@ -30,47 +34,54 @@  def run_whql_submission(test, params, env):
     dsso_test_binary = params.get("dsso_test_binary",
                                   "deps/whql_submission_15.exe")
     dsso_test_binary = kvm_utils.get_path(test.bindir, dsso_test_binary)
-    test_device = params.get("test_device")
-    job_filter = params.get("job_filter", ".*")
+    dsso_delete_machine_binary = params.get("dsso_delete_machine_binary",
+                                            "deps/whql_delete_machine_15.exe")
+    dsso_delete_machine_binary = kvm_utils.get_path(test.bindir,
+                                                    dsso_delete_machine_binary)
     test_timeout = float(params.get("test_timeout", 600))
-    wtt_services = params.get("wtt_services")
 
-    # Restart WTT service(s) on the client
-    logging.info("Restarting WTT services on client")
-    for svc in wtt_services.split():
-        kvm_test_utils.stop_windows_service(session, svc)
-    for svc in wtt_services.split():
-        kvm_test_utils.start_windows_service(session, svc)
-
-    # Run whql_pre_command
-    if params.get("whql_pre_command"):
-        session.cmd(params.get("whql_pre_command"),
-                    int(params.get("whql_pre_command_timeout", 600)))
-
-    # Copy dsso_test_binary to the server
-    rss_file_transfer.upload(server_address, server_file_transfer_port,
-                             dsso_test_binary, server_studio_path, timeout=60)
+    # Copy dsso binaries to the server
+    for filename in dsso_test_binary, dsso_delete_machine_binary:
+        rss_file_transfer.upload(server_address, server_file_transfer_port,
+                                 filename, server_studio_path, timeout=60)
 
     # Open a shell session with the server
     server_session = kvm_utils.remote_login("nc", server_address,
                                             server_shell_port, "", "",
-                                            session.prompt, session.linesep)
-    server_session.set_status_test_command(session.status_test_command)
+                                            sessions[0].prompt,
+                                            sessions[0].linesep)
+    server_session.set_status_test_command(sessions[0].status_test_command)
 
-    # Get the computer names of the server and client
+    # Get the computer names of the server and clients
     cmd = "echo %computername%"
     server_name = server_session.cmd_output(cmd).strip()
-    client_name = session.cmd_output(cmd).strip()
-    session.close()
+    client_names = [session.cmd_output(cmd).strip() for session in sessions]
 
-    # Run the automation program on the server
+    # Delete all client machines from the server's data store
     server_session.cmd("cd %s" % server_studio_path)
+    for client_name in client_names:
+        cmd = "%s %s %s" % (os.path.basename(dsso_delete_machine_binary),
+                            server_name, client_name)
+        server_session.cmd(cmd, print_func=logging.debug)
+
+    # Reboot the client machines
+    sessions = kvm_utils.parallel((kvm_test_utils.reboot, (vm, session))
+                                  for vm, session in zip(vms, sessions))
+
+    # Run whql_pre_command and close the sessions
+    if params.get("whql_pre_command"):
+        for session in sessions:
+            session.cmd(params.get("whql_pre_command"),
+                        int(params.get("whql_pre_command_timeout", 600)))
+            session.close()
+
+    # Run the automation program on the server
     cmd = "%s %s %s %s %s %s" % (os.path.basename(dsso_test_binary),
                                  server_name,
-                                 client_name,
-                                 "%s_pool" % client_name,
-                                 "%s_submission" % client_name,
-                                 test_timeout)
+                                 "%s_pool" % client_names[0],
+                                 "%s_submission" % client_names[0],
+                                 test_timeout,
+                                 " ".join(client_names))
     server_session.sendline(cmd)
 
     # Helper function: wait for a given prompt and raise an exception if an
@@ -89,13 +100,13 @@  def run_whql_submission(test, params, env):
 
     # Tell the automation program which device to test
     find_prompt("Device to test:")
-    server_session.sendline(test_device)
+    server_session.sendline(params.get("test_device"))
 
     # Tell the automation program which jobs to run
     find_prompt("Jobs to run:")
-    server_session.sendline(job_filter)
+    server_session.sendline(params.get("job_filter", ".*"))
 
-    # Give the automation program all the device data supplied by the user
+    # Set submission DeviceData
     find_prompt("DeviceData name:")
     for dd in kvm_utils.get_sub_dict_names(params, "device_data"):
         dd_params = kvm_utils.get_sub_dict(params, dd)
@@ -104,8 +115,7 @@  def run_whql_submission(test, params, env):
             server_session.sendline(dd_params.get("dd_data"))
     server_session.sendline()
 
-    # Give the automation program all the descriptor information supplied by
-    # the user
+    # Set submission descriptors
     find_prompt("Descriptor path:")
     for desc in kvm_utils.get_sub_dict_names(params, "descriptors"):
         desc_params = kvm_utils.get_sub_dict(params, desc)
@@ -113,6 +123,31 @@  def run_whql_submission(test, params, env):
             server_session.sendline(desc_params.get("desc_path"))
     server_session.sendline()
 
+    # Set machine dimensions for each client machine
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vm_params = kvm_utils.get_sub_dict(params, vm_name)
+        find_prompt(r"Dimension name\b.*:")
+        for dp in kvm_utils.get_sub_dict_names(vm_params, "dimensions"):
+            dp_params = kvm_utils.get_sub_dict(vm_params, dp)
+            if dp_params.get("dim_name") and dp_params.get("dim_value"):
+                server_session.sendline(dp_params.get("dim_name"))
+                server_session.sendline(dp_params.get("dim_value"))
+        server_session.sendline()
+
+    # Set extra parameters for tests that require them (e.g. NDISTest)
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vm_params = kvm_utils.get_sub_dict(params, vm_name)
+        find_prompt(r"Parameter name\b.*:")
+        for dp in kvm_utils.get_sub_dict_names(vm_params, "device_params"):
+            dp_params = kvm_utils.get_sub_dict(vm_params, dp)
+            if dp_params.get("dp_name") and dp_params.get("dp_regex"):
+                server_session.sendline(dp_params.get("dp_name"))
+                server_session.sendline(dp_params.get("dp_regex"))
+                # Make sure the prompt appears again (if the device isn't found
+                # the automation program will terminate)
+                find_prompt(r"Parameter name\b.*:")
+        server_session.sendline()
+
     # Wait for the automation program to terminate
     try:
         o = server_session.read_up_to_prompt(print_func=logging.info,
@@ -162,38 +197,63 @@  def run_whql_submission(test, params, env):
                 except (KeyError, OSError):
                     pass
 
-    # Print result summary
-    logging.info("")
-    logging.info("Result summary:")
-    name_length = max(len(r.get("job", "")) for r in results)
-    fmt = "%%-6s %%-%ds %%-15s %%-8s %%-8s %%-8s %%-15s" % name_length
-    logging.info(fmt % ("ID", "Job", "Status", "Pass", "Fail", "NotRun",
-                        "NotApplicable"))
-    logging.info(fmt % ("--", "---", "------", "----", "----", "------",
-                        "-------------"))
-    for r in results:
-        logging.info(fmt % (r.get("id"), r.get("job"), r.get("status"),
-                            r.get("pass"), r.get("fail"), r.get("notrun"),
-                            r.get("notapplicable")))
-    logging.info("(see logs and HTML reports in %s)" % test.debugdir)
-
-    # Kill the VM and fail if the automation program did not terminate on time
+    # Print result summary (both to the regular logs and to a file named
+    # 'summary' in test.debugdir)
+    def print_summary_line(f, line):
+        logging.info(line)
+        f.write(line + "\n")
+    if results:
+        # Make sure all results have the required keys
+        for r in results:
+            r["id"] = str(r.get("id"))
+            r["job"] = str(r.get("job"))
+            r["status"] = str(r.get("status"))
+            r["pass"] = int(r.get("pass", 0))
+            r["fail"] = int(r.get("fail", 0))
+            r["notrun"] = int(r.get("notrun", 0))
+            r["notapplicable"] = int(r.get("notapplicable", 0))
+        # Sort the results by failures and total test count in descending order
+        results = [(r["fail"],
+                    r["pass"] + r["fail"] + r["notrun"] + r["notapplicable"],
+                    r) for r in results]
+        results.sort(reverse=True)
+        results = [r[-1] for r in results]
+        # Print results
+        logging.info("")
+        logging.info("Result summary:")
+        name_length = max(len(r["job"]) for r in results)
+        fmt = "%%-6s %%-%ds %%-15s %%-8s %%-8s %%-8s %%-15s" % name_length
+        f = open(os.path.join(test.debugdir, "summary"), "w")
+        print_summary_line(f, fmt % ("ID", "Job", "Status", "Pass", "Fail",
+                                     "NotRun", "NotApplicable"))
+        print_summary_line(f, fmt % ("--", "---", "------", "----", "----",
+                                     "------", "-------------"))
+        for r in results:
+            print_summary_line(f, fmt % (r["id"], r["job"], r["status"],
+                                         r["pass"], r["fail"], r["notrun"],
+                                         r["notapplicable"]))
+        f.close()
+        logging.info("(see logs and HTML reports in %s)" % test.debugdir)
+
+    # Kill the client VMs and fail if the automation program did not terminate
+    # on time
     if not done:
-        vm.destroy()
+        kvm_utils.parallel(vm.destroy for vm in vms)
         raise error.TestFail("The automation program did not terminate "
                              "on time")
 
-    # Fail if there are failed or incomplete jobs (kill the VM if there are
-    # incomplete jobs)
-    failed_jobs = [r.get("job") for r in results
-                   if r.get("status", "").lower() == "investigate"]
-    running_jobs = [r.get("job") for r in results
-                    if r.get("status", "").lower() == "inprogress"]
+    # Fail if there are failed or incomplete jobs (kill the client VMs if there
+    # are incomplete jobs)
+    failed_jobs = [r["job"] for r in results
+                   if r["status"].lower() == "investigate"]
+    running_jobs = [r["job"] for r in results
+                    if r["status"].lower() == "inprogress"]
     errors = []
     if failed_jobs:
         errors += ["Jobs failed: %s." % failed_jobs]
     if running_jobs:
-        vm.destroy()
+        for vm in vms:
+            vm.destroy()
         errors += ["Jobs did not complete on time: %s." % running_jobs]
     if errors:
         raise error.TestFail(" ".join(errors))
diff --git a/client/tests/kvm/tests_base.cfg.sample b/client/tests/kvm/tests_base.cfg.sample
index 8d533ba..e81d879 100644
--- a/client/tests/kvm/tests_base.cfg.sample
+++ b/client/tests/kvm/tests_base.cfg.sample
@@ -347,11 +347,12 @@  variants:
         server_shell_port = 10022
         server_file_transfer_port = 10023
         server_studio_path = %programfiles%\Microsoft Driver Test Manager\Studio
+        dsso_test_binary = deps/whql_submission_15.exe
+        dsso_delete_machine_binary = deps/whql_delete_machine_15.exe
         wtt_services = wttsvc
         variants:
             - client_install:
                 type = whql_client_install
-                dsso_delete_machine_binary = deps/whql_delete_machine_15.exe
                 # The username and password are required for accessing the DTM client
                 # installer binary shared by the server
                 server_username = administrator
@@ -363,7 +364,7 @@  variants:
             - submission:    client_install
                 type = whql_submission
                 extra_params += " -snapshot"
-                dsso_test_binary = deps/whql_submission_15.exe
+                restart_vm = yes
                 test_timeout = 3600
                 device_data = cat0 cat1 cat2 cat3 logoarch logoos whqlos whqlqual prog desc filter virt
                 descriptors = desc1 desc2 desc3