1#!/usr/bin/env python3 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 _DexArchCachePaths(android_data_path): 120 """Returns paths to architecture specific caches. 121 122 Args: 123 android_data_path: string, path dalvik-cache resides in. 124 125 Returns: 126 Iterable paths to architecture specific caches. 127 """ 128 return ('{0}/dalvik-cache/{1}'.format(android_data_path, arch) 129 for arch in DALVIK_CACHE_ARCHS) 130 131 132def RunCommandForOutput(cmd, env, stdout, stderr, timeout=60): 133 """Runs command piping output to files, stderr or stdout. 134 135 Args: 136 cmd: list of strings, command to run. 137 env: shell environment to run the command with. 138 stdout: file handle or one of Subprocess.PIPE, Subprocess.STDOUT, 139 Subprocess.DEVNULL, see Popen. 140 stderr: file handle or one of Subprocess.PIPE, Subprocess.STDOUT, 141 Subprocess.DEVNULL, see Popen. 142 timeout: int, timeout in seconds. 143 144 Returns: 145 tuple (string, string, RetCode) stdout output, stderr output, normalized 146 return code. 147 """ 148 proc = Popen(cmd, stdout=stdout, stderr=stderr, env=env, 149 universal_newlines=True, start_new_session=True) 150 try: 151 (output, stderr_output) = proc.communicate(timeout=timeout) 152 if proc.returncode == 0: 153 retcode = RetCode.SUCCESS 154 else: 155 retcode = RetCode.ERROR 156 except TimeoutExpired: 157 os.killpg(os.getpgid(proc.pid), signal.SIGTERM) 158 (output, stderr_output) = proc.communicate() 159 retcode = RetCode.TIMEOUT 160 return (output, stderr_output, retcode) 161 162 163def _LogCmdOutput(logfile, cmd, output, retcode): 164 """Logs output of a command. 165 166 Args: 167 logfile: file handle to logfile. 168 cmd: list of strings, command. 169 output: command output. 170 retcode: RetCode, normalized retcode. 171 """ 172 logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format( 173 CommandListToCommandString(cmd), output, retcode)) 174 175 176def RunCommand(cmd, out, err, timeout=5): 177 """Executes a command, and returns its return code. 178 179 Args: 180 cmd: list of strings, a command to execute 181 out: string, file name to open for stdout (or None) 182 err: string, file name to open for stderr (or None) 183 timeout: int, time out in seconds 184 Returns: 185 RetCode, return code of running command (forced RetCode.TIMEOUT 186 on timeout) 187 """ 188 devnull = DEVNULL 189 outf = devnull 190 if out is not None: 191 outf = open(out, mode='w') 192 errf = devnull 193 if err is not None: 194 errf = open(err, mode='w') 195 (_, _, retcode) = RunCommandForOutput(cmd, None, outf, errf, timeout) 196 if outf != devnull: 197 outf.close() 198 if errf != devnull: 199 errf.close() 200 return retcode 201 202 203def CommandListToCommandString(cmd): 204 """Converts shell command represented as list of strings to a single string. 205 206 Each element of the list is wrapped in double quotes. 207 208 Args: 209 cmd: list of strings, shell command. 210 211 Returns: 212 string, shell command. 213 """ 214 return ' '.join([shlex.quote(segment) for segment in cmd]) 215 216 217class FatalError(Exception): 218 """Fatal error in script.""" 219 220 221class ITestEnv(object): 222 """Test environment abstraction. 223 224 Provides unified interface for interacting with host and device test 225 environments. Creates a test directory and expose methods to modify test files 226 and run commands. 227 """ 228 __meta_class__ = abc.ABCMeta 229 230 @abc.abstractmethod 231 def CreateFile(self, name=None): 232 """Creates a file in test directory. 233 234 Returned path to file can be used in commands run in the environment. 235 236 Args: 237 name: string, file name. If None file is named arbitrarily. 238 239 Returns: 240 string, environment specific path to file. 241 """ 242 243 @abc.abstractmethod 244 def WriteLines(self, file_path, lines): 245 """Writes lines to a file in test directory. 246 247 If file exists it gets overwritten. If file doest not exist it is created. 248 249 Args: 250 file_path: string, environment specific path to file. 251 lines: list of strings to write. 252 """ 253 254 @abc.abstractmethod 255 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 256 """Runs command in environment. 257 258 Args: 259 cmd: list of strings, command to run. 260 log_severity: LogSeverity, minimum severity of logs included in output. 261 Returns: 262 tuple (string, int) output, return code. 263 """ 264 265 @abc.abstractproperty 266 def logfile(self): 267 """Gets file handle to logfile residing on host.""" 268 269 270class HostTestEnv(ITestEnv): 271 """Host test environment. Concrete implementation of ITestEnv. 272 273 Maintains a test directory in /tmp/. Runs commands on the host in modified 274 shell environment. Mimics art script behavior. 275 276 For methods documentation see base class. 277 """ 278 279 def __init__(self, directory_prefix, cleanup=True, logfile_path=None, 280 timeout=60, x64=False): 281 """Constructor. 282 283 Args: 284 directory_prefix: string, prefix for environment directory name. 285 cleanup: boolean, if True remove test directory in destructor. 286 logfile_path: string, can be used to specify custom logfile location. 287 timeout: int, seconds, time to wait for single test run to finish. 288 x64: boolean, whether to setup in x64 mode. 289 """ 290 self._cleanup = cleanup 291 self._timeout = timeout 292 self._env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix) 293 if logfile_path is None: 294 self._logfile = open('{0}/log'.format(self._env_path), 'w+') 295 else: 296 self._logfile = open(logfile_path, 'w+') 297 os.mkdir('{0}/dalvik-cache'.format(self._env_path)) 298 for arch_cache_path in _DexArchCachePaths(self._env_path): 299 os.mkdir(arch_cache_path) 300 lib = 'lib64' if x64 else 'lib' 301 android_root = GetEnvVariableOrError('ANDROID_HOST_OUT') 302 android_i18n_root = android_root + '/com.android.i18n' 303 android_art_root = android_root + '/com.android.art' 304 android_tzdata_root = android_root + '/com.android.tzdata' 305 library_path = android_root + '/' + lib 306 path = android_root + '/bin' 307 self._shell_env = os.environ.copy() 308 self._shell_env['ANDROID_DATA'] = self._env_path 309 self._shell_env['ANDROID_ROOT'] = android_root 310 self._shell_env['ANDROID_I18N_ROOT'] = android_i18n_root 311 self._shell_env['ANDROID_ART_ROOT'] = android_art_root 312 self._shell_env['ANDROID_TZDATA_ROOT'] = android_tzdata_root 313 self._shell_env['LD_LIBRARY_PATH'] = library_path 314 self._shell_env['DYLD_LIBRARY_PATH'] = library_path 315 self._shell_env['PATH'] = (path + ':' + self._shell_env['PATH']) 316 # Using dlopen requires load bias on the host. 317 self._shell_env['LD_USE_LOAD_BIAS'] = '1' 318 319 def __del__(self): 320 if self._cleanup: 321 shutil.rmtree(self._env_path) 322 323 def CreateFile(self, name=None): 324 if name is None: 325 f = NamedTemporaryFile(dir=self._env_path, delete=False) 326 else: 327 f = open('{0}/{1}'.format(self._env_path, name), 'w+') 328 return f.name 329 330 def WriteLines(self, file_path, lines): 331 with open(file_path, 'w') as f: 332 f.writelines('{0}\n'.format(line) for line in lines) 333 return 334 335 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 336 self._EmptyDexCache() 337 env = self._shell_env.copy() 338 env.update({'ANDROID_LOG_TAGS':'*:' + log_severity.symbol.lower()}) 339 (output, err_output, retcode) = RunCommandForOutput( 340 cmd, env, PIPE, PIPE, self._timeout) 341 # We append err_output to output to stay consistent with DeviceTestEnv 342 # implementation. 343 output += err_output 344 _LogCmdOutput(self._logfile, cmd, output, retcode) 345 return (output, retcode) 346 347 @property 348 def logfile(self): 349 return self._logfile 350 351 def _EmptyDexCache(self): 352 """Empties dex cache. 353 354 Iterate over files in architecture specific cache directories and remove 355 them. 356 """ 357 for arch_cache_path in _DexArchCachePaths(self._env_path): 358 for file_path in os.listdir(arch_cache_path): 359 file_path = '{0}/{1}'.format(arch_cache_path, file_path) 360 if os.path.isfile(file_path): 361 os.unlink(file_path) 362 363 364class DeviceTestEnv(ITestEnv): 365 """Device test environment. Concrete implementation of ITestEnv. 366 367 For methods documentation see base class. 368 """ 369 370 def __init__(self, directory_prefix, cleanup=True, logfile_path=None, 371 timeout=60, specific_device=None): 372 """Constructor. 373 374 Args: 375 directory_prefix: string, prefix for environment directory name. 376 cleanup: boolean, if True remove test directory in destructor. 377 logfile_path: string, can be used to specify custom logfile location. 378 timeout: int, seconds, time to wait for single test run to finish. 379 specific_device: string, serial number of device to use. 380 """ 381 self._cleanup = cleanup 382 self._timeout = timeout 383 self._specific_device = specific_device 384 self._host_env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix) 385 if logfile_path is None: 386 self._logfile = open('{0}/log'.format(self._host_env_path), 'w+') 387 else: 388 self._logfile = open(logfile_path, 'w+') 389 self._device_env_path = '{0}/{1}'.format( 390 DEVICE_TMP_PATH, os.path.basename(self._host_env_path)) 391 self._shell_env = os.environ.copy() 392 393 self._AdbMkdir('{0}/dalvik-cache'.format(self._device_env_path)) 394 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 395 self._AdbMkdir(arch_cache_path) 396 397 def __del__(self): 398 if self._cleanup: 399 shutil.rmtree(self._host_env_path) 400 check_call(shlex.split( 401 'adb shell if [ -d "{0}" ]; then rm -rf "{0}"; fi' 402 .format(self._device_env_path))) 403 404 def CreateFile(self, name=None): 405 with NamedTemporaryFile(mode='w') as temp_file: 406 self._AdbPush(temp_file.name, self._device_env_path) 407 if name is None: 408 name = os.path.basename(temp_file.name) 409 return '{0}/{1}'.format(self._device_env_path, name) 410 411 def WriteLines(self, file_path, lines): 412 with NamedTemporaryFile(mode='w') as temp_file: 413 temp_file.writelines('{0}\n'.format(line) for line in lines) 414 temp_file.flush() 415 self._AdbPush(temp_file.name, file_path) 416 return 417 418 def _ExtractPid(self, brief_log_line): 419 """Extracts PID from a single logcat line in brief format.""" 420 pid_start_idx = brief_log_line.find('(') + 2 421 if pid_start_idx == -1: 422 return None 423 pid_end_idx = brief_log_line.find(')', pid_start_idx) 424 if pid_end_idx == -1: 425 return None 426 return brief_log_line[pid_start_idx:pid_end_idx] 427 428 def _ExtractSeverity(self, brief_log_line): 429 """Extracts LogSeverity from a single logcat line in brief format.""" 430 if not brief_log_line: 431 return None 432 return LogSeverity.FromSymbol(brief_log_line[0]) 433 434 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 435 self._EmptyDexCache() 436 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format( 437 self._device_env_path) 438 adb_cmd = ['adb'] 439 if self._specific_device: 440 adb_cmd += ['-s', self._specific_device] 441 logcat_cmd = adb_cmd + ['logcat', '-v', 'brief', '-s', '-b', 'main', 442 '-T', '1', 'dex2oat:*', 'dex2oatd:*'] 443 logcat_proc = Popen(logcat_cmd, stdout=PIPE, stderr=STDOUT, 444 universal_newlines=True) 445 cmd_str = CommandListToCommandString(cmd) 446 # Print PID of the shell and exec command. We later retrieve this PID and 447 # use it to filter dex2oat logs, keeping those with matching parent PID. 448 device_cmd = ('echo $$ && ' + env_vars_cmd + ' exec ' + cmd_str) 449 cmd = adb_cmd + ['shell', device_cmd] 450 (output, _, retcode) = RunCommandForOutput(cmd, self._shell_env, PIPE, 451 STDOUT, self._timeout) 452 # We need to make sure to only kill logcat once all relevant logs arrive. 453 # Sleep is used for simplicity. 454 time.sleep(0.5) 455 logcat_proc.kill() 456 end_of_first_line = output.find('\n') 457 if end_of_first_line != -1: 458 parent_pid = output[:end_of_first_line] 459 output = output[end_of_first_line + 1:] 460 logcat_output, _ = logcat_proc.communicate() 461 logcat_lines = logcat_output.splitlines(keepends=True) 462 dex2oat_pids = [] 463 for line in logcat_lines: 464 # Dex2oat was started by our runtime instance. 465 if 'Running dex2oat (parent PID = ' + parent_pid in line: 466 dex2oat_pids.append(self._ExtractPid(line)) 467 break 468 if dex2oat_pids: 469 for line in logcat_lines: 470 if (self._ExtractPid(line) in dex2oat_pids and 471 self._ExtractSeverity(line) >= log_severity): 472 output += line 473 _LogCmdOutput(self._logfile, cmd, output, retcode) 474 return (output, retcode) 475 476 @property 477 def logfile(self): 478 return self._logfile 479 480 def PushClasspath(self, classpath): 481 """Push classpath to on-device test directory. 482 483 Classpath can contain multiple colon separated file paths, each file is 484 pushed. Returns analogous classpath with paths valid on device. 485 486 Args: 487 classpath: string, classpath in format 'a/b/c:d/e/f'. 488 Returns: 489 string, classpath valid on device. 490 """ 491 paths = classpath.split(':') 492 device_paths = [] 493 for path in paths: 494 device_paths.append('{0}/{1}'.format( 495 self._device_env_path, os.path.basename(path))) 496 self._AdbPush(path, self._device_env_path) 497 return ':'.join(device_paths) 498 499 def _AdbPush(self, what, where): 500 check_call(shlex.split('adb push "{0}" "{1}"'.format(what, where)), 501 stdout=self._logfile, stderr=self._logfile) 502 503 def _AdbMkdir(self, path): 504 check_call(shlex.split('adb shell mkdir "{0}" -p'.format(path)), 505 stdout=self._logfile, stderr=self._logfile) 506 507 def _EmptyDexCache(self): 508 """Empties dex cache.""" 509 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 510 cmd = 'adb shell if [ -d "{0}" ]; then rm -f "{0}"/*; fi'.format( 511 arch_cache_path) 512 check_call(shlex.split(cmd), stdout=self._logfile, stderr=self._logfile) 513