Add mb try command

This command will be used by developers to debug their CL quickly. It
allows them to specify only certain test suites to run.

Bug: 1015682
Change-Id: I22e30c1cc29e31d4e484808d5a8302813cc4a386
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1856822
Reviewed-by: Dirk Pranke <[email protected]>
Commit-Queue: Stephen Martinis <[email protected]>
Cr-Commit-Position: refs/heads/master@{#707422}
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index 6d2784a..f6428ce 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -64,7 +64,6 @@
   mbw = MetaBuildWrapper()
   return mbw.Main(args)
 
-
 class MetaBuildWrapper(object):
   def __init__(self):
     self.chromium_src_dir = CHROMIUM_SRC_DIR
@@ -214,6 +213,13 @@
                            'implies --quiet')
     subp.set_defaults(func=self.CmdLookup)
 
+    subp = subps.add_parser('try',
+                            description='Try your change on a remote builder')
+    AddCommonOptions(subp)
+    subp.add_argument('target',
+                      help='ninja target to build and run')
+    subp.set_defaults(func=self.CmdTry)
+
     subp = subps.add_parser(
       'run', formatter_class=argparse.RawDescriptionHelpFormatter)
     subp.description = (
@@ -377,6 +383,61 @@
       self.PrintCmd(cmd, env)
     return 0
 
+  def CmdTry(self):
+    target = self.args.target
+    if not target.startswith('//'):
+      self.Print("Expected a GN target like //:foo, got %s" % target)
+      return 1
+
+    recipe_name = None
+    isolate_map = self.ReadIsolateMap()
+    for name, config in isolate_map.iteritems():
+      if 'label' in config and config['label'] == target:
+        recipe_name = name
+        break
+    if not recipe_name:
+      self.Print("Unable to find a recipe entry for %s." % target)
+
+    json_path = self.PathJoin(self.chromium_src_dir, 'out.json')
+    try:
+      ret, out, err = self.Run(
+        ['git', 'cl', 'issue', '--json=out.json'], force_verbose=False)
+      if ret != 0:
+        self.Print(
+          "Unable to fetch current issue. Output and error:\n%s\n%s" % (
+            out, err
+        ))
+        return ret
+      with open(json_path) as f:
+        issue_data = json.load(f)
+    finally:
+      if self.Exists(json_path):
+        os.unlink(json_path)
+
+    if not issue_data['issue']:
+      self.Print("Missing issue data. Upload your CL to Gerrit and try again.")
+      return 1
+
+    def run_cmd(previous_res, cmd):
+      res, out, err = self.Run(cmd, force_verbose=False, stdin=previous_res)
+      if res != 0:
+        self.Print("Err while running", cmd)
+        self.Print("Output", out)
+        raise Exception(err)
+      return out
+
+    result = LedResult(None, run_cmd).then(
+      # TODO(martiniss): maybe don't always assume the bucket?
+      'led', 'get-builder', 'luci.chromium.try:%s' % self.args.builder).then(
+      'led', 'edit', '-r', 'chromium_trybot_experimental',
+        '-p', 'tests=["%s"]' % recipe_name).then(
+      'led', 'edit-cr-cl', issue_data['issue_url']).then(
+      'led', 'launch').result
+
+    swarming_data = json.loads(result)['swarming']
+    self.Print("Launched task at https://%s/task?id=%s" % (
+      swarming_data['host_name'], swarming_data['task_id']))
+
   def CmdRun(self):
     vals = self.GetConfig()
     if not vals:
@@ -1658,14 +1719,16 @@
     ret, _, _ = self.Run(ninja_cmd, buffer_output=False)
     return ret
 
-  def Run(self, cmd, env=None, force_verbose=True, buffer_output=True):
+  def Run(self, cmd, env=None, force_verbose=True, buffer_output=True,
+          stdin=None):
     # This function largely exists so it can be overridden for testing.
     if self.args.dryrun or self.args.verbose or force_verbose:
       self.PrintCmd(cmd, env)
     if self.args.dryrun:
       return 0, '', ''
 
-    ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output)
+    ret, out, err = self.Call(cmd, env=env, buffer_output=buffer_output,
+                              stdin=stdin)
     if self.args.verbose or force_verbose:
       if ret:
         self.Print('  -> returned %d' % ret)
@@ -1676,12 +1739,12 @@
         self.Print(err, end='', file=sys.stderr)
     return ret, out, err
 
-  def Call(self, cmd, env=None, buffer_output=True):
+  def Call(self, cmd, env=None, buffer_output=True, stdin=None):
     if buffer_output:
       p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-                           env=env)
-      out, err = p.communicate()
+                           env=env, stdin=subprocess.PIPE)
+      out, err = p.communicate(input=stdin)
     else:
       p = subprocess.Popen(cmd, shell=False, cwd=self.chromium_src_dir,
                            env=env)
@@ -1761,6 +1824,28 @@
       return fp.write(contents)
 
 
+class LedResult(object):
+  """Holds the result of a led operation. Can be chained using |then|."""
+
+  def __init__(self, result, run_cmd):
+    self._result = result
+    self._run_cmd = run_cmd
+
+  @property
+  def result(self):
+    """The mutable result data of the previous led call as decoded JSON."""
+    return self._result
+
+  def then(self, *cmd):
+    """Invoke led, passing it the current `result` data as input.
+
+    Returns another LedResult object with the output of the command.
+    """
+    return self.__class__(
+        self._run_cmd(self._result, cmd), self._run_cmd)
+
+
+
 class MBErr(Exception):
   pass