Beispiel #1
0
def RunSteps(api):
  # TODO(martinis) change this
  # The api.step object is directly callable.
  api.step('hello', ['echo', 'Hello World'])
  api.step('hello', ['echo', 'Why hello, there.'])

  # You can also manipulate various aspects of the step, such as env.
  # These are passed straight through to subprocess.Popen.
  # Also, abusing bash -c in this way is a TERRIBLE IDEA DON'T DO IT.
  api.step('goodbye', ['bash', '-c', 'echo Good bye, $friend.'],
           env={'friend': 'Darth Vader'})

  # Finally, you can make your step accept any return code
  api.step('anything is cool', ['bash', '-c', 'exit 3'],
           ok_ret='any')

  # We can manipulate the step presentation arbitrarily until we run
  # the next step.
  step_result = api.step('hello', ['echo', 'hello'])
  step_result.presentation.status = api.step.EXCEPTION

  try:
    api.step('goodbye', ['echo', 'goodbye'])
    # Modifying step_result now would raise an AssertionError.
  except api.step.StepFailure:
    # Raising anything besides StepFailure or StepWarning causes the build to go 
    # purple.
    raise ValueError('goodbye must exit 0!')

  try:
    api.step('warning', ['echo', 'warning'])
  except api.step.StepFailure as e:
    e.result.presentation.status = api.step.WARNING
    raise api.step.StepWarning(e.message)


  # Aggregate failures from tests!
  try:
    with recipe_api.defer_results():
      api.step('testa', ['echo', 'testa'])
      api.step('testb', ['echo', 'testb'])
  except recipe_api.AggregatedStepFailure as f:
    raise api.step.StepFailure("You can catch step failures.")

  # Some steps are needed from an infrastructure point of view. If these
  # steps fail, the build stops, but doesn't get turned red because it's
  # not the developers' fault.
  try:
    api.step('cleanup', ['echo', 'cleaning', 'up', 'build'], infra_step=True)
  except api.step.InfraFailure as f:
    assert f.result.presentation.status == api.step.EXCEPTION

  # Run a step through a made-up wrapper program.
  api.step('application', ['echo', 'main', 'application'],
           wrapper=['python', '-c', 'import sys; print sys.argv'])

  if api.properties.get('access_invalid_data'):
    result = api.step('no-op', ['echo', 'I', 'do', 'nothing'])
    # Trying to access non-existent attributes on the result should raise.
    _ = result.json.output
Beispiel #2
0
  def roll_projects(self, projects):
    """Attempts to roll each project from the provided list.

    If rolling any of the projects leads to failures, other
    projects are not affected.
    """
    project_data = self.m.luci_config.get_projects()

    with self.m.tempfile.temp_dir('recipes') as recipes_dir:
      self.m.cipd.ensure(recipes_dir, {
          'infra/recipes-py': 'latest',
      })

      results = []
      with recipe_api.defer_results():
        for project in projects:
          with self.m.step.nest(str(project)):
            results.append(self._roll_project(
                project_data[project], recipes_dir))

      # We need to unwrap |DeferredResult|s.
      results = [r.get_result() for r in results]

      # Failures to roll are OK as long as at least one of the repos is moving
      # forward. For example, with repos with following dependencies:
      #
      #   A    <- B
      #   A, B <- C
      #
      # New commit in A repo will need to get rolled into B first. However,
      # it'd also appear as a candidate for C roll, leading to a failure there.
      if ROLL_FAILURE in results and ROLL_SUCCESS not in results:
        self.m.python.failing_step(
            'roll result',
            'manual intervention needed: automated roll attempt failed')
Beispiel #3
0
def RunSteps(api):
    # TODO(martinis) change this
    # The api.step object is directly callable.
    api.step('hello', ['echo', 'Hello World'])
    api.step('hello', ['echo', 'Why hello, there.'])

    # You can also manipulate various aspects of the step, such as env.
    # These are passed straight through to subprocess.Popen.
    # Also, abusing bash -c in this way is a TERRIBLE IDEA DON'T DO IT.
    api.step('goodbye', ['bash', '-c', 'echo Good bye, $friend.'],
             env={'friend': 'Darth Vader'})

    # Finally, you can make your step accept any return code
    api.step('anything is cool', ['bash', '-c', 'exit 3'], ok_ret='any')

    # We can manipulate the step presentation arbitrarily until we run
    # the next step.
    step_result = api.step('hello', ['echo', 'hello'])
    step_result.presentation.status = api.step.EXCEPTION

    try:
        api.step('goodbye', ['echo', 'goodbye'])
        # Modifying step_result now would raise an AssertionError.
    except api.step.StepFailure:
        # Raising anything besides StepFailure or StepWarning causes the build to go
        # purple.
        raise ValueError('goodbye must exit 0!')

    try:
        api.step('warning', ['echo', 'warning'])
    except api.step.StepFailure as e:
        e.result.presentation.status = api.step.WARNING
        raise api.step.StepWarning(e.message)

    # Aggregate failures from tests!
    try:
        with recipe_api.defer_results():
            api.step('testa', ['echo', 'testa'])
            api.step('testb', ['echo', 'testb'])
    except recipe_api.AggregatedStepFailure as f:
        raise api.step.StepFailure("You can catch step failures.")

    # Some steps are needed from an infrastructure point of view. If these
    # steps fail, the build stops, but doesn't get turned red because it's
    # not the developers' fault.
    try:
        api.step('cleanup', ['echo', 'cleaning', 'up', 'build'],
                 infra_step=True)
    except api.step.InfraFailure as f:
        assert f.result.presentation.status == api.step.EXCEPTION

    # Run a step through a made-up wrapper program.
    api.step('application', ['echo', 'main', 'application'],
             wrapper=['python', '-c', 'import sys; print sys.argv'])

    if api.properties.get('access_invalid_data'):
        result = api.step('no-op', ['echo', 'I', 'do', 'nothing'])
        # Trying to access non-existent attributes on the result should raise.
        _ = result.json.output
Beispiel #4
0
  def roll_projects(self, projects):
    """Attempts to roll each project from the provided list.

    If rolling any of the projects leads to failures, other
    projects are not affected.
    """
    project_data = self.m.luci_config.get_projects()
    with recipe_api.defer_results():
      for project in projects:
        with self.m.step.nest(str(project)):
          self._roll_project(project_data[project])
Beispiel #5
0
    def roll_projects(self, projects):
        """Attempts to roll each project from the provided list.

    If rolling any of the projects leads to failures, other
    projects are not affected.

    Args:
      projects: list of tuples of
        project_id (string): id as found in recipes.cfg.
        project_url (string): Git repository URL of the project.
    """
        recipes_dir = self.m.path['cache'].join('builder', 'recipe_engine')
        self.m.file.rmtree('ensure recipe_dir gone', recipes_dir)
        self.m.file.ensure_directory('ensure builder cache dir exists',
                                     self.m.path['cache'].join('builder'))

        with self.m.context(cwd=self.m.path['cache'].join('builder')):
            # Git clone really wants to have cwd set to something other than None.
            self.m.git(
                'clone',
                '--depth',
                '1',
                'https://chromium.googlesource.com/infra/luci/recipes-py',
                recipes_dir,
                name='clone recipe engine')

        results = []
        with recipe_api.defer_results():
            for project_id, project_url in projects:
                with self.m.step.nest(str(project_id)):
                    results.append(
                        self._roll_project(project_id, project_url,
                                           recipes_dir))

        # We need to unwrap |DeferredResult|s.
        results = [r.get_result() for r in results]

        # Failures to roll are OK as long as at least one of the repos is moving
        # forward. For example, with repos with following dependencies:
        #
        #   A    <- B
        #   A, B <- C
        #
        # New commit in A repo will need to get rolled into B first. However,
        # it'd also appear as a candidate for C roll, leading to a failure there.
        if ROLL_FAILURE in results and ROLL_SUCCESS not in results:
            self.m.python.failing_step(
                'roll result',
                'manual intervention needed: automated roll attempt failed')
Beispiel #6
0
def RunSteps(api, bad_return, raise_infra_failure, access_invalid_data):
  if bad_return:
    return RETURN_SCHEMA.new(test_me='this should fail')

  # TODO(martinis) change this
  # The api.step object is directly callable.
  api.step('hello', ['echo', 'Hello World'])
  api.step('hello', ['echo', 'Why hello, there.'])

  # You can also manipulate various aspects of the step, such as env.
  # These are passed straight through to subprocess.Popen.
  # Also, abusing bash -c in this way is a TERRIBLE IDEA DON'T DO IT.
  api.step('goodbye', ['bash', '-c', 'echo Good bye, $friend.'],
           env={'friend': 'Darth Vader'})

  # You can modify environment in terms of old environment. Environment
  # variables are substituted in for expressions of the form %(VARNAME)s.
  api.step('recipes help',
      ['recipes.py', '--help'],
      env={
        'PATH': api.path.pathsep.join(
            [str(api.step.package_resource()), '%(PATH)s']),
      })

  # Finally, you can make your step accept any return code
  api.step('anything is cool', ['bash', '-c', 'exit 3'],
           ok_ret='any')

  # We can manipulate the step presentation arbitrarily until we run
  # the next step.
  step_result = api.step('hello', ['echo', 'hello'])
  step_result.presentation.status = api.step.EXCEPTION
  step_result.presentation.logs['the reason'] = ['The reason\nit failed']

  # Without a command, a step can be used to present some data from the recipe.
  step_result = api.step('Just print stuff', cmd=None)
  step_result.presentation.logs['more'] = ['More stuff']

  try:
    api.step('goodbye', ['echo', 'goodbye'])
    # Modifying step_result now would raise an AssertionError.
  except api.step.StepFailure:
    # Raising anything besides StepFailure or StepWarning causes the build to go 
    # purple.
    raise ValueError('goodbye must exit 0!')

  try:
    api.step('warning', ['echo', 'warning'])
  except api.step.StepFailure as e:
    e.result.presentation.status = api.step.WARNING
    raise api.step.StepWarning(e.message)


  # Aggregate failures from tests!
  try:
    with recipe_api.defer_results():
      api.step('testa', ['echo', 'testa'])
      api.step('testb', ['echo', 'testb'])
  except recipe_api.AggregatedStepFailure as f:
    raise api.step.StepFailure("You can catch step failures.")

  # Some steps are needed from an infrastructure point of view. If these
  # steps fail, the build stops, but doesn't get turned red because it's
  # not the developers' fault.
  try:
    api.step('cleanup', ['echo', 'cleaning', 'up', 'build'], infra_step=True)
  except api.step.InfraFailure as f:
    assert f.result.presentation.status == api.step.EXCEPTION

  # Run a step through a made-up wrapper program.
  api.step('application', ['echo', 'main', 'application'],
           wrapper=['python', '-c', 'import sys; print sys.argv'])

  if access_invalid_data:
    result = api.step('no-op', ['echo', 'I', 'do', 'nothing'])
    # Trying to access non-existent attributes on the result should raise.
    _ = result.json.output

  return RETURN_SCHEMA(test_me=3)
def RunSteps(api, properties, env_properties):
  # Collect memory/cpu/process before task execution.
  api.os_utils.collect_os_info()
  """Steps to checkout flutter engine and execute web tests."""
  cache_root = api.path['cache'].join('builder')
  checkout = GetCheckoutPath(api)

  if properties.clobber:
    api.file.rmtree('Clobber cache', cache_root)
  api.file.rmtree('Clobber build output', checkout.join('out'))

  api.file.ensure_directory('Ensure checkout cache', cache_root)
  api.goma.ensure()
  dart_bin = checkout.join(
      'third_party', 'dart', 'tools', 'sdks', 'dart-sdk', 'bin'
  )

  android_home = checkout.join('third_party', 'android_tools', 'sdk')

  env = {
      'GOMA_DIR': api.goma.goma_dir, 'ANDROID_HOME': str(android_home),
      'CHROME_NO_SANDBOX': 'true', 'ENGINE_PATH': cache_root
  }
  env_prefixes = {'PATH': [dart_bin]}

  api.flutter_deps.certs(env, env_prefixes)
  # Checkout source code and build
  api.repo_util.engine_checkout(cache_root, env, env_prefixes)

  if api.platform.is_mac:
    api.web_util.clone_goldens_repo(checkout)

  with api.context(cwd=cache_root, env=env,
                   env_prefixes=env_prefixes), api.depot_tools.on_path():

    # Checks before building the engine. Only run on Linux.
    if api.platform.is_linux:
      api.json_util.validate_json(checkout.join('flutter', 'ci'))
      FormatAndDartTest(api)
      Lint(api)

    api.gclient.runhooks()

    target_name = 'host_debug_unopt'
    gn_flags = ['--unoptimized', '--full-dart-sdk']
    # Mac needs to install xcode as part of the building process.
    additional_args = []
    felt_cmd = [
        checkout.join('out', target_name, 'dart-sdk', 'bin', 'dart'),
        'dev/felt.dart'
    ]

    isolated_hash = ''
    builds = []
    if api.platform.is_linux:
      RunGN(api, *gn_flags)
      Build(api, target_name)
      # Archieve the engine. Start the drones. Due to capacity limits we are
      # Only using the drones on the Linux for now.
      # Archive build directory into isolate.
      isolated_hash = Archive(api, target_name)
      # Schedule builds.
      # TODO(nurhan): Currently this recipe only shards Linux. The web drones
      # recipe is written in a way that it can also support sharding for
      # macOS and Windows OSes. When more resources are available or when
      # MWE or WWE builders start running more than 1 hour, also shard those
      # builders.
      builds = schedule_builds_on_linux(api, isolated_hash)
    elif api.platform.is_mac:
      with SetupXcode(api):
        RunGN(api, *gn_flags)
        Build(api, target_name)
        additional_args = ['--browser', 'ios-safari']
    else:
      # Platform = windows.
      RunGN(api, *gn_flags)
      Build(api, target_name)
      if api.platform.is_win:
        felt_cmd = [
            checkout.join(
                'flutter', 'lib', 'web_ui', 'dev', 'felt_windows.bat'
            )
        ]

    # Update dart packages and run tests.
    local_pub = checkout.join('out', target_name, 'dart-sdk', 'bin', 'pub')
    with api.context(
        cwd=checkout.join('flutter', 'web_sdk', 'web_engine_tester')):
      api.step('pub get in web_engine_tester', [local_pub, 'get'])
    with api.context(cwd=checkout.join('flutter', 'lib', 'web_ui')):
      api.step('pub get in web_engine_tester', [local_pub, 'get'])
      # TODO(nurhan): carry licenses to another shard when we have more
      # resources.
      felt_licenses = copy.deepcopy(felt_cmd)
      felt_licenses.append('check-licenses')
      api.step('felt licenses', felt_licenses)
      if api.platform.is_mac:
        additional_args_safari_desktop = ['--browser', 'safari']
        felt_test_safari_desktop = copy.deepcopy(felt_cmd)
        felt_test_safari_desktop.append('test')
        felt_test_safari_desktop.extend(additional_args_safari_desktop)
        api.step('felt test safari desktop', felt_test_safari_desktop)
      if api.platform.is_linux:
        # TODO(nurhan): Web engine analysis can also be part of felt and used
        # in a shard.
        web_engine_analysis_cmd = [
            checkout.join(
                'flutter', 'lib', 'web_ui', 'dev', 'web_engine_analysis.sh'
            ),
        ]
        api.step('web engine analysis', web_engine_analysis_cmd)
        builds = api.shard_util.collect_builds(builds)
        api.display_util.display_builds(
            step_name='display builds',
            builds=builds,
            raise_on_failure=True,
        )
        CleanUpProcesses(api)
      elif api.platform.is_mac:
        with SetupXcode(api):
          with recipe_api.defer_results():
            felt_test = copy.deepcopy(felt_cmd)
            felt_test.append('test')
            felt_test.extend(additional_args)
            api.step('felt ios-safari test', felt_test)
            api.web_util.upload_failing_goldens(checkout, 'ios-safari')
            CleanUpProcesses(api)
      else:
        api.web_util.chrome(checkout)
        felt_test = copy.deepcopy(felt_cmd)
        felt_test.append('test')
        felt_test.extend(additional_args)
        api.step('felt test chrome', felt_test)
        CleanUpProcesses(api)
Beispiel #8
0
def RunSteps(api, bad_return, access_invalid_data, access_deep_invalid_data,
             assign_extra_junk, timeout):
    if timeout:
        # Timeout causes the recipe engine to raise an exception if your step takes
        # longer to run than you allow. Units are seconds.
        if timeout == 1:
            api.step('timeout', ['sleep', '20'], timeout=1)
        elif timeout == 2:
            try:
                api.step('caught timeout', ['sleep', '20'], timeout=1)
            except api.step.StepFailure:
                return

    # TODO(martinis) change this
    # The api.step object is directly callable.
    api.step('hello', ['echo', 'Hello World'])
    api.step('hello', ['echo', 'Why hello, there.'])

    # You can change the current working directory as well
    api.step('mk subdir', ['mkdir', '-p', 'something'])
    with api.context(cwd=api.path['start_dir'].join('something')):
        api.step('something',
                 ['bash', '-c', 'echo Why hello, there, in a subdir.'])

    # By default, all steps run in 'start_dir', or the cwd of the recipe engine
    # when the recipe begins. Because of this, setting cwd to start_dir doesn't
    # show anything in particular in the expectations.
    with api.context(cwd=api.path['start_dir']):
        api.step('start_dir ignored', ['bash', '-c', 'echo what happen'])

    # You can also manipulate various aspects of the step, such as env.
    # These are passed straight through to subprocess.Popen.
    # Also, abusing bash -c in this way is a TERRIBLE IDEA DON'T DO IT.
    with api.context(env={'friend': 'Darth Vader'}):
        api.step('goodbye', ['bash', '-c', 'echo Good bye, $friend.'])

    # You can modify environment in terms of old environment. Environment
    # variables are substituted in for expressions of the form %(VARNAME)s.
    with api.context(env={
            'PATH':
            api.path.pathsep.join([str(api.step.repo_resource()), '%(PATH)s'])
    }):
        api.step('recipes help', ['recipes.py', '--help'])

    # Finally, you can make your step accept any return code
    api.step('anything is cool', ['bash', '-c', 'exit 3'], ok_ret='any')

    # We can manipulate the step presentation arbitrarily until we run
    # the next step.
    step_result = api.step('hello', ['echo', 'hello'])
    step_result.presentation.status = api.step.EXCEPTION
    step_result.presentation.logs['the reason'] = ['The reason\nit failed']

    # Without a command, a step can be used to present some data from the recipe.
    step_result = api.step('Just print stuff', cmd=None)
    step_result.presentation.logs['more'] = ['More stuff']

    try:
        api.step('goodbye', ['echo', 'goodbye'])
        # Modifying step_result now would raise an AssertionError.
    except api.step.StepFailure:
        # Raising anything besides StepFailure or StepWarning causes the build to go
        # purple.
        raise ValueError('goodbye must exit 0!')

    try:
        api.step('warning', ['echo', 'warning'])
    except api.step.StepFailure as e:
        e.result.presentation.status = api.step.WARNING
        raise api.step.StepWarning(e.message)

    # Aggregate failures from tests!
    try:
        with recipe_api.defer_results():
            api.step('testa', ['echo', 'testa'])
            api.step('testb', ['echo', 'testb'], infra_step=True)
    except recipe_api.AggregatedStepFailure as f:
        # You can raise aggregated step failures.
        raise f

    # Some steps are needed from an infrastructure point of view. If these
    # steps fail, the build stops, but doesn't get turned red because it's
    # not the developers' fault.
    try:
        api.step('cleanup', ['echo', 'cleaning', 'up', 'build'],
                 infra_step=True)
    except api.step.InfraFailure as f:
        assert f.result.presentation.status == api.step.EXCEPTION

    # Run a step through a made-up wrapper program.
    api.step('application', ['echo', 'main', 'application'],
             wrapper=['python', '-c', 'import sys; print sys.argv'])

    if access_invalid_data:
        result = api.step('no-op', ['echo', 'I', 'do', 'nothing'])
        # Trying to access non-existent attributes on the result should raise.
        _ = result.json.output

    if access_deep_invalid_data:
        result = api.step('no-op', ['echo', api.json.output()])
        # Trying to access deep, non-existent attributes on the result should raise.
        _ = result.json.outpurt

    if assign_extra_junk:
        result = api.step('no-op', ['echo', 'I', 'do', 'nothing'])
        # Assigning extra junk to the result raises ValueError.
        result.json = "hi"
Beispiel #9
0
def RunSteps(api):
    # TODO(martinis) change this
    # The api.step object is directly callable.
    api.step("hello", ["echo", "Hello World"])
    api.step("hello", ["echo", "Why hello, there."])

    # You can also manipulate various aspects of the step, such as env.
    # These are passed straight through to subprocess.Popen.
    # Also, abusing bash -c in this way is a TERRIBLE IDEA DON'T DO IT.
    api.step("goodbye", ["bash", "-c", "echo Good bye, $friend."], env={"friend": "Darth Vader"})

    # Finally, you can make your step accept any return code
    api.step("anything is cool", ["bash", "-c", "exit 3"], ok_ret="any")

    # We can manipulate the step presentation arbitrarily until we run
    # the next step.
    step_result = api.step("hello", ["echo", "hello"])
    step_result.presentation.status = api.step.EXCEPTION
    step_result.presentation.logs["the reason"] = ["The reason\nit failed"]

    # Without a command, a step can be used to present some data from the recipe.
    step_result = api.step("Just print stuff", cmd=None)
    step_result.presentation.logs["more"] = ["More stuff"]

    try:
        api.step("goodbye", ["echo", "goodbye"])
        # Modifying step_result now would raise an AssertionError.
    except api.step.StepFailure:
        # Raising anything besides StepFailure or StepWarning causes the build to go
        # purple.
        raise ValueError("goodbye must exit 0!")

    try:
        api.step("warning", ["echo", "warning"])
    except api.step.StepFailure as e:
        e.result.presentation.status = api.step.WARNING
        raise api.step.StepWarning(e.message)

    # Aggregate failures from tests!
    try:
        with recipe_api.defer_results():
            api.step("testa", ["echo", "testa"])
            api.step("testb", ["echo", "testb"])
    except recipe_api.AggregatedStepFailure as f:
        raise api.step.StepFailure("You can catch step failures.")

    # Some steps are needed from an infrastructure point of view. If these
    # steps fail, the build stops, but doesn't get turned red because it's
    # not the developers' fault.
    try:
        api.step("cleanup", ["echo", "cleaning", "up", "build"], infra_step=True)
    except api.step.InfraFailure as f:
        assert f.result.presentation.status == api.step.EXCEPTION

    # Run a step through a made-up wrapper program.
    api.step("application", ["echo", "main", "application"], wrapper=["python", "-c", "import sys; print sys.argv"])

    if api.properties.get("access_invalid_data"):
        result = api.step("no-op", ["echo", "I", "do", "nothing"])
        # Trying to access non-existent attributes on the result should raise.
        _ = result.json.output
def RunSteps(api, properties, env_properties):
    """Steps to checkout flutter engine and execute web test shard.

  The test shard to run will be determined by `command_args` send as part of
  properties.
  """
    cache_root = api.path['cleanup'].join('builder')
    checkout = GetCheckoutPath(api)
    platform = api.platform.name.capitalize()
    if properties.clobber:
        api.file.rmtree('Clobber cache', cache_root)
    api.file.rmtree('Clobber build output: %s' % platform,
                    checkout.join('out'))

    api.file.ensure_directory('Ensure checkout cache', cache_root)
    api.goma.ensure()
    env = {}
    env_prefixes = {}

    # Checkout source code and build
    api.repo_util.engine_checkout(cache_root, env, env_prefixes)

    # Prepare the dependencies that web tests need.
    # These can be browsers, web drivers or other repositories.
    api.web_util.prepare_dependencies(checkout)

    with api.context(cwd=cache_root, env=env,
                     env_prefixes=env_prefixes), api.depot_tools.on_path():

        target_name = 'host_debug_unopt'

        # Load local engine information if available.
        api.flutter_deps.flutter_engine(env, env_prefixes)

        android_home = checkout.join('third_party', 'android_tools', 'sdk')
        env['GOMA_DIR'] = api.goma.goma_dir
        env['ANDROID_HOME'] = str(android_home)
        env['CHROME_NO_SANDBOX'] = 'true'
        env['ENGINE_PATH'] = cache_root
        # flutter_engine deps adds dart dependency as out/host_debug_unopt/dart-sdk
        # We are changing it with src/third_party/dart/tools/sdks/dart-sdk
        dart_bin = checkout.join('third_party', 'dart', 'tools', 'sdks',
                                 'dart-sdk', 'bin')
        paths = env_prefixes.get('PATH', [])
        paths.insert(0, dart_bin)
        env_prefixes['PATH'] = paths

        command_args = api.properties.get('command_args', ['test'])
        command_name = api.properties.get('command_name', 'test')
        felt_cmd = [
            checkout.join('out', target_name, 'dart-sdk', 'bin', 'dart'),
            'dev/felt.dart'
        ]
        felt_cmd.extend(command_args)

        with api.context(cwd=cache_root, env=env,
                         env_prefixes=env_prefixes), api.depot_tools.on_path():
            # Update dart packages and run tests.
            local_engine_path = env.get('LOCAL_ENGINE')
            local_pub = local_engine_path.join('dart-sdk', 'bin', 'pub')
            with api.context(cwd=checkout.join('flutter', 'web_sdk',
                                               'web_engine_tester')):
                api.step('pub get in web_engine_tester', [local_pub, 'get'])
            with api.context(cwd=checkout.join('flutter', 'lib', 'web_ui')):
                api.step('pub get in web_ui', [local_pub, 'get'])
                if api.platform.is_mac:
                    with api.osx_sdk('ios'):
                        with recipe_api.defer_results():
                            api.step('felt test: %s' % command_name, felt_cmd)
                            if api.properties.get(
                                    'dependencies'
                            ) and 'goldens_repo' in api.properties.get(
                                    'dependencies'):
                                api.web_util.upload_failing_goldens(
                                    checkout, 'ios-safari')
                            # This is to clean up leaked processes.
                            api.os_utils.kill_processes()
                            # Collect memory/cpu/process after task execution.
                            api.os_utils.collect_os_info()
                else:
                    with recipe_api.defer_results():
                        api.step('felt test: %s' % command_name, felt_cmd)
                        if api.properties.get(
                                'dependencies'
                        ) and 'goldens_repo' in api.properties.get(
                                'dependencies'):
                            api.web_util.upload_failing_goldens(
                                checkout, 'chrome')
                        # This is to clean up leaked processes.
                        api.os_utils.kill_processes()
                        # Collect memory/cpu/process after task execution.
                        api.os_utils.collect_os_info()