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