[PATCH 1/4] testbot: Add Missions support.

Francois Gouget fgouget at codeweavers.com
Sun Oct 21 20:05:08 CDT 2018


Missions allow the TestBot administrator to specify which tests a given
VM should perform for mailing list patches. For instance this can be
used to restrict a 64 bit Windows VM to 32 bit tests (useful when it
duplicates another VM with just a different locale).
Furthermore missions can get options. Currently only the 'nosubmit'
option is supported and it disables submitting the WineTest results
online.

Signed-off-by: Francois Gouget <fgouget at codeweavers.com>
---

Eventually missions will make it possible to specify how much testing a 
Wine VM should do, from running only patched tests to running every test 
for every patch; against which Wine build; only in the English locale, 
or in other locales too.

 testbot/bin/CheckForWinetestUpdate.pl | 105 ++++++++++++++------
 testbot/bin/WineRunTask.pl            |  12 ++-
 testbot/bin/WineRunWineTest.pl        |  31 +++---
 testbot/bin/build/WineTest.pl         | 131 +++++++++++++------------
 testbot/ddl/update41.sql              |  48 +++++++++
 testbot/ddl/winetestbot.sql           |   2 +
 testbot/doc/winetestbot-schema.dia    |  46 +++++++++
 testbot/lib/WineTestBot/Missions.pm   |  99 +++++++++++++++++++
 testbot/lib/WineTestBot/PatchUtils.pm |   8 +-
 testbot/lib/WineTestBot/Patches.pm    | 134 +++++++++++++++-----------
 testbot/lib/WineTestBot/StepsTasks.pm |  55 +++++++----
 testbot/lib/WineTestBot/Tasks.pm      |   1 +
 testbot/lib/WineTestBot/VMs.pm        |  41 ++++++++
 testbot/web/JobDetails.pl             |   1 +
 testbot/web/Submit.pl                 |  80 ++++++++-------
 testbot/web/admin/VMDetails.pl        |  16 +++
 16 files changed, 587 insertions(+), 223 deletions(-)
 create mode 100644 testbot/ddl/update41.sql
 create mode 100644 testbot/lib/WineTestBot/Missions.pm

diff --git a/testbot/bin/CheckForWinetestUpdate.pl b/testbot/bin/CheckForWinetestUpdate.pl
index 29953642ef..95cb13d8ed 100755
--- a/testbot/bin/CheckForWinetestUpdate.pl
+++ b/testbot/bin/CheckForWinetestUpdate.pl
@@ -53,6 +53,7 @@ use WineTestBot::Config;
 use WineTestBot::Engine::Notify;
 use WineTestBot::Jobs;
 use WineTestBot::Log;
+use WineTestBot::Missions;
 use WineTestBot::PatchUtils;
 use WineTestBot::Users;
 use WineTestBot::Utils;
@@ -201,21 +202,38 @@ sub AddJob($$$)
   $NewJob->Priority($BaseJob && $Build eq "exe32" ? 8 : 9);
   $NewJob->Remarks($Remarks);
 
-  # Add a step to the job
-  my $Steps = $NewJob->Steps;
-  my $NewStep = $Steps->Add();
-  $NewStep->Type("suite");
-  $NewStep->FileName($LatestBaseName);
-  $NewStep->FileType($Build);
-
   # Add a task for each VM
-  my $Tasks = $NewStep->Tasks;
+  my $Tasks;
   foreach my $VMKey (@{$VMs->SortKeysBySortOrder($VMs->GetKeys())})
   {
-    Debug("  $VMKey $Build\n");
-    my $Task = $Tasks->Add();
-    $Task->VM($VMs->GetItem($VMKey));
-    $Task->Timeout(GetTestTimeout(undef, { $Build => 1 }));
+    my $VM = $VMs->GetItem($VMKey);
+    my ($ErrMessage, $Missions) = ParseMissionStatement($VM->Missions);
+    if (defined $ErrMessage)
+    {
+      Debug("$VMKey has an invalid mission statement: $!\n");
+      next;
+    }
+
+    foreach my $TaskMissions (@$Missions)
+    {
+      next if (!$TaskMissions->{Builds}->{$Build});
+
+      if (!$Tasks)
+      {
+        # Add a step to the job
+        my $TestStep = $NewJob->Steps->Add();
+        $TestStep->Type("suite");
+        $TestStep->FileName($LatestBaseName);
+        $TestStep->FileType($Build);
+        $Tasks = $TestStep->Tasks;
+      }
+
+      Debug("  $VMKey $Build\n");
+      my $Task = $Tasks->Add();
+      $Task->VM($VM);
+      $Task->Timeout($SuiteTimeout);
+      $Task->Missions($TaskMissions->{Statement});
+    }
   }
 
   # Save it all
@@ -246,9 +264,6 @@ sub AddJob($$$)
   return 1;
 }
 
-my @ExeBuilds = qw(exe32 exe64);
-my @WineBuilds = qw(win32 wow32 wow64);
-
 sub AddReconfigJob($)
 {
   my ($VMType) = @_;
@@ -287,9 +302,23 @@ sub AddReconfigJob($)
     Debug("  $VMKey $VMType reconfig\n");
     my $Task = $BuildStep->Tasks->Add();
     $Task->VM($VM);
-    my $Builds;
-    map { $Builds->{$_} = 1 } ($VMType eq "wine" ? @WineBuilds : @ExeBuilds);
-    $Task->Timeout(GetBuildTimeout(undef, $Builds));
+
+    # Merge all the tasks into one so we only recreate the base snapshot once
+    my $MissionStatement = $VMType ne "wine" ? "exe32:exe64" :
+                           MergeMissionStatementTasks($VM->Missions);
+    my ($ErrMessage, $Missions) = ParseMissionStatement($MissionStatement);
+    if (defined $ErrMessage)
+    {
+      Debug("$VMKey has an invalid mission statement: $!\n");
+      next;
+    }
+    if (@$Missions != 1)
+    {
+      Debug("Found no mission or too many task missions for $VMKey\n");
+      next;
+    }
+    $Task->Timeout(GetBuildTimeout(undef, $Missions->[0]));
+    $Task->Missions($Missions->[0]->{Statement});
   }
 
   # Save the build step so the others can reference it.
@@ -303,22 +332,38 @@ sub AddReconfigJob($)
   if ($VMType eq "wine")
   {
     # Add steps to run WineTest on Wine
-    foreach my $Build (@WineBuilds)
+    my $Tasks;
+    foreach my $VMKey (@$SortedKeys)
     {
-      # Add a step to the job
-      my $NewStep = $Steps->Add();
-      $NewStep->PreviousNo($BuildStep->No);
-      $NewStep->Type("suite");
-      $NewStep->FileType("none");
+      my $VM = $VMs->GetItem($VMKey);
+      # Move all the missions into separate tasks so we don't have one very
+      # long task hogging the VM forever. Note that this also ok because the
+      # WineTest tasks don't have to recompile Wine.
+      my $MissionStatement = SplitMissionStatementTasks($VM->Missions);
+      my ($ErrMessage, $Missions) = ParseMissionStatement($MissionStatement);
+      if (defined $ErrMessage)
+      {
+        Debug("$VMKey has an invalid mission statement: $!\n");
+        next;
+      }
 
-      foreach my $VMKey (@$SortedKeys)
+      foreach my $TaskMissions (@$Missions)
       {
-        my $VM = $VMs->GetItem($VMKey);
-        Debug("  $VMKey $Build\n");
-        my $Task = $NewStep->Tasks->Add();
+        if (!$Tasks)
+        {
+          # Add a step to the job
+          my $TestStep = $Steps->Add();
+          $TestStep->PreviousNo($BuildStep->No);
+          $TestStep->Type("suite");
+          $TestStep->FileType("none");
+          $Tasks = $TestStep->Tasks;
+        }
+
+        Debug("  $VMKey $TaskMissions->{Statement}\n");
+        my $Task = $Tasks->Add();
         $Task->VM($VM);
-        $Task->CmdLineArg($Build);
-        $Task->Timeout(GetTestTimeout(undef, { $Build => 1 }));
+        $Task->Timeout(GetTestTimeout(undef, $TaskMissions));
+        $Task->Missions($TaskMissions->{Statement});
       }
     }
   }
diff --git a/testbot/bin/WineRunTask.pl b/testbot/bin/WineRunTask.pl
index 077426b232..fea8c03f5e 100755
--- a/testbot/bin/WineRunTask.pl
+++ b/testbot/bin/WineRunTask.pl
@@ -45,6 +45,7 @@ use WineTestBot::Engine::Notify;
 use WineTestBot::Jobs;
 use WineTestBot::Log;
 use WineTestBot::LogUtils;
+use WineTestBot::Missions;
 use WineTestBot::Utils;
 use WineTestBot::VMs;
 
@@ -391,6 +392,13 @@ if ($Step->FileType ne "exe32" and $Step->FileType ne "exe64")
   FatalError("Unexpected file type '". $Step->FileType ."' found for ". $Step->Type ." step\n");
 }
 
+my ($ErrMessage, $Missions) = ParseMissionStatement($Task->Missions);
+FatalError "$ErrMessage\n" if (defined $ErrMessage);
+FatalError "Empty mission statement\n" if (!@$Missions);
+FatalError "Cannot specify missions for multiple tasks\n" if (@$Missions > 1);
+FatalError "Cannot specify multiple missions\n" if (@{$Missions->[0]->{Missions}} > 1);
+my $Mission = $Missions->[0]->{Missions}->[0];
+
 
 #
 # Setup the VM
@@ -477,8 +485,8 @@ elsif ($Step->Type eq "suite")
   $Info =~ s/"/\\"/g;
   $Info =~ s/%/%%/g;
   $Info =~ s/%/%%/g;
-  $Script .= "-q -o $RptFileName -t $Tag -m \"$EMail\" -i \"$Info\"\r\n".
-             "$FileName -q -s $RptFileName\r\n";
+  $Script .= "-q -o $RptFileName -t $Tag -m \"$EMail\" -i \"$Info\"\r\n";
+  $Script .= "$FileName -q -s $RptFileName\r\n" if (!$Mission->{nosubmit});
 }
 Debug(Elapsed($Start), " Sending the script: [$Script]\n");
 if (!$TA->SendFileFromString($Script, "script.bat", $TestAgent::SENDFILE_EXE))
diff --git a/testbot/bin/WineRunWineTest.pl b/testbot/bin/WineRunWineTest.pl
index 0e438f957c..f47a02413a 100755
--- a/testbot/bin/WineRunWineTest.pl
+++ b/testbot/bin/WineRunWineTest.pl
@@ -41,13 +41,14 @@ $Name0 =~ s+^.*/++;
 
 
 use WineTestBot::Config;
+use WineTestBot::Engine::Notify;
 use WineTestBot::Jobs;
+use WineTestBot::Missions;
 use WineTestBot::PatchUtils;
-use WineTestBot::VMs;
 use WineTestBot::Log;
 use WineTestBot::LogUtils;
 use WineTestBot::Utils;
-use WineTestBot::Engine::Notify;
+use WineTestBot::VMs;
 
 
 #
@@ -220,6 +221,8 @@ sub LogTaskError($)
   }
 }
 
+my $TaskMissions;
+
 sub WrapUpAndExit($;$$$)
 {
   my ($Status, $TestFailures, $Retry, $TimedOut) = @_;
@@ -287,9 +290,7 @@ sub WrapUpAndExit($;$$$)
 
   if ($Step->Type eq 'suite' and $Status eq 'completed' and !$TimedOut)
   {
-    my $BuildList = $Task->CmdLineArg;
-    $BuildList =~ s/ .*$//;
-    foreach my $Build (split /,/, $BuildList)
+    foreach my $Build (keys %{$TaskMissions->{Builds}})
     {
       # Keep the old report if the new one is missing
       my $RptFileName = "$Build.report";
@@ -392,6 +393,12 @@ if (($Step->Type eq "suite" and $Step->FileType ne "none") or
   FatalError("Unexpected file type '". $Step->FileType ."' found for ". $Step->Type ." step\n");
 }
 
+my ($ErrMessage, $Missions) = ParseMissionStatement($Task->Missions);
+FatalError "$ErrMessage\n" if (defined $ErrMessage);
+FatalError "Empty mission statement\n" if (!@$Missions);
+FatalError "Cannot specify missions for multiple tasks\n" if (@$Missions > 1);
+$TaskMissions = $Missions->[0];
+
 
 #
 # Setup the VM
@@ -431,7 +438,7 @@ $Script .= "  ../bin/build/WineTest.pl ";
 if ($Step->Type eq "suite")
 {
   my $BaseTag = BuildTag($VM->Name);
-  $Script .= "--winetest ". $Task->CmdLineArg ." $BaseTag ";
+  $Script .= "--winetest ". $Task->Missions ." $BaseTag ";
   if (defined $WebHostName)
   {
     my $StepTask = 100 * $StepNo + $TaskNo;
@@ -447,7 +454,7 @@ if ($Step->Type eq "suite")
 }
 else
 {
-  $Script .= "--testpatch ". $Task->CmdLineArg ." patch.diff";
+  $Script .= "--testpatch ". $Task->Missions ." patch.diff";
 }
 $Script .= "\n) >Task.log 2>&1\n";
 Debug(Elapsed($Start), " Sending the script: [$Script]\n");
@@ -475,7 +482,7 @@ if (!$Pid)
 #
 
 my $NewStatus = 'completed';
-my ($TaskFailures, $TaskTimedOut, $ErrMessage, $TAError, $PossibleCrash);
+my ($TaskFailures, $TaskTimedOut, $TAError, $PossibleCrash);
 Debug(Elapsed($Start), " Waiting for the script (", $Task->Timeout, "s timeout)\n");
 if (!defined $TA->Wait($Pid, $Task->Timeout, 60))
 {
@@ -542,9 +549,7 @@ elsif (!defined $TAError)
 
 if ($Step->Type ne "build")
 {
-  my $BuildList = $Task->CmdLineArg;
-  $BuildList =~ s/ .*$//;
-  foreach my $Build (split /,/, $BuildList)
+  foreach my $Build (keys %{$TaskMissions->{Builds}})
   {
     my $RptFileName = "$Build.report";
     Debug(Elapsed($Start), " Retrieving '$RptFileName'\n");
@@ -602,9 +607,7 @@ if ($NewStatus eq 'completed')
 {
   my $LatestDir = "$DataDir/latest";
   my $StepDir = $Step->GetDir();
-  my $BuildList = $Task->CmdLineArg;
-  $BuildList =~ s/ .*$//;
-  foreach my $Build (split /,/, $BuildList)
+  foreach my $Build (keys %{$TaskMissions->{Builds}})
   {
     my $RptFileName = "$Build.report";
     my $RefReport = $Task->VM->Name ."_$RptFileName";
diff --git a/testbot/bin/build/WineTest.pl b/testbot/bin/build/WineTest.pl
index 712341b339..c5d38df9d4 100755
--- a/testbot/bin/build/WineTest.pl
+++ b/testbot/bin/build/WineTest.pl
@@ -43,6 +43,7 @@ sub BEGIN
 
 use Build::Utils;
 use WineTestBot::Config;
+use WineTestBot::Missions;
 use WineTestBot::Utils;
 
 
@@ -52,9 +53,9 @@ use WineTestBot::Utils;
 
 sub BuildWine($$)
 {
-  my ($Targets, $Build) = @_;
+  my ($TaskMissions, $Build) = @_;
 
-  return 1 if (!$Targets->{$Build});
+  return 1 if (!$TaskMissions->{Builds}->{$Build});
 
   InfoMsg "\nRebuilding the $Build Wine\n";
   my $CPUCount = GetCPUCount();
@@ -74,44 +75,40 @@ sub BuildWine($$)
 # Test helpers
 #
 
-
-sub DailyWineTest($$$$$)
+sub DailyWineTest($$$$)
 {
-  my ($Targets, $Build, $NoSubmit, $BaseTag, $Args) = @_;
-
-  return 1 if (!$Targets->{$Build});
+  my ($Mission, $NoSubmit, $BaseTag, $Args) = @_;
 
-  InfoMsg "\nRunning WineTest in the $Build Wine\n";
-  SetupWineEnvironment($Build);
+  InfoMsg "\nRunning WineTest in the $Mission->{Build} Wine\n";
+  SetupWineEnvironment($Mission->{Build});
 
   # Run WineTest. Ignore the exit code since it returns non-zero whenever
   # there are test failures.
-  my $Tag = SanitizeTag("$BaseTag-$Build");
-  RunWine($Build, "./programs/winetest/winetest.exe.so",
-          "-c -o '../$Build.report' -t $Tag ". ShArgv2Cmd(@$Args));
-  if (!-f "$Build.report")
+  my $Tag = SanitizeTag("$BaseTag-$Mission->{Build}");
+  RunWine($Mission->{Build}, "./programs/winetest/winetest.exe.so",
+          "-c -o '../$Mission->{Build}.report' -t $Tag ".
+          ShArgv2Cmd(@$Args));
+  if (!-f "$Mission->{Build}.report")
   {
     LogMsg "WineTest did not produce a report file\n";
     return 0;
   }
 
   # Send the report to the website
-  if (!$NoSubmit and
-      RunWine($Build, "./programs/winetest/winetest.exe.so",
-              "-c -s '../$Build.report'"))
+  if ((!$NoSubmit and !$Mission->{nosubmit}) and
+      RunWine($Mission->{Build}, "./programs/winetest/winetest.exe.so",
+              "-c -s '../$Mission->{Build}.report'"))
   {
-    LogMsg "WineTest failed to send the $Build report\n";
+    LogMsg "WineTest failed to send the $Mission->{Build} report\n";
     # Soldier on in case it's just a network issue
   }
 
   return 1;
 }
 
-sub TestPatch($$$)
+sub TestPatch($$)
 {
-  my ($Targets, $Build, $Impacts) = @_;
-
-  return 1 if (!$Targets->{"test$Build"});
+  my ($Mission, $Impacts) = @_;
 
   my @TestList;
   foreach my $Module (sort keys %{$Impacts->{Tests}})
@@ -131,14 +128,15 @@ sub TestPatch($$$)
   }
   return 1 if (!@TestList);
 
-  InfoMsg "\nRunning the tests in the $Build Wine\n";
-  SetupWineEnvironment($Build);
+  InfoMsg "\nRunning the tests in the $Mission->{Build} Wine\n";
+  SetupWineEnvironment($Mission->{Build});
 
   # Run WineTest. Ignore the exit code since it returns non-zero whenever
   # there are test failures.
-  RunWine($Build, "./programs/winetest/winetest.exe.so",
-          "-c -o '../$Build.report' -t test-$Build ". join(" ", @TestList));
-  if (!-f "$Build.report")
+  RunWine($Mission->{Build}, "./programs/winetest/winetest.exe.so",
+          "-c -o '../$Mission->{Build}.report' -t test-$Mission->{Build} ".
+          join(" ", @TestList));
+  if (!-f "$Mission->{Build}.report")
   {
     LogMsg "WineTest did not produce a report file\n";
     return 0;
@@ -155,11 +153,8 @@ sub TestPatch($$$)
 $ENV{PATH} = "/usr/lib/ccache:/usr/bin:/bin";
 delete $ENV{ENV};
 
-my %AllTargets;
-map { $AllTargets{$_} = 1 } qw(win32 wow32 wow64);
-
 my $Action = "";
-my ($Usage, $OptNoSubmit, $TargetList, $FileName, $BaseTag);
+my ($Usage, $OptNoSubmit, $MissionStatement, $FileName, $BaseTag);
 while (@ARGV)
 {
   my $Arg = shift @ARGV;
@@ -186,9 +181,9 @@ while (@ARGV)
     $Usage = 2;
     last;
   }
-  elsif (!defined $TargetList)
+  elsif (!defined $MissionStatement)
   {
-    $TargetList = $Arg;
+    $MissionStatement = $Arg;
   }
   elsif ($Action eq "winetest")
   {
@@ -223,24 +218,35 @@ while (@ARGV)
 }
 
 # Check and untaint parameters
-my $Targets;
+my $TaskMissions;
 if (!defined $Usage)
 {
-  if (defined $TargetList)
+  if (defined $MissionStatement)
   {
-    foreach my $Target (split /[,:]/, $TargetList)
+    my ($ErrMessage, $Missions) = ParseMissionStatement($MissionStatement);
+    if (defined $ErrMessage)
     {
-      if (!$AllTargets{$Target})
-      {
-        Error "invalid target name $Target\n";
-        $Usage = 2;
-      }
-      $Targets->{$Target} = 1;
+      Error "$ErrMessage\n";
+      $Usage = 2;
+    }
+    elsif (!@$Missions)
+    {
+      Error "empty mission statement\n";
+      $Usage = 2;
+    }
+    elsif (@$Missions > 1)
+    {
+      Error "cannot specify missions for multiple tasks\n";
+      $Usage = 2;
+    }
+    else
+    {
+      $TaskMissions = $Missions->[0];
     }
   }
   else
   {
-    Error "specify at least one target\n";
+    Error "you must specify the mission statement\n";
     $Usage = 2;
   }
 
@@ -268,16 +274,13 @@ if (!defined $Usage)
   }
   else
   {
-    foreach my $Build ("win32", "wow32", "wow64")
-    {
-      $Targets->{"test$Build"} = 1 if ($Targets->{$Build});
-    }
-    if ($Targets->{"wow32"} or $Targets->{"wow64"})
+    my $Builds = $TaskMissions->{Builds};
+    if ($Builds->{"wow32"} or $Builds->{"wow64"})
     {
       # Always rebuild both WoW targets before running the tests to make sure
       # we don't run into issues caused by the two Wine builds being out of
       # sync.
-      $Targets->{"wow32"} = $Targets->{"wow64"} = 1;
+      $Builds->{"wow32"} = $Builds->{"wow64"} = 1;
     }
   }
 
@@ -295,8 +298,8 @@ if (defined $Usage)
     Error "try '$Name0 --help' for more information\n";
     exit $Usage;
   }
-  print "Usage: $Name0 [--help] --testpatch TARGETS PATCH\n";
-  print "or     $Name0 [--help] --winetest [--no-submit] TARGETS BASETAG ARGS\n";
+  print "Usage: $Name0 [--help] --testpatch MISSIONS PATCH\n";
+  print "or     $Name0 [--help] --winetest [--no-submit] MISSIONS BASETAG ARGS\n";
   print "\n";
   print "Tests the specified patch or runs WineTest in Wine.\n";
   print "\n";
@@ -304,7 +307,7 @@ if (defined $Usage)
   print "  --testpatch  Verify that the patch compiles and run the impacted tests.\n";
   print "  --winetest   Run WineTest and submit the result to the website.\n";
   print "  --no-submit  Do not submit the WineTest results to the website.\n";
-  print "  TARGETS      Is a comma-separated list of targets for the specified action.\n";
+  print "  MISSIONS     Is a colon-separated list of missions for the specified action.\n";
   print "               - win32: The regular 32 bit Wine build.\n";
   print "               - wow32: The 32 bit WoW Wine build.\n";
   print "               - wow64: The 64 bit WoW Wine build.\n";
@@ -328,26 +331,26 @@ if ($DataDir =~ /'/)
 #
 
 # Clean up old reports
-map { unlink("$_.report") } keys %AllTargets;
+map { unlink("$_.report") } keys %{$TaskMissions->{Builds}};
 
+my $Impacts;
 if ($Action eq "testpatch")
 {
-  my $Impacts = ApplyPatch("wine", $FileName);
+  $Impacts = ApplyPatch("wine", $FileName);
   exit(1) if (!$Impacts or
-              !BuildWine($Targets, "win32") or
-              !BuildWine($Targets, "wow64") or
-              !BuildWine($Targets, "wow32") or
-              !TestPatch($Targets, "win32", $Impacts) or
-              !TestPatch($Targets, "wow64", $Impacts) or
-              !TestPatch($Targets, "wow32", $Impacts));
+              !BuildWine($TaskMissions, "win32") or
+              !BuildWine($TaskMissions, "wow64") or
+              !BuildWine($TaskMissions, "wow32"));
 }
-elsif ($Action eq "winetest")
+foreach my $Mission (@{$TaskMissions->{Missions}})
 {
-  if (!DailyWineTest($Targets, "win32", $OptNoSubmit, $BaseTag, \@ARGV) or
-      !DailyWineTest($Targets, "wow64", $OptNoSubmit, $BaseTag, \@ARGV) or
-      !DailyWineTest($Targets, "wow32", $OptNoSubmit, $BaseTag, \@ARGV))
+  if ($Action eq "testpatch")
   {
-    exit(1);
+    exit(1) if (!TestPatch($Mission, $Impacts));
+  }
+  elsif ($Action eq "winetest")
+  {
+    exit(1) if (!DailyWineTest($Mission,  $OptNoSubmit, $BaseTag, \@ARGV));
   }
 }
 
diff --git a/testbot/ddl/update41.sql b/testbot/ddl/update41.sql
new file mode 100644
index 0000000000..f1a5e1a240
--- /dev/null
+++ b/testbot/ddl/update41.sql
@@ -0,0 +1,48 @@
+USE winetestbot;
+
+ALTER TABLE Tasks
+  ADD Missions VARCHAR(256) NULL
+      AFTER Timeout;
+
+UPDATE Tasks, VMs
+  SET Tasks.Missions = 'build'
+  WHERE Tasks.Missions is NULL AND Tasks.VMName = VMs.Name AND VMs.Type = 'build';
+
+UPDATE Tasks, VMs
+  SET Tasks.Missions = 'exe32'
+  WHERE Tasks.Missions is NULL AND Tasks.VMName = VMs.Name AND VMs.Type = 'win32';
+
+UPDATE Tasks, VMs
+  SET Tasks.Missions = 'exe32|exe64'
+  WHERE Tasks.Missions is NULL AND Tasks.VMName = VMs.Name AND VMs.Type = 'win64';
+
+UPDATE Tasks, VMs
+  SET Tasks.Missions = 'win32:wow64'
+  WHERE Tasks.Missions is NULL AND Tasks.VMName = VMs.Name AND VMs.Type = 'wine';
+
+ALTER TABLE Tasks
+  MODIFY Missions VARCHAR(256) NOT NULL;
+
+
+ALTER TABLE VMs
+  ADD Missions VARCHAR(256) NULL
+      AFTER Role;
+
+UPDATE VMs
+  SET Missions = 'build'
+  WHERE Missions is NULL AND Type = 'build';
+
+UPDATE VMs
+  SET Missions = 'exe32'
+  WHERE Missions is NULL AND Type = 'win32';
+
+UPDATE VMs
+  SET Missions = 'exe32|exe64'
+  WHERE Missions is NULL AND Type = 'win64';
+
+UPDATE VMs
+  SET Missions = 'win32|wow64'
+  WHERE Missions is NULL AND Type = 'wine';
+
+ALTER TABLE VMs
+  MODIFY Missions VARCHAR(256) NOT NULL;
diff --git a/testbot/ddl/winetestbot.sql b/testbot/ddl/winetestbot.sql
index 8862c6c9dc..e6114e0917 100644
--- a/testbot/ddl/winetestbot.sql
+++ b/testbot/ddl/winetestbot.sql
@@ -48,6 +48,7 @@ CREATE TABLE VMs
   SortOrder     INT(3)           NOT NULL,
   Type          ENUM('win32', 'win64', 'build', 'wine') NOT NULL,
   Role          ENUM('extra', 'base', 'winetest', 'retired', 'deleted') NOT NULL,
+  Missions      VARCHAR(256)     NOT NULL,
   Status        ENUM('dirty', 'reverting', 'sleeping', 'idle', 'running', 'off', 'offline', 'maintenance') NOT NULL,
   Errors        INT(2)           NULL,
   ChildPid      INT(5)           NULL,
@@ -149,6 +150,7 @@ CREATE TABLE Tasks
   Status       ENUM('queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled', 'skipped') NOT NULL,
   VMName       VARCHAR(20) NOT NULL,
   Timeout      INT(4) NOT NULL,
+  Missions     VARCHAR(256) NOT NULL,
   CmdLineArg   VARCHAR(256) NULL,
   Started      DATETIME NULL,
   Ended        DATETIME NULL,
diff --git a/testbot/doc/winetestbot-schema.dia b/testbot/doc/winetestbot-schema.dia
index c54897a831..372d3bf006 100644
--- a/testbot/doc/winetestbot-schema.dia
+++ b/testbot/doc/winetestbot-schema.dia
@@ -2106,6 +2106,29 @@
             <dia:string>##</dia:string>
           </dia:attribute>
         </dia:composite>
+        <dia:composite type="table_attribute">
+          <dia:attribute name="name">
+            <dia:string>#Missions#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="type">
+            <dia:string>#VARCHAR(256)#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="comment">
+            <dia:string>##</dia:string>
+          </dia:attribute>
+          <dia:attribute name="primary_key">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="nullable">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="unique">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="default_value">
+            <dia:string>##</dia:string>
+          </dia:attribute>
+        </dia:composite>
         <dia:composite type="table_attribute">
           <dia:attribute name="name">
             <dia:string>#CmdLineArg#</dia:string>
@@ -2360,6 +2383,29 @@
             <dia:string>##</dia:string>
           </dia:attribute>
         </dia:composite>
+        <dia:composite type="table_attribute">
+          <dia:attribute name="name">
+            <dia:string>#Missions#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="type">
+            <dia:string>#VARCHAR(256)#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="comment">
+            <dia:string>##</dia:string>
+          </dia:attribute>
+          <dia:attribute name="primary_key">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="nullable">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="unique">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="default_value">
+            <dia:string>##</dia:string>
+          </dia:attribute>
+        </dia:composite>
         <dia:composite type="table_attribute">
           <dia:attribute name="name">
             <dia:string>#Status#</dia:string>
diff --git a/testbot/lib/WineTestBot/Missions.pm b/testbot/lib/WineTestBot/Missions.pm
new file mode 100644
index 0000000000..58b33403fb
--- /dev/null
+++ b/testbot/lib/WineTestBot/Missions.pm
@@ -0,0 +1,99 @@
+# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*-
+# Copyright 2018 Francois Gouget
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+use strict;
+
+package WineTestBot::Missions;
+
+=head1 NAME
+
+WineTestBot::Missions - Missions parser and helper functions
+
+=cut
+
+use Exporter 'import';
+our @EXPORT = qw(DumpMissions ParseMissionStatement
+                 MergeMissionStatementTasks SplitMissionStatementTasks);
+
+
+sub DumpMissions($$)
+{
+  my ($Label, $Missions) = @_;
+
+  print STDERR "$Label:\n";
+  foreach my $TaskMissions (@$Missions)
+  {
+    print STDERR "Builds=", join(",", sort keys %{$TaskMissions->{Builds}}), "\n";
+    foreach my $Mission (@{$TaskMissions->{Missions}})
+    {
+      print STDERR "  [$Mission->{Build}]\n";
+      print STDERR "    \"$_\"=\"$Mission->{$_}\"\n" for (sort grep(!/^Build$/,keys %$Mission));
+    }
+  }
+}
+
+sub ParseMissionStatement($)
+{
+  my ($MissionStatement) = @_;
+
+  my @Missions;
+  foreach my $TaskStatement (split /[|]/, $MissionStatement)
+  {
+    my $TaskMissions = { Statement => $TaskStatement };
+    push @Missions, $TaskMissions;
+    foreach my $Statement (split /:/, $TaskStatement)
+    {
+      my ($Build, @Options) = split /,/, $Statement;
+      if ($Build !~ /^([a-z0-9]+)$/)
+      {
+        return ("Invalid mission name '$Build'", undef);
+      }
+      $Build = $1; # untaint
+      $TaskMissions->{Builds}->{$Build} = 1;
+      my $Mission = { Build => $Build, Statement => $Statement };
+      push @{$TaskMissions->{Missions}}, $Mission;
+
+      foreach my $Option (@Options)
+      {
+        if ($Option !~ s/^([a-z0-9_]+)//)
+        {
+          return ("Invalid option name '$Option'", undef);
+        }
+        my $Name = $1; # untaint
+        # do not untaint the value
+        $Mission->{$Name} = ($Option =~ s/^=//) ? $Option : 1;
+      }
+    }
+  }
+  return (undef, \@Missions);
+}
+
+sub MergeMissionStatementTasks($)
+{
+  my ($MissionStatement) = @_;
+  $MissionStatement =~ s/\|/:/g;
+  return $MissionStatement;
+}
+
+sub SplitMissionStatementTasks($)
+{
+  my ($MissionStatement) = @_;
+  $MissionStatement =~ s/:/|/g;
+  return $MissionStatement;
+}
+
+1;
diff --git a/testbot/lib/WineTestBot/PatchUtils.pm b/testbot/lib/WineTestBot/PatchUtils.pm
index 7df50f9b21..444ffaf491 100644
--- a/testbot/lib/WineTestBot/PatchUtils.pm
+++ b/testbot/lib/WineTestBot/PatchUtils.pm
@@ -457,10 +457,10 @@ sub GetPatchImpacts($)
 
 sub GetBuildTimeout($$)
 {
-  my ($Impacts, $Builds) = @_;
+  my ($Impacts, $TaskMissions) = @_;
 
   my ($ExeCount, $WineCount);
-  map {$_ =~ /^exe/ ? $ExeCount++ : $WineCount++ } keys %$Builds;
+  map {$_ =~ /^exe/ ? $ExeCount++ : $WineCount++ } keys %{$TaskMissions->{Builds}};
 
   # Set $ModuleCount to 0 if a full rebuild is needed
   my $ModuleCount = (!$Impacts or $Impacts->{RebuildRoot}) ? 0 :
@@ -486,7 +486,7 @@ sub GetBuildTimeout($$)
 
 sub GetTestTimeout($$)
 {
-  my ($Impacts, $Builds) = @_;
+  my ($Impacts, $TaskMissions) = @_;
 
   my $Timeout = $SuiteTimeout;
   if ($Impacts)
@@ -496,7 +496,7 @@ sub GetTestTimeout($$)
                        max(0, $UnitCount - 2) * $SingleAvgTime;
     $Timeout = min($SuiteTimeout, $TestsTimeout);
   }
-  return scalar(keys %$Builds) * $Timeout;
+  return @{$TaskMissions->{Missions}} * $Timeout;
 }
 
 1;
diff --git a/testbot/lib/WineTestBot/Patches.pm b/testbot/lib/WineTestBot/Patches.pm
index f685e86b32..e621e8845d 100644
--- a/testbot/lib/WineTestBot/Patches.pm
+++ b/testbot/lib/WineTestBot/Patches.pm
@@ -40,12 +40,13 @@ use Encode qw/decode/;
 use File::Basename;
 
 use WineTestBot::Config;
+use WineTestBot::Engine::Notify;
 use WineTestBot::Jobs;
+use WineTestBot::Missions;
 use WineTestBot::PatchUtils;
 use WineTestBot::Users;
 use WineTestBot::Utils;
 use WineTestBot::VMs;
-use WineTestBot::Engine::Notify;
 
 
 sub InitializeNew($$)
@@ -175,23 +176,8 @@ sub Submit($$$)
   $BuildVMs->AddFilter("Role", ["base"]);
   if ($Impacts->{TestUnitCount} and !$BuildVMs->IsEmpty())
   {
-    # Create the Build Step
-    my $BuildStep = $NewJob->Steps->Add();
-    $BuildStep->FileName("patch.diff");
-    $BuildStep->FileType("patch");
-    $BuildStep->Type("build");
-    $BuildStep->DebugLevel(0);
-
-    # Save the build step so the others can reference it.
-    my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save();
-    if (defined($ErrMessage))
-    {
-      $self->Disposition("Failed to submit build step");
-      return $ErrMessage;
-    }
-
     # Create steps for the Windows tests
-    my $Builds;
+    my ($BuildStep, $BuildMissions);
     foreach my $Module (sort keys %{$Impacts->{Tests}})
     {
       my $TestInfo = $Impacts->{Tests}->{$Module};
@@ -202,26 +188,54 @@ sub Submit($$$)
           my $WinVMs = CreateVMs();
           $WinVMs->AddFilter("Type", $Bits eq "32" ? ["win32", "win64"] : ["win64"]);
           $WinVMs->AddFilter("Role", ["base"]);
-          if (!$WinVMs->IsEmpty())
+          my $SortedKeys = $WinVMs->SortKeysBySortOrder($WinVMs->GetKeys());
+
+          my $Tasks;
+          foreach my $VMKey (@$SortedKeys)
           {
-            # Create one Step per (module, unit, bitness) combination
-            my $NewStep = $NewJob->Steps->Add();
-            $NewStep->PreviousNo($BuildStep->No);
-            my $FileName = $TestInfo->{ExeBase};
-            $FileName .= "64" if ($Bits eq "64");
-            $NewStep->FileName("$FileName.exe");
-            $NewStep->FileType("exe$Bits");
-            $Builds->{"exe$Bits"} = 1;
-
-            # And a task for each VM
-            my $Tasks = $NewStep->Tasks;
-            my $SortedKeys = $WinVMs->SortKeysBySortOrder($WinVMs->GetKeys());
-            foreach my $VMKey (@$SortedKeys)
+            my $VM = $WinVMs->GetItem($VMKey);
+            my ($ErrMessage, $Missions) = ParseMissionStatement($VM->Missions);
+            next if (defined $ErrMessage);
+
+            foreach my $TaskMissions (@$Missions)
             {
-              my $VM = $WinVMs->GetItem($VMKey);
+              next if (!$TaskMissions->{Builds}->{"exe$Bits"});
+
+              if (!$BuildStep)
+              {
+                # Create the Build Step
+                $BuildStep = $NewJob->Steps->Add();
+                $BuildStep->FileName("patch.diff");
+                $BuildStep->FileType("patch");
+                $BuildStep->Type("build");
+                $BuildStep->DebugLevel(0);
+
+                # Save the build step so the others can reference it.
+                my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save();
+                if (defined($ErrMessage))
+                {
+                  $self->Disposition("Failed to submit build step");
+                  return $ErrMessage;
+                }
+              }
+
+              if (!$Tasks)
+              {
+                # Create one Step per (module, unit, bitness) combination
+                my $NewStep = $NewJob->Steps->Add();
+                $NewStep->PreviousNo($BuildStep->No);
+                my $FileName = $TestInfo->{ExeBase};
+                $FileName .= "64" if ($Bits eq "64");
+                $NewStep->FileName("$FileName.exe");
+                $NewStep->FileType("exe$Bits");
+                $BuildMissions->{Builds}->{"exe$Bits"} = 1;
+                $Tasks = $NewStep->Tasks;
+              }
+
               my $Task = $Tasks->Add();
               $Task->VM($VM);
               $Task->Timeout($SingleTimeout);
+              $Task->Missions($TaskMissions->{Statement});
               $Task->CmdLineArg($Unit);
             }
           }
@@ -229,38 +243,48 @@ sub Submit($$$)
       }
     }
 
-    # Add the build task
-    my $BuildVM = ${$BuildVMs->GetItems()}[0];
-    my $BuildTask = $BuildStep->Tasks->Add();
-    $BuildTask->VM($BuildVM);
-    $BuildTask->Timeout(GetBuildTimeout($Impacts, $Builds));
+    if ($BuildStep)
+    {
+      # Add the build task
+      my $BuildVM = ${$BuildVMs->GetItems()}[0];
+      my $BuildTask = $BuildStep->Tasks->Add();
+      $BuildTask->VM($BuildVM);
+      $BuildMissions->{Statement} = join(":", keys %{$BuildMissions->{Builds}});
+      $BuildTask->Timeout(GetBuildTimeout($Impacts, $BuildMissions));
+      $BuildTask->Missions($BuildMissions->{Statement});
+    }
   }
 
   my $WineVMs = CreateVMs();
   $WineVMs->AddFilter("Type", ["wine"]);
   $WineVMs->AddFilter("Role", ["base"]);
-  if (!$WineVMs->IsEmpty())
+  my $SortedKeys = $WineVMs->SortKeysBySortOrder($WineVMs->GetKeys());
+
+  my $Tasks;
+  foreach my $VMKey (@$SortedKeys)
   {
-    # Add a Wine step to the job
-    my $NewStep = $NewJob->Steps->Add();
-    $NewStep->FileName("patch.diff");
-    $NewStep->FileType("patch");
-    $NewStep->Type("single");
-    $NewStep->DebugLevel(0);
-
-    # And a task for each VM
-    my $Tasks = $NewStep->Tasks;
-    my $SortedKeys = $WineVMs->SortKeysBySortOrder($WineVMs->GetKeys());
-    foreach my $VMKey (@$SortedKeys)
+    my $VM = $WineVMs->GetItem($VMKey);
+    my ($ErrMessage, $Missions) = ParseMissionStatement($VM->Missions);
+    next if (defined $ErrMessage);
+
+    foreach my $TaskMissions (@$Missions)
     {
-      my $VM = $WineVMs->GetItem($VMKey);
+      if (!$Tasks)
+      {
+        # Add a Wine step to the job
+        my $TestStep = $NewJob->Steps->Add();
+        $TestStep->FileName("patch.diff");
+        $TestStep->FileType("patch");
+        $TestStep->Type("single");
+        $TestStep->DebugLevel(0);
+        $Tasks = $TestStep->Tasks;
+      }
+
       my $Task = $Tasks->Add();
       $Task->VM($VM);
-      # Only verify that the win32 version compiles
-      my $Builds = { "win32" => 1 };
-      $Task->Timeout(GetBuildTimeout($Impacts, $Builds) +
-                     GetTestTimeout($Impacts, $Builds));
-      $Task->CmdLineArg(join(",", keys %$Builds));
+      $Task->Timeout(GetBuildTimeout($Impacts, $TaskMissions) +
+                     GetTestTimeout($Impacts, $TaskMissions));
+      $Task->Missions($TaskMissions->{Statement});
     }
   }
 
diff --git a/testbot/lib/WineTestBot/StepsTasks.pm b/testbot/lib/WineTestBot/StepsTasks.pm
index a50f61ada3..cfd78e52e2 100644
--- a/testbot/lib/WineTestBot/StepsTasks.pm
+++ b/testbot/lib/WineTestBot/StepsTasks.pm
@@ -30,6 +30,7 @@ use WineTestBot::WineTestBotObjects;
 our @ISA = qw(WineTestBot::WineTestBotItem);
 
 use WineTestBot::Config;
+use WineTestBot::Missions;
 
 
 sub GetStepDir($)
@@ -68,34 +69,50 @@ sub GetTitle($)
 {
   my ($self) = @_;
 
-  my $Title = "";
-  if ($self->Type eq "single")
+  my @TitleParts;
+  if ($self->Type eq "build")
   {
-    if ($self->FileType eq "exe32")
-    {
-      $Title .= "32 bit ";
-    }
-    elsif ($self->FileType eq "exe64")
-    {
-      $Title .= "64 bit ";
-    }
-    $Title .= $self->CmdLineArg || "";
+    push @TitleParts, "build";
   }
-  elsif ($self->Type eq "build")
+  elsif ($self->FileType eq "exe32")
   {
-    $Title = "build";
+    push @TitleParts, "32 bit";
   }
-  $Title =~ s/\s*$//;
-
-  if ($Title)
+  elsif ($self->FileType eq "exe64")
   {
-    $Title = $self->VM->Name . " (" . $Title . ")";
+    push @TitleParts, "64 bit";
   }
   else
   {
-    $Title = $self->VM->Name;
+    my ($ErrMessage, $Missions) = ParseMissionStatement($self->Missions);
+    if (!defined $ErrMessage and @$Missions == 1)
+    {
+      my $Builds = $Missions->[0]->{Builds};
+      if ($Builds->{build})
+      {
+        push @TitleParts, "build";
+      }
+      elsif ($Builds->{wow64} and ($Builds->{win32} or $Builds->{wow32}))
+      {
+        push @TitleParts, "32 & 64 bit";
+      }
+      elsif ($Builds->{win32} or $Builds->{wow32})
+      {
+        push @TitleParts, "32 bit";
+      }
+      elsif ($Builds->{wow64})
+      {
+        push @TitleParts, "64 bit";
+      }
+    }
+  }
+  if ($self->Type ne "suite" and $self->CmdLineArg)
+  {
+    push @TitleParts, $self->CmdLineArg;
   }
 
+  my $Title = $self->VM->Name;
+  $Title .= " (@TitleParts)" if (@TitleParts);
   return $Title;
 }
 
@@ -148,6 +165,7 @@ sub _initialize($$)
       $StepTask->Timeout($Task->Timeout);
       $StepTask->FileName($Step->FileName);
       $StepTask->FileType($Step->FileType);
+      $StepTask->Missions($Task->Missions);
       $StepTask->CmdLineArg($Task->CmdLineArg);
       $StepTask->Started($Task->Started);
       $StepTask->Ended($Task->Ended);
@@ -179,6 +197,7 @@ my @PropertyDescriptors = (
   CreateBasicPropertyDescriptor("Timeout", "Timeout", !1, 1, "N", 4),
   CreateBasicPropertyDescriptor("FileName", "File name", !1, !1, "A", 100),
   CreateBasicPropertyDescriptor("FileType", "File Type", !1, 1, "A", 32),
+  CreateBasicPropertyDescriptor("Missions", "Missions", !1, 1, "A", 256),
   CreateBasicPropertyDescriptor("CmdLineArg", "Command line args", !1, !1, "A", 256),
   CreateBasicPropertyDescriptor("Started", "Execution started", !1, !1, "DT", 19),
   CreateBasicPropertyDescriptor("Ended", "Execution ended", !1, !1, "DT", 19),
diff --git a/testbot/lib/WineTestBot/Tasks.pm b/testbot/lib/WineTestBot/Tasks.pm
index 084784a3ba..f9e84d471b 100644
--- a/testbot/lib/WineTestBot/Tasks.pm
+++ b/testbot/lib/WineTestBot/Tasks.pm
@@ -298,6 +298,7 @@ my @PropertyDescriptors = (
   CreateEnumPropertyDescriptor("Status", "Status",  !1,  1, ['queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled', 'skipped']),
   CreateItemrefPropertyDescriptor("VM", "VM", !1,  1, \&CreateVMs, ["VMName"]),
   CreateBasicPropertyDescriptor("Timeout", "Timeout", !1, 1, "N", 4),
+  CreateBasicPropertyDescriptor("Missions", "Missions", !1, 1, "A", 256),
   CreateBasicPropertyDescriptor("CmdLineArg", "Command line args", !1, !1, "A", 256),
   CreateBasicPropertyDescriptor("Started", "Execution started", !1, !1, "DT", 19),
   CreateBasicPropertyDescriptor("Ended", "Execution ended", !1, !1, "DT", 19),
diff --git a/testbot/lib/WineTestBot/VMs.pm b/testbot/lib/WineTestBot/VMs.pm
index cf7bd5ab75..e7cd277494 100644
--- a/testbot/lib/WineTestBot/VMs.pm
+++ b/testbot/lib/WineTestBot/VMs.pm
@@ -151,6 +151,7 @@ use ObjectModel::BackEnd;
 use WineTestBot::Config;
 use WineTestBot::Engine::Notify;
 use WineTestBot::LibvirtDomain;
+use WineTestBot::Missions;
 use WineTestBot::RecordGroups;
 use WineTestBot::TestAgent;
 
@@ -302,6 +303,24 @@ sub KillChild($)
   $self->ChildPid(undef);
 }
 
+sub PutColValue($$$)
+{
+  my ($self, $ColName, $Value) = @_;
+
+  $self->SUPER::PutColValue($ColName, $Value);
+  if ($self->{IsModified} and ($ColName eq "Type" or $ColName eq "Missions"))
+  {
+    $self->{ValidateMissions} = 1;
+  }
+}
+
+my $_SupportedMissions = {
+  "build" => { "build" => 1 },
+  "win32" => { "exe32" => 1 },
+  "win64" => { "exe32" => 1, "exe64" => 1 },
+  "wine"  => { "win32" => 1, "wow32" => 1, "wow64" => 1 },
+};
+
 sub Validate($)
 {
   my ($self) = @_;
@@ -311,6 +330,27 @@ sub Validate($)
   {
     return ("Role", "Only win32, win64 and wine VMs can have a role of '" . $self->Role . "'");
   }
+  if ($self->{ValidateMissions})
+  {
+    my ($ErrMessage, $Missions) = ParseMissionStatement($self->Missions);
+    return ("Missions", $ErrMessage) if (defined $ErrMessage);
+    foreach my $TaskMissions (@$Missions)
+    {
+      if ($self->Type ne "wine" and @{$TaskMissions->{Missions}} > 1)
+      {
+        return ("Missions", "Only wine VMs can handle more than one mission per task");
+      }
+      foreach my $Mission (@{$TaskMissions->{Missions}})
+      {
+        if (!$_SupportedMissions->{$self->Type}->{$Mission->{Build}})
+        {
+          return ("Missions", ucfirst($self->Type) ." VMs only support ". join(", ", sort keys %{$_SupportedMissions->{$self->Type}}) ." missions");
+        }
+      }
+    }
+    delete $self->{ValidateMissions};
+  }
+
   return $self->SUPER::Validate();
 }
 
@@ -666,6 +706,7 @@ my @PropertyDescriptors = (
   CreateBasicPropertyDescriptor("SortOrder", "Display order", !1, 1, "N", 3),
   CreateEnumPropertyDescriptor("Type", "Type of VM", !1, 1, ['win32', 'win64', 'build', 'wine']),
   CreateEnumPropertyDescriptor("Role", "VM Role", !1, 1, ['extra', 'base', 'winetest', 'retired', 'deleted']),
+  CreateBasicPropertyDescriptor("Missions", "Missions", !1, 1, "A", 256),
   CreateEnumPropertyDescriptor("Status", "Current status", !1, 1, ['dirty', 'reverting', 'sleeping', 'idle', 'running', 'off', 'offline', 'maintenance']),
   CreateBasicPropertyDescriptor("Errors", "Errors", !1, !1, "N", 2),
   CreateBasicPropertyDescriptor("ChildPid", "Child process id", !1, !1, "N", 5),
diff --git a/testbot/web/JobDetails.pl b/testbot/web/JobDetails.pl
index 8efe5eefcf..de2f9ccb40 100644
--- a/testbot/web/JobDetails.pl
+++ b/testbot/web/JobDetails.pl
@@ -398,6 +398,7 @@ EOF
     print "<details><summary>",
           $self->CGI->escapeHTML($VM->Description || $VM->Name), "</summary>",
           $self->CGI->escapeHTML($VM->Details || "No details!"),
+          ($StepTask->Missions ? "<br>Missions: ". $StepTask->Missions : ""),
           "</details>\n";
 
     my $MoreInfo = $self->{More}->{$Key};
diff --git a/testbot/web/Submit.pl b/testbot/web/Submit.pl
index c10a267909..bcb5458ad5 100644
--- a/testbot/web/Submit.pl
+++ b/testbot/web/Submit.pl
@@ -36,6 +36,7 @@ use WineTestBot::Branches;
 use WineTestBot::Config;
 use WineTestBot::Engine::Notify;
 use WineTestBot::Jobs;
+use WineTestBot::Missions;
 use WineTestBot::PatchUtils;
 use WineTestBot::Steps;
 use WineTestBot::Utils;
@@ -814,13 +815,18 @@ sub SubmitJob($$$)
           my $Task = $BuildStep->Tasks->Add();
           $Task->VM($BuildVM);
 
-          my $Builds = { "exe32" => 1 };
-          $Builds->{"exe64"} = 1 if defined $self->GetParam("Run64");
-          $Task->Timeout(GetBuildTimeout($Impacts, $Builds));
+          my $MissionStatement = "exe32";
+          $MissionStatement .= ":exe64" if (defined $self->GetParam("Run64"));
+          my ($ErrMessage, $Missions) = ParseMissionStatement($MissionStatement);
+          if (!defined $ErrMessage)
+          {
+            $Task->Timeout(GetBuildTimeout($Impacts, $Missions->[0]));
+            $Task->Missions($Missions->[0]);
 
-          # Save the build step so the others can reference it
-          my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save();
-          if (defined($ErrMessage))
+            # Save the build step so the others can reference it
+            (my $ErrKey, my $ErrProperty, $ErrMessage) = $Jobs->Save();
+          }
+          if (defined $ErrMessage)
           {
             $self->{ErrMessage} = $ErrMessage;
             return !1;
@@ -851,51 +857,53 @@ sub SubmitJob($$$)
       my $Task = $Tasks->Add();
       $Task->VM($VM);
       $Task->Timeout($SingleTimeout);
+      $Task->Missions("exe$Bits");
       $Task->CmdLineArg($self->GetParam("CmdLineArg"));
     }
   }
 
   if ($FileType eq "patch")
   {
-    my $Tasks;
+    my ($Tasks, $MissionStatement, $Timeout);
     my $VMs = CreateVMs();
     $VMs->AddFilter("Type", ["wine"]);
     my $SortedKeys = $VMs->SortKeysBySortOrder($VMs->GetKeys());
-    foreach my $Build ("win32", "wow64")
+    foreach my $VMKey (@$SortedKeys)
     {
-      next if ($Build eq "wow64" and !defined($self->GetParam("Run64")));
+      my $VM = $VMs->GetItem($VMKey);
+      my $FieldName = "vm_" . $self->CGI->escapeHTML($VMKey);
+      next if (!$self->GetParam($FieldName)); # skip unselected VMs
 
-      my $Timeout;
-      foreach my $VMKey (@$SortedKeys)
+      if (!$Tasks)
       {
-        my $VM = $VMs->GetItem($VMKey);
-        my $FieldName = "vm_" . $self->CGI->escapeHTML($VMKey);
-        next if (!$self->GetParam($FieldName)); # skip unselected VMs
-
-        if (!$Tasks)
+        # First create the Wine test step
+        my $WineStep = $Steps->Add();
+        $WineStep->FileName($BaseName);
+        $WineStep->FileType($FileType);
+        $WineStep->Type("single");
+        $WineStep->DebugLevel($self->GetParam("DebugLevel"));
+        $WineStep->ReportSuccessfulTests(defined($self->GetParam("ReportSuccessfulTests")));
+        $Tasks = $WineStep->Tasks;
+
+        $MissionStatement = "win32";
+        $MissionStatement.= ":wow64" if (defined $self->GetParam("Run64"));
+
+        my ($ErrMessage, $Missions) = ParseMissionStatement($MissionStatement);
+        if (defined $ErrMessage)
         {
-          # First create the Wine test step
-          my $WineStep = $Steps->Add();
-          $WineStep->FileName($BaseName);
-          $WineStep->FileType($FileType);
-          $WineStep->Type("single");
-          $WineStep->DebugLevel($self->GetParam("DebugLevel"));
-          $WineStep->ReportSuccessfulTests(defined($self->GetParam("ReportSuccessfulTests")));
-          $Tasks = $WineStep->Tasks;
+          $self->{ErrMessage} = $ErrMessage;
+          return !1;
         }
-        if (!defined $Timeout)
-        {
-          my $Builds = { $Build => 1 };
-          $Timeout = GetBuildTimeout($Impacts, $Builds) +
-                     GetTestTimeout($Impacts, $Builds);
-        }
-
-        # Then add a task for this VM
-        my $Task = $Tasks->Add();
-        $Task->VM($VM);
-        $Task->CmdLineArg($Build);
-        $Task->Timeout($Timeout);
+        $Missions = $Missions->[0];
+        $Timeout = GetBuildTimeout($Impacts, $Missions) +
+                   GetTestTimeout($Impacts, $Missions);
       }
+
+      # Then add a task for this VM
+      my $Task = $Tasks->Add();
+      $Task->VM($VM);
+      $Task->Timeout($Timeout);
+      $Task->Missions($MissionStatement);
     }
   }
 
diff --git a/testbot/web/admin/VMDetails.pl b/testbot/web/admin/VMDetails.pl
index 9603e346a8..5f49c684d8 100644
--- a/testbot/web/admin/VMDetails.pl
+++ b/testbot/web/admin/VMDetails.pl
@@ -67,6 +67,22 @@ sub Save($)
   return ! defined($self->{ErrMessage});
 }
 
+sub GenerateFooter($)
+{
+  my ($self) = @_;
+  print "<p></p><div class='CollectionBlock'><table>\n";
+  print "<thead><tr><th class='Record'>Legend</th></tr></thead>\n";
+  print "<tbody><tr><td class='Record'>\n";
+
+  print "<p>The Missions syntax is <i>mission1:mission2:...|mission3|...</i> where <i>mission1</i> and <i>mission2</i> will be run in the same task, and <i>mission3</i> in a separate task.<br>\n";
+  print "Each mission is composed of a build and options separated by commas: <i>build,option1=value,option2,...</i>. The value can be omitted for boolean options and defaults to true.<br>\n";
+  print "The supported builds are <i>build</i> for build VMs; <i>exe32</i> and <i>exe64</i> for Windows VMs;<i> win32</i>, <i>wow32</i> and <i>wow64</i> for Wine VMs.</p>\n";
+  print "<p>On Wine VMs:<br>\n";
+  print "If set, the <i>nosubmit</i> option specifies that the WineTest results should not be published online.</p>\n";
+  print "</td></tr></tbody>\n";
+  print "</table></div>\n";
+  $self->SUPER::GenerateFooter();
+}
 
 package main;
 
-- 
2.19.1




More information about the wine-devel mailing list