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

#!/usr/bin/env vpython3
# Copyright 2018 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 xcodebuild_runner.py."""

import logging
import mock
import os
import unittest
import sys

import mac_util
import iossim_util
import result_sink_util
import test_apps
from test_result_util import ResultCollection, TestResult, TestStatus
import test_runner
import test_runner_test
import xcode_log_parser
import xcode_util
import xcodebuild_runner

# if the current directory is in scripts, then we need to add plugin
# path in order to import from that directory
if os.path.split(os.path.dirname(__file__))[1] != 'plugin':
  sys.path.append(
      os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugin'))
import test_plugin_service

_ROOT_FOLDER_PATH = 'root/folder'
_XCODE_BUILD_VERSION = '10B61'
_DESTINATION = 'A4E66321-177A-450A-9BA1-488D85B7278E'
_OUT_DIR = 'out/dir'
_XTEST_RUN = '/tmp/temp_file.xctestrun'
_EGTESTS_APP_PATH = '%s/any_egtests.app' % _ROOT_FOLDER_PATH
_ALL_EG_TEST_NAMES = [('Class1', 'passedTest1'), ('Class1', 'passedTest2')]
_FLAKY_EGTEST_APP_PATH = 'path/to/ios_chrome_flaky_eg2test_module.app'

_ENUMERATE_TESTS_OUTPUT = """
{
  "errors" : [

  ],
  "values" : [
    {
      "children" : [
        {
          "children" : [
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "BaseEarlGreyTestCase"
            },
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "ChromeTestCase"
            },
            {
              "children" : [
                {
                  "children" : [

                  ],
                  "disabled" : false,
                  "kind" : "test",
                  "name" : "passedTest1"
                },
                {
                  "children" : [

                  ],
                  "disabled" : false,
                  "kind" : "test",
                  "name" : "passedTest2"
                }
              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "Class1"
            },
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "WebHttpServerChromeTestCase"
            }
          ],
          "disabled" : false,
          "kind" : "target",
          "name" : "ios_chrome_ui_eg2tests_module-Runner_module"
        }
      ],
      "disabled" : false,
      "kind" : "plan",
      "name" : ""
    }
  ]
}
"""

_ENUMERATE_DISABLED_TESTS_OUTPUT = """
{
  "errors" : [

  ],
  "values" : [
    {
      "children" : [
        {
          "children" : [
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "BaseEarlGreyTestCase"
            },
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "ChromeTestCase"
            },
            {
              "children" : [
                {
                  "children" : [

                  ],
                  "disabled" : false,
                  "kind" : "test",
                  "name" : "disabled_test3"
                }
              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "Class2"
            },
            {
              "children" : [

              ],
              "disabled" : false,
              "kind" : "class",
              "name" : "WebHttpServerChromeTestCase"
            }
          ],
          "disabled" : false,
          "kind" : "target",
          "name" : "ios_chrome_ui_eg2tests_module-Runner_module"
        }
      ],
      "disabled" : false,
      "kind" : "plan",
      "name" : ""
    }
  ]
}
"""

class XCodebuildRunnerTest(test_runner_test.TestCase):
  """Test case to test xcodebuild_runner."""

  def setUp(self):
    super(XCodebuildRunnerTest, self).setUp()
    self.mock(os.path, 'exists', lambda _: True)
    self.mock(os, 'listdir', lambda _: ['any_egtests.xctest'])
    self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False)
    self.mock(result_sink_util.ResultSinkClient,
              'post', lambda *args, **kwargs: None)
    self.mock(test_apps.EgtestsApp, 'get_all_tests',
              lambda _: ['Class1/passedTest1', 'Class1/passedTest2'])
    self.mock(test_apps.EgtestsApp, 'fill_xctest_run',
              lambda _1, _2: 'xctestrun')
    self.mock(iossim_util, 'get_simulator', lambda _1, _2: 'sim-UUID')
    self.mock(test_apps, 'get_bundle_id', lambda _: "fake-bundle-id")
    self.mock(test_apps, 'is_running_rosetta', lambda: False)
    self.mock(test_apps.plistlib, 'dump', lambda _1, _2: '')
    self.mock(test_runner.SimulatorTestRunner, 'tear_down', lambda _: None)
    self.mock(test_runner.DeviceTestRunner, 'tear_down', lambda _: None)
    self.mock(xcodebuild_runner.subprocess,
              'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
    self.mock(test_runner, 'print_process_output', lambda _, timeout: [])
    self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path')
    self.mock(os.path, 'isfile', lambda _: True)
    self.mock(xcodebuild_runner.SimulatorParallelTestRunner,
              '_create_xctest_run_enum_tests',
              lambda _, include_disabled: 'fake-path')

  def tearDown(self):
    super(XCodebuildRunnerTest, self).tearDown()

  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  def testLaunchCommand_restartCrashed1stAttempt(self, mock_collect_results):
    egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES)
    crashed_collection = ResultCollection()
    crashed_collection.crashed = True
    mock_collect_results.side_effect = [
        crashed_collection,
        ResultCollection(test_results=[
            TestResult('Class1/passedTest1', TestStatus.PASS),
            TestResult('Class1/passedTest2', TestStatus.PASS)
        ])
    ]
    launch_command = xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    overall_result = launch_command.launch()
    self.assertFalse(overall_result.crashed)
    self.assertEqual(len(overall_result.all_test_names()), 2)
    self.assertEqual(overall_result.expected_tests(),
                     set(['Class1/passedTest1', 'Class1/passedTest2']))

  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  def testLaunchCommand_notRestartPassedTest(self, mock_collect_results):
    egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES)
    collection = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    mock_collect_results.side_effect = [collection]
    launch_command = xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    launch_command.launch()
    xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    self.assertEqual(1, len(mock_collect_results.mock_calls))

  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  def test_launch_command_restart_failed_attempt(self, mock_collect_results):
    egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES)
    mock_collect_results.side_effect = [
        ResultCollection(test_results=[
            TestResult('Class1/passedTest1', TestStatus.FAIL),
            TestResult('Class1/passedTest2', TestStatus.FAIL)
        ]),
        ResultCollection(test_results=[
            TestResult('Class1/passedTest1', TestStatus.PASS),
            TestResult('Class1/passedTest2', TestStatus.PASS)
        ])
    ]
    launch_command = xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    overall_result = launch_command.launch()
    self.assertEqual(len(overall_result.all_test_names()), 2)
    self.assertEqual(overall_result.expected_tests(),
                     set(['Class1/passedTest1', 'Class1/passedTest2']))

  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  def test_launch_command_not_restart_crashed_attempt(self,
                                                      mock_collect_results):
    """Crashed first attempt of runtime select test suite won't be retried."""
    egtests = test_apps.EgtestsApp(_FLAKY_EGTEST_APP_PATH, _ALL_EG_TEST_NAMES)
    crashed_collection = ResultCollection()
    crashed_collection.crashed = True
    mock_collect_results.return_value = crashed_collection
    launch_command = xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    overall_result = launch_command.launch()
    self.assertEqual(len(overall_result.all_test_names()), 0)
    self.assertEqual(overall_result.expected_tests(), set([]))
    self.assertTrue(overall_result.crashed)

  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  def test_launch_command_reset_video_plugin_before_attempt(
      self, mock_collect_results):
    egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES)
    collection = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    mock_collect_results.side_effect = [collection]
    mock_plugin_service = mock.MagicMock()
    launch_command = xcodebuild_runner.LaunchCommand(
        egtests,
        _DESTINATION,
        clones=1,
        retries=3,
        readline_timeout=180,
        test_plugin_service=mock_plugin_service)
    launch_command.launch()
    xcodebuild_runner.LaunchCommand(
        egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180)
    self.assertEqual(1, len(mock_collect_results.mock_calls))
    mock_plugin_service.reset.assert_called_once_with()


class DeviceXcodeTestRunnerTest(test_runner_test.TestCase):
  """Test case to test xcodebuild_runner.DeviceXcodeTestRunner."""

  def setUp(self):
    super(DeviceXcodeTestRunnerTest, self).setUp()
    self.mock(os.path, 'exists', lambda _: True)
    self.mock(test_runner, 'get_current_xcode_info', lambda: {
        'version': 'test version', 'build': 'test build', 'path': 'test/path'})
    self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)

    self.mock(result_sink_util.ResultSinkClient,
              'post', lambda *args, **kwargs: None)
    self.mock(
        test_runner.subprocess,
        'check_output',
        lambda _, stderr=None: b'fake-output')
    self.mock(test_runner.subprocess, 'check_call', lambda _: b'fake-out')
    self.mock(test_runner.subprocess,
              'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
    self.mock(test_runner.TestRunner, 'set_sigterm_handler',
              lambda self, handler: 0)
    self.mock(os, 'listdir', lambda _: [])
    self.mock(xcodebuild_runner.subprocess,
              'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
    self.mock(test_runner, 'print_process_output', lambda _, timeout: [])
    self.mock(test_runner.TestRunner, 'start_proc', lambda self, cmd: 0)
    self.mock(test_runner.DeviceTestRunner, 'get_installed_packages',
              lambda self: [])
    self.mock(test_runner.DeviceTestRunner, 'wipe_derived_data', lambda _: None)
    self.mock(test_runner.TestRunner, 'retrieve_derived_data', lambda _: None)
    self.mock(test_runner.TestRunner, 'process_xcresult_dir', lambda _: None)
    self.mock(test_apps.EgtestsApp,
              'fill_xctest_run', lambda _1, _2: 'xctestrun')
    self.mock(test_apps.EgtestsApp, 'get_all_tests',
              lambda _: ['Class1/passedTest1', 'Class1/passedTest2'])
    self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False)
    self.mock(xcode_util, 'using_xcode_15_or_higher', lambda: True)
    self.mock(mac_util, 'kill_usbmuxd', lambda: None)
    self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path')
    self.mock(os.path, 'isfile', lambda _: True)
    self.mock(xcodebuild_runner.SimulatorParallelTestRunner,
              '_create_xctest_run_enum_tests',
              lambda _, include_disabled: 'fake-path')

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('platform.system', return_value='Darwin')
  def test_launch(self, _, mock_result):
    """Tests launch method in DeviceXcodeTestRunner"""
    tr = xcodebuild_runner.DeviceXcodeTestRunner("fake-app-path",
                                                 "fake-host-app-path",
                                                 "fake-out-dir")
    mock_result.return_value = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    self.assertTrue(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 2)

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('platform.system', return_value='Darwin')
  def test_unexpected_skipped_crash_reported(self, _, mock_result):
    """Tests launch method in DeviceXcodeTestRunner"""
    tr = xcodebuild_runner.DeviceXcodeTestRunner("fake-app-path",
                                                 "fake-host-app-path",
                                                 "fake-out-dir")
    crashed_collection = ResultCollection(
        test_results=[TestResult('Class1/passedTest1', TestStatus.PASS)])
    crashed_collection.crashed = True
    mock_result.return_value = crashed_collection
    self.assertFalse(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 2)
    tests = tr.test_results['tests']
    self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS')
    self.assertEqual(tests['Class1/passedTest2']['actual'], 'SKIP')
    self.assertEqual(tests['Class1/passedTest2']['expected'], 'PASS')

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('platform.system', return_value='Darwin')
  def test_unexpected_skipped_not_reported(self, _, mock_result):
    """Unexpected skip not reported for these selecting tests at runtime."""
    crashed_collection = ResultCollection(
        test_results=[TestResult('Class1/passedTest1', TestStatus.PASS)])
    crashed_collection.crashed = True
    mock_result.return_value = crashed_collection
    tr = xcodebuild_runner.DeviceXcodeTestRunner(_FLAKY_EGTEST_APP_PATH,
                                                 "fake-host-app-path",
                                                 "fake-out-dir")
    self.assertFalse(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 1)
    tests = tr.test_results['tests']
    self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS')
    # Class1/passedTest2 doesn't appear in test results.

  @mock.patch(
      'builtins.open',
      new=mock.mock_open(read_data=_ENUMERATE_DISABLED_TESTS_OUTPUT))
  @mock.patch('xcodebuild_runner.isinstance', return_value=True)
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('test_apps.EgtestsApp', autospec=True)
  @mock.patch('platform.system', return_value='Darwin')
  def test_disabled_reported(self, _, mock_test_app, mock_result, __):
    """Tests launch method in DeviceXcodeTestRunner"""
    test_app = mock_test_app.return_value
    test_app.test_app_path = _EGTESTS_APP_PATH
    test_app.get_all_tests.return_value = [
        'Class1/passedTest1', 'Class1/passedTest2'
    ]
    mock_result.return_value = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    tr = xcodebuild_runner.DeviceXcodeTestRunner(
        "fake-app-path",
        "fake-host-app-path",
        "fake-out-dir",
        output_disabled_tests=True)
    self.assertTrue(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 3)
    tests = tr.test_results['tests']
    self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS')
    self.assertEqual(tests['Class1/passedTest2']['actual'], 'PASS')
    self.assertEqual(tests['Class2/disabled_test3']['actual'], 'SKIP')
    self.assertEqual(tests['Class2/disabled_test3']['expected'], 'SKIP')

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  def test_tear_down(self):
    tr = xcodebuild_runner.DeviceXcodeTestRunner(
        "fake-app-path", "fake-host-app-path", "fake-out-dir")
    tr.tear_down()


class SimulatorParallelTestRunnerTest(test_runner_test.TestCase):
  """Test case to test xcodebuild_runner.SimulatorParallelTestRunner"""

  def setUp(self):
    super(SimulatorParallelTestRunnerTest, self).setUp()
    self.mock(iossim_util, 'get_simulator', lambda _1, _2: 'sim-UUID')

    def set_up(self):
      return

    self.mock(xcodebuild_runner.SimulatorParallelTestRunner, 'set_up', set_up)
    self.mock(os.path, 'exists', lambda _: True)
    self.mock(
        test_runner, 'get_current_xcode_info', lambda: {
            'version': 'test version',
            'build': 'test build',
            'path': 'test/path'
        })
    self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path)

    self.mock(result_sink_util.ResultSinkClient,
              'post', lambda *args, **kwargs: None)
    self.mock(
        test_runner.subprocess,
        'check_output',
        lambda _, stderr=None: b'fake-output')
    self.mock(test_runner.subprocess, 'check_call', lambda _: b'fake-out')
    self.mock(test_runner.subprocess,
              'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
    self.mock(test_runner.TestRunner,
              'set_sigterm_handler', lambda self, handler: 0)
    self.mock(os, 'listdir', lambda _: [])
    self.mock(xcodebuild_runner.subprocess,
              'Popen', lambda cmd, env, stdout, stderr: 'fake-out')
    self.mock(test_runner, 'print_process_output', lambda _, timeout: [])
    self.mock(test_runner.TestRunner, 'start_proc', lambda self, cmd: 0)
    self.mock(test_runner.TestRunner, 'retrieve_derived_data', lambda _: None)
    self.mock(test_runner.TestRunner, 'process_xcresult_dir', lambda _: None)
    self.mock(test_apps.EgtestsApp, 'fill_xctest_run',
              lambda _1, _2: 'xctestrun')
    self.mock(test_apps.EgtestsApp, 'get_all_tests',
              lambda _: ['Class1/passedTest1', 'Class1/passedTest2'])
    self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False)
    self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path')
    self.mock(os.path, 'isfile', lambda _: True)
    self.mock(xcodebuild_runner.SimulatorParallelTestRunner,
              '_create_xctest_run_enum_tests',
              lambda _, include_disabled: 'fake-path')

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('platform.system', return_value='Darwin')
  def test_launch_egtest(self, _, mock_result):
    """Tests launch method in SimulatorParallelTestRunner"""
    tr = xcodebuild_runner.SimulatorParallelTestRunner(
        "fake-app-path", "fake-host-app-path", "fake-iossim_path",
        "fake-version", "fake-platform", "fake-out-dir")
    mock_result.return_value = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    self.assertTrue(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 2)

  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT))
  @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results')
  @mock.patch('xcodebuild_runner.TestPluginServicerWrapper')
  @mock.patch('platform.system', return_value='Darwin')
  def test_launch_egtest_with_plugin_service(self, _, mock_plugin_service,
                                             mock_result):
    """ Tests launch method in SimulatorParallelTestRunner
        with plugin service running """
    tr = xcodebuild_runner.SimulatorParallelTestRunner(
        "fake-app-path",
        "fake-host-app-path",
        "fake-iossim_path",
        "fake-version",
        "fake-platform",
        "fake-out-dir",
        video_plugin_option='failed_only')
    self.assertTrue(tr.test_plugin_service != None)
    tr.test_plugin_service = mock_plugin_service
    mock_result.return_value = ResultCollection(test_results=[
        TestResult('Class1/passedTest1', TestStatus.PASS),
        TestResult('Class1/passedTest2', TestStatus.PASS)
    ])
    self.assertTrue(tr.launch())
    self.assertEqual(len(tr.test_results['tests']), 2)
    mock_plugin_service.start_server.assert_called_once_with()
    mock_plugin_service.reset.assert_called_once_with()
    mock_plugin_service.tear_down.assert_called_once_with()


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