OSDN Git Service

Bisection bug search tool
authorWojciech Staszkiewicz <staszkiewicz@google.com>
Thu, 11 Aug 2016 21:04:20 +0000 (14:04 -0700)
committerWojciech Staszkiewicz <staszkiewicz@google.com>
Thu, 25 Aug 2016 23:03:48 +0000 (16:03 -0700)
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

tools/bisection-search/README.md [new file with mode: 0644]
tools/bisection-search/bisection_search.py [new file with mode: 0755]
tools/bisection-search/bisection_test.py [new file with mode: 0755]
tools/bisection-search/common.py [new file with mode: 0755]

diff --git a/tools/bisection-search/README.md b/tools/bisection-search/README.md
new file mode 100644 (file)
index 0000000..857c930
--- /dev/null
@@ -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 (executable)
index 0000000..d6c1749
--- /dev/null
@@ -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 (executable)
index 0000000..9aa08fb
--- /dev/null
@@ -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 (executable)
index 0000000..8361fc9
--- /dev/null
@@ -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)