blob: fc9d879aa938194c066ddb36d5826ecf465b0c0e [file] [log] [blame]
Aart Bikd432acd2018-03-08 11:48:27 -08001#!/usr/bin/env python3
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -07002#
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
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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700132def 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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700163def _LogCmdOutput(logfile, cmd, output, retcode):
164 """Logs output of a command.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700165
166 Args:
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700167 logfile: file handle to logfile.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700168 cmd: list of strings, command.
169 output: command output.
170 retcode: RetCode, normalized retcode.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700171 """
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700172 logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format(
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700173 CommandListToCommandString(cmd), output, retcode))
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700174
175
Aart Bike0347482016-09-20 14:34:13 -0700176def 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700203def CommandListToCommandString(cmd):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700204 """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 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700214 return ' '.join([shlex.quote(segment) for segment in cmd])
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700215
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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700255 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
256 """Runs command in environment.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700257
258 Args:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700259 cmd: list of strings, command to run.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700260 log_severity: LogSeverity, minimum severity of logs included in output.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700261 Returns:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700262 tuple (string, int) output, return code.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700263 """
264
265 @abc.abstractproperty
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700266 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700279 def __init__(self, directory_prefix, cleanup=True, logfile_path=None,
280 timeout=60, x64=False):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700281 """Constructor.
282
283 Args:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700284 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.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700288 x64: boolean, whether to setup in x64 mode.
289 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700290 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+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700297 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')
Victor Chang64611242019-07-05 16:32:41 +0100302 android_i18n_root = android_root + '/com.android.i18n'
Martin Stjernholme58624f2019-09-20 15:53:40 +0100303 android_art_root = android_root + '/com.android.art'
Neil Fuller26a5dd62019-03-13 15:16:35 +0000304 android_tzdata_root = android_root + '/com.android.tzdata'
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700305 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
Victor Chang64611242019-07-05 16:32:41 +0100310 self._shell_env['ANDROID_I18N_ROOT'] = android_i18n_root
Martin Stjernholme58624f2019-09-20 15:53:40 +0100311 self._shell_env['ANDROID_ART_ROOT'] = android_art_root
Neil Fuller26a5dd62019-03-13 15:16:35 +0000312 self._shell_env['ANDROID_TZDATA_ROOT'] = android_tzdata_root
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700313 self._shell_env['LD_LIBRARY_PATH'] = library_path
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700314 self._shell_env['DYLD_LIBRARY_PATH'] = library_path
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700315 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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700319 def __del__(self):
320 if self._cleanup:
321 shutil.rmtree(self._env_path)
322
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700323 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
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700335 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700336 self._EmptyDexCache()
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700337 env = self._shell_env.copy()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700338 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)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700346
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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700367 For methods documentation see base class.
368 """
369
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700370 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+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700389 self._device_env_path = '{0}/{1}'.format(
390 DEVICE_TMP_PATH, os.path.basename(self._host_env_path))
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700391 self._shell_env = os.environ.copy()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700392
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
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700397 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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700404 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)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700414 temp_file.flush()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700415 self._AdbPush(temp_file.name, file_path)
416 return
417
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700418 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):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700435 self._EmptyDexCache()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700436 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format(
437 self._device_env_path)
438 adb_cmd = ['adb']
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700439 if self._specific_device:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700440 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)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700475
476 @property
477 def logfile(self):
478 return self._logfile
479
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700480 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
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700499 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)