blob: 5f527b804b55c39aa6d8f80685e215c536076424 [file] [log] [blame]
Aart Bik7593b992016-08-17 16:51:12 -07001#!/usr/bin/env python2
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
17import abc
18import argparse
19import subprocess
20import sys
21import os
22
23from tempfile import mkdtemp
24from threading import Timer
25
26# Normalized return codes.
27EXIT_SUCCESS = 0
28EXIT_TIMEOUT = 1
29EXIT_NOTCOMPILED = 2
30EXIT_NOTRUN = 3
31
32#
33# Utility methods.
34#
35
36def RunCommand(cmd, args, out, err, timeout = 5):
37 """Executes a command, and returns its return code.
38
39 Args:
40 cmd: string, a command to execute
41 args: string, arguments to pass to command (or None)
42 out: string, file name to open for stdout (or None)
43 err: string, file name to open for stderr (or None)
44 timeout: int, time out in seconds
45 Returns:
46 return code of running command (forced EXIT_TIMEOUT on timeout)
47 """
48 cmd = 'exec ' + cmd # preserve pid
49 if args != None:
50 cmd = cmd + ' ' + args
51 outf = None
52 if out != None:
53 outf = open(out, mode='w')
54 errf = None
55 if err != None:
56 errf = open(err, mode='w')
57 proc = subprocess.Popen(cmd, stdout=outf, stderr=errf, shell=True)
58 timer = Timer(timeout, proc.kill) # enforces timeout
59 timer.start()
60 proc.communicate()
61 if timer.is_alive():
62 timer.cancel()
63 returncode = proc.returncode
64 else:
65 returncode = EXIT_TIMEOUT
66 if outf != None:
67 outf.close()
68 if errf != None:
69 errf.close()
70 return returncode
71
72def GetJackClassPath():
73 """Returns Jack's classpath."""
74 top = os.environ.get('ANDROID_BUILD_TOP')
75 if top == None:
76 raise FatalError('Cannot find AOSP build top')
77 libdir = top + '/out/host/common/obj/JAVA_LIBRARIES'
78 return libdir + '/core-libart-hostdex_intermediates/classes.jack:' \
79 + libdir + '/core-oj-hostdex_intermediates/classes.jack'
80
Aart Bikb16d4132016-08-19 15:45:11 -070081def GetExecutionModeRunner(device, mode):
Aart Bik7593b992016-08-17 16:51:12 -070082 """Returns a runner for the given execution mode.
83
84 Args:
Aart Bikb16d4132016-08-19 15:45:11 -070085 device: string, target device serial number (or None)
Aart Bik7593b992016-08-17 16:51:12 -070086 mode: string, execution mode
87 Returns:
88 TestRunner with given execution mode
89 Raises:
90 FatalError: error for unknown execution mode
91 """
92 if mode == 'ri':
93 return TestRunnerRIOnHost()
94 if mode == 'hint':
95 return TestRunnerArtOnHost(True)
96 if mode == 'hopt':
97 return TestRunnerArtOnHost(False)
98 if mode == 'tint':
Aart Bikb16d4132016-08-19 15:45:11 -070099 return TestRunnerArtOnTarget(device, True)
Aart Bik7593b992016-08-17 16:51:12 -0700100 if mode == 'topt':
Aart Bikb16d4132016-08-19 15:45:11 -0700101 return TestRunnerArtOnTarget(device, False)
Aart Bik7593b992016-08-17 16:51:12 -0700102 raise FatalError('Unknown execution mode')
103
104def GetReturnCode(retc):
105 """Returns a string representation of the given normalized return code.
106 Args:
107 retc: int, normalized return code
108 Returns:
109 string representation of normalized return code
110 Raises:
111 FatalError: error for unknown normalized return code
112 """
113 if retc == EXIT_SUCCESS:
114 return 'SUCCESS'
115 if retc == EXIT_TIMEOUT:
116 return 'TIMED-OUT'
117 if retc == EXIT_NOTCOMPILED:
118 return 'NOT-COMPILED'
119 if retc == EXIT_NOTRUN:
120 return 'NOT-RUN'
121 raise FatalError('Unknown normalized return code')
122
123#
124# Execution mode classes.
125#
126
127class TestRunner(object):
128 """Abstraction for running a test in a particular execution mode."""
129 __meta_class__ = abc.ABCMeta
130
131 def GetDescription(self):
132 """Returns a description string of the execution mode."""
133 return self._description
134
135 def GetId(self):
136 """Returns a short string that uniquely identifies the execution mode."""
137 return self._id
138
139 @abc.abstractmethod
140 def CompileAndRunTest(self):
141 """Compile and run the generated test.
142
143 Ensures that the current Test.java in the temporary directory is compiled
144 and executed under the current execution mode. On success, transfers the
145 generated output to the file GetId()_out.txt in the temporary directory.
146 Cleans up after itself.
147
148 Most nonzero return codes are assumed non-divergent, since systems may
149 exit in different ways. This is enforced by normalizing return codes.
150
151 Returns:
152 normalized return code
153 """
154 pass
155
156class TestRunnerRIOnHost(TestRunner):
157 """Concrete test runner of the reference implementation on host."""
158
159 def __init__(self):
160 """Constructor for the RI tester."""
161 self._description = 'RI on host'
162 self._id = 'RI'
163
164 def CompileAndRunTest(self):
165 if RunCommand('javac', 'Test.java',
166 out=None, err=None, timeout=30) == EXIT_SUCCESS:
167 retc = RunCommand('java', 'Test', 'RI_run_out.txt', err=None)
168 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
169 retc = EXIT_NOTRUN
170 else:
171 retc = EXIT_NOTCOMPILED
172 # Cleanup and return.
173 RunCommand('rm', '-f Test.class', out=None, err=None)
174 return retc
175
176class TestRunnerArtOnHost(TestRunner):
177 """Concrete test runner of Art on host (interpreter or optimizing)."""
178
179 def __init__(self, interpreter):
180 """Constructor for the Art on host tester.
181
182 Args:
183 interpreter: boolean, selects between interpreter or optimizing
184 """
185 self._art_args = '-cp classes.dex Test'
186 if interpreter:
187 self._description = 'Art interpreter on host'
188 self._id = 'HInt'
189 self._art_args = '-Xint ' + self._art_args
190 else:
191 self._description = 'Art optimizing on host'
192 self._id = 'HOpt'
193 self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
194
195 def CompileAndRunTest(self):
196 if RunCommand('jack', self._jack_args,
197 out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
198 out = self.GetId() + '_run_out.txt'
199 retc = RunCommand('art', self._art_args, out, 'arterr.txt')
200 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
201 retc = EXIT_NOTRUN
202 else:
203 retc = EXIT_NOTCOMPILED
204 # Cleanup and return.
205 RunCommand('rm', '-rf classes.dex jackerr.txt arterr.txt android-data*',
206 out=None, err=None)
207 return retc
208
209# TODO: very rough first version without proper cache,
210# reuse staszkiewicz' module for properly setting up dalvikvm on target.
211class TestRunnerArtOnTarget(TestRunner):
212 """Concrete test runner of Art on target (interpreter or optimizing)."""
213
Aart Bikb16d4132016-08-19 15:45:11 -0700214 def __init__(self, device, interpreter):
Aart Bik7593b992016-08-17 16:51:12 -0700215 """Constructor for the Art on target tester.
216
217 Args:
Aart Bikb16d4132016-08-19 15:45:11 -0700218 device: string, target device serial number (or None)
Aart Bik7593b992016-08-17 16:51:12 -0700219 interpreter: boolean, selects between interpreter or optimizing
220 """
Aart Bikb16d4132016-08-19 15:45:11 -0700221 self._dalvik_args = 'shell dalvikvm -cp /data/local/tmp/classes.dex Test'
Aart Bik7593b992016-08-17 16:51:12 -0700222 if interpreter:
223 self._description = 'Art interpreter on target'
224 self._id = 'TInt'
225 self._dalvik_args = '-Xint ' + self._dalvik_args
226 else:
227 self._description = 'Art optimizing on target'
228 self._id = 'TOpt'
Aart Bikb16d4132016-08-19 15:45:11 -0700229 self._adb = 'adb'
230 if device != None:
231 self._adb = self._adb + ' -s ' + device
Aart Bik7593b992016-08-17 16:51:12 -0700232 self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
233
234 def CompileAndRunTest(self):
235 if RunCommand('jack', self._jack_args,
236 out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
Aart Bikb16d4132016-08-19 15:45:11 -0700237 if RunCommand(self._adb, 'push classes.dex /data/local/tmp/',
Aart Bik7593b992016-08-17 16:51:12 -0700238 'adb.txt', err=None) != EXIT_SUCCESS:
239 raise FatalError('Cannot push to target device')
240 out = self.GetId() + '_run_out.txt'
Aart Bikb16d4132016-08-19 15:45:11 -0700241 retc = RunCommand(self._adb, self._dalvik_args, out, err=None)
Aart Bik7593b992016-08-17 16:51:12 -0700242 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
243 retc = EXIT_NOTRUN
244 else:
245 retc = EXIT_NOTCOMPILED
246 # Cleanup and return.
247 RunCommand('rm', '-f classes.dex jackerr.txt adb.txt',
248 out=None, err=None)
Aart Bikb16d4132016-08-19 15:45:11 -0700249 RunCommand(self._adb, 'shell rm -f /data/local/tmp/classes.dex',
Aart Bik7593b992016-08-17 16:51:12 -0700250 out=None, err=None)
251 return retc
252
253#
254# Tester classes.
255#
256
257class FatalError(Exception):
258 """Fatal error in the tester."""
259 pass
260
261class JavaFuzzTester(object):
262 """Tester that runs JavaFuzz many times and report divergences."""
263
Aart Bikb16d4132016-08-19 15:45:11 -0700264 def __init__(self, num_tests, device, mode1, mode2):
Aart Bik7593b992016-08-17 16:51:12 -0700265 """Constructor for the tester.
266
267 Args:
268 num_tests: int, number of tests to run
Aart Bikb16d4132016-08-19 15:45:11 -0700269 device: string, target device serial number (or None)
Aart Bik7593b992016-08-17 16:51:12 -0700270 mode1: string, execution mode for first runner
271 mode2: string, execution mode for second runner
272 """
273 self._num_tests = num_tests
Aart Bikb16d4132016-08-19 15:45:11 -0700274 self._device = device
275 self._runner1 = GetExecutionModeRunner(device, mode1)
276 self._runner2 = GetExecutionModeRunner(device, mode2)
Aart Bik7593b992016-08-17 16:51:12 -0700277 self._save_dir = None
278 self._tmp_dir = None
279 # Statistics.
280 self._test = 0
281 self._num_success = 0
282 self._num_not_compiled = 0
283 self._num_not_run = 0
284 self._num_timed_out = 0
285 self._num_divergences = 0
286
287 def __enter__(self):
288 """On entry, enters new temp directory after saving current directory.
289
290 Raises:
291 FatalError: error when temp directory cannot be constructed
292 """
293 self._save_dir = os.getcwd()
294 self._tmp_dir = mkdtemp(dir="/tmp/")
295 if self._tmp_dir == None:
296 raise FatalError('Cannot obtain temp directory')
297 os.chdir(self._tmp_dir)
298 return self
299
300 def __exit__(self, etype, evalue, etraceback):
301 """On exit, re-enters previously saved current directory and cleans up."""
302 os.chdir(self._save_dir)
303 if self._num_divergences == 0:
304 RunCommand('rm', '-rf ' + self._tmp_dir, out=None, err=None)
305
306 def Run(self):
307 """Runs JavaFuzz many times and report divergences."""
308 print
309 print '**\n**** JavaFuzz Testing\n**'
310 print
311 print '#Tests :', self._num_tests
Aart Bikb16d4132016-08-19 15:45:11 -0700312 print 'Device :', self._device
Aart Bik7593b992016-08-17 16:51:12 -0700313 print 'Directory :', self._tmp_dir
314 print 'Exec-mode1:', self._runner1.GetDescription()
315 print 'Exec-mode2:', self._runner2.GetDescription()
316 print
317 self.ShowStats()
318 for self._test in range(1, self._num_tests + 1):
319 self.RunJavaFuzzTest()
320 self.ShowStats()
321 if self._num_divergences == 0:
322 print '\n\nsuccess (no divergences)\n'
323 else:
324 print '\n\nfailure (divergences)\n'
325
326 def ShowStats(self):
327 """Shows current statistics (on same line) while tester is running."""
328 print '\rTests:', self._test, \
329 'Success:', self._num_success, \
330 'Not-compiled:', self._num_not_compiled, \
331 'Not-run:', self._num_not_run, \
332 'Timed-out:', self._num_timed_out, \
333 'Divergences:', self._num_divergences,
334 sys.stdout.flush()
335
336 def RunJavaFuzzTest(self):
337 """Runs a single JavaFuzz test, comparing two execution modes."""
338 self.ConstructTest()
339 retc1 = self._runner1.CompileAndRunTest()
340 retc2 = self._runner2.CompileAndRunTest()
341 self.CheckForDivergence(retc1, retc2)
342 self.CleanupTest()
343
344 def ConstructTest(self):
345 """Use JavaFuzz to generate next Test.java test.
346
347 Raises:
348 FatalError: error when javafuzz fails
349 """
350 if RunCommand('javafuzz', args=None,
351 out='Test.java', err=None) != EXIT_SUCCESS:
352 raise FatalError('Unexpected error while running JavaFuzz')
353
354 def CheckForDivergence(self, retc1, retc2):
355 """Checks for divergences and updates statistics.
356
357 Args:
358 retc1: int, normalized return code of first runner
359 retc2: int, normalized return code of second runner
360 """
361 if retc1 == retc2:
362 # Non-divergent in return code.
363 if retc1 == EXIT_SUCCESS:
364 # Both compilations and runs were successful, inspect generated output.
365 args = self._runner1.GetId() + '_run_out.txt ' \
366 + self._runner2.GetId() + '_run_out.txt'
367 if RunCommand('diff', args, out=None, err=None) != EXIT_SUCCESS:
368 self.ReportDivergence('divergence in output')
369 else:
370 self._num_success += 1
371 elif retc1 == EXIT_TIMEOUT:
372 self._num_timed_out += 1
373 elif retc1 == EXIT_NOTCOMPILED:
374 self._num_not_compiled += 1
375 else:
376 self._num_not_run += 1
377 else:
378 # Divergent in return code.
379 self.ReportDivergence('divergence in return code: ' +
380 GetReturnCode(retc1) + ' vs. ' +
381 GetReturnCode(retc2))
382
383 def ReportDivergence(self, reason):
384 """Reports and saves a divergence."""
385 self._num_divergences += 1
386 print '\n', self._test, reason
387 # Save.
388 ddir = 'divergence' + str(self._test)
389 RunCommand('mkdir', ddir, out=None, err=None)
390 RunCommand('mv', 'Test.java *.txt ' + ddir, out=None, err=None)
391
392 def CleanupTest(self):
393 """Cleans up after a single test run."""
394 RunCommand('rm', '-f Test.java *.txt', out=None, err=None)
395
396
397def main():
398 # Handle arguments.
399 parser = argparse.ArgumentParser()
400 parser.add_argument('--num_tests', default=10000,
401 type=int, help='number of tests to run')
Aart Bikb16d4132016-08-19 15:45:11 -0700402 parser.add_argument('--device', help='target device serial number')
Aart Bik7593b992016-08-17 16:51:12 -0700403 parser.add_argument('--mode1', default='ri',
404 help='execution mode 1 (default: ri)')
405 parser.add_argument('--mode2', default='hopt',
406 help='execution mode 2 (default: hopt)')
407 args = parser.parse_args()
408 if args.mode1 == args.mode2:
409 raise FatalError("Identical execution modes given")
410 # Run the JavaFuzz tester.
Aart Bikb16d4132016-08-19 15:45:11 -0700411 with JavaFuzzTester(args.num_tests, args.device,
412 args.mode1, args.mode2) as fuzzer:
Aart Bik7593b992016-08-17 16:51:12 -0700413 fuzzer.Run()
414
415if __name__ == "__main__":
416 main()