1# Copyright 2018, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Robolectric test runner class. 17 18This test runner will be short lived, once robolectric support v2 is in, then 19robolectric tests will be invoked through AtestTFTestRunner. 20""" 21 22import json 23import logging 24import os 25import re 26import tempfile 27import time 28 29from functools import partial 30 31# pylint: disable=import-error 32import atest_utils 33import constants 34 35from event_handler import EventHandler 36from test_runners import test_runner_base 37 38POLL_FREQ_SECS = 0.1 39# A pattern to match event like below 40#TEST_FAILED {'className':'SomeClass', 'testName':'SomeTestName', 41# 'trace':'{"trace":"AssertionError: <true> is equal to <false>\n 42# at FailureStrategy.fail(FailureStrategy.java:24)\n 43# at FailureStrategy.fail(FailureStrategy.java:20)\n"}\n\n 44EVENT_RE = re.compile(r'^(?P<event_name>[A-Z_]+) (?P<json_data>{(.\r*|\n)*})(?:\n|$)') 45 46 47class RobolectricTestRunner(test_runner_base.TestRunnerBase): 48 """Robolectric Test Runner class.""" 49 NAME = 'RobolectricTestRunner' 50 # We don't actually use EXECUTABLE because we're going to use 51 # atest_utils.build to kick off the test but if we don't set it, the base 52 # class will raise an exception. 53 EXECUTABLE = 'make' 54 55 # pylint: disable=useless-super-delegation 56 def __init__(self, results_dir, **kwargs): 57 """Init stuff for robolectric runner class.""" 58 super(RobolectricTestRunner, self).__init__(results_dir, **kwargs) 59 self.is_verbose = logging.getLogger().isEnabledFor(logging.DEBUG) 60 61 def run_tests(self, test_infos, extra_args, reporter): 62 """Run the list of test_infos. See base class for more. 63 64 Args: 65 test_infos: A list of TestInfos. 66 extra_args: Dict of extra args to add to test run. 67 reporter: An instance of result_report.ResultReporter. 68 69 Returns: 70 0 if tests succeed, non-zero otherwise. 71 """ 72 if os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR): 73 return self.run_tests_raw(test_infos, extra_args, reporter) 74 return self.run_tests_pretty(test_infos, extra_args, reporter) 75 76 def run_tests_raw(self, test_infos, extra_args, reporter): 77 """Run the list of test_infos with raw output. 78 79 Args: 80 test_infos: List of TestInfo. 81 extra_args: Dict of extra args to add to test run. 82 reporter: A ResultReporter Instance. 83 84 Returns: 85 0 if tests succeed, non-zero otherwise. 86 """ 87 reporter.register_unsupported_runner(self.NAME) 88 ret_code = constants.EXIT_CODE_SUCCESS 89 for test_info in test_infos: 90 full_env_vars = self._get_full_build_environ(test_info, 91 extra_args) 92 run_cmd = self.generate_run_commands([test_info], extra_args)[0] 93 subproc = self.run(run_cmd, 94 output_to_stdout=self.is_verbose, 95 env_vars=full_env_vars) 96 ret_code |= self.wait_for_subprocess(subproc) 97 return ret_code 98 99 def run_tests_pretty(self, test_infos, extra_args, reporter): 100 """Run the list of test_infos with pretty output mode. 101 102 Args: 103 test_infos: List of TestInfo. 104 extra_args: Dict of extra args to add to test run. 105 reporter: A ResultReporter Instance. 106 107 Returns: 108 0 if tests succeed, non-zero otherwise. 109 """ 110 ret_code = constants.EXIT_CODE_SUCCESS 111 for test_info in test_infos: 112 # Create a temp communication file. 113 with tempfile.NamedTemporaryFile(mode='w+r', 114 dir=self.results_dir) as event_file: 115 # Prepare build environment parameter. 116 full_env_vars = self._get_full_build_environ(test_info, 117 extra_args, 118 event_file) 119 run_cmd = self.generate_run_commands([test_info], extra_args)[0] 120 subproc = self.run(run_cmd, 121 output_to_stdout=self.is_verbose, 122 env_vars=full_env_vars) 123 event_handler = EventHandler(reporter, self.NAME) 124 # Start polling. 125 self.handle_subprocess(subproc, partial(self._exec_with_robo_polling, 126 event_file, 127 subproc, 128 event_handler)) 129 ret_code |= self.wait_for_subprocess(subproc) 130 return ret_code 131 132 def _get_full_build_environ(self, test_info=None, extra_args=None, event_file=None): 133 """Helper to get full build environment. 134 135 Args: 136 test_info: TestInfo object. 137 extra_args: Dict of extra args to add to test run. 138 event_file: A file-like object that can be used as a temporary storage area. 139 """ 140 full_env_vars = os.environ.copy() 141 env_vars = self.generate_env_vars(test_info, 142 extra_args, 143 event_file) 144 full_env_vars.update(env_vars) 145 return full_env_vars 146 147 def _exec_with_robo_polling(self, communication_file, robo_proc, event_handler): 148 """Polling data from communication file 149 150 Polling data from communication file. Exit when communication file 151 is empty and subprocess ended. 152 153 Args: 154 communication_file: A monitored communication file. 155 robo_proc: The build process. 156 event_handler: A file-like object storing the events of robolectric tests. 157 """ 158 buf = '' 159 while True: 160 data = communication_file.read() 161 buf += data 162 reg = re.compile(r'(.|\n)*}\n\n') 163 if not reg.match(buf) or data == '': 164 if robo_proc.poll() is not None: 165 logging.debug('Build process exited early') 166 return 167 time.sleep(POLL_FREQ_SECS) 168 else: 169 # Read all new data and handle it at one time. 170 for event in re.split(r'\n\n', buf): 171 match = EVENT_RE.match(event) 172 if match: 173 try: 174 event_data = json.loads(match.group('json_data'), 175 strict=False) 176 except ValueError: 177 # Parse event fail, continue to parse next one. 178 logging.debug('"%s" is not valid json format.', 179 match.group('json_data')) 180 continue 181 event_name = match.group('event_name') 182 event_handler.process_event(event_name, event_data) 183 buf = '' 184 185 @staticmethod 186 def generate_env_vars(test_info, extra_args, event_file=None): 187 """Turn the args into env vars. 188 189 Robolectric tests specify args through env vars, so look for class 190 filters and debug args to apply to the env. 191 192 Args: 193 test_info: TestInfo class that holds the class filter info. 194 extra_args: Dict of extra args to apply for test run. 195 event_file: A file-like object storing the events of robolectric tests. 196 197 Returns: 198 Dict of env vars to pass into invocation. 199 """ 200 env_var = {} 201 for arg in extra_args: 202 if constants.WAIT_FOR_DEBUGGER == arg: 203 env_var['DEBUG_ROBOLECTRIC'] = 'true' 204 continue 205 filters = test_info.data.get(constants.TI_FILTER) 206 if filters: 207 robo_filter = next(iter(filters)) 208 env_var['ROBOTEST_FILTER'] = robo_filter.class_name 209 if robo_filter.methods: 210 logging.debug('method filtering not supported for robolectric ' 211 'tests yet.') 212 if event_file: 213 env_var['EVENT_FILE_ROBOLECTRIC'] = event_file.name 214 return env_var 215 216 def host_env_check(self): 217 """Check that host env has everything we need. 218 219 We actually can assume the host env is fine because we have the same 220 requirements that atest has. Update this to check for android env vars 221 if that changes. 222 """ 223 pass 224 225 def get_test_runner_build_reqs(self): 226 """Return the build requirements. 227 228 Returns: 229 Set of build targets. 230 """ 231 return set() 232 233 # pylint: disable=unused-argument 234 def generate_run_commands(self, test_infos, extra_args, port=None): 235 """Generate a list of run commands from TestInfos. 236 237 Args: 238 test_infos: A set of TestInfo instances. 239 extra_args: A Dict of extra args to append. 240 port: Optional. An int of the port number to send events to. 241 Subprocess reporter in TF won't try to connect if it's None. 242 243 Returns: 244 A list of run commands to run the tests. 245 """ 246 run_cmds = [] 247 for test_info in test_infos: 248 robo_command = atest_utils.BUILD_CMD + [str(test_info.test_name)] 249 run_cmd = ' '.join(x for x in robo_command) 250 if constants.DRY_RUN in extra_args: 251 run_cmd = run_cmd.replace( 252 os.environ.get(constants.ANDROID_BUILD_TOP) + os.sep, '') 253 run_cmds.append(run_cmd) 254 return run_cmds 255