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