blob: b822dcadb7a52b49d684b624a90d26266d6b78e7 [file] [log] [blame]
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -07001#!/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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070021import signal
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070022import shlex
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070023import shutil
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -070024import time
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070025
Aart Bike0347482016-09-20 14:34:13 -070026from enum import Enum
27from enum import unique
28
29from subprocess import DEVNULL
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070030from subprocess import check_call
31from subprocess import PIPE
32from subprocess import Popen
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070033from subprocess import STDOUT
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070034from 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070046@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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -070056@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
Aart Bike0347482016-09-20 14:34:13 -070095 return NotImplemented
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -070096
97
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070098def 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
Aart Bike0347482016-09-20 14:34:13 -0700119def 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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700127def _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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700140def 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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700171def _LogCmdOutput(logfile, cmd, output, retcode):
172 """Logs output of a command.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700173
174 Args:
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700175 logfile: file handle to logfile.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700176 cmd: list of strings, command.
177 output: command output.
178 retcode: RetCode, normalized retcode.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700179 """
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700180 logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format(
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700181 CommandListToCommandString(cmd), output, retcode))
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700182
183
Aart Bike0347482016-09-20 14:34:13 -0700184def 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700211def CommandListToCommandString(cmd):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700212 """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 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700222 return ' '.join([shlex.quote(segment) for segment in cmd])
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700223
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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700263 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
264 """Runs command in environment.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700265
266 Args:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700267 cmd: list of strings, command to run.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700268 log_severity: LogSeverity, minimum severity of logs included in output.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700269 Returns:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700270 tuple (string, int) output, return code.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700271 """
272
273 @abc.abstractproperty
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700274 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700287 def __init__(self, directory_prefix, cleanup=True, logfile_path=None,
288 timeout=60, x64=False):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700289 """Constructor.
290
291 Args:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700292 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.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700296 x64: boolean, whether to setup in x64 mode.
297 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700298 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+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700305 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
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700316 self._shell_env['DYLD_LIBRARY_PATH'] = library_path
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700317 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700321 def __del__(self):
322 if self._cleanup:
323 shutil.rmtree(self._env_path)
324
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700325 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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700337 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700338 self._EmptyDexCache()
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700339 env = self._shell_env.copy()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700340 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)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700348
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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700369 For methods documentation see base class.
370 """
371
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700372 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+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700391 self._device_env_path = '{0}/{1}'.format(
392 DEVICE_TMP_PATH, os.path.basename(self._host_env_path))
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700393 self._shell_env = os.environ.copy()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700394
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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700399 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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700406 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)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700416 temp_file.flush()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700417 self._AdbPush(temp_file.name, file_path)
418 return
419
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700420 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):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700437 self._EmptyDexCache()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700438 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format(
439 self._device_env_path)
440 adb_cmd = ['adb']
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700441 if self._specific_device:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700442 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)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700477
478 @property
479 def logfile(self):
480 return self._logfile
481
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700482 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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700501 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)