Make bisection search and javafuzz python modules
Adds __init__.py files required for bisection search and javafuzz
to be considered modules.
Renames bisection-search directory to bisection_search. This is
required because bisection-search is not a valid python module
name.
Test: manual
Change-Id: If85ac5392b10184add17816eab2294b260f72452
diff --git a/tools/bisection_search/README.md b/tools/bisection_search/README.md
new file mode 100644
index 0000000..a7485c2
--- /dev/null
+++ b/tools/bisection_search/README.md
@@ -0,0 +1,43 @@
+Bisection Bug Search
+====================
+
+Bisection Bug Search is a tool for finding compiler optimizations bugs. It
+accepts a program which exposes a bug by producing incorrect output and expected
+output for the program. It then attempts to narrow down the issue to a single
+method and optimization pass under the assumption that interpreter is correct.
+
+Given methods in order M0..Mn finds smallest i such that compiling Mi and
+interpreting all other methods produces incorrect output. Then, given ordered
+optimization passes P0..Pl, finds smallest j such that compiling Mi with passes
+P0..Pj-1 produces expected output and compiling Mi with passes P0..Pj produces
+incorrect output. Prints Mi and Pj.
+
+How to run Bisection Bug Search
+===============================
+
+ bisection_search.py [-h] [-cp CLASSPATH] [--class CLASSNAME] [--lib LIB]
+ [--dalvikvm-option [OPT [OPT ...]]] [--arg [ARG [ARG ...]]]
+ [--image IMAGE] [--raw-cmd RAW_CMD]
+ [--64] [--device] [--expected-output EXPECTED_OUTPUT]
+ [--check-script CHECK_SCRIPT] [--verbose]
+
+ Tool for finding compiler bugs. Either --raw-cmd or both -cp and --class are required.
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ dalvikvm command options:
+ -cp CLASSPATH, --classpath CLASSPATH classpath
+ --class CLASSNAME name of main class
+ --lib LIB lib to use, default: libart.so
+ --dalvikvm-option [OPT [OPT ...]] additional dalvikvm option
+ --arg [ARG [ARG ...]] argument passed to test
+ --image IMAGE path to image
+ --raw-cmd RAW_CMD bisect with this command, ignore other command options
+
+ bisection options:
+ --64 x64 mode
+ --device run on device
+ --expected-output EXPECTED_OUTPUT file containing expected output
+ --check-script CHECK_SCRIPT script comparing output and expected output
+ --verbose enable verbose output
diff --git a/tools/bisection_search/__init__.py b/tools/bisection_search/__init__.py
new file mode 100644
index 0000000..0a42789
--- /dev/null
+++ b/tools/bisection_search/__init__.py
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# This file is intentionally left empty. It indicates that the directory is a Python package.
diff --git a/tools/bisection_search/bisection_search.py b/tools/bisection_search/bisection_search.py
new file mode 100755
index 0000000..110ef82
--- /dev/null
+++ b/tools/bisection_search/bisection_search.py
@@ -0,0 +1,396 @@
+#!/usr/bin/env python3.4
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Performs bisection bug search on methods and optimizations.
+
+See README.md.
+
+Example usage:
+./bisection-search.py -cp classes.dex --expected-output output Test
+"""
+
+import abc
+import argparse
+import re
+import shlex
+from subprocess import call
+import sys
+from tempfile import NamedTemporaryFile
+
+from common import DeviceTestEnv
+from common import FatalError
+from common import GetEnvVariableOrError
+from common import HostTestEnv
+
+
+# Passes that are never disabled during search process because disabling them
+# would compromise correctness.
+MANDATORY_PASSES = ['dex_cache_array_fixups_arm',
+ 'dex_cache_array_fixups_mips',
+ 'instruction_simplifier$before_codegen',
+ 'pc_relative_fixups_x86',
+ 'pc_relative_fixups_mips',
+ 'x86_memory_operand_generation']
+
+# Passes that show up as optimizations in compiler verbose output but aren't
+# driven by run-passes mechanism. They are mandatory and will always run, we
+# never pass them to --run-passes.
+NON_PASSES = ['builder', 'prepare_for_register_allocation',
+ 'liveness', 'register']
+
+
+class Dex2OatWrapperTestable(object):
+ """Class representing a testable compilation.
+
+ Accepts filters on compiled methods and optimization passes.
+ """
+
+ def __init__(self, base_cmd, test_env, output_checker=None, verbose=False):
+ """Constructor.
+
+ Args:
+ base_cmd: list of strings, base command to run.
+ test_env: ITestEnv.
+ output_checker: IOutputCheck, output checker.
+ verbose: bool, enable verbose output.
+ """
+ self._base_cmd = base_cmd
+ self._test_env = test_env
+ self._output_checker = output_checker
+ self._compiled_methods_path = self._test_env.CreateFile('compiled_methods')
+ self._passes_to_run_path = self._test_env.CreateFile('run_passes')
+ self._verbose = verbose
+
+ def Test(self, compiled_methods, passes_to_run=None):
+ """Tests compilation with compiled_methods and run_passes switches active.
+
+ If compiled_methods is None then compiles all methods.
+ If passes_to_run is None then runs default passes.
+
+ Args:
+ compiled_methods: list of strings representing methods to compile or None.
+ passes_to_run: list of strings representing passes to run or None.
+
+ Returns:
+ True if test passes with given settings. False otherwise.
+ """
+ if self._verbose:
+ print('Testing methods: {0} passes: {1}.'.format(
+ compiled_methods, passes_to_run))
+ cmd = self._PrepareCmd(compiled_methods=compiled_methods,
+ passes_to_run=passes_to_run,
+ verbose_compiler=False)
+ (output, ret_code) = self._test_env.RunCommand(
+ cmd, {'ANDROID_LOG_TAGS': '*:e'})
+ res = ((self._output_checker is None and ret_code == 0)
+ or self._output_checker.Check(output))
+ if self._verbose:
+ print('Test passed: {0}.'.format(res))
+ return res
+
+ def GetAllMethods(self):
+ """Get methods compiled during the test.
+
+ Returns:
+ List of strings representing methods compiled during the test.
+
+ Raises:
+ FatalError: An error occurred when retrieving methods list.
+ """
+ cmd = self._PrepareCmd(verbose_compiler=True)
+ (output, _) = self._test_env.RunCommand(cmd, {'ANDROID_LOG_TAGS': '*:i'})
+ match_methods = re.findall(r'Building ([^\n]+)\n', output)
+ if not match_methods:
+ raise FatalError('Failed to retrieve methods list. '
+ 'Not recognized output format.')
+ return match_methods
+
+ def GetAllPassesForMethod(self, compiled_method):
+ """Get all optimization passes ran for a method during the test.
+
+ Args:
+ compiled_method: string representing method to compile.
+
+ Returns:
+ List of strings representing passes ran for compiled_method during test.
+
+ Raises:
+ FatalError: An error occurred when retrieving passes list.
+ """
+ cmd = self._PrepareCmd(compiled_methods=[compiled_method],
+ verbose_compiler=True)
+ (output, _) = self._test_env.RunCommand(cmd, {'ANDROID_LOG_TAGS': '*:i'})
+ match_passes = re.findall(r'Starting pass: ([^\n]+)\n', output)
+ if not match_passes:
+ raise FatalError('Failed to retrieve passes list. '
+ 'Not recognized output format.')
+ return [p for p in match_passes if p not in NON_PASSES]
+
+ def _PrepareCmd(self, compiled_methods=None, passes_to_run=None,
+ verbose_compiler=False):
+ """Prepare command to run."""
+ cmd = [self._base_cmd[0]]
+ # insert additional arguments
+ if compiled_methods is not None:
+ self._test_env.WriteLines(self._compiled_methods_path, compiled_methods)
+ cmd += ['-Xcompiler-option', '--compiled-methods={0}'.format(
+ self._compiled_methods_path)]
+ if passes_to_run is not None:
+ self._test_env.WriteLines(self._passes_to_run_path, passes_to_run)
+ cmd += ['-Xcompiler-option', '--run-passes={0}'.format(
+ self._passes_to_run_path)]
+ if verbose_compiler:
+ cmd += ['-Xcompiler-option', '--runtime-arg', '-Xcompiler-option',
+ '-verbose:compiler', '-Xcompiler-option', '-j1']
+ cmd += self._base_cmd[1:]
+ return cmd
+
+
+class IOutputCheck(object):
+ """Abstract output checking class.
+
+ Checks if output is correct.
+ """
+ __meta_class__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def Check(self, output):
+ """Check if output is correct.
+
+ Args:
+ output: string, output to check.
+
+ Returns:
+ boolean, True if output is correct, False otherwise.
+ """
+
+
+class EqualsOutputCheck(IOutputCheck):
+ """Concrete output checking class checking for equality to expected output."""
+
+ def __init__(self, expected_output):
+ """Constructor.
+
+ Args:
+ expected_output: string, expected output.
+ """
+ self._expected_output = expected_output
+
+ def Check(self, output):
+ """See base class."""
+ return self._expected_output == output
+
+
+class ExternalScriptOutputCheck(IOutputCheck):
+ """Concrete output checking class calling an external script.
+
+ The script should accept two arguments, path to expected output and path to
+ program output. It should exit with 0 return code if outputs are equivalent
+ and with different return code otherwise.
+ """
+
+ def __init__(self, script_path, expected_output_path, logfile):
+ """Constructor.
+
+ Args:
+ script_path: string, path to checking script.
+ expected_output_path: string, path to file with expected output.
+ logfile: file handle, logfile.
+ """
+ self._script_path = script_path
+ self._expected_output_path = expected_output_path
+ self._logfile = logfile
+
+ def Check(self, output):
+ """See base class."""
+ ret_code = None
+ with NamedTemporaryFile(mode='w', delete=False) as temp_file:
+ temp_file.write(output)
+ temp_file.flush()
+ ret_code = call(
+ [self._script_path, self._expected_output_path, temp_file.name],
+ stdout=self._logfile, stderr=self._logfile, universal_newlines=True)
+ return ret_code == 0
+
+
+def BinarySearch(start, end, test):
+ """Binary search integers using test function to guide the process."""
+ while start < end:
+ mid = (start + end) // 2
+ if test(mid):
+ start = mid + 1
+ else:
+ end = mid
+ return start
+
+
+def FilterPasses(passes, cutoff_idx):
+ """Filters passes list according to cutoff_idx but keeps mandatory passes."""
+ return [opt_pass for idx, opt_pass in enumerate(passes)
+ if opt_pass in MANDATORY_PASSES or idx < cutoff_idx]
+
+
+def BugSearch(testable):
+ """Find buggy (method, optimization pass) pair for a given testable.
+
+ Args:
+ testable: Dex2OatWrapperTestable.
+
+ Returns:
+ (string, string) tuple. First element is name of method which when compiled
+ exposes test failure. Second element is name of optimization pass such that
+ for aforementioned method running all passes up to and excluding the pass
+ results in test passing but running all passes up to and including the pass
+ results in test failing.
+
+ (None, None) if test passes when compiling all methods.
+ (string, None) if a method is found which exposes the failure, but the
+ failure happens even when running just mandatory passes.
+
+ Raises:
+ FatalError: Testable fails with no methods compiled.
+ AssertionError: Method failed for all passes when bisecting methods, but
+ passed when bisecting passes. Possible sporadic failure.
+ """
+ all_methods = testable.GetAllMethods()
+ faulty_method_idx = BinarySearch(
+ 0,
+ len(all_methods) + 1,
+ lambda mid: testable.Test(all_methods[0:mid]))
+ if faulty_method_idx == len(all_methods) + 1:
+ return (None, None)
+ if faulty_method_idx == 0:
+ raise FatalError('Testable fails with no methods compiled. '
+ 'Perhaps issue lies outside of compiler.')
+ faulty_method = all_methods[faulty_method_idx - 1]
+ all_passes = testable.GetAllPassesForMethod(faulty_method)
+ faulty_pass_idx = BinarySearch(
+ 0,
+ len(all_passes) + 1,
+ lambda mid: testable.Test([faulty_method],
+ FilterPasses(all_passes, mid)))
+ if faulty_pass_idx == 0:
+ return (faulty_method, None)
+ assert faulty_pass_idx != len(all_passes) + 1, ('Method must fail for some '
+ 'passes.')
+ faulty_pass = all_passes[faulty_pass_idx - 1]
+ return (faulty_method, faulty_pass)
+
+
+def PrepareParser():
+ """Prepares argument parser."""
+ parser = argparse.ArgumentParser(
+ description='Tool for finding compiler bugs. Either --raw-cmd or both '
+ '-cp and --class are required.')
+ command_opts = parser.add_argument_group('dalvikvm command options')
+ command_opts.add_argument('-cp', '--classpath', type=str, help='classpath')
+ command_opts.add_argument('--class', dest='classname', type=str,
+ help='name of main class')
+ command_opts.add_argument('--lib', dest='lib', type=str, default='libart.so',
+ help='lib to use, default: libart.so')
+ command_opts.add_argument('--dalvikvm-option', dest='dalvikvm_opts',
+ metavar='OPT', nargs='*', default=[],
+ help='additional dalvikvm option')
+ command_opts.add_argument('--arg', dest='test_args', nargs='*', default=[],
+ metavar='ARG', help='argument passed to test')
+ command_opts.add_argument('--image', type=str, help='path to image')
+ command_opts.add_argument('--raw-cmd', dest='raw_cmd', type=str,
+ help='bisect with this command, ignore other '
+ 'command options')
+ bisection_opts = parser.add_argument_group('bisection options')
+ bisection_opts.add_argument('--64', dest='x64', action='store_true',
+ default=False, help='x64 mode')
+ bisection_opts.add_argument(
+ '--device', action='store_true', default=False, help='run on device')
+ bisection_opts.add_argument('--expected-output', type=str,
+ help='file containing expected output')
+ bisection_opts.add_argument(
+ '--check-script', dest='check_script', type=str,
+ help='script comparing output and expected output')
+ bisection_opts.add_argument('--verbose', action='store_true',
+ default=False, help='enable verbose output')
+ return parser
+
+
+def PrepareBaseCommand(args, classpath):
+ """Prepares base command used to run test."""
+ if args.raw_cmd:
+ return shlex.split(args.raw_cmd)
+ else:
+ base_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
+ if not args.device:
+ base_cmd += ['-XXlib:{0}'.format(args.lib)]
+ if not args.image:
+ image_path = '{0}/framework/core-optimizing-pic.art'.format(
+ GetEnvVariableOrError('ANDROID_HOST_OUT'))
+ else:
+ image_path = args.image
+ base_cmd += ['-Ximage:{0}'.format(image_path)]
+ if args.dalvikvm_opts:
+ base_cmd += args.dalvikvm_opts
+ base_cmd += ['-cp', classpath, args.classname] + args.test_args
+ return base_cmd
+
+
+def main():
+ # Parse arguments
+ parser = PrepareParser()
+ args = parser.parse_args()
+ if not args.raw_cmd and (not args.classpath or not args.classname):
+ parser.error('Either --raw-cmd or both -cp and --class are required')
+
+ # Prepare environment
+ classpath = args.classpath
+ if args.device:
+ test_env = DeviceTestEnv()
+ if classpath:
+ classpath = test_env.PushClasspath(classpath)
+ else:
+ test_env = HostTestEnv(args.x64)
+ base_cmd = PrepareBaseCommand(args, classpath)
+ output_checker = None
+ if args.expected_output:
+ if args.check_script:
+ output_checker = ExternalScriptOutputCheck(
+ args.check_script, args.expected_output, test_env.logfile)
+ else:
+ with open(args.expected_output, 'r') as expected_output_file:
+ output_checker = EqualsOutputCheck(expected_output_file.read())
+
+ # Perform the search
+ try:
+ testable = Dex2OatWrapperTestable(base_cmd, test_env, output_checker,
+ args.verbose)
+ (method, opt_pass) = BugSearch(testable)
+ except Exception as e:
+ print('Error. Refer to logfile: {0}'.format(test_env.logfile.name))
+ test_env.logfile.write('Exception: {0}\n'.format(e))
+ raise
+
+ # Report results
+ if method is None:
+ print('Couldn\'t find any bugs.')
+ elif opt_pass is None:
+ print('Faulty method: {0}. Fails with just mandatory passes.'.format(
+ method))
+ else:
+ print('Faulty method and pass: {0}, {1}.'.format(method, opt_pass))
+ print('Logfile: {0}'.format(test_env.logfile.name))
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/bisection_search/bisection_test.py b/tools/bisection_search/bisection_test.py
new file mode 100755
index 0000000..9aa08fb
--- /dev/null
+++ b/tools/bisection_search/bisection_test.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3.4
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for bisection-search module."""
+
+import unittest
+
+from unittest.mock import Mock
+
+from bisection_search import BugSearch
+from bisection_search import Dex2OatWrapperTestable
+from bisection_search import FatalError
+from bisection_search import MANDATORY_PASSES
+
+
+class BisectionTestCase(unittest.TestCase):
+ """BugSearch method test case.
+
+ Integer constants were chosen arbitrarily. They should be large enough and
+ random enough to ensure binary search does nontrivial work.
+
+ Attributes:
+ _METHODS: list of strings, methods compiled by testable
+ _PASSES: list of strings, passes run by testable
+ _FAILING_METHOD: string, name of method which fails in some tests
+ _FAILING_PASS: string, name of pass which fails in some tests
+ _MANDATORY_PASS: string, name of a mandatory pass
+ """
+ _METHODS_COUNT = 1293
+ _PASSES_COUNT = 573
+ _FAILING_METHOD_IDX = 237
+ _FAILING_PASS_IDX = 444
+ _METHODS = ['method_{0}'.format(i) for i in range(_METHODS_COUNT)]
+ _PASSES = ['pass_{0}'.format(i) for i in range(_PASSES_COUNT)]
+ _FAILING_METHOD = _METHODS[_FAILING_METHOD_IDX]
+ _FAILING_PASS = _PASSES[_FAILING_PASS_IDX]
+ _MANDATORY_PASS = MANDATORY_PASSES[0]
+
+ def setUp(self):
+ self.testable_mock = Mock(spec=Dex2OatWrapperTestable)
+ self.testable_mock.GetAllMethods.return_value = self._METHODS
+ self.testable_mock.GetAllPassesForMethod.return_value = self._PASSES
+
+ def MethodFailsForAllPasses(self, compiled_methods, run_passes=None):
+ return self._FAILING_METHOD not in compiled_methods
+
+ def MethodFailsForAPass(self, compiled_methods, run_passes=None):
+ return (self._FAILING_METHOD not in compiled_methods or
+ (run_passes is not None and self._FAILING_PASS not in run_passes))
+
+ def testNeverFails(self):
+ self.testable_mock.Test.return_value = True
+ res = BugSearch(self.testable_mock)
+ self.assertEqual(res, (None, None))
+
+ def testAlwaysFails(self):
+ self.testable_mock.Test.return_value = False
+ with self.assertRaises(FatalError):
+ BugSearch(self.testable_mock)
+
+ def testAMethodFailsForAllPasses(self):
+ self.testable_mock.Test.side_effect = self.MethodFailsForAllPasses
+ res = BugSearch(self.testable_mock)
+ self.assertEqual(res, (self._FAILING_METHOD, None))
+
+ def testAMethodFailsForAPass(self):
+ self.testable_mock.Test.side_effect = self.MethodFailsForAPass
+ res = BugSearch(self.testable_mock)
+ self.assertEqual(res, (self._FAILING_METHOD, self._FAILING_PASS))
+
+ def testMandatoryPassPresent(self):
+ self.testable_mock.GetAllPassesForMethod.return_value += (
+ [self._MANDATORY_PASS])
+ self.testable_mock.Test.side_effect = self.MethodFailsForAPass
+ BugSearch(self.testable_mock)
+ for (ordered_args, keyword_args) in self.testable_mock.Test.call_args_list:
+ passes = None
+ if 'run_passes' in keyword_args:
+ passes = keyword_args['run_passes']
+ if len(ordered_args) > 1: # run_passes passed as ordered argument
+ passes = ordered_args[1]
+ if passes is not None:
+ self.assertIn(self._MANDATORY_PASS, passes)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/bisection_search/common.py b/tools/bisection_search/common.py
new file mode 100755
index 0000000..d5029bb
--- /dev/null
+++ b/tools/bisection_search/common.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python3.4
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Module containing common logic from python testing tools."""
+
+import abc
+import os
+import shlex
+
+from subprocess import check_call
+from subprocess import PIPE
+from subprocess import Popen
+from subprocess import STDOUT
+from subprocess import TimeoutExpired
+
+from tempfile import mkdtemp
+from tempfile import NamedTemporaryFile
+
+# Temporary directory path on device.
+DEVICE_TMP_PATH = '/data/local/tmp'
+
+# Architectures supported in dalvik cache.
+DALVIK_CACHE_ARCHS = ['arm', 'arm64', 'x86', 'x86_64']
+
+
+def GetEnvVariableOrError(variable_name):
+ """Gets value of an environmental variable.
+
+ If the variable is not set raises FatalError.
+
+ Args:
+ variable_name: string, name of variable to get.
+
+ Returns:
+ string, value of requested variable.
+
+ Raises:
+ FatalError: Requested variable is not set.
+ """
+ top = os.environ.get(variable_name)
+ if top is None:
+ raise FatalError('{0} environmental variable not set.'.format(
+ variable_name))
+ return top
+
+
+def _DexArchCachePaths(android_data_path):
+ """Returns paths to architecture specific caches.
+
+ Args:
+ android_data_path: string, path dalvik-cache resides in.
+
+ Returns:
+ Iterable paths to architecture specific caches.
+ """
+ return ('{0}/dalvik-cache/{1}'.format(android_data_path, arch)
+ for arch in DALVIK_CACHE_ARCHS)
+
+
+def _RunCommandForOutputAndLog(cmd, env, logfile, timeout=60):
+ """Runs command and logs its output. Returns the output.
+
+ Args:
+ cmd: list of strings, command to run.
+ env: shell environment to run the command with.
+ logfile: file handle to logfile.
+ timeout: int, timeout in seconds
+
+ Returns:
+ tuple (string, string, int) stdout output, stderr output, return code.
+ """
+ proc = Popen(cmd, stderr=STDOUT, stdout=PIPE, env=env,
+ universal_newlines=True)
+ timeouted = False
+ try:
+ (output, _) = proc.communicate(timeout=timeout)
+ except TimeoutExpired:
+ timeouted = True
+ proc.kill()
+ (output, _) = proc.communicate()
+ logfile.write('Command:\n{0}\n{1}\nReturn code: {2}\n'.format(
+ _CommandListToCommandString(cmd), output,
+ 'TIMEOUT' if timeouted else proc.returncode))
+ ret_code = 1 if timeouted else proc.returncode
+ return (output, ret_code)
+
+
+def _CommandListToCommandString(cmd):
+ """Converts shell command represented as list of strings to a single string.
+
+ Each element of the list is wrapped in double quotes.
+
+ Args:
+ cmd: list of strings, shell command.
+
+ Returns:
+ string, shell command.
+ """
+ return ' '.join(['"{0}"'.format(segment) for segment in cmd])
+
+
+class FatalError(Exception):
+ """Fatal error in script."""
+
+
+class ITestEnv(object):
+ """Test environment abstraction.
+
+ Provides unified interface for interacting with host and device test
+ environments. Creates a test directory and expose methods to modify test files
+ and run commands.
+ """
+ __meta_class__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def CreateFile(self, name=None):
+ """Creates a file in test directory.
+
+ Returned path to file can be used in commands run in the environment.
+
+ Args:
+ name: string, file name. If None file is named arbitrarily.
+
+ Returns:
+ string, environment specific path to file.
+ """
+
+ @abc.abstractmethod
+ def WriteLines(self, file_path, lines):
+ """Writes lines to a file in test directory.
+
+ If file exists it gets overwritten. If file doest not exist it is created.
+
+ Args:
+ file_path: string, environment specific path to file.
+ lines: list of strings to write.
+ """
+
+ @abc.abstractmethod
+ def RunCommand(self, cmd, env_updates=None):
+ """Runs command in environment with updated environmental variables.
+
+ Args:
+ cmd: list of strings, command to run.
+ env_updates: dict, string to string, maps names of variables to their
+ updated values.
+ Returns:
+ tuple (string, string, int) stdout output, stderr output, return code.
+ """
+
+ @abc.abstractproperty
+ def logfile(self):
+ """Gets file handle to logfile residing on host."""
+
+
+class HostTestEnv(ITestEnv):
+ """Host test environment. Concrete implementation of ITestEnv.
+
+ Maintains a test directory in /tmp/. Runs commands on the host in modified
+ shell environment. Mimics art script behavior.
+
+ For methods documentation see base class.
+ """
+
+ def __init__(self, x64):
+ """Constructor.
+
+ Args:
+ x64: boolean, whether to setup in x64 mode.
+ """
+ self._env_path = mkdtemp(dir='/tmp/', prefix='bisection_search_')
+ self._logfile = open('{0}/log'.format(self._env_path), 'w+')
+ os.mkdir('{0}/dalvik-cache'.format(self._env_path))
+ for arch_cache_path in _DexArchCachePaths(self._env_path):
+ os.mkdir(arch_cache_path)
+ lib = 'lib64' if x64 else 'lib'
+ android_root = GetEnvVariableOrError('ANDROID_HOST_OUT')
+ library_path = android_root + '/' + lib
+ path = android_root + '/bin'
+ self._shell_env = os.environ.copy()
+ self._shell_env['ANDROID_DATA'] = self._env_path
+ self._shell_env['ANDROID_ROOT'] = android_root
+ self._shell_env['LD_LIBRARY_PATH'] = library_path
+ self._shell_env['DYLD_LIBRARY_PATH'] = library_path
+ self._shell_env['PATH'] = (path + ':' + self._shell_env['PATH'])
+ # Using dlopen requires load bias on the host.
+ self._shell_env['LD_USE_LOAD_BIAS'] = '1'
+
+ def CreateFile(self, name=None):
+ if name is None:
+ f = NamedTemporaryFile(dir=self._env_path, delete=False)
+ else:
+ f = open('{0}/{1}'.format(self._env_path, name), 'w+')
+ return f.name
+
+ def WriteLines(self, file_path, lines):
+ with open(file_path, 'w') as f:
+ f.writelines('{0}\n'.format(line) for line in lines)
+ return
+
+ def RunCommand(self, cmd, env_updates=None):
+ if not env_updates:
+ env_updates = {}
+ self._EmptyDexCache()
+ env = self._shell_env.copy()
+ env.update(env_updates)
+ return _RunCommandForOutputAndLog(cmd, env, self._logfile)
+
+ @property
+ def logfile(self):
+ return self._logfile
+
+ def _EmptyDexCache(self):
+ """Empties dex cache.
+
+ Iterate over files in architecture specific cache directories and remove
+ them.
+ """
+ for arch_cache_path in _DexArchCachePaths(self._env_path):
+ for file_path in os.listdir(arch_cache_path):
+ file_path = '{0}/{1}'.format(arch_cache_path, file_path)
+ if os.path.isfile(file_path):
+ os.unlink(file_path)
+
+
+class DeviceTestEnv(ITestEnv):
+ """Device test environment. Concrete implementation of ITestEnv.
+
+ Makes use of HostTestEnv to maintain a test directory on host. Creates an
+ on device test directory which is kept in sync with the host one.
+
+ For methods documentation see base class.
+ """
+
+ def __init__(self):
+ """Constructor."""
+ self._host_env_path = mkdtemp(dir='/tmp/', prefix='bisection_search_')
+ self._logfile = open('{0}/log'.format(self._host_env_path), 'w+')
+ self._device_env_path = '{0}/{1}'.format(
+ DEVICE_TMP_PATH, os.path.basename(self._host_env_path))
+ self._shell_env = os.environ.copy()
+
+ self._AdbMkdir('{0}/dalvik-cache'.format(self._device_env_path))
+ for arch_cache_path in _DexArchCachePaths(self._device_env_path):
+ self._AdbMkdir(arch_cache_path)
+
+ def CreateFile(self, name=None):
+ with NamedTemporaryFile(mode='w') as temp_file:
+ self._AdbPush(temp_file.name, self._device_env_path)
+ if name is None:
+ name = os.path.basename(temp_file.name)
+ return '{0}/{1}'.format(self._device_env_path, name)
+
+ def WriteLines(self, file_path, lines):
+ with NamedTemporaryFile(mode='w') as temp_file:
+ temp_file.writelines('{0}\n'.format(line) for line in lines)
+ temp_file.flush()
+ self._AdbPush(temp_file.name, file_path)
+ return
+
+ def RunCommand(self, cmd, env_updates=None):
+ if not env_updates:
+ env_updates = {}
+ self._EmptyDexCache()
+ if 'ANDROID_DATA' not in env_updates:
+ env_updates['ANDROID_DATA'] = self._device_env_path
+ env_updates_cmd = ' '.join(['{0}={1}'.format(var, val) for var, val
+ in env_updates.items()])
+ cmd = _CommandListToCommandString(cmd)
+ cmd = ('adb shell "logcat -c && {0} {1} ; logcat -d -s dex2oat:* dex2oatd:*'
+ '| grep -v "^---------" 1>&2"').format(env_updates_cmd, cmd)
+ return _RunCommandForOutputAndLog(
+ shlex.split(cmd), self._shell_env, self._logfile)
+
+ @property
+ def logfile(self):
+ return self._logfile
+
+ def PushClasspath(self, classpath):
+ """Push classpath to on-device test directory.
+
+ Classpath can contain multiple colon separated file paths, each file is
+ pushed. Returns analogous classpath with paths valid on device.
+
+ Args:
+ classpath: string, classpath in format 'a/b/c:d/e/f'.
+ Returns:
+ string, classpath valid on device.
+ """
+ paths = classpath.split(':')
+ device_paths = []
+ for path in paths:
+ device_paths.append('{0}/{1}'.format(
+ self._device_env_path, os.path.basename(path)))
+ self._AdbPush(path, self._device_env_path)
+ return ':'.join(device_paths)
+
+ def _AdbPush(self, what, where):
+ check_call(shlex.split('adb push "{0}" "{1}"'.format(what, where)),
+ stdout=self._logfile, stderr=self._logfile)
+
+ def _AdbMkdir(self, path):
+ check_call(shlex.split('adb shell mkdir "{0}" -p'.format(path)),
+ stdout=self._logfile, stderr=self._logfile)
+
+ def _EmptyDexCache(self):
+ """Empties dex cache."""
+ for arch_cache_path in _DexArchCachePaths(self._device_env_path):
+ cmd = 'adb shell if [ -d "{0}" ]; then rm -f "{0}"/*; fi'.format(
+ arch_cache_path)
+ check_call(shlex.split(cmd), stdout=self._logfile, stderr=self._logfile)