Bisection bug search tool
Bisection Bug Search is a tool for finding compiler optimization
bugs. It accepts a program which exposes a bug by producing incorrect
output and expected correct output for the program. The tool will
then attempt to narrow down the issue to a single method and
optimization pass.
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.
Test: unit tests ./art/tools/bisection-search/tests.py
Manual testing:
./bisection-search.py -cp classes.dex --expected-output output Test
Change-Id: Ic40a82184975d42c9a403f697995e5c9654b8e52
diff --git a/tools/bisection-search/README.md b/tools/bisection-search/README.md
new file mode 100644
index 0000000..857c930
--- /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
+ [--expected-output EXPECTED_OUTPUT] [--device]
+ [--lib LIB] [--64]
+ [--dalvikvm-option [OPTION [OPTION ...]]]
+ [--arg [TEST_ARGS [TEST_ARGS ...]]] [--image IMAGE]
+ [--verbose]
+ classname
+
+ positional arguments:
+ classname name of class to run
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -cp CLASSPATH, --classpath CLASSPATH
+ classpath
+ --expected-output EXPECTED_OUTPUT
+ file containing expected output
+ --device run on device
+ --lib LIB lib to use, default: libart.so
+ --64 x64 mode
+ --dalvikvm-option [OPTION [OPTION ...]]
+ additional dalvikvm option
+ --arg [TEST_ARGS [TEST_ARGS ...]]
+ argument to pass to program
+ --image IMAGE path to image
+ --verbose enable verbose output
diff --git a/tools/bisection-search/bisection_search.py b/tools/bisection-search/bisection_search.py
new file mode 100755
index 0000000..d6c1749
--- /dev/null
+++ b/tools/bisection-search/bisection_search.py
@@ -0,0 +1,300 @@
+#!/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 argparse
+import re
+import sys
+
+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, class_name, args,
+ expected_output=None, verbose=False):
+ """Constructor.
+
+ Args:
+ base_cmd: list of strings, base command to run.
+ test_env: ITestEnv.
+ class_name: string, name of class to run.
+ args: list of strings, program arguments to pass.
+ expected_output: string, expected output to compare against or None.
+ verbose: bool, enable verbose output.
+ """
+ self._base_cmd = base_cmd
+ self._test_env = test_env
+ self._class_name = class_name
+ self._args = args
+ self._expected_output = expected_output
+ 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=True)
+ (output, _, ret_code) = self._test_env.RunCommand(cmd)
+ res = ret_code == 0 and (self._expected_output is None
+ or output == self._expected_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)
+ (_, err_output, _) = self._test_env.RunCommand(cmd)
+ match_methods = re.findall(r'Building ([^\n]+)\n', err_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)
+ (_, err_output, _) = self._test_env.RunCommand(cmd)
+ match_passes = re.findall(r'Starting pass: ([^\n]+)\n', err_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 = list(self._base_cmd)
+ 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']
+ cmd += ['-classpath', self._test_env.classpath, self._class_name]
+ cmd += self._args
+ return cmd
+
+
+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),
+ lambda mid: testable.Test(all_methods[0:mid]))
+ if faulty_method_idx == len(all_methods):
+ 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),
+ 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), '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()
+ parser.add_argument(
+ '-cp', '--classpath', required=True, type=str, help='classpath')
+ parser.add_argument('--expected-output', type=str,
+ help='file containing expected output')
+ parser.add_argument(
+ '--device', action='store_true', default=False, help='run on device')
+ parser.add_argument('classname', type=str, help='name of class to run')
+ parser.add_argument('--lib', dest='lib', type=str, default='libart.so',
+ help='lib to use, default: libart.so')
+ parser.add_argument('--64', dest='x64', action='store_true',
+ default=False, help='x64 mode')
+ parser.add_argument('--dalvikvm-option', dest='dalvikvm_opts',
+ metavar='OPTION', nargs='*', default=[],
+ help='additional dalvikvm option')
+ parser.add_argument('--arg', dest='test_args', nargs='*', default=[],
+ help='argument to pass to program')
+ parser.add_argument('--image', type=str, help='path to image')
+ parser.add_argument('--verbose', action='store_true',
+ default=False, help='enable verbose output')
+ return parser
+
+
+def main():
+ # Parse arguments
+ parser = PrepareParser()
+ args = parser.parse_args()
+
+ # Prepare environment
+ if args.expected_output is not None:
+ with open(args.expected_output, 'r') as f:
+ expected_output = f.read()
+ else:
+ expected_output = None
+ if args.device:
+ run_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
+ test_env = DeviceTestEnv(args.classpath)
+ else:
+ run_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
+ run_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
+ run_cmd += ['-Ximage:{0}'.format(image_path)]
+ if args.dalvikvm_opts:
+ run_cmd += args.dalvikvm_opts
+ test_env = HostTestEnv(args.classpath, args.x64)
+
+ # Perform the search
+ try:
+ testable = Dex2OatWrapperTestable(run_cmd, test_env, args.classname,
+ args.test_args, expected_output,
+ 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..8361fc9
--- /dev/null
+++ b/tools/bisection-search/common.py
@@ -0,0 +1,318 @@
+#!/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 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=PIPE, stdout=PIPE, env=env, universal_newlines=True)
+ timeouted = False
+ try:
+ (output, err_output) = proc.communicate(timeout=timeout)
+ except TimeoutExpired:
+ timeouted = True
+ proc.kill()
+ (output, err_output) = proc.communicate()
+ logfile.write('Command:\n{0}\n{1}{2}\nReturn code: {3}\n'.format(
+ _CommandListToCommandString(cmd), err_output, output,
+ 'TIMEOUT' if timeouted else proc.returncode))
+ ret_code = 1 if timeouted else proc.returncode
+ return (output, err_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):
+ """Runs command in environment.
+
+ Args:
+ cmd: string, command to run.
+
+ Returns:
+ tuple (string, string, int) stdout output, stderr output, return code.
+ """
+
+ @abc.abstractproperty
+ def classpath(self):
+ """Gets environment specific classpath with test class."""
+
+ @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, classpath, x64):
+ """Constructor.
+
+ Args:
+ classpath: string, classpath with test class.
+ x64: boolean, whether to setup in x64 mode.
+ """
+ self._classpath = classpath
+ 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['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):
+ self._EmptyDexCache()
+ return _RunCommandForOutputAndLog(cmd, self._shell_env, self._logfile)
+
+ @property
+ def classpath(self):
+ return self._classpath
+
+ @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, classpath):
+ """Constructor.
+
+ Args:
+ classpath: string, classpath with test class.
+ """
+ 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._classpath = os.path.join(
+ self._device_env_path, os.path.basename(classpath))
+ self._shell_env = os.environ
+
+ 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)
+
+ 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)
+ self._classpath = ':'.join(device_paths)
+
+ 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)
+ self._AdbPush(temp_file.name, file_path)
+ return
+
+ def RunCommand(self, cmd):
+ self._EmptyDexCache()
+ cmd = _CommandListToCommandString(cmd)
+ cmd = ('adb shell "logcat -c && ANDROID_DATA={0} {1} && '
+ 'logcat -d dex2oat:* *:S 1>&2"').format(self._device_env_path, cmd)
+ return _RunCommandForOutputAndLog(shlex.split(cmd), self._shell_env,
+ self._logfile)
+
+ @property
+ def classpath(self):
+ return self._classpath
+
+ @property
+ def logfile(self):
+ return self._logfile
+
+ 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)