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