1#!/usr/bin/env python3.4 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Module containing common logic from python testing tools.""" 18 19import abc 20import os 21import signal 22import shlex 23import shutil 24import time 25 26from enum import Enum 27from enum import unique 28 29from subprocess import DEVNULL 30from subprocess import check_call 31from subprocess import PIPE 32from subprocess import Popen 33from subprocess import STDOUT 34from subprocess import TimeoutExpired 35 36from tempfile import mkdtemp 37from tempfile import NamedTemporaryFile 38 39# Temporary directory path on device. 40DEVICE_TMP_PATH = '/data/local/tmp' 41 42# Architectures supported in dalvik cache. 43DALVIK_CACHE_ARCHS = ['arm', 'arm64', 'x86', 'x86_64'] 44 45 46@unique 47class RetCode(Enum): 48 """Enum representing normalized return codes.""" 49 SUCCESS = 0 50 TIMEOUT = 1 51 ERROR = 2 52 NOTCOMPILED = 3 53 NOTRUN = 4 54 55 56@unique 57class LogSeverity(Enum): 58 VERBOSE = 0 59 DEBUG = 1 60 INFO = 2 61 WARNING = 3 62 ERROR = 4 63 FATAL = 5 64 SILENT = 6 65 66 @property 67 def symbol(self): 68 return self.name[0] 69 70 @classmethod 71 def FromSymbol(cls, s): 72 for log_severity in LogSeverity: 73 if log_severity.symbol == s: 74 return log_severity 75 raise ValueError("{0} is not a valid log severity symbol".format(s)) 76 77 def __ge__(self, other): 78 if self.__class__ is other.__class__: 79 return self.value >= other.value 80 return NotImplemented 81 82 def __gt__(self, other): 83 if self.__class__ is other.__class__: 84 return self.value > other.value 85 return NotImplemented 86 87 def __le__(self, other): 88 if self.__class__ is other.__class__: 89 return self.value <= other.value 90 return NotImplemented 91 92 def __lt__(self, other): 93 if self.__class__ is other.__class__: 94 return self.value < other.value 95 return NotImplemented 96 97 98def GetEnvVariableOrError(variable_name): 99 """Gets value of an environmental variable. 100 101 If the variable is not set raises FatalError. 102 103 Args: 104 variable_name: string, name of variable to get. 105 106 Returns: 107 string, value of requested variable. 108 109 Raises: 110 FatalError: Requested variable is not set. 111 """ 112 top = os.environ.get(variable_name) 113 if top is None: 114 raise FatalError('{0} environmental variable not set.'.format( 115 variable_name)) 116 return top 117 118 119def GetJackClassPath(): 120 """Returns Jack's classpath.""" 121 top = GetEnvVariableOrError('ANDROID_BUILD_TOP') 122 libdir = top + '/out/host/common/obj/JAVA_LIBRARIES' 123 return libdir + '/core-libart-hostdex_intermediates/classes.jack:' \ 124 + libdir + '/core-oj-hostdex_intermediates/classes.jack' 125 126 127def _DexArchCachePaths(android_data_path): 128 """Returns paths to architecture specific caches. 129 130 Args: 131 android_data_path: string, path dalvik-cache resides in. 132 133 Returns: 134 Iterable paths to architecture specific caches. 135 """ 136 return ('{0}/dalvik-cache/{1}'.format(android_data_path, arch) 137 for arch in DALVIK_CACHE_ARCHS) 138 139 140def RunCommandForOutput(cmd, env, stdout, stderr, timeout=60): 141 """Runs command piping output to files, stderr or stdout. 142 143 Args: 144 cmd: list of strings, command to run. 145 env: shell environment to run the command with. 146 stdout: file handle or one of Subprocess.PIPE, Subprocess.STDOUT, 147 Subprocess.DEVNULL, see Popen. 148 stderr: file handle or one of Subprocess.PIPE, Subprocess.STDOUT, 149 Subprocess.DEVNULL, see Popen. 150 timeout: int, timeout in seconds. 151 152 Returns: 153 tuple (string, string, RetCode) stdout output, stderr output, normalized 154 return code. 155 """ 156 proc = Popen(cmd, stdout=stdout, stderr=stderr, env=env, 157 universal_newlines=True, start_new_session=True) 158 try: 159 (output, stderr_output) = proc.communicate(timeout=timeout) 160 if proc.returncode == 0: 161 retcode = RetCode.SUCCESS 162 else: 163 retcode = RetCode.ERROR 164 except TimeoutExpired: 165 os.killpg(os.getpgid(proc.pid), signal.SIGTERM) 166 (output, stderr_output) = proc.communicate() 167 retcode = RetCode.TIMEOUT 168 return (output, stderr_output, retcode) 169 170 171def _LogCmdOutput(logfile, cmd, output, retcode): 172 """Logs output of a command. 173 174 Args: 175 logfile: file handle to logfile. 176 cmd: list of strings, command. 177 output: command output. 178 retcode: RetCode, normalized retcode. 179 """ 180 logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format( 181 CommandListToCommandString(cmd), output, retcode)) 182 183 184def RunCommand(cmd, out, err, timeout=5): 185 """Executes a command, and returns its return code. 186 187 Args: 188 cmd: list of strings, a command to execute 189 out: string, file name to open for stdout (or None) 190 err: string, file name to open for stderr (or None) 191 timeout: int, time out in seconds 192 Returns: 193 RetCode, return code of running command (forced RetCode.TIMEOUT 194 on timeout) 195 """ 196 devnull = DEVNULL 197 outf = devnull 198 if out is not None: 199 outf = open(out, mode='w') 200 errf = devnull 201 if err is not None: 202 errf = open(err, mode='w') 203 (_, _, retcode) = RunCommandForOutput(cmd, None, outf, errf, timeout) 204 if outf != devnull: 205 outf.close() 206 if errf != devnull: 207 errf.close() 208 return retcode 209 210 211def CommandListToCommandString(cmd): 212 """Converts shell command represented as list of strings to a single string. 213 214 Each element of the list is wrapped in double quotes. 215 216 Args: 217 cmd: list of strings, shell command. 218 219 Returns: 220 string, shell command. 221 """ 222 return ' '.join([shlex.quote(segment) for segment in cmd]) 223 224 225class FatalError(Exception): 226 """Fatal error in script.""" 227 228 229class ITestEnv(object): 230 """Test environment abstraction. 231 232 Provides unified interface for interacting with host and device test 233 environments. Creates a test directory and expose methods to modify test files 234 and run commands. 235 """ 236 __meta_class__ = abc.ABCMeta 237 238 @abc.abstractmethod 239 def CreateFile(self, name=None): 240 """Creates a file in test directory. 241 242 Returned path to file can be used in commands run in the environment. 243 244 Args: 245 name: string, file name. If None file is named arbitrarily. 246 247 Returns: 248 string, environment specific path to file. 249 """ 250 251 @abc.abstractmethod 252 def WriteLines(self, file_path, lines): 253 """Writes lines to a file in test directory. 254 255 If file exists it gets overwritten. If file doest not exist it is created. 256 257 Args: 258 file_path: string, environment specific path to file. 259 lines: list of strings to write. 260 """ 261 262 @abc.abstractmethod 263 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 264 """Runs command in environment. 265 266 Args: 267 cmd: list of strings, command to run. 268 log_severity: LogSeverity, minimum severity of logs included in output. 269 Returns: 270 tuple (string, int) output, return code. 271 """ 272 273 @abc.abstractproperty 274 def logfile(self): 275 """Gets file handle to logfile residing on host.""" 276 277 278class HostTestEnv(ITestEnv): 279 """Host test environment. Concrete implementation of ITestEnv. 280 281 Maintains a test directory in /tmp/. Runs commands on the host in modified 282 shell environment. Mimics art script behavior. 283 284 For methods documentation see base class. 285 """ 286 287 def __init__(self, directory_prefix, cleanup=True, logfile_path=None, 288 timeout=60, x64=False): 289 """Constructor. 290 291 Args: 292 directory_prefix: string, prefix for environment directory name. 293 cleanup: boolean, if True remove test directory in destructor. 294 logfile_path: string, can be used to specify custom logfile location. 295 timeout: int, seconds, time to wait for single test run to finish. 296 x64: boolean, whether to setup in x64 mode. 297 """ 298 self._cleanup = cleanup 299 self._timeout = timeout 300 self._env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix) 301 if logfile_path is None: 302 self._logfile = open('{0}/log'.format(self._env_path), 'w+') 303 else: 304 self._logfile = open(logfile_path, 'w+') 305 os.mkdir('{0}/dalvik-cache'.format(self._env_path)) 306 for arch_cache_path in _DexArchCachePaths(self._env_path): 307 os.mkdir(arch_cache_path) 308 lib = 'lib64' if x64 else 'lib' 309 android_root = GetEnvVariableOrError('ANDROID_HOST_OUT') 310 library_path = android_root + '/' + lib 311 path = android_root + '/bin' 312 self._shell_env = os.environ.copy() 313 self._shell_env['ANDROID_DATA'] = self._env_path 314 self._shell_env['ANDROID_ROOT'] = android_root 315 self._shell_env['LD_LIBRARY_PATH'] = library_path 316 self._shell_env['DYLD_LIBRARY_PATH'] = library_path 317 self._shell_env['PATH'] = (path + ':' + self._shell_env['PATH']) 318 # Using dlopen requires load bias on the host. 319 self._shell_env['LD_USE_LOAD_BIAS'] = '1' 320 321 def __del__(self): 322 if self._cleanup: 323 shutil.rmtree(self._env_path) 324 325 def CreateFile(self, name=None): 326 if name is None: 327 f = NamedTemporaryFile(dir=self._env_path, delete=False) 328 else: 329 f = open('{0}/{1}'.format(self._env_path, name), 'w+') 330 return f.name 331 332 def WriteLines(self, file_path, lines): 333 with open(file_path, 'w') as f: 334 f.writelines('{0}\n'.format(line) for line in lines) 335 return 336 337 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 338 self._EmptyDexCache() 339 env = self._shell_env.copy() 340 env.update({'ANDROID_LOG_TAGS':'*:' + log_severity.symbol.lower()}) 341 (output, err_output, retcode) = RunCommandForOutput( 342 cmd, env, PIPE, PIPE, self._timeout) 343 # We append err_output to output to stay consistent with DeviceTestEnv 344 # implementation. 345 output += err_output 346 _LogCmdOutput(self._logfile, cmd, output, retcode) 347 return (output, retcode) 348 349 @property 350 def logfile(self): 351 return self._logfile 352 353 def _EmptyDexCache(self): 354 """Empties dex cache. 355 356 Iterate over files in architecture specific cache directories and remove 357 them. 358 """ 359 for arch_cache_path in _DexArchCachePaths(self._env_path): 360 for file_path in os.listdir(arch_cache_path): 361 file_path = '{0}/{1}'.format(arch_cache_path, file_path) 362 if os.path.isfile(file_path): 363 os.unlink(file_path) 364 365 366class DeviceTestEnv(ITestEnv): 367 """Device test environment. Concrete implementation of ITestEnv. 368 369 For methods documentation see base class. 370 """ 371 372 def __init__(self, directory_prefix, cleanup=True, logfile_path=None, 373 timeout=60, specific_device=None): 374 """Constructor. 375 376 Args: 377 directory_prefix: string, prefix for environment directory name. 378 cleanup: boolean, if True remove test directory in destructor. 379 logfile_path: string, can be used to specify custom logfile location. 380 timeout: int, seconds, time to wait for single test run to finish. 381 specific_device: string, serial number of device to use. 382 """ 383 self._cleanup = cleanup 384 self._timeout = timeout 385 self._specific_device = specific_device 386 self._host_env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix) 387 if logfile_path is None: 388 self._logfile = open('{0}/log'.format(self._host_env_path), 'w+') 389 else: 390 self._logfile = open(logfile_path, 'w+') 391 self._device_env_path = '{0}/{1}'.format( 392 DEVICE_TMP_PATH, os.path.basename(self._host_env_path)) 393 self._shell_env = os.environ.copy() 394 395 self._AdbMkdir('{0}/dalvik-cache'.format(self._device_env_path)) 396 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 397 self._AdbMkdir(arch_cache_path) 398 399 def __del__(self): 400 if self._cleanup: 401 shutil.rmtree(self._host_env_path) 402 check_call(shlex.split( 403 'adb shell if [ -d "{0}" ]; then rm -rf "{0}"; fi' 404 .format(self._device_env_path))) 405 406 def CreateFile(self, name=None): 407 with NamedTemporaryFile(mode='w') as temp_file: 408 self._AdbPush(temp_file.name, self._device_env_path) 409 if name is None: 410 name = os.path.basename(temp_file.name) 411 return '{0}/{1}'.format(self._device_env_path, name) 412 413 def WriteLines(self, file_path, lines): 414 with NamedTemporaryFile(mode='w') as temp_file: 415 temp_file.writelines('{0}\n'.format(line) for line in lines) 416 temp_file.flush() 417 self._AdbPush(temp_file.name, file_path) 418 return 419 420 def _ExtractPid(self, brief_log_line): 421 """Extracts PID from a single logcat line in brief format.""" 422 pid_start_idx = brief_log_line.find('(') + 2 423 if pid_start_idx == -1: 424 return None 425 pid_end_idx = brief_log_line.find(')', pid_start_idx) 426 if pid_end_idx == -1: 427 return None 428 return brief_log_line[pid_start_idx:pid_end_idx] 429 430 def _ExtractSeverity(self, brief_log_line): 431 """Extracts LogSeverity from a single logcat line in brief format.""" 432 if not brief_log_line: 433 return None 434 return LogSeverity.FromSymbol(brief_log_line[0]) 435 436 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 437 self._EmptyDexCache() 438 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format( 439 self._device_env_path) 440 adb_cmd = ['adb'] 441 if self._specific_device: 442 adb_cmd += ['-s', self._specific_device] 443 logcat_cmd = adb_cmd + ['logcat', '-v', 'brief', '-s', '-b', 'main', 444 '-T', '1', 'dex2oat:*', 'dex2oatd:*'] 445 logcat_proc = Popen(logcat_cmd, stdout=PIPE, stderr=STDOUT, 446 universal_newlines=True) 447 cmd_str = CommandListToCommandString(cmd) 448 # Print PID of the shell and exec command. We later retrieve this PID and 449 # use it to filter dex2oat logs, keeping those with matching parent PID. 450 device_cmd = ('echo $$ && ' + env_vars_cmd + ' exec ' + cmd_str) 451 cmd = adb_cmd + ['shell', device_cmd] 452 (output, _, retcode) = RunCommandForOutput(cmd, self._shell_env, PIPE, 453 STDOUT, self._timeout) 454 # We need to make sure to only kill logcat once all relevant logs arrive. 455 # Sleep is used for simplicity. 456 time.sleep(0.5) 457 logcat_proc.kill() 458 end_of_first_line = output.find('\n') 459 if end_of_first_line != -1: 460 parent_pid = output[:end_of_first_line] 461 output = output[end_of_first_line + 1:] 462 logcat_output, _ = logcat_proc.communicate() 463 logcat_lines = logcat_output.splitlines(keepends=True) 464 dex2oat_pids = [] 465 for line in logcat_lines: 466 # Dex2oat was started by our runtime instance. 467 if 'Running dex2oat (parent PID = ' + parent_pid in line: 468 dex2oat_pids.append(self._ExtractPid(line)) 469 break 470 if dex2oat_pids: 471 for line in logcat_lines: 472 if (self._ExtractPid(line) in dex2oat_pids and 473 self._ExtractSeverity(line) >= log_severity): 474 output += line 475 _LogCmdOutput(self._logfile, cmd, output, retcode) 476 return (output, retcode) 477 478 @property 479 def logfile(self): 480 return self._logfile 481 482 def PushClasspath(self, classpath): 483 """Push classpath to on-device test directory. 484 485 Classpath can contain multiple colon separated file paths, each file is 486 pushed. Returns analogous classpath with paths valid on device. 487 488 Args: 489 classpath: string, classpath in format 'a/b/c:d/e/f'. 490 Returns: 491 string, classpath valid on device. 492 """ 493 paths = classpath.split(':') 494 device_paths = [] 495 for path in paths: 496 device_paths.append('{0}/{1}'.format( 497 self._device_env_path, os.path.basename(path))) 498 self._AdbPush(path, self._device_env_path) 499 return ':'.join(device_paths) 500 501 def _AdbPush(self, what, where): 502 check_call(shlex.split('adb push "{0}" "{1}"'.format(what, where)), 503 stdout=self._logfile, stderr=self._logfile) 504 505 def _AdbMkdir(self, path): 506 check_call(shlex.split('adb shell mkdir "{0}" -p'.format(path)), 507 stdout=self._logfile, stderr=self._logfile) 508 509 def _EmptyDexCache(self): 510 """Empties dex cache.""" 511 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 512 cmd = 'adb shell if [ -d "{0}" ]; then rm -f "{0}"/*; fi'.format( 513 arch_cache_path) 514 check_call(shlex.split(cmd), stdout=self._logfile, stderr=self._logfile) 515