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