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_runtime_root = android_root + '/com.android.runtime' 303 android_tzdata_root = android_root + '/com.android.tzdata' 304 library_path = android_root + '/' + lib 305 path = android_root + '/bin' 306 self._shell_env = os.environ.copy() 307 self._shell_env['ANDROID_DATA'] = self._env_path 308 self._shell_env['ANDROID_ROOT'] = android_root 309 self._shell_env['ANDROID_RUNTIME_ROOT'] = android_runtime_root 310 self._shell_env['ANDROID_TZDATA_ROOT'] = android_tzdata_root 311 self._shell_env['LD_LIBRARY_PATH'] = library_path 312 self._shell_env['DYLD_LIBRARY_PATH'] = library_path 313 self._shell_env['PATH'] = (path + ':' + self._shell_env['PATH']) 314 # Using dlopen requires load bias on the host. 315 self._shell_env['LD_USE_LOAD_BIAS'] = '1' 316 317 def __del__(self): 318 if self._cleanup: 319 shutil.rmtree(self._env_path) 320 321 def CreateFile(self, name=None): 322 if name is None: 323 f = NamedTemporaryFile(dir=self._env_path, delete=False) 324 else: 325 f = open('{0}/{1}'.format(self._env_path, name), 'w+') 326 return f.name 327 328 def WriteLines(self, file_path, lines): 329 with open(file_path, 'w') as f: 330 f.writelines('{0}\n'.format(line) for line in lines) 331 return 332 333 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 334 self._EmptyDexCache() 335 env = self._shell_env.copy() 336 env.update({'ANDROID_LOG_TAGS':'*:' + log_severity.symbol.lower()}) 337 (output, err_output, retcode) = RunCommandForOutput( 338 cmd, env, PIPE, PIPE, self._timeout) 339 # We append err_output to output to stay consistent with DeviceTestEnv 340 # implementation. 341 output += err_output 342 _LogCmdOutput(self._logfile, cmd, output, retcode) 343 return (output, retcode) 344 345 @property 346 def logfile(self): 347 return self._logfile 348 349 def _EmptyDexCache(self): 350 """Empties dex cache. 351 352 Iterate over files in architecture specific cache directories and remove 353 them. 354 """ 355 for arch_cache_path in _DexArchCachePaths(self._env_path): 356 for file_path in os.listdir(arch_cache_path): 357 file_path = '{0}/{1}'.format(arch_cache_path, file_path) 358 if os.path.isfile(file_path): 359 os.unlink(file_path) 360 361 362class DeviceTestEnv(ITestEnv): 363 """Device test environment. Concrete implementation of ITestEnv. 364 365 For methods documentation see base class. 366 """ 367 368 def __init__(self, directory_prefix, cleanup=True, logfile_path=None, 369 timeout=60, specific_device=None): 370 """Constructor. 371 372 Args: 373 directory_prefix: string, prefix for environment directory name. 374 cleanup: boolean, if True remove test directory in destructor. 375 logfile_path: string, can be used to specify custom logfile location. 376 timeout: int, seconds, time to wait for single test run to finish. 377 specific_device: string, serial number of device to use. 378 """ 379 self._cleanup = cleanup 380 self._timeout = timeout 381 self._specific_device = specific_device 382 self._host_env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix) 383 if logfile_path is None: 384 self._logfile = open('{0}/log'.format(self._host_env_path), 'w+') 385 else: 386 self._logfile = open(logfile_path, 'w+') 387 self._device_env_path = '{0}/{1}'.format( 388 DEVICE_TMP_PATH, os.path.basename(self._host_env_path)) 389 self._shell_env = os.environ.copy() 390 391 self._AdbMkdir('{0}/dalvik-cache'.format(self._device_env_path)) 392 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 393 self._AdbMkdir(arch_cache_path) 394 395 def __del__(self): 396 if self._cleanup: 397 shutil.rmtree(self._host_env_path) 398 check_call(shlex.split( 399 'adb shell if [ -d "{0}" ]; then rm -rf "{0}"; fi' 400 .format(self._device_env_path))) 401 402 def CreateFile(self, name=None): 403 with NamedTemporaryFile(mode='w') as temp_file: 404 self._AdbPush(temp_file.name, self._device_env_path) 405 if name is None: 406 name = os.path.basename(temp_file.name) 407 return '{0}/{1}'.format(self._device_env_path, name) 408 409 def WriteLines(self, file_path, lines): 410 with NamedTemporaryFile(mode='w') as temp_file: 411 temp_file.writelines('{0}\n'.format(line) for line in lines) 412 temp_file.flush() 413 self._AdbPush(temp_file.name, file_path) 414 return 415 416 def _ExtractPid(self, brief_log_line): 417 """Extracts PID from a single logcat line in brief format.""" 418 pid_start_idx = brief_log_line.find('(') + 2 419 if pid_start_idx == -1: 420 return None 421 pid_end_idx = brief_log_line.find(')', pid_start_idx) 422 if pid_end_idx == -1: 423 return None 424 return brief_log_line[pid_start_idx:pid_end_idx] 425 426 def _ExtractSeverity(self, brief_log_line): 427 """Extracts LogSeverity from a single logcat line in brief format.""" 428 if not brief_log_line: 429 return None 430 return LogSeverity.FromSymbol(brief_log_line[0]) 431 432 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR): 433 self._EmptyDexCache() 434 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format( 435 self._device_env_path) 436 adb_cmd = ['adb'] 437 if self._specific_device: 438 adb_cmd += ['-s', self._specific_device] 439 logcat_cmd = adb_cmd + ['logcat', '-v', 'brief', '-s', '-b', 'main', 440 '-T', '1', 'dex2oat:*', 'dex2oatd:*'] 441 logcat_proc = Popen(logcat_cmd, stdout=PIPE, stderr=STDOUT, 442 universal_newlines=True) 443 cmd_str = CommandListToCommandString(cmd) 444 # Print PID of the shell and exec command. We later retrieve this PID and 445 # use it to filter dex2oat logs, keeping those with matching parent PID. 446 device_cmd = ('echo $$ && ' + env_vars_cmd + ' exec ' + cmd_str) 447 cmd = adb_cmd + ['shell', device_cmd] 448 (output, _, retcode) = RunCommandForOutput(cmd, self._shell_env, PIPE, 449 STDOUT, self._timeout) 450 # We need to make sure to only kill logcat once all relevant logs arrive. 451 # Sleep is used for simplicity. 452 time.sleep(0.5) 453 logcat_proc.kill() 454 end_of_first_line = output.find('\n') 455 if end_of_first_line != -1: 456 parent_pid = output[:end_of_first_line] 457 output = output[end_of_first_line + 1:] 458 logcat_output, _ = logcat_proc.communicate() 459 logcat_lines = logcat_output.splitlines(keepends=True) 460 dex2oat_pids = [] 461 for line in logcat_lines: 462 # Dex2oat was started by our runtime instance. 463 if 'Running dex2oat (parent PID = ' + parent_pid in line: 464 dex2oat_pids.append(self._ExtractPid(line)) 465 break 466 if dex2oat_pids: 467 for line in logcat_lines: 468 if (self._ExtractPid(line) in dex2oat_pids and 469 self._ExtractSeverity(line) >= log_severity): 470 output += line 471 _LogCmdOutput(self._logfile, cmd, output, retcode) 472 return (output, retcode) 473 474 @property 475 def logfile(self): 476 return self._logfile 477 478 def PushClasspath(self, classpath): 479 """Push classpath to on-device test directory. 480 481 Classpath can contain multiple colon separated file paths, each file is 482 pushed. Returns analogous classpath with paths valid on device. 483 484 Args: 485 classpath: string, classpath in format 'a/b/c:d/e/f'. 486 Returns: 487 string, classpath valid on device. 488 """ 489 paths = classpath.split(':') 490 device_paths = [] 491 for path in paths: 492 device_paths.append('{0}/{1}'.format( 493 self._device_env_path, os.path.basename(path))) 494 self._AdbPush(path, self._device_env_path) 495 return ':'.join(device_paths) 496 497 def _AdbPush(self, what, where): 498 check_call(shlex.split('adb push "{0}" "{1}"'.format(what, where)), 499 stdout=self._logfile, stderr=self._logfile) 500 501 def _AdbMkdir(self, path): 502 check_call(shlex.split('adb shell mkdir "{0}" -p'.format(path)), 503 stdout=self._logfile, stderr=self._logfile) 504 505 def _EmptyDexCache(self): 506 """Empties dex cache.""" 507 for arch_cache_path in _DexArchCachePaths(self._device_env_path): 508 cmd = 'adb shell if [ -d "{0}" ]; then rm -f "{0}"/*; fi'.format( 509 arch_cache_path) 510 check_call(shlex.split(cmd), stdout=self._logfile, stderr=self._logfile) 511