WIP draft
authorWilliam Lachance <wlachance@mozilla.com>
Wed, 22 Feb 2017 22:11:46 -0500
changeset 490557 928da129aea3de4f48997727fc65994823f05c10
parent 489279 6f2117c0b9895dfeb06fa74d2dc91bff660386ce
child 547290 3d53ce6c437b0a8ca57459f9e84cfb9b2f67e780
push id47131
push userwlachance@mozilla.com
push dateTue, 28 Feb 2017 19:34:42 +0000
milestone54.0a1
WIP MozReview-Commit-ID: BYyN5nNGD4k
taskcluster/actions/hello-action.py
taskcluster/actions/mochitest-retrigger-action.py
taskcluster/actions/registry.py
taskcluster/ci/test/tests.yml
taskcluster/mach_commands.py
taskcluster/scripts/tester/test-ubuntu.sh
testing/mozharness/scripts/desktop_unittest.py
--- a/taskcluster/actions/hello-action.py
+++ b/taskcluster/actions/hello-action.py
@@ -23,8 +23,9 @@ But you can also use the default value `
         """.strip(),
     },
 )
 def hello_world_action(parameters, input, task_group_id, task_id, task):
     print "This message was triggered from context-menu of taskId: {}".format(task_id)
     print ""
     print "Hello {}".format(input)
     print "--- Action is now executed"
+
new file mode 100644
--- /dev/null
+++ b/taskcluster/actions/mochitest-retrigger-action.py
@@ -0,0 +1,92 @@
+import copy
+import logging
+
+import requests
+from slugid import nice as slugid
+
+from .registry import register_callback_action
+from taskgraph.create import create_task
+from taskgraph.util.time import (
+    current_json_time,
+    json_time_from_now
+)
+
+TASKCLUSTER_QUEUE_URL = "https://queue.taskcluster.net/v1/task"
+
+logger = logging.getLogger(__name__)
+
+
+@register_callback_action(
+    title='Schedule mochitest retrigger',
+    symbol='mdr',
+    description="Retriggers the specified mochitest job with additional options",
+    order=10000,  # Put this at the very bottom/end of any menu (default)
+    context=[{}],  # Applies to any task
+    available=lambda parameters: True,  # available regardless decision parameters (default)
+    schema={
+        'type': 'object',
+        'properties': {
+            'path': {
+                'type': 'string',
+                'maxLength': 255,
+                'default': '',
+                'title': 'Path name',
+                'description': 'Path of mochitest to retrigger'
+            },
+            'logLevel': {
+                'type': 'string',
+                'enum': ['debug', 'info', 'warning', 'error', 'critical'],
+                'default': 'debug',
+                'title': 'Log level',
+                'description': 'Log level for output (default is DEBUG, which is highest)'
+            },
+            'runUntilFail': {
+                'type': 'boolean',
+                'default': False,
+                'title': 'Run until failure',
+                'description': 'Runs the specified set of tests repeatedly until failure (or 30 times)'
+            },
+            'environment': {
+                'type': 'object',
+                'default': {'MY_ENV_1': 'myvalue1'},
+                'title': 'Extra environment variables',
+                'description': 'Extra environment variables to use for this run'
+            }
+        }
+    }
+)
+def mochitest_retrigger_action(parameters, input, task_group_id, task_id, task):
+    logger.info("Fetching task definition for mochitest task id %s...", task_id)
+    task_definition = requests.get(url="{}/{}".format(TASKCLUSTER_QUEUE_URL,
+                                                      task_id)).json()
+    new_task_definition = copy.copy(task_definition)
+
+    # set new created, deadline, and expiry fields
+    new_task_definition['created'] = current_json_time()
+    new_task_definition['deadline'] = json_time_from_now('1d')
+    new_task_definition['expires'] = json_time_from_now('30d')
+
+    # don't want to run mozharness tests
+    new_task_definition['payload']['command'] += ['--no-run-tests']
+
+    custom_mach_command = ['mochitest']
+    custom_mach_command += ['--log-mach=-',
+                            '--log-mach-level={}'.format(input['logLevel'])]
+    if input['runUntilFail']:
+        custom_mach_command += ['--run-until-fail']
+    custom_mach_command += [input['path']]
+    new_task_definition['payload']['env']['CUSTOM_MACH_COMMAND'] = ' '.join(
+        custom_mach_command)
+
+    # update environment
+    new_task_definition['payload']['env'].update(input['environment'])
+
+    # tweak the treeherder symbol
+    new_task_definition['extra']['treeherder']['symbol'] = 'custom'
+
+    print new_task_definition
+    # actually create the new task
+    new_task_id = slugid()
+    logger.info("Creating new mochitest task with id %s", new_task_id)
+    session = requests.Session()
+    create_task(session, new_task_id, 'mochitest-debug', new_task_definition)
--- a/taskcluster/actions/registry.py
+++ b/taskcluster/actions/registry.py
@@ -219,29 +219,17 @@ def register_callback_action(title, symb
                     'image': docker_image('decision'),
                     'maxRunTime': 1800,
                     'command': [
                         '/home/worker/bin/run-task', '--vcs-checkout=/home/worker/checkouts/gecko',
                         '--', 'bash', '-cx',
                         """\
 cd /home/worker/checkouts/gecko &&
 ln -s /home/worker/artifacts artifacts &&
-./mach --log-no-times taskgraph action-callback """ + ' '.join([
-                            "--pushlog-id='{}'".format(parameters['pushlog_id']),
-                            "--pushdate='{}'".format(parameters['pushdate']),
-                            "--project='{}'".format(parameters['project']),
-                            "--message='{}'".format(parameters['message'].replace("'", "'\\''")),
-                            "--owner='{}'".format(parameters['owner']),
-                            "--level='{}'".format(parameters['level']),
-                            "--base-repository='https://hg.mozilla.org/mozilla-central'",
-                            "--head-repository='{}'".format(parameters['head_repository']),
-                            "--head-ref='{}'".format(parameters['head_ref']),
-                            "--head-rev='{}'".format(parameters['head_rev']),
-                            "--revision-hash='{}'\n".format(parameters['head_rev']),
-                        ]),
+./mach --log-no-times taskgraph action-callback"""
                     ],
                 },
                 'extra': {
                       'treeherder': {
                         'groupName': 'action-callback',
                         'groupSymbol': 'AC',
                         'symbol': symbol,
                       },
@@ -294,17 +282,17 @@ def trigger_action_callback():
     Trigger action callback using arguments from environment variables.
     """
     global callbacks
     task_group_id = os.environ.get('ACTION_TASK_GROUP_ID', None)
     task_id = json.loads(os.environ.get('ACTION_TASK_ID', 'null'))
     task = json.loads(os.environ.get('ACTION_TASK', 'null'))
     input = json.loads(os.environ.get('ACTION_INPUT', 'null'))
     callback = os.environ.get('ACTION_CALLBACK', None)
-    parameters = json.loads(os.environ.get('ACTION_PARAMETERS', 'null'))
+    parameters = json.loads(os.environ.get('ACTION_PARAMETERS', 'null')) or {}
     cb = callbacks.get(callback, None)
     if not cb:
         raise Exception('Unknown callback: {}'.format(callback))
     cb(Parameters(**parameters), input, task_group_id, task_id, task)
 
 
 # Load all modules from this folder, relying on the side-effects of register_
 # functions to populate the action registry.
--- a/taskcluster/ci/test/tests.yml
+++ b/taskcluster/ci/test/tests.yml
@@ -1,9 +1,10 @@
 # Each stanza here describes a particular test suite or sub-suite.  These are
+
 # processed through the transformations described in kind.yml to produce a
 # bunch of tasks.  See the schema in `test-descriptions.py` for a description
 # of the fields used here.
 
 # Note that these are in lexical order
 
 cppunit:
     description: "CPP Unit Tests"
--- a/taskcluster/mach_commands.py
+++ b/taskcluster/mach_commands.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 
+
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import json
@@ -301,17 +302,22 @@ class MachCommands(MachCommandBase):
         except Exception:
             traceback.print_exc()
             sys.exit(1)
 
     @SubCommand('taskgraph', 'action-callback',
                 description='Run action callback used by action tasks')
     def action_callback(self, **options):
         import actions
-        actions.trigger_action_callback()
+        try:
+            self.setup_logging()
+            return actions.trigger_action_callback()
+        except Exception:
+            traceback.print_exc()
+            sys.exit(1)
 
     def setup_logging(self, quiet=False, verbose=True):
         """
         Set up Python logging for all loggers, sending results to stderr (so
         that command output can be redirected easily) and adding the typical
         mach timestamp.
         """
         # remove the old terminal handler
--- a/taskcluster/scripts/tester/test-ubuntu.sh
+++ b/taskcluster/scripts/tester/test-ubuntu.sh
@@ -181,8 +181,14 @@ exec \${cmd}" > ${mozharness_bin}
 chmod +x ${mozharness_bin}
 
 # In interactive mode, the user will be prompted with options for what to do.
 if ! $TASKCLUSTER_INTERACTIVE; then
   # run the given mozharness script and configs, but pass the rest of the
   # arguments in from our own invocation
   ${mozharness_bin};
 fi
+
+# Run a custom mach command (this is typically used by action tasks to run
+# harnesses in a particular way)
+if [ "$CUSTOM_MACH_COMMAND" ]; then
+    eval "/home/worker/workspace/build/tests/mach ${CUSTOM_MACH_COMMAND}"
+fi
--- a/testing/mozharness/scripts/desktop_unittest.py
+++ b/testing/mozharness/scripts/desktop_unittest.py
@@ -145,16 +145,29 @@ class DesktopUnittest(TestingMixin, Merc
             "help": "Number of this chunk"}
          ],
         [["--allow-software-gl-layers"], {
             "action": "store_true",
             "dest": "allow_software_gl_layers",
             "default": False,
             "help": "Permits a software GL implementation (such as LLVMPipe) to use the GL compositor."}
          ],
+        [["--test-path"], {
+            "action": "extend",
+            "dest": "test_path",
+            "type": "string",
+            "help": "Specify which test path to run (in lieu of chunking)"
+        }
+         ],
+        [["--run-until-fail"], {
+            "action": "store_true",
+            "dest": "run_until_fail",
+            "default": False,
+            "help": "Passes the --run-until-fail option to the mochitest harness"}
+         ],
     ] + copy.deepcopy(testing_config_options) + \
         copy.deepcopy(blobupload_config_options) + \
         copy.deepcopy(code_coverage_config_options)
 
     def __init__(self, require_config_file=True):
         # abs_dirs defined already in BaseScript but is here to make pylint happy
         self.abs_dirs = None
         super(DesktopUnittest, self).__init__(
@@ -410,24 +423,33 @@ class DesktopUnittest(TestingMixin, Merc
                     option = option % str_format_values
                     if not option.endswith('None'):
                         base_cmd.append(option)
                 if self.structured_output(
                     suite_category,
                     self._query_try_flavor(suite_category, suite)
                 ):
                     base_cmd.append("--log-raw=-")
-                return base_cmd
             else:
                 self.warning("Suite options for %s could not be determined."
                              "\nIf you meant to have options for this suite, "
                              "please make sure they are specified in your "
                              "config under %s_options" %
                              (suite_category, suite_category))
 
+            if c.get('run_until_fail'):
+                base_cmd.extend(['--run-until-fail'])
+
+            if c.get('log_level'):
+                base_cmd += ['--console-level', c['log_level'].upper()]
+
+            # Specify test path last
+            if c.get('test_path'):
+                base_cmd.extend(c['test_path'])
+
             return base_cmd
         else:
             self.fatal("'binary_path' could not be determined.\n This should "
                        "be like '/path/build/application/firefox/firefox'"
                        "\nIf you are running this script without the 'install' "
                        "action (where binary_path is set), please ensure you are"
                        " either:\n(1) specifying it in the config file under "
                        "binary_path\n(2) specifying it on command line with the"