| # Copyright 2012 Google Inc. All Rights Reserved. |
| # |
| # 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. |
| |
| """Wrapper for running a unittest under Application Verifier.""" |
| |
| import logging |
| import optparse |
| import os |
| import re |
| import subprocess |
| import sys |
| import verifier |
| |
| |
| _THIRD_PARTY = os.path.abspath(os.path.join(os.path.dirname(__file__), |
| '..', '..', 'third_party')) |
| sys.path.append(_THIRD_PARTY) |
| import colorama |
| |
| |
| _LOGGER = logging.getLogger(os.path.basename(__file__)) |
| |
| # A list of per-test Application Verifier checks to not run. |
| _DISABLED_CHECKS = { |
| 'agent_common_unittests.exe': [ |
| # We have a test that deliberately causes an exception which is caught and |
| # handled by the code under test. However, AV propogates this exception and |
| # launches a modal dialog window, which causes the test to timeout. |
| 'Exceptions' |
| ], |
| } |
| |
| # A list of per-test Application Verifier exceptions. |
| _EXCEPTIONS = { |
| 'basic_block_entry_unittests.exe': [ |
| # This leak occurs due to a leaky global variable in ScopedHandle. |
| ('Error', 'Leak', 2304, '.*::BasicBlockEntryTest::UnloadDll'), |
| # This leak occurs due to a leaky global lock in ScopedHandle. |
| ('Error', 'Locks', 513, '.*::BasicBlockEntryTest::UnloadDll'), |
| # This is a known (semi-intentional) leak of the TLS index and the last |
| # active thread's TLS data on module unload. |
| ('Error', 'TLS', 848, '.*::BasicBlockEntryTest::UnloadDll'), |
| ], |
| 'coverage_unittests.exe': [ |
| # This leak occurs due to a leaky global variable in ScopedHandle. |
| ('Error', 'Leak', 2304, '.*::CoverageClientTest::UnloadDll'), |
| # This leak occurs only in Debug, which leaks a thread local variable |
| # used to check thread restrictions. |
| ('Error', 'TLS', 848, '.*::CoverageClientTest::UnloadDll'), |
| ], |
| 'instrument_unittests.exe': [ |
| # The ASAN runtime ends up freeing a heap while holding it's critical |
| # section. |
| ('Error', 'Locks', 513, '.*::PELibUnitTest::CheckTestDll'), |
| # This leak occurs due to a leaky global lock in ScopedHandle. |
| ('Error', 'Locks', 514, '.*::PELibUnitTest::CheckTestDll'), |
| # This leak occurs only in Debug, which leaks a thread local variable |
| # used to check thread restrictions. |
| ('Error', 'TLS', 848, '.*::PELibUnitTest::CheckTestDll'), |
| ], |
| 'parse_unittests.exe': [ |
| # This leak occurs due to a leaky global variable in ScopedHandle. |
| ('Error', 'Leak', 2304, '.*::ParseEngineRpcTest::UnloadCallTraceDll'), |
| # This leak occurs only in Debug, which leaks a thread local variable |
| # used to check thread restrictions. |
| ('Error', 'TLS', 848, '.*::ParseEngineRpcTest::UnloadCallTraceDll'), |
| ], |
| 'profile_unittests.exe': [ |
| # This leak occurs due to a leaky global variable in ScopedHandle. |
| ('Error', 'Leak', 2304, '.*::ProfilerTest::UnloadDll'), |
| ('Error', 'Leak', 2305, '.*::ProfilerTest::UnloadDll'), |
| # This leak occurs due to a leaky global lock in ScopedHandle. |
| ('Error', 'Locks', 513, 'agent::profiler::.*::ProfilerTest::UnloadDll'), |
| # This leak occurs only in Debug, which leaks a thread local variable |
| # used to check thread restrictions. |
| ('Error', 'TLS', 848, 'agent::profiler::.*::ProfilerTest::UnloadDll'), |
| ], |
| } |
| |
| |
| # A list of unittests that should not be run under the application verifier at |
| # all. |
| _BLACK_LIST = [ |
| # These can't be run under AppVerifier because we end up double hooking the |
| # operating system heap function, leading to nonsense. |
| 'integration_tests.exe', |
| 'syzyasan_rtl_unittests.exe', |
| ] |
| |
| |
| class Error(Exception): |
| """Base class used for exceptions thrown in this module.""" |
| pass |
| |
| |
| def Colorize(text): |
| """Colorizes the given app verifier output with ANSI color codes.""" |
| fore = colorama.Fore |
| style = colorama.Style |
| def _ColorizeLine(line): |
| line = re.sub('^(Error.*:)(.*)', |
| style.BRIGHT + fore.RED + '\\1' + fore.YELLOW + '\\2' + |
| style.RESET_ALL, |
| line) |
| line = re.sub('^(Warning:)(.*)', |
| style.BRIGHT + fore.YELLOW + '\\1' + style.RESET_ALL + '\\2', |
| line) |
| return line |
| |
| return '\n'.join([_ColorizeLine(line) for line in text.split('\n')]) |
| |
| |
| def FilterExceptions(image_name, errors): |
| """Filter out the Application Verifier errors that have exceptions.""" |
| exceptions = _EXCEPTIONS.get(image_name, []) |
| |
| def _HasNoException(error): |
| # Iterate over all the exceptions. |
| for (severity, layer, stopcode, regexp) in exceptions: |
| # And see if they match, first by type. |
| if (error.severity == severity and |
| error.layer == layer and |
| error.stopcode == stopcode): |
| # And then by regexpr match to the trace symbols. |
| for trace in error.trace: |
| if trace.symbol and re.match(regexp, trace.symbol): |
| return False |
| |
| return True |
| |
| filtered_errors = filter(_HasNoException, errors) |
| error_count = len(filtered_errors) |
| filtered_count = len(errors) - error_count |
| |
| if error_count: |
| suffix = '' if error_count == 1 else 's' |
| filtered_errors.append( |
| 'Error: Encountered %d AppVerifier exception%s for %s.' % |
| (error_count, suffix, image_name)) |
| |
| if filtered_count: |
| suffix1 = '' if filtered_count == 1 else 's' |
| suffix2 = '' if len(exceptions) == 1 else 's' |
| filtered_errors.append( |
| 'Warning: Filtered %d AppVerifier exception%s for %s using %d rule%s.' % |
| (filtered_count, suffix1, image_name, len(exceptions), suffix2)) |
| |
| return (error_count, filtered_errors) |
| |
| |
| def _RunUnderAppVerifier(command): |
| runner = verifier.AppverifierTestRunner(False) |
| image_path = os.path.abspath(command[0]) |
| image_name = os.path.basename(image_path) |
| disabled_checks = _DISABLED_CHECKS.get(image_name, []) |
| |
| if not os.path.isfile(image_path): |
| raise Error('Path not found: %s' % image_path) |
| |
| # Set up the verifier configuration. |
| runner.SetImageDefaults(image_name, disabled_checks=disabled_checks) |
| runner.ClearImageLogs(image_name) |
| |
| # Run the executable. We disable exception catching as it interferes with |
| # Application Verifier. |
| command = [image_path] + command[1:] + ['--gtest_catch_exceptions=0'] |
| _LOGGER.info('Running %s.', command) |
| popen = subprocess.Popen(command) |
| (dummy_stdout, dummy_stderr) = popen.communicate() |
| |
| # Process the AppVerifier logs, filtering exceptions. |
| app_verifier_errors = runner.ProcessLogs(image_name) |
| (error_count, app_verifier_errors) = FilterExceptions( |
| image_name, app_verifier_errors) |
| |
| # Generate warnings for error categories that were disabled. |
| for check in disabled_checks: |
| app_verifier_errors.append( |
| 'Warning: Disabled AppVerifier %s checks.' % check) |
| |
| # Output all warnings and errors. |
| for error in app_verifier_errors: |
| msg = Colorize(str(error) + '\n') |
| sys.stderr.write(msg) |
| |
| # Clear the verifier settings for the image. |
| runner.ClearImageLogs(image_name) |
| runner.ResetImage(image_name) |
| |
| if popen.returncode: |
| _LOGGER.error('%s failed with return code %d.', image_name, |
| popen.returncode) |
| |
| if error_count: |
| suffix = '' if error_count == 1 else 's' |
| _LOGGER.error('%s failed AppVerifier test with %d exception%s.', |
| image_name, error_count, suffix) |
| |
| |
| if popen.returncode: |
| return popen.returncode |
| |
| return error_count |
| |
| |
| def _RunNormally(command): |
| # We reset the image settings so that AppVerifier isn't left incidentally |
| # configured. |
| runner = verifier.AppverifierTestRunner(False) |
| image_path = os.path.abspath(command[0]) |
| image_name = os.path.basename(image_path) |
| runner.ClearImageLogs(image_name) |
| runner.ResetImage(image_name) |
| |
| image_path = os.path.abspath(command[0]) |
| command = [image_path] + command[1:] |
| _LOGGER.info('Running %s outside of AppVerifier.' % command) |
| popen = subprocess.Popen(command) |
| (dummy_stdout, dummy_stderr) = popen.communicate() |
| |
| # To be consistent with _RunUnderAppVerifier we output warnings at the end. |
| sys.stderr.write(Colorize( |
| 'Warning: AppVerifier was disabled for this test.\n')) |
| return popen.returncode |
| |
| |
| _USAGE = '%prog [options] APPLICATION -- [application options]' |
| |
| |
| def _IsBlacklisted(command): |
| image_base = os.path.basename(command[0]) |
| if image_base in _BLACK_LIST: |
| _LOGGER.info('Executable is blacklisted: %s.' % image_base) |
| return True |
| return False |
| |
| |
| def _ParseArgs(): |
| parser = optparse.OptionParser(usage=_USAGE) |
| parser.add_option('-v', '--verbose', dest='verbose', |
| action='store_true', default=False, |
| help='Enable verbose logging.') |
| parser.add_option('--on-waterfall', dest='on_waterfall', |
| action='store_true', default=False, |
| help='Indicate that we are running on the waterfall.') |
| (opts, args) = parser.parse_args() |
| |
| if not len(args): |
| parser.error('You must specify an application.') |
| |
| if opts.verbose: |
| logging.basicConfig(level=logging.INFO) |
| else: |
| logging.basicConfig(level=logging.ERROR) |
| |
| return (opts, args) |
| |
| |
| def Main(): |
| colorama.init() |
| (opts, args) = _ParseArgs() |
| |
| if _IsBlacklisted(args): |
| return_code = _RunNormally(args) |
| else: |
| return_code = _RunUnderAppVerifier(args) |
| if return_code and opts.on_waterfall: |
| command = [args[0]] + ['--'] + args[1:] |
| command = 'python build\\app_verifier.py %s' % ' '.join(command) |
| sys.stderr.write('To reproduce this error locally run the following ' |
| 'command from the Syzygy root directory:\n') |
| sys.stderr.write(command + '\n') |
| |
| sys.exit(return_code) |
| |
| |
| if __name__ == '__main__': |
| Main() |