| # Copyright 2021 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Runner class for variations smoke tests.""" |
| |
| from datetime import datetime |
| import json |
| import logging |
| import os |
| import subprocess |
| import sys |
| |
| import iossim_util |
| import test_apps |
| import test_runner |
| from test_result_util import ResultCollection, TestResult, TestStatus |
| from xcodebuild_runner import SimulatorParallelTestRunner |
| from xcode_log_parser import XcodeLogParser |
| |
| _THIS_DIR = os.path.dirname(os.path.abspath(__file__)) |
| _SRC_DIR = os.path.join(_THIS_DIR, os.path.pardir, os.path.pardir, |
| os.path.pardir, os.path.pardir) |
| _VARIATIONS_SMOKE_TEST_DIR = os.path.join(_SRC_DIR, 'testing', 'scripts') |
| sys.path.insert(0, _VARIATIONS_SMOKE_TEST_DIR) |
| |
| import variations_seed_access_helper as seed_helper |
| |
| |
| # Constants around the variation keys. |
| _LOCAL_STATE_VARIATIONS_LAST_FETCH_TIME_KEY = 'variations_last_fetch_time' |
| # Test argument to make EG2 test verify the fetch happens in current app launch. |
| _VERIFY_FETCHED_IN_CURRENT_LAUNCH_ARG = '--verify-fetched-in-current-launch' |
| LOGGER = logging.getLogger(__name__) |
| # Test arguments to make EG2 test load the seed instead of fetching a new one. |
| _SEED_ARG = '--seed={}' |
| _SIGNATURE_ARG = '--signature={}' |
| # Test arguments to make EG2 test assign the app to a variations channel. |
| _VARIATIONS_CHANNEL_ARG = '--variations-channel={}' |
| |
| |
| def _load_crashing_seed(): |
| """Reads and parses the test variations seed.""" |
| seed_path = os.path.join(_THIS_DIR, 'test_data', 'crash_seed.json') |
| with open(seed_path, 'r') as f: |
| seed_json = json.load(f) |
| return (seed_json['variations_compressed_seed'], |
| seed_json['variations_seed_signature']) |
| |
| |
| class SeedData: |
| """Class to store the Seed Data for the tests.""" |
| |
| def __init__(self, seed, signature): |
| self.seed = seed |
| self.signature = signature |
| |
| def as_test_arguments(self): |
| return [ |
| _SEED_ARG.format(self.seed), |
| _SIGNATURE_ARG.format(self.signature), |
| ] |
| |
| |
| class VariationsSimulatorParallelTestRunner(SimulatorParallelTestRunner): |
| """Variations simulator runner.""" |
| |
| def __init__(self, app_path, host_app_path, iossim_path, version, platform, |
| out_dir, variations_seed_path, **kwargs): |
| super(VariationsSimulatorParallelTestRunner, |
| self).__init__(app_path, host_app_path, iossim_path, version, |
| platform, out_dir, **kwargs) |
| self.variations_seed_path = variations_seed_path |
| self.host_app_bundle_id = test_apps.get_bundle_id(self.host_app_path) |
| self.test_app = self.get_launch_test_app() |
| |
| def _user_data_dir(self): |
| """Returns path to user data dir containing "Local State" file. |
| |
| Note: The path is under app data directory of host Chrome app under test. |
| The path changes each time launching app but the content is consistent. |
| """ |
| # This is required for next cmd to work. |
| iossim_util.boot_simulator_if_not_booted(self.udid) |
| app_data_path = iossim_util.get_app_data_directory(self.host_app_bundle_id, |
| self.udid) |
| return os.path.join(app_data_path, 'Library', 'Application Support', |
| 'Google', 'Chrome') |
| |
| def _reset_last_fetch_time(self): |
| """Resets last fetch time to one day before so the next fetch can happen. |
| |
| On mobile devices the fetch will only happen 30 min after last fetch by |
| checking |variations_last_fetch_time| key in Local State. |
| """ |
| # Last fetch time in local state uses win timestamp in microseconds. |
| win_delta = datetime.utcnow() - datetime(1601, 1, 1) |
| win_now = int(win_delta.total_seconds()) |
| win_one_day_before = win_now - 60 * 60 * 24 |
| win_one_day_before_microseconds = win_one_day_before * 1000000 |
| |
| seed_helper.update_local_state( |
| self._user_data_dir(), { |
| _LOCAL_STATE_VARIATIONS_LAST_FETCH_TIME_KEY: |
| str(win_one_day_before_microseconds) |
| }) |
| LOGGER.info('Reset last fetch time to %s in Local State.' % |
| win_one_day_before_microseconds) |
| |
| def _launch_app_once(self, |
| out_sub_dir, |
| verify_fetched_within_launch=False, |
| seed_data=None, |
| variations_channel=None): |
| """Launches app once. |
| |
| Args: |
| out_sub_dir: (str) Sub dir under |self.out_dir| for this attempt output. |
| verify_fetched_within_launch: (bool) Whether to verify that the fetch |
| would happens in current launch. |
| seed_data: (None or SeedData) instance to pass as args to the test. |
| variations_channel: (None or str) variations channel to pass as args to |
| the test. |
| |
| Returns: |
| (test_result_util.ResultCollection): Raw EG test result of the launch. |
| """ |
| launch_out_dir = os.path.join(self.out_dir, out_sub_dir) |
| |
| # Args that will be passed to the test and cleared after its execution. |
| current_launch_args = [] |
| |
| if verify_fetched_within_launch: |
| current_launch_args.append(_VERIFY_FETCHED_IN_CURRENT_LAUNCH_ARG) |
| |
| if seed_data: |
| current_launch_args.extend(seed_data.as_test_arguments()) |
| |
| if variations_channel: |
| current_launch_args.append( |
| _VARIATIONS_CHANNEL_ARG.format(variations_channel)) |
| |
| self.test_app.test_args.extend(current_launch_args) |
| |
| cmd = self.test_app.command(launch_out_dir, 'id=%s' % self.udid, 1) |
| proc = subprocess.Popen( |
| cmd, |
| env=self.env_vars or {}, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| ) |
| output = test_runner.print_process_output(proc, self.readline_timeout) |
| |
| # Clear the current launch args for future launches |
| for arg in current_launch_args: |
| if arg in self.test_app.test_args: |
| self.test_app.test_args.remove(arg) |
| |
| return XcodeLogParser.collect_test_results(launch_out_dir, output) |
| |
| def _launch_variations_smoke_test(self): |
| """Runs variations smoke test logic which involves multiple test launches. |
| |
| Returns: |
| Tuple of (bool, str) Success status and reason. |
| """ |
| # Launch app to make it fetch seed from server. |
| fetch_launch_result = self._launch_app_once( |
| 'fetch_launch', verify_fetched_within_launch=True) |
| if not fetch_launch_result.passed_tests(): |
| log = 'Test failure at app launch to fetch variations seed.' |
| LOGGER.error(log) |
| return False, log |
| |
| # Launch app with a valid seed. |
| seed, signature = seed_helper.load_test_seed_from_file( |
| self.variations_seed_path) |
| injected_launch_result = self._launch_app_once( |
| 'injected_launch', seed_data=SeedData(seed, signature)) |
| if not injected_launch_result.passed_tests(): |
| log = 'Test failure at app launch after the seed is injected.' |
| LOGGER.error(log) |
| return False, log |
| |
| # Launch app with a crashing seed. The app should crash on launch. |
| # We need to assign a channel so that the app enables the crashing study. |
| crashing_seed, crashing_signature = _load_crashing_seed() |
| crashing_launch_result = self._launch_app_once( |
| 'crashing_launch', |
| seed_data=SeedData(crashing_seed, crashing_signature), |
| variations_channel='dev') |
| if crashing_launch_result.passed_tests(): |
| log = "Expected test to crash, but app didn't crash" |
| LOGGER.error(log) |
| return False, log |
| |
| return True, 'Variations smoke test passed all steps!' |
| |
| def launch(self): |
| """Entrance to launch tests in this runner.""" |
| success, log = self._launch_variations_smoke_test() |
| |
| test_status = TestStatus.PASS if success else TestStatus.FAIL |
| # Report a single test named |Variations.SmokeTest| as part of runner |
| # output. |
| overall_result = ResultCollection(test_results=[ |
| TestResult('Variations.SmokeTest', test_status, test_log=log) |
| ]) |
| overall_result.report_to_result_sink() |
| self.test_results = overall_result.standard_json_output(path_delimiter='/') |
| self.logs.update(overall_result.test_runner_logs()) |
| self.tear_down() |
| |
| return success |