blob: 110ef82433508939b7faaa0ddf52d02f3a5db5f8 [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"""Performs bisection bug search on methods and optimizations.
18
19See README.md.
20
21Example usage:
22./bisection-search.py -cp classes.dex --expected-output output Test
23"""
24
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070025import abc
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070026import argparse
27import re
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070028import shlex
29from subprocess import call
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070030import sys
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070031from tempfile import NamedTemporaryFile
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070032
33from common import DeviceTestEnv
34from common import FatalError
35from common import GetEnvVariableOrError
36from common import HostTestEnv
37
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070038
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070039# Passes that are never disabled during search process because disabling them
40# would compromise correctness.
41MANDATORY_PASSES = ['dex_cache_array_fixups_arm',
42 'dex_cache_array_fixups_mips',
43 'instruction_simplifier$before_codegen',
44 'pc_relative_fixups_x86',
45 'pc_relative_fixups_mips',
46 'x86_memory_operand_generation']
47
48# Passes that show up as optimizations in compiler verbose output but aren't
49# driven by run-passes mechanism. They are mandatory and will always run, we
50# never pass them to --run-passes.
51NON_PASSES = ['builder', 'prepare_for_register_allocation',
52 'liveness', 'register']
53
54
55class Dex2OatWrapperTestable(object):
56 """Class representing a testable compilation.
57
58 Accepts filters on compiled methods and optimization passes.
59 """
60
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070061 def __init__(self, base_cmd, test_env, output_checker=None, verbose=False):
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070062 """Constructor.
63
64 Args:
65 base_cmd: list of strings, base command to run.
66 test_env: ITestEnv.
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070067 output_checker: IOutputCheck, output checker.
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070068 verbose: bool, enable verbose output.
69 """
70 self._base_cmd = base_cmd
71 self._test_env = test_env
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070072 self._output_checker = output_checker
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070073 self._compiled_methods_path = self._test_env.CreateFile('compiled_methods')
74 self._passes_to_run_path = self._test_env.CreateFile('run_passes')
75 self._verbose = verbose
76
77 def Test(self, compiled_methods, passes_to_run=None):
78 """Tests compilation with compiled_methods and run_passes switches active.
79
80 If compiled_methods is None then compiles all methods.
81 If passes_to_run is None then runs default passes.
82
83 Args:
84 compiled_methods: list of strings representing methods to compile or None.
85 passes_to_run: list of strings representing passes to run or None.
86
87 Returns:
88 True if test passes with given settings. False otherwise.
89 """
90 if self._verbose:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070091 print('Testing methods: {0} passes: {1}.'.format(
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -070092 compiled_methods, passes_to_run))
93 cmd = self._PrepareCmd(compiled_methods=compiled_methods,
94 passes_to_run=passes_to_run,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -070095 verbose_compiler=False)
96 (output, ret_code) = self._test_env.RunCommand(
97 cmd, {'ANDROID_LOG_TAGS': '*:e'})
98 res = ((self._output_checker is None and ret_code == 0)
99 or self._output_checker.Check(output))
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700100 if self._verbose:
101 print('Test passed: {0}.'.format(res))
102 return res
103
104 def GetAllMethods(self):
105 """Get methods compiled during the test.
106
107 Returns:
108 List of strings representing methods compiled during the test.
109
110 Raises:
111 FatalError: An error occurred when retrieving methods list.
112 """
113 cmd = self._PrepareCmd(verbose_compiler=True)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700114 (output, _) = self._test_env.RunCommand(cmd, {'ANDROID_LOG_TAGS': '*:i'})
115 match_methods = re.findall(r'Building ([^\n]+)\n', output)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700116 if not match_methods:
117 raise FatalError('Failed to retrieve methods list. '
118 'Not recognized output format.')
119 return match_methods
120
121 def GetAllPassesForMethod(self, compiled_method):
122 """Get all optimization passes ran for a method during the test.
123
124 Args:
125 compiled_method: string representing method to compile.
126
127 Returns:
128 List of strings representing passes ran for compiled_method during test.
129
130 Raises:
131 FatalError: An error occurred when retrieving passes list.
132 """
133 cmd = self._PrepareCmd(compiled_methods=[compiled_method],
134 verbose_compiler=True)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700135 (output, _) = self._test_env.RunCommand(cmd, {'ANDROID_LOG_TAGS': '*:i'})
136 match_passes = re.findall(r'Starting pass: ([^\n]+)\n', output)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700137 if not match_passes:
138 raise FatalError('Failed to retrieve passes list. '
139 'Not recognized output format.')
140 return [p for p in match_passes if p not in NON_PASSES]
141
142 def _PrepareCmd(self, compiled_methods=None, passes_to_run=None,
143 verbose_compiler=False):
144 """Prepare command to run."""
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700145 cmd = [self._base_cmd[0]]
146 # insert additional arguments
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700147 if compiled_methods is not None:
148 self._test_env.WriteLines(self._compiled_methods_path, compiled_methods)
149 cmd += ['-Xcompiler-option', '--compiled-methods={0}'.format(
150 self._compiled_methods_path)]
151 if passes_to_run is not None:
152 self._test_env.WriteLines(self._passes_to_run_path, passes_to_run)
153 cmd += ['-Xcompiler-option', '--run-passes={0}'.format(
154 self._passes_to_run_path)]
155 if verbose_compiler:
156 cmd += ['-Xcompiler-option', '--runtime-arg', '-Xcompiler-option',
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700157 '-verbose:compiler', '-Xcompiler-option', '-j1']
158 cmd += self._base_cmd[1:]
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700159 return cmd
160
161
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700162class IOutputCheck(object):
163 """Abstract output checking class.
164
165 Checks if output is correct.
166 """
167 __meta_class__ = abc.ABCMeta
168
169 @abc.abstractmethod
170 def Check(self, output):
171 """Check if output is correct.
172
173 Args:
174 output: string, output to check.
175
176 Returns:
177 boolean, True if output is correct, False otherwise.
178 """
179
180
181class EqualsOutputCheck(IOutputCheck):
182 """Concrete output checking class checking for equality to expected output."""
183
184 def __init__(self, expected_output):
185 """Constructor.
186
187 Args:
188 expected_output: string, expected output.
189 """
190 self._expected_output = expected_output
191
192 def Check(self, output):
193 """See base class."""
194 return self._expected_output == output
195
196
197class ExternalScriptOutputCheck(IOutputCheck):
198 """Concrete output checking class calling an external script.
199
200 The script should accept two arguments, path to expected output and path to
201 program output. It should exit with 0 return code if outputs are equivalent
202 and with different return code otherwise.
203 """
204
205 def __init__(self, script_path, expected_output_path, logfile):
206 """Constructor.
207
208 Args:
209 script_path: string, path to checking script.
210 expected_output_path: string, path to file with expected output.
211 logfile: file handle, logfile.
212 """
213 self._script_path = script_path
214 self._expected_output_path = expected_output_path
215 self._logfile = logfile
216
217 def Check(self, output):
218 """See base class."""
219 ret_code = None
220 with NamedTemporaryFile(mode='w', delete=False) as temp_file:
221 temp_file.write(output)
222 temp_file.flush()
223 ret_code = call(
224 [self._script_path, self._expected_output_path, temp_file.name],
225 stdout=self._logfile, stderr=self._logfile, universal_newlines=True)
226 return ret_code == 0
227
228
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700229def BinarySearch(start, end, test):
230 """Binary search integers using test function to guide the process."""
231 while start < end:
232 mid = (start + end) // 2
233 if test(mid):
234 start = mid + 1
235 else:
236 end = mid
237 return start
238
239
240def FilterPasses(passes, cutoff_idx):
241 """Filters passes list according to cutoff_idx but keeps mandatory passes."""
242 return [opt_pass for idx, opt_pass in enumerate(passes)
243 if opt_pass in MANDATORY_PASSES or idx < cutoff_idx]
244
245
246def BugSearch(testable):
247 """Find buggy (method, optimization pass) pair for a given testable.
248
249 Args:
250 testable: Dex2OatWrapperTestable.
251
252 Returns:
253 (string, string) tuple. First element is name of method which when compiled
254 exposes test failure. Second element is name of optimization pass such that
255 for aforementioned method running all passes up to and excluding the pass
256 results in test passing but running all passes up to and including the pass
257 results in test failing.
258
259 (None, None) if test passes when compiling all methods.
260 (string, None) if a method is found which exposes the failure, but the
261 failure happens even when running just mandatory passes.
262
263 Raises:
264 FatalError: Testable fails with no methods compiled.
265 AssertionError: Method failed for all passes when bisecting methods, but
266 passed when bisecting passes. Possible sporadic failure.
267 """
268 all_methods = testable.GetAllMethods()
269 faulty_method_idx = BinarySearch(
270 0,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700271 len(all_methods) + 1,
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700272 lambda mid: testable.Test(all_methods[0:mid]))
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700273 if faulty_method_idx == len(all_methods) + 1:
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700274 return (None, None)
275 if faulty_method_idx == 0:
276 raise FatalError('Testable fails with no methods compiled. '
277 'Perhaps issue lies outside of compiler.')
278 faulty_method = all_methods[faulty_method_idx - 1]
279 all_passes = testable.GetAllPassesForMethod(faulty_method)
280 faulty_pass_idx = BinarySearch(
281 0,
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700282 len(all_passes) + 1,
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700283 lambda mid: testable.Test([faulty_method],
284 FilterPasses(all_passes, mid)))
285 if faulty_pass_idx == 0:
286 return (faulty_method, None)
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700287 assert faulty_pass_idx != len(all_passes) + 1, ('Method must fail for some '
288 'passes.')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700289 faulty_pass = all_passes[faulty_pass_idx - 1]
290 return (faulty_method, faulty_pass)
291
292
293def PrepareParser():
294 """Prepares argument parser."""
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700295 parser = argparse.ArgumentParser(
296 description='Tool for finding compiler bugs. Either --raw-cmd or both '
297 '-cp and --class are required.')
298 command_opts = parser.add_argument_group('dalvikvm command options')
299 command_opts.add_argument('-cp', '--classpath', type=str, help='classpath')
300 command_opts.add_argument('--class', dest='classname', type=str,
301 help='name of main class')
302 command_opts.add_argument('--lib', dest='lib', type=str, default='libart.so',
303 help='lib to use, default: libart.so')
304 command_opts.add_argument('--dalvikvm-option', dest='dalvikvm_opts',
305 metavar='OPT', nargs='*', default=[],
306 help='additional dalvikvm option')
307 command_opts.add_argument('--arg', dest='test_args', nargs='*', default=[],
308 metavar='ARG', help='argument passed to test')
309 command_opts.add_argument('--image', type=str, help='path to image')
310 command_opts.add_argument('--raw-cmd', dest='raw_cmd', type=str,
311 help='bisect with this command, ignore other '
312 'command options')
313 bisection_opts = parser.add_argument_group('bisection options')
314 bisection_opts.add_argument('--64', dest='x64', action='store_true',
315 default=False, help='x64 mode')
316 bisection_opts.add_argument(
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700317 '--device', action='store_true', default=False, help='run on device')
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700318 bisection_opts.add_argument('--expected-output', type=str,
319 help='file containing expected output')
320 bisection_opts.add_argument(
321 '--check-script', dest='check_script', type=str,
322 help='script comparing output and expected output')
323 bisection_opts.add_argument('--verbose', action='store_true',
324 default=False, help='enable verbose output')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700325 return parser
326
327
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700328def PrepareBaseCommand(args, classpath):
329 """Prepares base command used to run test."""
330 if args.raw_cmd:
331 return shlex.split(args.raw_cmd)
332 else:
333 base_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
334 if not args.device:
335 base_cmd += ['-XXlib:{0}'.format(args.lib)]
336 if not args.image:
337 image_path = '{0}/framework/core-optimizing-pic.art'.format(
338 GetEnvVariableOrError('ANDROID_HOST_OUT'))
339 else:
340 image_path = args.image
341 base_cmd += ['-Ximage:{0}'.format(image_path)]
342 if args.dalvikvm_opts:
343 base_cmd += args.dalvikvm_opts
344 base_cmd += ['-cp', classpath, args.classname] + args.test_args
345 return base_cmd
346
347
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700348def main():
349 # Parse arguments
350 parser = PrepareParser()
351 args = parser.parse_args()
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700352 if not args.raw_cmd and (not args.classpath or not args.classname):
353 parser.error('Either --raw-cmd or both -cp and --class are required')
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700354
355 # Prepare environment
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700356 classpath = args.classpath
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700357 if args.device:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700358 test_env = DeviceTestEnv()
359 if classpath:
360 classpath = test_env.PushClasspath(classpath)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700361 else:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700362 test_env = HostTestEnv(args.x64)
363 base_cmd = PrepareBaseCommand(args, classpath)
364 output_checker = None
365 if args.expected_output:
366 if args.check_script:
367 output_checker = ExternalScriptOutputCheck(
368 args.check_script, args.expected_output, test_env.logfile)
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700369 else:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700370 with open(args.expected_output, 'r') as expected_output_file:
371 output_checker = EqualsOutputCheck(expected_output_file.read())
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700372
373 # Perform the search
374 try:
Wojciech Staszkiewicz86379942016-09-01 14:36:13 -0700375 testable = Dex2OatWrapperTestable(base_cmd, test_env, output_checker,
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -0700376 args.verbose)
377 (method, opt_pass) = BugSearch(testable)
378 except Exception as e:
379 print('Error. Refer to logfile: {0}'.format(test_env.logfile.name))
380 test_env.logfile.write('Exception: {0}\n'.format(e))
381 raise
382
383 # Report results
384 if method is None:
385 print('Couldn\'t find any bugs.')
386 elif opt_pass is None:
387 print('Faulty method: {0}. Fails with just mandatory passes.'.format(
388 method))
389 else:
390 print('Faulty method and pass: {0}, {1}.'.format(method, opt_pass))
391 print('Logfile: {0}'.format(test_env.logfile.name))
392 sys.exit(0)
393
394
395if __name__ == '__main__':
396 main()