chromium/ios/build/bots/scripts/test_runner_test.py

#!/usr/bin/env vpython3
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Unittests for test_runner.py."""

import collections
import logging
import mock
import os
import tempfile
import unittest

import iossim_util
import result_sink_util
import test_apps
from test_result_util import ResultCollection, TestResult, TestStatus
import test_runner
import xcode_util


class TestCase(unittest.TestCase):
  """Test case which supports installing mocks. Uninstalls on tear down."""

  def __init__(self, *args, **kwargs):
    """Initializes a new instance of this class."""
    super(TestCase, self).__init__(*args, **kwargs)

    # Maps object to a dict which maps names of mocked members to their
    # original values.
    self._mocks = collections.OrderedDict()

  def mock(self, obj, member, mock):
    """Installs mock in place of the named member of the given obj.

    Args:
      obj: Any object.
      member: String naming the attribute of the object to mock.
      mock: The mock to install.
    """
    self._mocks.setdefault(obj, collections.OrderedDict()).setdefault(
        member, getattr(obj, member))
    setattr(obj, member, mock)

  def unmock(self, obj, member):
    """Uninstalls the mock from the named member of given obj.

    Args:
      obj: An obj who's member has been mocked
      member: String naming the attribute of the object to unmock
    """
    if self._mocks[obj][member]:
      setattr(obj, member, self._mocks[obj][member])

  def tearDown(self, *args, **kwargs):
    """Uninstalls mocks."""
    super(TestCase, self).tearDown(*args, **kwargs)

    for obj in self._mocks:
      for member, original_value in self._mocks[obj].items():
        setattr(obj, member, original_value)


class SimulatorTestRunnerTest(TestCase):
  """Tests for test_runner.SimulatorTestRunner."""

  def setUp(self):
    super(SimulatorTestRunnerTest, self).setUp()
    self.mock(iossim_util, 'get_simulator', lambda _1, _2: 'sim-UUID')
    self.mock(result_sink_util.ResultSinkClient,
              'post', lambda *args, **kwargs: None)

    self.mock(test_runner, 'get_current_xcode_info', lambda: {
        'version': 'test version', 'build': 'test build', 'path': 'test/path'})
    self.mock(test_apps, 'get_bundle_id', lambda _: 'fake-bundle-id')
    self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path')
    self.mock(test_apps.plistlib, 'dump', lambda _1, _2: '')
    self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)
    self.mock(os.path, 'exists', lambda _: True)
    self.mock(test_runner.TestRunner, 'set_sigterm_handler',
      lambda self, handler: 0)
    self.mock(os, 'listdir', lambda _: [])
    self.mock(test_apps.GTestsApp, 'fill_xctest_run',
              lambda _, folder: '/abs/path/to/%s' % folder)

  def test_app_not_found(self):
    """Ensures AppNotFoundError is raised."""

    self.mock(os.path, 'exists', lambda p: not p.endswith('fake-app'))

    with self.assertRaises(test_runner.AppNotFoundError):
      test_runner.SimulatorTestRunner(
        'fake-app',
        'fake-iossim',
        'platform',
        'os',
        'out-dir',
      )

  def test_iossim_not_found(self):
    """Ensures SimulatorNotFoundError is raised."""
    self.mock(os.path, 'exists', lambda p: not p.endswith('fake-iossim'))

    with self.assertRaises(test_runner.SimulatorNotFoundError):
      test_runner.SimulatorTestRunner(
          'fake-app',
          'fake-iossim',
          'iPhone X',
          '11.4',
          'out-dir',
      )

  def test_init(self):
    """Ensures instance is created."""
    tr = test_runner.SimulatorTestRunner(
        'fake-app',
        'fake-iossim',
        'iPhone X',
        '11.4',
        'out-dir',
    )

    self.assertTrue(tr)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_startup_crash(self, mock_run, _1, _2):
    """Ensures test is relaunched once on startup crash."""
    result = ResultCollection()
    result.crashed = True
    mock_run.return_value = result

    tr = test_runner.SimulatorTestRunner(
        'fake-app',
        'fake-iossim',
        'iPhone X',
        '11.4',
        'out-dir',
        xctest=True,
    )
    with self.assertRaises(test_runner.AppLaunchError):
      tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 2)

  def test_relaunch(self):
    """Ensures test is relaunched on test crash until tests complete."""
    def set_up(self):
      return

    @staticmethod
    def _run(cmd, clones=None):
      if not any('retry_after_crash' in cmd_arg for cmd_arg in cmd):
        # First run, has no test filter supplied. Mock a crash.
        result = ResultCollection(
            test_results=[TestResult('crash', TestStatus.CRASH)])
        result.crashed = True
        result.add_test_result(TestResult('pass', TestStatus.PASS))
        result.add_test_result(
            TestResult('fail', TestStatus.FAIL, test_log='some logs'))
        return result
      else:
        return ResultCollection(
            test_results=[TestResult('crash', TestStatus.PASS)])

    def tear_down(self):
      return

    self.mock(test_runner.SimulatorTestRunner, 'set_up', set_up)
    self.mock(test_runner.TestRunner, '_run', _run)
    self.mock(test_runner.SimulatorTestRunner, 'tear_down', tear_down)

    tr = test_runner.SimulatorTestRunner(
        'fake-app',
        'fake-iossim',
        'iPhone X',
        '11.4',
        'out-dir',
    )
    tr.launch()
    self.assertTrue(tr.logs)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_failed_test_retry(self, mock_run, _1, _2):
    test1_fail_result = TestResult('test1', TestStatus.FAIL)
    test2_fail_result = TestResult('test2', TestStatus.FAIL)
    test1_pass_result = TestResult('test1', TestStatus.PASS)
    test2_pass_result = TestResult('test2', TestStatus.PASS)
    result1 = ResultCollection(
        test_results=[test1_fail_result, test2_fail_result])
    retry_result1 = ResultCollection(test_results=[test1_pass_result])
    retry_result2 = ResultCollection(test_results=[test2_pass_result])
    mock_run.side_effect = [result1, retry_result1, retry_result2]
    tr = test_runner.SimulatorTestRunner(
        'fake-app', 'fake-iossim', 'iPhone X', '11.4', 'out-dir', retries=3)
    tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 3)
    self.assertTrue(tr.logs)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_crashed_if_crash_in_final_crash_retry(self, mock_run, _1, _2):
    test1_crash_result = TestResult('test1', TestStatus.CRASH)
    test2_crash_result = TestResult('test2', TestStatus.CRASH)
    test3_pass_result = TestResult('test3', TestStatus.PASS)
    test1_pass_result = TestResult('test1', TestStatus.PASS)
    test2_pass_result = TestResult('test2', TestStatus.PASS)
    initial_result = ResultCollection(test_results=[test1_crash_result])
    initial_result.crashed = True
    crash_retry1_result = ResultCollection(test_results=[test2_crash_result])
    crash_retry1_result.crashed = True
    crash_retry2_result = ResultCollection(test_results=[test3_pass_result])
    crash_retry2_result.crashed = True
    test_retry1_result = ResultCollection(test_results=[test1_pass_result])
    test_retry2_result = ResultCollection(test_results=[test2_pass_result])
    mock_run.side_effect = [
        initial_result, crash_retry1_result, crash_retry2_result,
        test_retry1_result, test_retry2_result
    ]
    tr = test_runner.SimulatorTestRunner(
        'fake-app', 'fake-iossim', 'iPhone X', '11.4', 'out-dir', retries=3)
    tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 5)
    self.assertTrue(tr.test_results['interrupted'])
    self.assertIn('test suite crash', tr.logs)
    self.assertTrue(tr.logs)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_not_crashed_if_no_crash_in_final_crash_retry(self, mock_run, _1, _2):
    test1_crash_result = TestResult('test1', TestStatus.CRASH)
    test2_crash_result = TestResult('test2', TestStatus.CRASH)
    test3_pass_result = TestResult('test3', TestStatus.PASS)
    test1_pass_result = TestResult('test1', TestStatus.PASS)
    test2_pass_result = TestResult('test2', TestStatus.PASS)
    initial_result = ResultCollection(test_results=[test1_crash_result])
    initial_result.crashed = True
    crash_retry1_result = ResultCollection(test_results=[test2_crash_result])
    crash_retry1_result.crashed = True
    crash_retry2_result = ResultCollection(test_results=[test3_pass_result])
    test_retry1_result = ResultCollection(test_results=[test1_pass_result])
    test_retry2_result = ResultCollection(test_results=[test2_pass_result])
    mock_run.side_effect = [
        initial_result, crash_retry1_result, crash_retry2_result,
        test_retry1_result, test_retry2_result
    ]
    tr = test_runner.SimulatorTestRunner(
        'fake-app', 'fake-iossim', 'iPhone X', '11.4', 'out-dir', retries=3)
    tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 5)
    self.assertFalse(tr.test_results['interrupted'])
    self.assertTrue(tr.logs)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_not_crashed_if_crashed_in_failed_test_retry(self, mock_run, _1, _2):
    test1_fail_result = TestResult('test1', TestStatus.FAIL)
    initial_result = ResultCollection(test_results=[test1_fail_result])
    test1_retry1_result = ResultCollection(test_results=[test1_fail_result])
    test1_retry2_result = ResultCollection(test_results=[test1_fail_result])
    test1_retry3_result = ResultCollection()
    test1_retry3_result.crashed = True

    mock_run.side_effect = [
        initial_result, test1_retry1_result, test1_retry2_result,
        test1_retry3_result
    ]
    tr = test_runner.SimulatorTestRunner(
        'fake-app', 'fake-iossim', 'iPhone X', '11.4', 'out-dir', retries=3)
    tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 4)
    self.assertFalse(tr.test_results['interrupted'])
    self.assertEqual(tr.test_results['tests']['test1']['actual'],
                     'FAIL FAIL FAIL SKIP')
    self.assertTrue(tr.logs)

  @mock.patch('test_runner.SimulatorTestRunner.tear_down')
  @mock.patch('test_runner.SimulatorTestRunner.set_up')
  @mock.patch('test_runner.TestRunner._run')
  def test_crashed_spawning_launcher_no_retry(self, mock_run, _1, _2):
    test1_crash_result = TestResult('test1', TestStatus.CRASH)
    initial_result = ResultCollection(test_results=[test1_crash_result])
    initial_result.crashed = True
    initial_result.spawning_test_launcher = True
    mock_run.side_effect = [initial_result]
    tr = test_runner.SimulatorTestRunner(
        'fake-app', 'fake-iossim', 'iPhone X', '11.4', 'out-dir', retries=3)
    tr.launch()
    self.assertEqual(len(mock_run.mock_calls), 1)
    self.assertTrue(tr.test_results['interrupted'])
    self.assertIn('test suite crash', tr.logs)
    self.assertTrue(tr.logs)


class DeviceTestRunnerTest(TestCase):
  def setUp(self):
    super(DeviceTestRunnerTest, self).setUp()

    def install_xcode(build, mac_toolchain_cmd, xcode_app_path):
      return True

    self.mock(result_sink_util.ResultSinkClient,
              'post', lambda *args, **kwargs: None)
    self.mock(test_runner, 'get_current_xcode_info', lambda: {
        'version': 'test version', 'build': 'test build', 'path': 'test/path'})
    self.mock(test_runner, 'install_xcode', install_xcode)
    self.mock(test_runner.subprocess,
              'check_output', lambda _: b'fake-bundle-id')
    self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)
    self.mock(os.path, 'exists', lambda _: True)
    self.mock(os, 'listdir', lambda _: [])
    self.mock(tempfile, 'mkstemp', lambda: '/tmp/tmp_file')
    self.tr = test_runner.DeviceTestRunner(
        'fake-app',
        'xcode-version',
        'xcode-build',
        'out-dir',
    )
    self.tr.xctestrun_data = {'TestTargetName': {}}


if __name__ == '__main__':
  logging.basicConfig(format='[%(asctime)s:%(levelname)s] %(message)s',
    level=logging.DEBUG, datefmt='%I:%M:%S')
  unittest.main()