| """Build step class for executing unit tests.""" |
| |
| from __future__ import absolute_import |
| import logging |
| import os |
| |
| from helpers import git_utils |
| from slave import base_step |
| from slave.step import shell_step |
| |
| class Error(RuntimeError): |
| """Error for this module.""" |
| |
| |
| class FuchsiaUnitTestStep(base_step.BaseStep): |
| """Build step class to run runner scripts as unit tests.""" |
| |
| def __init__(self, out_dir, fuchsia_workdir, recipe_steps, |
| name='fuchsia_unit_test', **kwargs): |
| base_step.BaseStep.__init__(self, name=name, **kwargs) |
| self._changed_files = None |
| self._fuchsia_workdir = fuchsia_workdir |
| self._out_dir = out_dir |
| self._step_kwargs = kwargs |
| self._recipe_steps = recipe_steps |
| |
| def _current_step_index(self): |
| """Returns first index of this class in _recipe_steps.""" |
| for step in self._recipe_steps: |
| # Assume there is only one instanceof this class. |
| if isinstance(step, FuchsiaUnitTestStep): |
| return self._recipe_steps.index(step) |
| return -1 |
| |
| def _gunit_retry_if(self, returncode, stdout, stderr): |
| """Returns whether or not the chromium unit test should be retried. |
| |
| See more: |
| //chromium/src/build/fuchsia/runner_exceptions.py |
| """ |
| # Test runner exits between [60, 80] for infra related flakes. |
| if 60 < returncode < 80: |
| return True |
| # Not all exceptions are caught by the test runner, and some require |
| # extra checks |
| reasons = [ |
| # Package server failed to transfer blobs in time for test |
| "Error while resolving", |
| # Long running tests may experience the ssh connection dropping |
| "Timeout when executing", |
| ] |
| for retry_reason in reasons: |
| if retry_reason in stdout: |
| return True |
| return False |
| |
| |
| def add_unittest_step(self, suite_name, runner_cmd, fail_fast=False): |
| # 15 mins max timeout per test suite. Some tests like net_unittest |
| # take more than 5 mins to run. |
| max_timeout_secs = 15 * 60 |
| # This step name makes it easy to filter Fuchsia test flakiness in the |
| # CQ build reliability dashboard |
| step_name = "Fuchsia Unittest %s" % suite_name |
| step = shell_step.ShellStep( |
| command=runner_cmd, |
| num_retries=2, |
| retry_if=self._gunit_retry_if, |
| directory=os.getcwd(), |
| halt_on_failure=fail_fast, |
| name=step_name, |
| timeout_secs=max_timeout_secs, |
| **self._step_kwargs) |
| # This step should be added after the current fuchsia unittest step. |
| # However, it is created during the recipe run, which is non-trivial. |
| current_step_index = self._current_step_index() |
| if current_step_index == -1: |
| raise Error('Could not find FuchsiaUnitTestStep class to append to') |
| self._recipe_steps.insert(current_step_index + 1, step) |
| |
| def read_file(self, file_path): # pylint: disable=method-hidden |
| """Reads a file. |
| |
| Args: |
| file_path: A string with the path to the file open. |
| |
| Returns: |
| A list of strings with the file content. |
| """ |
| content_list = [] |
| with open(file_path) as f: |
| content_list = f.readlines() |
| return [line.strip() for line in content_list] |
| |
| def run(self): |
| test_list_path = os.path.join(os.getcwd(), self._out_dir, 'tests', |
| 'fuchsia_run_test_list.txt') |
| runner_folder = os.path.join(os.getcwd(), self._out_dir, 'bin') |
| test_list = self.read_file(test_list_path) |
| for suite_cmd in test_list: |
| suite_cmd_parts = suite_cmd.split() |
| suite_name = suite_cmd_parts[0] |
| |
| runner_script = 'run_{}'.format(suite_name) |
| fuchsia_out_dir = os.path.join(self._fuchsia_workdir, 'packages') |
| logs_dir = os.path.join(self.get_gcs_dir(), suite_name) |
| os.makedirs(logs_dir, exist_ok=True) |
| |
| runner_cmd = [ |
| os.path.join(runner_folder, runner_script), |
| '--logs-dir', logs_dir, |
| '--device', 'device', |
| '--fuchsia-out-dir', fuchsia_out_dir |
| ] + suite_cmd_parts[1:] # Append gtest filters to the command |
| |
| self.add_unittest_step(suite_name, runner_cmd, fail_fast=False) |
| return True |