Home | History | Annotate | Download | only in common
      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 
     19 import abc
     20 import os
     21 import signal
     22 import shlex
     23 import shutil
     24 import time
     25 
     26 from enum import Enum
     27 from enum import unique
     28 
     29 from subprocess import DEVNULL
     30 from subprocess import check_call
     31 from subprocess import PIPE
     32 from subprocess import Popen
     33 from subprocess import STDOUT
     34 from subprocess import TimeoutExpired
     35 
     36 from tempfile import mkdtemp
     37 from tempfile import NamedTemporaryFile
     38 
     39 # Temporary directory path on device.
     40 DEVICE_TMP_PATH = '/data/local/tmp'
     41 
     42 # Architectures supported in dalvik cache.
     43 DALVIK_CACHE_ARCHS = ['arm', 'arm64', 'x86', 'x86_64']
     44 
     45 
     46 @unique
     47 class 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
     57 class 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 
     98 def 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 
    119 def 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 
    127 def _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 
    140 def 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 
    171 def _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 
    184 def 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 
    211 def 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 
    225 class FatalError(Exception):
    226   """Fatal error in script."""
    227 
    228 
    229 class 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 
    278 class 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 
    366 class 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