blob: 5f527b804b55c39aa6d8f80685e215c536076424 [file] [log] [blame]
#!/usr/bin/env python2
#
# 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.
import abc
import argparse
import subprocess
import sys
import os
from tempfile import mkdtemp
from threading import Timer
# Normalized return codes.
EXIT_SUCCESS = 0
EXIT_TIMEOUT = 1
EXIT_NOTCOMPILED = 2
EXIT_NOTRUN = 3
#
# Utility methods.
#
def RunCommand(cmd, args, out, err, timeout = 5):
"""Executes a command, and returns its return code.
Args:
cmd: string, a command to execute
args: string, arguments to pass to command (or None)
out: string, file name to open for stdout (or None)
err: string, file name to open for stderr (or None)
timeout: int, time out in seconds
Returns:
return code of running command (forced EXIT_TIMEOUT on timeout)
"""
cmd = 'exec ' + cmd # preserve pid
if args != None:
cmd = cmd + ' ' + args
outf = None
if out != None:
outf = open(out, mode='w')
errf = None
if err != None:
errf = open(err, mode='w')
proc = subprocess.Popen(cmd, stdout=outf, stderr=errf, shell=True)
timer = Timer(timeout, proc.kill) # enforces timeout
timer.start()
proc.communicate()
if timer.is_alive():
timer.cancel()
returncode = proc.returncode
else:
returncode = EXIT_TIMEOUT
if outf != None:
outf.close()
if errf != None:
errf.close()
return returncode
def GetJackClassPath():
"""Returns Jack's classpath."""
top = os.environ.get('ANDROID_BUILD_TOP')
if top == None:
raise FatalError('Cannot find AOSP build top')
libdir = top + '/out/host/common/obj/JAVA_LIBRARIES'
return libdir + '/core-libart-hostdex_intermediates/classes.jack:' \
+ libdir + '/core-oj-hostdex_intermediates/classes.jack'
def GetExecutionModeRunner(device, mode):
"""Returns a runner for the given execution mode.
Args:
device: string, target device serial number (or None)
mode: string, execution mode
Returns:
TestRunner with given execution mode
Raises:
FatalError: error for unknown execution mode
"""
if mode == 'ri':
return TestRunnerRIOnHost()
if mode == 'hint':
return TestRunnerArtOnHost(True)
if mode == 'hopt':
return TestRunnerArtOnHost(False)
if mode == 'tint':
return TestRunnerArtOnTarget(device, True)
if mode == 'topt':
return TestRunnerArtOnTarget(device, False)
raise FatalError('Unknown execution mode')
def GetReturnCode(retc):
"""Returns a string representation of the given normalized return code.
Args:
retc: int, normalized return code
Returns:
string representation of normalized return code
Raises:
FatalError: error for unknown normalized return code
"""
if retc == EXIT_SUCCESS:
return 'SUCCESS'
if retc == EXIT_TIMEOUT:
return 'TIMED-OUT'
if retc == EXIT_NOTCOMPILED:
return 'NOT-COMPILED'
if retc == EXIT_NOTRUN:
return 'NOT-RUN'
raise FatalError('Unknown normalized return code')
#
# Execution mode classes.
#
class TestRunner(object):
"""Abstraction for running a test in a particular execution mode."""
__meta_class__ = abc.ABCMeta
def GetDescription(self):
"""Returns a description string of the execution mode."""
return self._description
def GetId(self):
"""Returns a short string that uniquely identifies the execution mode."""
return self._id
@abc.abstractmethod
def CompileAndRunTest(self):
"""Compile and run the generated test.
Ensures that the current Test.java in the temporary directory is compiled
and executed under the current execution mode. On success, transfers the
generated output to the file GetId()_out.txt in the temporary directory.
Cleans up after itself.
Most nonzero return codes are assumed non-divergent, since systems may
exit in different ways. This is enforced by normalizing return codes.
Returns:
normalized return code
"""
pass
class TestRunnerRIOnHost(TestRunner):
"""Concrete test runner of the reference implementation on host."""
def __init__(self):
"""Constructor for the RI tester."""
self._description = 'RI on host'
self._id = 'RI'
def CompileAndRunTest(self):
if RunCommand('javac', 'Test.java',
out=None, err=None, timeout=30) == EXIT_SUCCESS:
retc = RunCommand('java', 'Test', 'RI_run_out.txt', err=None)
if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
retc = EXIT_NOTRUN
else:
retc = EXIT_NOTCOMPILED
# Cleanup and return.
RunCommand('rm', '-f Test.class', out=None, err=None)
return retc
class TestRunnerArtOnHost(TestRunner):
"""Concrete test runner of Art on host (interpreter or optimizing)."""
def __init__(self, interpreter):
"""Constructor for the Art on host tester.
Args:
interpreter: boolean, selects between interpreter or optimizing
"""
self._art_args = '-cp classes.dex Test'
if interpreter:
self._description = 'Art interpreter on host'
self._id = 'HInt'
self._art_args = '-Xint ' + self._art_args
else:
self._description = 'Art optimizing on host'
self._id = 'HOpt'
self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
def CompileAndRunTest(self):
if RunCommand('jack', self._jack_args,
out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
out = self.GetId() + '_run_out.txt'
retc = RunCommand('art', self._art_args, out, 'arterr.txt')
if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
retc = EXIT_NOTRUN
else:
retc = EXIT_NOTCOMPILED
# Cleanup and return.
RunCommand('rm', '-rf classes.dex jackerr.txt arterr.txt android-data*',
out=None, err=None)
return retc
# TODO: very rough first version without proper cache,
# reuse staszkiewicz' module for properly setting up dalvikvm on target.
class TestRunnerArtOnTarget(TestRunner):
"""Concrete test runner of Art on target (interpreter or optimizing)."""
def __init__(self, device, interpreter):
"""Constructor for the Art on target tester.
Args:
device: string, target device serial number (or None)
interpreter: boolean, selects between interpreter or optimizing
"""
self._dalvik_args = 'shell dalvikvm -cp /data/local/tmp/classes.dex Test'
if interpreter:
self._description = 'Art interpreter on target'
self._id = 'TInt'
self._dalvik_args = '-Xint ' + self._dalvik_args
else:
self._description = 'Art optimizing on target'
self._id = 'TOpt'
self._adb = 'adb'
if device != None:
self._adb = self._adb + ' -s ' + device
self._jack_args = '-cp ' + GetJackClassPath() + ' --output-dex . Test.java'
def CompileAndRunTest(self):
if RunCommand('jack', self._jack_args,
out=None, err='jackerr.txt', timeout=30) == EXIT_SUCCESS:
if RunCommand(self._adb, 'push classes.dex /data/local/tmp/',
'adb.txt', err=None) != EXIT_SUCCESS:
raise FatalError('Cannot push to target device')
out = self.GetId() + '_run_out.txt'
retc = RunCommand(self._adb, self._dalvik_args, out, err=None)
if retc != EXIT_SUCCESS and retc != EXIT_TIMEOUT:
retc = EXIT_NOTRUN
else:
retc = EXIT_NOTCOMPILED
# Cleanup and return.
RunCommand('rm', '-f classes.dex jackerr.txt adb.txt',
out=None, err=None)
RunCommand(self._adb, 'shell rm -f /data/local/tmp/classes.dex',
out=None, err=None)
return retc
#
# Tester classes.
#
class FatalError(Exception):
"""Fatal error in the tester."""
pass
class JavaFuzzTester(object):
"""Tester that runs JavaFuzz many times and report divergences."""
def __init__(self, num_tests, device, mode1, mode2):
"""Constructor for the tester.
Args:
num_tests: int, number of tests to run
device: string, target device serial number (or None)
mode1: string, execution mode for first runner
mode2: string, execution mode for second runner
"""
self._num_tests = num_tests
self._device = device
self._runner1 = GetExecutionModeRunner(device, mode1)
self._runner2 = GetExecutionModeRunner(device, mode2)
self._save_dir = None
self._tmp_dir = None
# Statistics.
self._test = 0
self._num_success = 0
self._num_not_compiled = 0
self._num_not_run = 0
self._num_timed_out = 0
self._num_divergences = 0
def __enter__(self):
"""On entry, enters new temp directory after saving current directory.
Raises:
FatalError: error when temp directory cannot be constructed
"""
self._save_dir = os.getcwd()
self._tmp_dir = mkdtemp(dir="/tmp/")
if self._tmp_dir == None:
raise FatalError('Cannot obtain temp directory')
os.chdir(self._tmp_dir)
return self
def __exit__(self, etype, evalue, etraceback):
"""On exit, re-enters previously saved current directory and cleans up."""
os.chdir(self._save_dir)
if self._num_divergences == 0:
RunCommand('rm', '-rf ' + self._tmp_dir, out=None, err=None)
def Run(self):
"""Runs JavaFuzz many times and report divergences."""
print
print '**\n**** JavaFuzz Testing\n**'
print
print '#Tests :', self._num_tests
print 'Device :', self._device
print 'Directory :', self._tmp_dir
print 'Exec-mode1:', self._runner1.GetDescription()
print 'Exec-mode2:', self._runner2.GetDescription()
print
self.ShowStats()
for self._test in range(1, self._num_tests + 1):
self.RunJavaFuzzTest()
self.ShowStats()
if self._num_divergences == 0:
print '\n\nsuccess (no divergences)\n'
else:
print '\n\nfailure (divergences)\n'
def ShowStats(self):
"""Shows current statistics (on same line) while tester is running."""
print '\rTests:', self._test, \
'Success:', self._num_success, \
'Not-compiled:', self._num_not_compiled, \
'Not-run:', self._num_not_run, \
'Timed-out:', self._num_timed_out, \
'Divergences:', self._num_divergences,
sys.stdout.flush()
def RunJavaFuzzTest(self):
"""Runs a single JavaFuzz test, comparing two execution modes."""
self.ConstructTest()
retc1 = self._runner1.CompileAndRunTest()
retc2 = self._runner2.CompileAndRunTest()
self.CheckForDivergence(retc1, retc2)
self.CleanupTest()
def ConstructTest(self):
"""Use JavaFuzz to generate next Test.java test.
Raises:
FatalError: error when javafuzz fails
"""
if RunCommand('javafuzz', args=None,
out='Test.java', err=None) != EXIT_SUCCESS:
raise FatalError('Unexpected error while running JavaFuzz')
def CheckForDivergence(self, retc1, retc2):
"""Checks for divergences and updates statistics.
Args:
retc1: int, normalized return code of first runner
retc2: int, normalized return code of second runner
"""
if retc1 == retc2:
# Non-divergent in return code.
if retc1 == EXIT_SUCCESS:
# Both compilations and runs were successful, inspect generated output.
args = self._runner1.GetId() + '_run_out.txt ' \
+ self._runner2.GetId() + '_run_out.txt'
if RunCommand('diff', args, out=None, err=None) != EXIT_SUCCESS:
self.ReportDivergence('divergence in output')
else:
self._num_success += 1
elif retc1 == EXIT_TIMEOUT:
self._num_timed_out += 1
elif retc1 == EXIT_NOTCOMPILED:
self._num_not_compiled += 1
else:
self._num_not_run += 1
else:
# Divergent in return code.
self.ReportDivergence('divergence in return code: ' +
GetReturnCode(retc1) + ' vs. ' +
GetReturnCode(retc2))
def ReportDivergence(self, reason):
"""Reports and saves a divergence."""
self._num_divergences += 1
print '\n', self._test, reason
# Save.
ddir = 'divergence' + str(self._test)
RunCommand('mkdir', ddir, out=None, err=None)
RunCommand('mv', 'Test.java *.txt ' + ddir, out=None, err=None)
def CleanupTest(self):
"""Cleans up after a single test run."""
RunCommand('rm', '-f Test.java *.txt', out=None, err=None)
def main():
# Handle arguments.
parser = argparse.ArgumentParser()
parser.add_argument('--num_tests', default=10000,
type=int, help='number of tests to run')
parser.add_argument('--device', help='target device serial number')
parser.add_argument('--mode1', default='ri',
help='execution mode 1 (default: ri)')
parser.add_argument('--mode2', default='hopt',
help='execution mode 2 (default: hopt)')
args = parser.parse_args()
if args.mode1 == args.mode2:
raise FatalError("Identical execution modes given")
# Run the JavaFuzz tester.
with JavaFuzzTester(args.num_tests, args.device,
args.mode1, args.mode2) as fuzzer:
fuzzer.Run()
if __name__ == "__main__":
main()