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