blob: 3d92ee5a60afdb3d0f922644025ea044ca62f95f [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
26from subprocess import check_call
27from subprocess import PIPE
28from subprocess import Popen
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070029from subprocess import STDOUT
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070030from subprocess import TimeoutExpired
31
32from tempfile import mkdtemp
33from tempfile import NamedTemporaryFile
34
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070035from enum import Enum
36from enum import unique
37
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070038# Temporary directory path on device.
39DEVICE_TMP_PATH = '/data/local/tmp'
40
41# Architectures supported in dalvik cache.
42DALVIK_CACHE_ARCHS = ['arm', 'arm64', 'x86', 'x86_64']
43
44
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -070045@unique
46class RetCode(Enum):
47 """Enum representing normalized return codes."""
48 SUCCESS = 0
49 TIMEOUT = 1
50 ERROR = 2
51 NOTCOMPILED = 3
52 NOTRUN = 4
53
54
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -070055@unique
56class LogSeverity(Enum):
57 VERBOSE = 0
58 DEBUG = 1
59 INFO = 2
60 WARNING = 3
61 ERROR = 4
62 FATAL = 5
63 SILENT = 6
64
65 @property
66 def symbol(self):
67 return self.name[0]
68
69 @classmethod
70 def FromSymbol(cls, s):
71 for log_severity in LogSeverity:
72 if log_severity.symbol == s:
73 return log_severity
74 raise ValueError("{0} is not a valid log severity symbol".format(s))
75
76 def __ge__(self, other):
77 if self.__class__ is other.__class__:
78 return self.value >= other.value
79 return NotImplemented
80
81 def __gt__(self, other):
82 if self.__class__ is other.__class__:
83 return self.value > other.value
84 return NotImplemented
85
86 def __le__(self, other):
87 if self.__class__ is other.__class__:
88 return self.value <= other.value
89 return NotImplemented
90
91 def __lt__(self, other):
92 if self.__class__ is other.__class__:
93 return self.value < other.value
94 return NotImplemented
95
96
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070097def GetEnvVariableOrError(variable_name):
98 """Gets value of an environmental variable.
99
100 If the variable is not set raises FatalError.
101
102 Args:
103 variable_name: string, name of variable to get.
104
105 Returns:
106 string, value of requested variable.
107
108 Raises:
109 FatalError: Requested variable is not set.
110 """
111 top = os.environ.get(variable_name)
112 if top is None:
113 raise FatalError('{0} environmental variable not set.'.format(
114 variable_name))
115 return top
116
117
118def _DexArchCachePaths(android_data_path):
119 """Returns paths to architecture specific caches.
120
121 Args:
122 android_data_path: string, path dalvik-cache resides in.
123
124 Returns:
125 Iterable paths to architecture specific caches.
126 """
127 return ('{0}/dalvik-cache/{1}'.format(android_data_path, arch)
128 for arch in DALVIK_CACHE_ARCHS)
129
130
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700131def RunCommandForOutput(cmd, env, stdout, stderr, timeout=60):
132 """Runs command piping output to files, stderr or stdout.
133
134 Args:
135 cmd: list of strings, command to run.
136 env: shell environment to run the command with.
137 stdout: file handle or one of Subprocess.PIPE, Subprocess.STDOUT,
138 Subprocess.DEVNULL, see Popen.
139 stderr: file handle or one of Subprocess.PIPE, Subprocess.STDOUT,
140 Subprocess.DEVNULL, see Popen.
141 timeout: int, timeout in seconds.
142
143 Returns:
144 tuple (string, string, RetCode) stdout output, stderr output, normalized
145 return code.
146 """
147 proc = Popen(cmd, stdout=stdout, stderr=stderr, env=env,
148 universal_newlines=True, start_new_session=True)
149 try:
150 (output, stderr_output) = proc.communicate(timeout=timeout)
151 if proc.returncode == 0:
152 retcode = RetCode.SUCCESS
153 else:
154 retcode = RetCode.ERROR
155 except TimeoutExpired:
156 os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
157 (output, stderr_output) = proc.communicate()
158 retcode = RetCode.TIMEOUT
159 return (output, stderr_output, retcode)
160
161
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700162def _LogCmdOutput(logfile, cmd, output, retcode):
163 """Logs output of a command.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700164
165 Args:
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700166 logfile: file handle to logfile.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700167 cmd: list of strings, command.
168 output: command output.
169 retcode: RetCode, normalized retcode.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700170 """
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700171 logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format(
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700172 CommandListToCommandString(cmd), output, retcode))
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700173
174
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700175def CommandListToCommandString(cmd):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700176 """Converts shell command represented as list of strings to a single string.
177
178 Each element of the list is wrapped in double quotes.
179
180 Args:
181 cmd: list of strings, shell command.
182
183 Returns:
184 string, shell command.
185 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700186 return ' '.join([shlex.quote(segment) for segment in cmd])
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700187
188
189class FatalError(Exception):
190 """Fatal error in script."""
191
192
193class ITestEnv(object):
194 """Test environment abstraction.
195
196 Provides unified interface for interacting with host and device test
197 environments. Creates a test directory and expose methods to modify test files
198 and run commands.
199 """
200 __meta_class__ = abc.ABCMeta
201
202 @abc.abstractmethod
203 def CreateFile(self, name=None):
204 """Creates a file in test directory.
205
206 Returned path to file can be used in commands run in the environment.
207
208 Args:
209 name: string, file name. If None file is named arbitrarily.
210
211 Returns:
212 string, environment specific path to file.
213 """
214
215 @abc.abstractmethod
216 def WriteLines(self, file_path, lines):
217 """Writes lines to a file in test directory.
218
219 If file exists it gets overwritten. If file doest not exist it is created.
220
221 Args:
222 file_path: string, environment specific path to file.
223 lines: list of strings to write.
224 """
225
226 @abc.abstractmethod
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700227 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
228 """Runs command in environment.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700229
230 Args:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700231 cmd: list of strings, command to run.
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700232 log_severity: LogSeverity, minimum severity of logs included in output.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700233 Returns:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700234 tuple (string, int) output, return code.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700235 """
236
237 @abc.abstractproperty
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700238 def logfile(self):
239 """Gets file handle to logfile residing on host."""
240
241
242class HostTestEnv(ITestEnv):
243 """Host test environment. Concrete implementation of ITestEnv.
244
245 Maintains a test directory in /tmp/. Runs commands on the host in modified
246 shell environment. Mimics art script behavior.
247
248 For methods documentation see base class.
249 """
250
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700251 def __init__(self, directory_prefix, cleanup=True, logfile_path=None,
252 timeout=60, x64=False):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700253 """Constructor.
254
255 Args:
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700256 directory_prefix: string, prefix for environment directory name.
257 cleanup: boolean, if True remove test directory in destructor.
258 logfile_path: string, can be used to specify custom logfile location.
259 timeout: int, seconds, time to wait for single test run to finish.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700260 x64: boolean, whether to setup in x64 mode.
261 """
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700262 self._cleanup = cleanup
263 self._timeout = timeout
264 self._env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix)
265 if logfile_path is None:
266 self._logfile = open('{0}/log'.format(self._env_path), 'w+')
267 else:
268 self._logfile = open(logfile_path, 'w+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700269 os.mkdir('{0}/dalvik-cache'.format(self._env_path))
270 for arch_cache_path in _DexArchCachePaths(self._env_path):
271 os.mkdir(arch_cache_path)
272 lib = 'lib64' if x64 else 'lib'
273 android_root = GetEnvVariableOrError('ANDROID_HOST_OUT')
274 library_path = android_root + '/' + lib
275 path = android_root + '/bin'
276 self._shell_env = os.environ.copy()
277 self._shell_env['ANDROID_DATA'] = self._env_path
278 self._shell_env['ANDROID_ROOT'] = android_root
279 self._shell_env['LD_LIBRARY_PATH'] = library_path
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700280 self._shell_env['DYLD_LIBRARY_PATH'] = library_path
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700281 self._shell_env['PATH'] = (path + ':' + self._shell_env['PATH'])
282 # Using dlopen requires load bias on the host.
283 self._shell_env['LD_USE_LOAD_BIAS'] = '1'
284
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700285 def __del__(self):
286 if self._cleanup:
287 shutil.rmtree(self._env_path)
288
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700289 def CreateFile(self, name=None):
290 if name is None:
291 f = NamedTemporaryFile(dir=self._env_path, delete=False)
292 else:
293 f = open('{0}/{1}'.format(self._env_path, name), 'w+')
294 return f.name
295
296 def WriteLines(self, file_path, lines):
297 with open(file_path, 'w') as f:
298 f.writelines('{0}\n'.format(line) for line in lines)
299 return
300
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700301 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700302 self._EmptyDexCache()
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700303 env = self._shell_env.copy()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700304 env.update({'ANDROID_LOG_TAGS':'*:' + log_severity.symbol.lower()})
305 (output, err_output, retcode) = RunCommandForOutput(
306 cmd, env, PIPE, PIPE, self._timeout)
307 # We append err_output to output to stay consistent with DeviceTestEnv
308 # implementation.
309 output += err_output
310 _LogCmdOutput(self._logfile, cmd, output, retcode)
311 return (output, retcode)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700312
313 @property
314 def logfile(self):
315 return self._logfile
316
317 def _EmptyDexCache(self):
318 """Empties dex cache.
319
320 Iterate over files in architecture specific cache directories and remove
321 them.
322 """
323 for arch_cache_path in _DexArchCachePaths(self._env_path):
324 for file_path in os.listdir(arch_cache_path):
325 file_path = '{0}/{1}'.format(arch_cache_path, file_path)
326 if os.path.isfile(file_path):
327 os.unlink(file_path)
328
329
330class DeviceTestEnv(ITestEnv):
331 """Device test environment. Concrete implementation of ITestEnv.
332
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700333 For methods documentation see base class.
334 """
335
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700336 def __init__(self, directory_prefix, cleanup=True, logfile_path=None,
337 timeout=60, specific_device=None):
338 """Constructor.
339
340 Args:
341 directory_prefix: string, prefix for environment directory name.
342 cleanup: boolean, if True remove test directory in destructor.
343 logfile_path: string, can be used to specify custom logfile location.
344 timeout: int, seconds, time to wait for single test run to finish.
345 specific_device: string, serial number of device to use.
346 """
347 self._cleanup = cleanup
348 self._timeout = timeout
349 self._specific_device = specific_device
350 self._host_env_path = mkdtemp(dir='/tmp/', prefix=directory_prefix)
351 if logfile_path is None:
352 self._logfile = open('{0}/log'.format(self._host_env_path), 'w+')
353 else:
354 self._logfile = open(logfile_path, 'w+')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700355 self._device_env_path = '{0}/{1}'.format(
356 DEVICE_TMP_PATH, os.path.basename(self._host_env_path))
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700357 self._shell_env = os.environ.copy()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700358
359 self._AdbMkdir('{0}/dalvik-cache'.format(self._device_env_path))
360 for arch_cache_path in _DexArchCachePaths(self._device_env_path):
361 self._AdbMkdir(arch_cache_path)
362
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700363 def __del__(self):
364 if self._cleanup:
365 shutil.rmtree(self._host_env_path)
366 check_call(shlex.split(
367 'adb shell if [ -d "{0}" ]; then rm -rf "{0}"; fi'
368 .format(self._device_env_path)))
369
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700370 def CreateFile(self, name=None):
371 with NamedTemporaryFile(mode='w') as temp_file:
372 self._AdbPush(temp_file.name, self._device_env_path)
373 if name is None:
374 name = os.path.basename(temp_file.name)
375 return '{0}/{1}'.format(self._device_env_path, name)
376
377 def WriteLines(self, file_path, lines):
378 with NamedTemporaryFile(mode='w') as temp_file:
379 temp_file.writelines('{0}\n'.format(line) for line in lines)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700380 temp_file.flush()
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700381 self._AdbPush(temp_file.name, file_path)
382 return
383
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700384 def _ExtractPid(self, brief_log_line):
385 """Extracts PID from a single logcat line in brief format."""
386 pid_start_idx = brief_log_line.find('(') + 2
387 if pid_start_idx == -1:
388 return None
389 pid_end_idx = brief_log_line.find(')', pid_start_idx)
390 if pid_end_idx == -1:
391 return None
392 return brief_log_line[pid_start_idx:pid_end_idx]
393
394 def _ExtractSeverity(self, brief_log_line):
395 """Extracts LogSeverity from a single logcat line in brief format."""
396 if not brief_log_line:
397 return None
398 return LogSeverity.FromSymbol(brief_log_line[0])
399
400 def RunCommand(self, cmd, log_severity=LogSeverity.ERROR):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700401 self._EmptyDexCache()
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700402 env_vars_cmd = 'ANDROID_DATA={0} ANDROID_LOG_TAGS=*:i'.format(
403 self._device_env_path)
404 adb_cmd = ['adb']
Wojciech Staszkiewicz0d0fd4a2016-09-07 18:52:52 -0700405 if self._specific_device:
Wojciech Staszkiewicz698e4b32016-09-16 13:44:09 -0700406 adb_cmd += ['-s', self._specific_device]
407 logcat_cmd = adb_cmd + ['logcat', '-v', 'brief', '-s', '-b', 'main',
408 '-T', '1', 'dex2oat:*', 'dex2oatd:*']
409 logcat_proc = Popen(logcat_cmd, stdout=PIPE, stderr=STDOUT,
410 universal_newlines=True)
411 cmd_str = CommandListToCommandString(cmd)
412 # Print PID of the shell and exec command. We later retrieve this PID and
413 # use it to filter dex2oat logs, keeping those with matching parent PID.
414 device_cmd = ('echo $$ && ' + env_vars_cmd + ' exec ' + cmd_str)
415 cmd = adb_cmd + ['shell', device_cmd]
416 (output, _, retcode) = RunCommandForOutput(cmd, self._shell_env, PIPE,
417 STDOUT, self._timeout)
418 # We need to make sure to only kill logcat once all relevant logs arrive.
419 # Sleep is used for simplicity.
420 time.sleep(0.5)
421 logcat_proc.kill()
422 end_of_first_line = output.find('\n')
423 if end_of_first_line != -1:
424 parent_pid = output[:end_of_first_line]
425 output = output[end_of_first_line + 1:]
426 logcat_output, _ = logcat_proc.communicate()
427 logcat_lines = logcat_output.splitlines(keepends=True)
428 dex2oat_pids = []
429 for line in logcat_lines:
430 # Dex2oat was started by our runtime instance.
431 if 'Running dex2oat (parent PID = ' + parent_pid in line:
432 dex2oat_pids.append(self._ExtractPid(line))
433 break
434 if dex2oat_pids:
435 for line in logcat_lines:
436 if (self._ExtractPid(line) in dex2oat_pids and
437 self._ExtractSeverity(line) >= log_severity):
438 output += line
439 _LogCmdOutput(self._logfile, cmd, output, retcode)
440 return (output, retcode)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700441
442 @property
443 def logfile(self):
444 return self._logfile
445
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700446 def PushClasspath(self, classpath):
447 """Push classpath to on-device test directory.
448
449 Classpath can contain multiple colon separated file paths, each file is
450 pushed. Returns analogous classpath with paths valid on device.
451
452 Args:
453 classpath: string, classpath in format 'a/b/c:d/e/f'.
454 Returns:
455 string, classpath valid on device.
456 """
457 paths = classpath.split(':')
458 device_paths = []
459 for path in paths:
460 device_paths.append('{0}/{1}'.format(
461 self._device_env_path, os.path.basename(path)))
462 self._AdbPush(path, self._device_env_path)
463 return ':'.join(device_paths)
464
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700465 def _AdbPush(self, what, where):
466 check_call(shlex.split('adb push "{0}" "{1}"'.format(what, where)),
467 stdout=self._logfile, stderr=self._logfile)
468
469 def _AdbMkdir(self, path):
470 check_call(shlex.split('adb shell mkdir "{0}" -p'.format(path)),
471 stdout=self._logfile, stderr=self._logfile)
472
473 def _EmptyDexCache(self):
474 """Empties dex cache."""
475 for arch_cache_path in _DexArchCachePaths(self._device_env_path):
476 cmd = 'adb shell if [ -d "{0}" ]; then rm -f "{0}"/*; fi'.format(
477 arch_cache_path)
478 check_call(shlex.split(cmd), stdout=self._logfile, stderr=self._logfile)