blob: 4f192e7c4407e4590357849451755d9c416508c6 [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
81def GetExecutionModeRunner(mode):
82 """Returns a runner for the given execution mode.
83
84 Args:
85 mode: string, execution mode
86 Returns:
87 TestRunner with given execution mode
88 Raises:
89 FatalError: error for unknown execution mode
90 """
91 if mode == 'ri':
92 return TestRunnerRIOnHost()
93 if mode == 'hint':
94 return TestRunnerArtOnHost(True)
95 if mode == 'hopt':
96 return TestRunnerArtOnHost(False)
97 if mode == 'tint':
98 return TestRunnerArtOnTarget(True)
99 if mode == 'topt':
100 return TestRunnerArtOnTarget(False)
101 raise FatalError('Unknown execution mode')
102
103def GetReturnCode(retc):
104 """Returns a string representation of the given normalized return code.
105 Args:
106 retc: int, normalized return code
107 Returns:
108 string representation of normalized return code
109 Raises:
110 FatalError: error for unknown normalized return code
111 """
112 if retc == EXIT_SUCCESS:
113 return 'SUCCESS'
114 if retc == EXIT_TIMEOUT:
115 return 'TIMED-OUT'
116 if retc == EXIT_NOTCOMPILED:
117 return 'NOT-COMPILED'
118 if retc == EXIT_NOTRUN:
119 return 'NOT-RUN'
120 raise FatalError('Unknown normalized return code')
121
122#
123# Execution mode classes.
124#
125
126class TestRunner(object):
127 """Abstraction for running a test in a particular execution mode."""
128 __meta_class__ = abc.ABCMeta
129
130 def GetDescription(self):
131 """Returns a description string of the execution mode."""
132 return self._description
133
134 def GetId(self):
135 """Returns a short string that uniquely identifies the execution mode."""
136 return self._id
137
138 @abc.abstractmethod
139 def CompileAndRunTest(self):
140 """Compile and run the generated test.
141
142 Ensures that the current Test.java in the temporary directory is compiled
143 and executed under the current execution mode. On success, transfers the
144 generated output to the file GetId()_out.txt in the temporary directory.
145 Cleans up after itself.
146
147 Most nonzero return codes are assumed non-divergent, since systems may
148 exit in different ways. This is enforced by normalizing return codes.
149
150 Returns:
151 normalized return code
152 """
153 pass
154
155class TestRunnerRIOnHost(TestRunner):
156 """Concrete test runner of the reference implementation on host."""
157
158 def __init__(self):
159 """Constructor for the RI tester."""
160 self._description = 'RI on host'
161 self._id = 'RI'
162
163 def CompileAndRunTest(self):
164 if RunCommand('javac', 'Test.java',
165 out=None, err=None, timeout=30) == EXIT_SUCCESS:
166 retc = RunCommand('java', 'Test', 'RI_run_out.txt', err=None)
167 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
168 retc = EXIT_NOTRUN
169 else:
170 retc = EXIT_NOTCOMPILED
171 # Cleanup and return.
172 RunCommand('rm', '-f Test.class', out=None, err=None)
173 return retc
174
175class TestRunnerArtOnHost(TestRunner):
176 """Concrete test runner of Art on host (interpreter or optimizing)."""
177
178 def __init__(self, interpreter):
179 """Constructor for the Art on host tester.
180
181 Args:
182 interpreter: boolean, selects between interpreter or optimizing
183 """
184 self._art_args = '-cp classes.dex Test'
185 if interpreter:
186 self._description = 'Art interpreter on host'
187 self._id = 'HInt'
188 self._art_args = '-Xint ' + self._art_args
189 else:
190 self._description = 'Art optimizing on host'
191 self._id = 'HOpt'
192 self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
193
194 def CompileAndRunTest(self):
195 if RunCommand('jack', self._jack_args,
196 out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
197 out = self.GetId() + '_run_out.txt'
198 retc = RunCommand('art', self._art_args, out, 'arterr.txt')
199 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
200 retc = EXIT_NOTRUN
201 else:
202 retc = EXIT_NOTCOMPILED
203 # Cleanup and return.
204 RunCommand('rm', '-rf classes.dex jackerr.txt arterr.txt android-data*',
205 out=None, err=None)
206 return retc
207
208# TODO: very rough first version without proper cache,
209# reuse staszkiewicz' module for properly setting up dalvikvm on target.
210class TestRunnerArtOnTarget(TestRunner):
211 """Concrete test runner of Art on target (interpreter or optimizing)."""
212
213 def __init__(self, interpreter):
214 """Constructor for the Art on target tester.
215
216 Args:
217 interpreter: boolean, selects between interpreter or optimizing
218 """
219 self._dalvik_args = '-cp /data/local/tmp/classes.dex Test'
220 if interpreter:
221 self._description = 'Art interpreter on target'
222 self._id = 'TInt'
223 self._dalvik_args = '-Xint ' + self._dalvik_args
224 else:
225 self._description = 'Art optimizing on target'
226 self._id = 'TOpt'
227 self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
228
229 def CompileAndRunTest(self):
230 if RunCommand('jack', self._jack_args,
231 out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
232 if RunCommand('adb push', 'classes.dex /data/local/tmp/',
233 'adb.txt', err=None) != EXIT_SUCCESS:
234 raise FatalError('Cannot push to target device')
235 out = self.GetId() + '_run_out.txt'
236 retc = RunCommand('adb shell dalvikvm', self._dalvik_args, out, err=None)
237 if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
238 retc = EXIT_NOTRUN
239 else:
240 retc = EXIT_NOTCOMPILED
241 # Cleanup and return.
242 RunCommand('rm', '-f classes.dex jackerr.txt adb.txt',
243 out=None, err=None)
244 RunCommand('adb shell', 'rm -f /data/local/tmp/classes.dex',
245 out=None, err=None)
246 return retc
247
248#
249# Tester classes.
250#
251
252class FatalError(Exception):
253 """Fatal error in the tester."""
254 pass
255
256class JavaFuzzTester(object):
257 """Tester that runs JavaFuzz many times and report divergences."""
258
259 def __init__(self, num_tests, mode1, mode2):
260 """Constructor for the tester.
261
262 Args:
263 num_tests: int, number of tests to run
264 mode1: string, execution mode for first runner
265 mode2: string, execution mode for second runner
266 """
267 self._num_tests = num_tests
268 self._runner1 = GetExecutionModeRunner(mode1)
269 self._runner2 = GetExecutionModeRunner(mode2)
270 self._save_dir = None
271 self._tmp_dir = None
272 # Statistics.
273 self._test = 0
274 self._num_success = 0
275 self._num_not_compiled = 0
276 self._num_not_run = 0
277 self._num_timed_out = 0
278 self._num_divergences = 0
279
280 def __enter__(self):
281 """On entry, enters new temp directory after saving current directory.
282
283 Raises:
284 FatalError: error when temp directory cannot be constructed
285 """
286 self._save_dir = os.getcwd()
287 self._tmp_dir = mkdtemp(dir="/tmp/")
288 if self._tmp_dir == None:
289 raise FatalError('Cannot obtain temp directory')
290 os.chdir(self._tmp_dir)
291 return self
292
293 def __exit__(self, etype, evalue, etraceback):
294 """On exit, re-enters previously saved current directory and cleans up."""
295 os.chdir(self._save_dir)
296 if self._num_divergences == 0:
297 RunCommand('rm', '-rf ' + self._tmp_dir, out=None, err=None)
298
299 def Run(self):
300 """Runs JavaFuzz many times and report divergences."""
301 print
302 print '**\n**** JavaFuzz Testing\n**'
303 print
304 print '#Tests :', self._num_tests
305 print 'Directory :', self._tmp_dir
306 print 'Exec-mode1:', self._runner1.GetDescription()
307 print 'Exec-mode2:', self._runner2.GetDescription()
308 print
309 self.ShowStats()
310 for self._test in range(1, self._num_tests + 1):
311 self.RunJavaFuzzTest()
312 self.ShowStats()
313 if self._num_divergences == 0:
314 print '\n\nsuccess (no divergences)\n'
315 else:
316 print '\n\nfailure (divergences)\n'
317
318 def ShowStats(self):
319 """Shows current statistics (on same line) while tester is running."""
320 print '\rTests:', self._test, \
321 'Success:', self._num_success, \
322 'Not-compiled:', self._num_not_compiled, \
323 'Not-run:', self._num_not_run, \
324 'Timed-out:', self._num_timed_out, \
325 'Divergences:', self._num_divergences,
326 sys.stdout.flush()
327
328 def RunJavaFuzzTest(self):
329 """Runs a single JavaFuzz test, comparing two execution modes."""
330 self.ConstructTest()
331 retc1 = self._runner1.CompileAndRunTest()
332 retc2 = self._runner2.CompileAndRunTest()
333 self.CheckForDivergence(retc1, retc2)
334 self.CleanupTest()
335
336 def ConstructTest(self):
337 """Use JavaFuzz to generate next Test.java test.
338
339 Raises:
340 FatalError: error when javafuzz fails
341 """
342 if RunCommand('javafuzz', args=None,
343 out='Test.java', err=None) != EXIT_SUCCESS:
344 raise FatalError('Unexpected error while running JavaFuzz')
345
346 def CheckForDivergence(self, retc1, retc2):
347 """Checks for divergences and updates statistics.
348
349 Args:
350 retc1: int, normalized return code of first runner
351 retc2: int, normalized return code of second runner
352 """
353 if retc1 == retc2:
354 # Non-divergent in return code.
355 if retc1 == EXIT_SUCCESS:
356 # Both compilations and runs were successful, inspect generated output.
357 args = self._runner1.GetId() + '_run_out.txt ' \
358 + self._runner2.GetId() + '_run_out.txt'
359 if RunCommand('diff', args, out=None, err=None) != EXIT_SUCCESS:
360 self.ReportDivergence('divergence in output')
361 else:
362 self._num_success += 1
363 elif retc1 == EXIT_TIMEOUT:
364 self._num_timed_out += 1
365 elif retc1 == EXIT_NOTCOMPILED:
366 self._num_not_compiled += 1
367 else:
368 self._num_not_run += 1
369 else:
370 # Divergent in return code.
371 self.ReportDivergence('divergence in return code: ' +
372 GetReturnCode(retc1) + ' vs. ' +
373 GetReturnCode(retc2))
374
375 def ReportDivergence(self, reason):
376 """Reports and saves a divergence."""
377 self._num_divergences += 1
378 print '\n', self._test, reason
379 # Save.
380 ddir = 'divergence' + str(self._test)
381 RunCommand('mkdir', ddir, out=None, err=None)
382 RunCommand('mv', 'Test.java *.txt ' + ddir, out=None, err=None)
383
384 def CleanupTest(self):
385 """Cleans up after a single test run."""
386 RunCommand('rm', '-f Test.java *.txt', out=None, err=None)
387
388
389def main():
390 # Handle arguments.
391 parser = argparse.ArgumentParser()
392 parser.add_argument('--num_tests', default=10000,
393 type=int, help='number of tests to run')
394 parser.add_argument('--mode1', default='ri',
395 help='execution mode 1 (default: ri)')
396 parser.add_argument('--mode2', default='hopt',
397 help='execution mode 2 (default: hopt)')
398 args = parser.parse_args()
399 if args.mode1 == args.mode2:
400 raise FatalError("Identical execution modes given")
401 # Run the JavaFuzz tester.
402 with JavaFuzzTester(args.num_tests, args.mode1, args.mode2) as fuzzer:
403 fuzzer.Run()
404
405if __name__ == "__main__":
406 main()