You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
628 lines
28 KiB
628 lines
28 KiB
#!/usr/bin/env python3
|
|
# Copyright 2016 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.
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
import textwrap
|
|
import re
|
|
import sys
|
|
import shlex
|
|
|
|
import itertools
|
|
|
|
import subprocess
|
|
|
|
from absl.testing import parameterized
|
|
|
|
from fruit_test_config import *
|
|
|
|
from absl.testing import absltest
|
|
|
|
run_under_valgrind = RUN_TESTS_UNDER_VALGRIND.lower() not in ('false', 'off', 'no', '0', '')
|
|
|
|
def pretty_print_command(command, env):
|
|
return 'cd %s; env -i %s %s' % (
|
|
shlex.quote(os.getcwd()),
|
|
' '.join('%s=%s' % (var_name, shlex.quote(value)) for var_name, value in env.items() if var_name != 'PWD'),
|
|
' '.join(shlex.quote(x) for x in command))
|
|
|
|
def multiple_parameters(*param_lists):
|
|
param_lists = [[params if isinstance(params, tuple) else (params,)
|
|
for params in param_list]
|
|
for param_list in param_lists]
|
|
result = param_lists[0]
|
|
for param_list in param_lists[1:]:
|
|
result = [(*args1, *args2)
|
|
for args1 in result
|
|
for args2 in param_list]
|
|
return parameterized.parameters(*result)
|
|
|
|
def multiple_named_parameters(*param_lists):
|
|
result = param_lists[0]
|
|
for param_list in param_lists[1:]:
|
|
result = [(name1 + ', ' + name2, *args1, *args2)
|
|
for name1, *args1 in result
|
|
for name2, *args2 in param_list]
|
|
return parameterized.named_parameters(*result)
|
|
|
|
class CommandFailedException(Exception):
|
|
def __init__(self, command, env, stdout, stderr, error_code):
|
|
self.command = command
|
|
self.env = env
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
self.error_code = error_code
|
|
|
|
def __str__(self):
|
|
return textwrap.dedent('''\
|
|
Ran command: {command}
|
|
Exit code {error_code}
|
|
Stdout:
|
|
{stdout}
|
|
|
|
Stderr:
|
|
{stderr}
|
|
''').format(command=pretty_print_command(self.command, self.env), error_code=self.error_code, stdout=self.stdout, stderr=self.stderr)
|
|
|
|
def run_command(executable, args=[], modify_env=lambda env: env):
|
|
command = [executable] + args
|
|
modified_env = modify_env(os.environ)
|
|
print('Executing command:', pretty_print_command(command, modified_env))
|
|
try:
|
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=modified_env)
|
|
(stdout, stderr) = p.communicate()
|
|
except Exception as e:
|
|
raise Exception("While executing: %s" % command)
|
|
if p.returncode != 0:
|
|
raise CommandFailedException(command, modified_env, stdout, stderr, p.returncode)
|
|
print('Execution successful.')
|
|
print('stdout:')
|
|
print(stdout)
|
|
print('')
|
|
print('stderr:')
|
|
print(stderr)
|
|
print('')
|
|
return (stdout, stderr)
|
|
|
|
def run_compiled_executable(executable):
|
|
if run_under_valgrind:
|
|
args = VALGRIND_FLAGS.split() + [executable]
|
|
run_command('valgrind', args = args, modify_env = modify_env_for_compiled_executables)
|
|
else:
|
|
run_command(executable, modify_env = modify_env_for_compiled_executables)
|
|
|
|
class CompilationFailedException(Exception):
|
|
def __init__(self, command, env, error_message):
|
|
self.command = command
|
|
self.env = env
|
|
self.error_message = error_message
|
|
|
|
def __str__(self):
|
|
return textwrap.dedent('''\
|
|
Ran command: {command}
|
|
Error message:
|
|
{error_message}
|
|
''').format(command=pretty_print_command(self.command, self.env), error_message=self.error_message)
|
|
|
|
class PosixCompiler:
|
|
def __init__(self):
|
|
self.executable = CXX
|
|
self.name = CXX_COMPILER_NAME
|
|
|
|
def compile_discarding_output(self, source, include_dirs, args=[]):
|
|
try:
|
|
args = args + ['-c', source, '-o', os.path.devnull]
|
|
self._compile(include_dirs, args=args)
|
|
except CommandFailedException as e:
|
|
raise CompilationFailedException(e.command, e.env, e.stderr)
|
|
|
|
def compile_and_link(self, source, include_dirs, output_file_name, args=[]):
|
|
self._compile(
|
|
include_dirs,
|
|
args = (
|
|
[source]
|
|
+ ADDITIONAL_LINKER_FLAGS.split()
|
|
+ args
|
|
+ ['-o', output_file_name]
|
|
))
|
|
|
|
def _compile(self, include_dirs, args):
|
|
include_flags = ['-I%s' % include_dir for include_dir in include_dirs]
|
|
args = (
|
|
FRUIT_COMPILE_FLAGS.split()
|
|
+ include_flags
|
|
+ ['-g0', '-Werror']
|
|
+ args
|
|
)
|
|
run_command(self.executable, args)
|
|
|
|
def get_disable_deprecation_warning_flags(self):
|
|
return ['-Wno-deprecated-declarations']
|
|
|
|
def get_disable_all_warnings_flags(self):
|
|
return ['-Wno-error']
|
|
|
|
class MsvcCompiler:
|
|
def __init__(self):
|
|
self.executable = CXX
|
|
self.name = CXX_COMPILER_NAME
|
|
|
|
def compile_discarding_output(self, source, include_dirs, args=[]):
|
|
try:
|
|
args = args + ['/c', source]
|
|
self._compile(include_dirs, args = args)
|
|
except CommandFailedException as e:
|
|
# Note that we use stdout here, unlike above. MSVC reports compilation warnings and errors on stdout.
|
|
raise CompilationFailedException(e.command, e.env, e.stdout)
|
|
|
|
def compile_and_link(self, source, include_dirs, output_file_name, args=[]):
|
|
self._compile(
|
|
include_dirs,
|
|
args = (
|
|
[source]
|
|
+ ADDITIONAL_LINKER_FLAGS.split()
|
|
+ args
|
|
+ ['/Fe' + output_file_name]
|
|
))
|
|
|
|
def _compile(self, include_dirs, args):
|
|
include_flags = ['-I%s' % include_dir for include_dir in include_dirs]
|
|
args = (
|
|
FRUIT_COMPILE_FLAGS.split()
|
|
+ include_flags
|
|
+ ['/WX']
|
|
+ args
|
|
)
|
|
run_command(self.executable, args)
|
|
|
|
def get_disable_deprecation_warning_flags(self):
|
|
return ['/wd4996']
|
|
|
|
def get_disable_all_warnings_flags(self):
|
|
return ['/WX:NO']
|
|
|
|
if CXX_COMPILER_NAME == 'MSVC':
|
|
compiler = MsvcCompiler()
|
|
if PATH_TO_COMPILED_FRUIT_LIB.endswith('.dll'):
|
|
path_to_fruit_lib = PATH_TO_COMPILED_FRUIT_LIB[:-4] + '.lib'
|
|
else:
|
|
path_to_fruit_lib = PATH_TO_COMPILED_FRUIT_LIB
|
|
if PATH_TO_COMPILED_TEST_HEADERS_LIB.endswith('.dll'):
|
|
path_to_test_headers_lib = PATH_TO_COMPILED_TEST_HEADERS_LIB[:-4] + '.lib'
|
|
else:
|
|
path_to_test_headers_lib = PATH_TO_COMPILED_TEST_HEADERS_LIB
|
|
fruit_tests_linker_flags = [path_to_fruit_lib, path_to_test_headers_lib]
|
|
fruit_error_message_extraction_regex = 'error C2338: (.*)'
|
|
else:
|
|
compiler = PosixCompiler()
|
|
fruit_tests_linker_flags = [
|
|
'-lfruit',
|
|
'-ltest_headers_copy',
|
|
'-L' + PATH_TO_COMPILED_FRUIT,
|
|
'-Wl,-rpath,' + PATH_TO_COMPILED_FRUIT,
|
|
'-L' + PATH_TO_COMPILED_TEST_HEADERS,
|
|
'-Wl,-rpath,' + PATH_TO_COMPILED_TEST_HEADERS,
|
|
]
|
|
fruit_error_message_extraction_regex = 'static.assert(.*)'
|
|
|
|
fruit_tests_include_dirs = ADDITIONAL_INCLUDE_DIRS.splitlines() + [
|
|
PATH_TO_FRUIT_TEST_HEADERS,
|
|
PATH_TO_FRUIT_STATIC_HEADERS,
|
|
PATH_TO_FRUIT_GENERATED_HEADERS,
|
|
]
|
|
|
|
_assert_helper = unittest.TestCase()
|
|
|
|
def modify_env_for_compiled_executables(env):
|
|
env = env.copy()
|
|
path_to_fruit_lib_dir = os.path.dirname(PATH_TO_COMPILED_FRUIT_LIB)
|
|
path_to_fruit_test_headers_dir = os.path.dirname(PATH_TO_COMPILED_TEST_HEADERS_LIB)
|
|
print('PATH_TO_COMPILED_FRUIT_LIB:', PATH_TO_COMPILED_FRUIT_LIB)
|
|
print('PATH_TO_COMPILED_TEST_HEADERS_LIB:', PATH_TO_COMPILED_TEST_HEADERS_LIB)
|
|
print('Adding directory to PATH:', path_to_fruit_lib_dir)
|
|
print('Adding directory to PATH:', path_to_fruit_test_headers_dir)
|
|
env["PATH"] += os.pathsep + path_to_fruit_lib_dir + os.pathsep + path_to_fruit_test_headers_dir
|
|
return env
|
|
|
|
def _create_temporary_file(file_content, file_name_suffix=''):
|
|
file_descriptor, file_name = tempfile.mkstemp(text=True, suffix=file_name_suffix)
|
|
file = os.fdopen(file_descriptor, mode='w')
|
|
file.write(file_content)
|
|
file.close()
|
|
return file_name
|
|
|
|
def _cap_to_lines(s, n):
|
|
lines = s.splitlines()
|
|
if len(lines) <= n:
|
|
return s
|
|
else:
|
|
return '\n'.join(lines[0:n] + ['...'])
|
|
|
|
def _replace_using_test_params(s, test_params):
|
|
for var_name, value in test_params.items():
|
|
if isinstance(value, str):
|
|
s = re.sub(r'\b%s\b' % var_name, value, s)
|
|
return s
|
|
|
|
def _construct_final_source_code(setup_source_code, source_code, test_params):
|
|
setup_source_code = textwrap.dedent(setup_source_code)
|
|
source_code = textwrap.dedent(source_code)
|
|
source_code = _replace_using_test_params(source_code, test_params)
|
|
return setup_source_code + source_code
|
|
|
|
def try_remove_temporary_file(filename):
|
|
try:
|
|
os.remove(filename)
|
|
except:
|
|
# When running Fruit tests on Windows using Appveyor, the remove command fails for temporary files sometimes.
|
|
# This shouldn't cause the tests to fail, so we ignore the exception and go ahead.
|
|
pass
|
|
|
|
def normalize_error_message_lines(lines):
|
|
# Different compilers output a different number of spaces when pretty-printing types.
|
|
# When using libc++, sometimes std::foo identifiers are reported as std::__1::foo.
|
|
return [line.replace(' ', '').replace('std::__1::', 'std::') for line in lines]
|
|
|
|
def expect_compile_error_helper(
|
|
check_error_fun,
|
|
setup_source_code,
|
|
source_code,
|
|
test_params={},
|
|
ignore_deprecation_warnings=False,
|
|
ignore_warnings=False):
|
|
source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
|
|
|
|
source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
|
|
|
|
try:
|
|
args = []
|
|
if ignore_deprecation_warnings:
|
|
args += compiler.get_disable_deprecation_warning_flags()
|
|
if ignore_warnings:
|
|
args += compiler.get_disable_all_warnings_flags()
|
|
if ENABLE_COVERAGE:
|
|
# When collecting coverage these arguments are enabled by default; however we must disable them in tests
|
|
# expected to fail at compile-time because GCC would otherwise fail with an error like:
|
|
# /tmp/tmp4m22cey7.cpp:1:0: error: cannot open /dev/null.gcno
|
|
args += ['-fno-profile-arcs', '-fno-test-coverage']
|
|
compiler.compile_discarding_output(
|
|
source=source_file_name,
|
|
include_dirs=fruit_tests_include_dirs,
|
|
args=args)
|
|
raise Exception('The test should have failed to compile, but it compiled successfully')
|
|
except CompilationFailedException as e1:
|
|
e = e1
|
|
|
|
error_message = e.error_message
|
|
error_message_lines = error_message.splitlines()
|
|
error_message_head = _cap_to_lines(error_message, 40)
|
|
|
|
check_error_fun(e, error_message_lines, error_message_head)
|
|
|
|
try_remove_temporary_file(source_file_name)
|
|
|
|
def apply_any_error_context_replacements(error_string, following_lines):
|
|
if CXX_COMPILER_NAME == 'MSVC':
|
|
# MSVC errors are of the form:
|
|
#
|
|
# C:\Path\To\header\foo.h(59): note: see reference to class template instantiation 'fruit::impl::NoBindingFoundError<fruit::Annotated<Annotation,U>>' being compiled
|
|
# with
|
|
# [
|
|
# Annotation=Annotation1,
|
|
# U=std::function<std::unique_ptr<ScalerImpl,std::default_delete<ScalerImpl>> (double)>
|
|
# ]
|
|
#
|
|
# So we need to parse the following few lines and use them to replace the placeholder types in the Fruit error type.
|
|
replacement_lines = []
|
|
if len(following_lines) >= 4 and following_lines[0].strip() == 'with':
|
|
assert following_lines[1].strip() == '[', 'Line was: ' + following_lines[1]
|
|
for line in itertools.islice(following_lines, 2, None):
|
|
line = line.strip()
|
|
if line == ']':
|
|
break
|
|
if line.endswith(','):
|
|
line = line[:-1]
|
|
replacement_lines.append(line)
|
|
|
|
for replacement_line in replacement_lines:
|
|
match = re.search('([A-Za-z0-9_-]*)=(.*)', replacement_line)
|
|
if not match:
|
|
raise Exception('Failed to parse replacement line: %s' % replacement_line)
|
|
(type_variable, type_expression) = match.groups()
|
|
error_string = re.sub(r'\b' + type_variable + r'\b', type_expression, error_string)
|
|
return error_string
|
|
|
|
def expect_generic_compile_error(expected_error_regex, setup_source_code, source_code, test_params={}):
|
|
"""
|
|
Tests that the given source produces the expected error during compilation.
|
|
|
|
:param expected_fruit_error_regex: A regex used to match the Fruit error type,
|
|
e.g. 'NoBindingFoundForAbstractClassError<ScalerImpl>'.
|
|
Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
|
|
:param expected_fruit_error_desc_regex: A regex used to match the Fruit error description,
|
|
e.g. 'No explicit binding was found for C, and C is an abstract class'.
|
|
:param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
|
|
*not* subject to test_params, unlike source_code.
|
|
:param source_code: The second part of the source code. Any identifiers will be replaced using test_params
|
|
(where a replacement is defined). This will be dedented.
|
|
:param test_params: A dict containing the definition of some identifiers. Each identifier in
|
|
expected_fruit_error_regex and source_code will be replaced (textually) with its definition (if a definition
|
|
was provided).
|
|
"""
|
|
|
|
expected_error_regex = _replace_using_test_params(expected_error_regex, test_params)
|
|
expected_error_regex = expected_error_regex.replace(' ', '')
|
|
|
|
def check_error(e, error_message_lines, error_message_head):
|
|
error_message_lines_with_replacements = [
|
|
apply_any_error_context_replacements(line, error_message_lines[line_number + 1:])
|
|
for line_number, line in enumerate(error_message_lines)]
|
|
|
|
normalized_error_message_lines = normalize_error_message_lines(error_message_lines_with_replacements)
|
|
|
|
for line in normalized_error_message_lines:
|
|
if re.search(expected_error_regex, line):
|
|
return
|
|
raise Exception(textwrap.dedent('''\
|
|
Expected error {expected_error} but the compiler output did not contain that.
|
|
Compiler command line: {compiler_command}
|
|
Error message was:
|
|
{error_message}
|
|
''').format(expected_error = expected_error_regex, compiler_command=e.command, error_message = error_message_head))
|
|
|
|
expect_compile_error_helper(check_error, setup_source_code, source_code, test_params)
|
|
|
|
def expect_compile_error(
|
|
expected_fruit_error_regex,
|
|
expected_fruit_error_desc_regex,
|
|
setup_source_code,
|
|
source_code,
|
|
test_params={},
|
|
ignore_deprecation_warnings=False,
|
|
ignore_warnings=False,
|
|
disable_error_line_number_check=False):
|
|
"""
|
|
Tests that the given source produces the expected error during compilation.
|
|
|
|
:param expected_fruit_error_regex: A regex used to match the Fruit error type,
|
|
e.g. 'NoBindingFoundForAbstractClassError<ScalerImpl>'.
|
|
Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
|
|
:param expected_fruit_error_desc_regex: A regex used to match the Fruit error description,
|
|
e.g. 'No explicit binding was found for C, and C is an abstract class'.
|
|
:param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
|
|
*not* subject to test_params, unlike source_code.
|
|
:param source_code: The second part of the source code. Any identifiers will be replaced using test_params
|
|
(where a replacement is defined). This will be dedented.
|
|
:param test_params: A dict containing the definition of some identifiers. Each identifier in
|
|
expected_fruit_error_regex and source_code will be replaced (textually) with its definition (if a definition
|
|
was provided).
|
|
:param ignore_deprecation_warnings: A boolean. If True, deprecation warnings will be ignored.
|
|
:param ignore_warnings: A boolean. If True, all warnings will be ignored.
|
|
:param disable_error_line_number_check: A boolean. If True, the test will not fail if there are other diagnostic
|
|
lines before the expected error.
|
|
"""
|
|
if '\n' in expected_fruit_error_regex:
|
|
raise Exception('expected_fruit_error_regex should not contain newlines')
|
|
if '\n' in expected_fruit_error_desc_regex:
|
|
raise Exception('expected_fruit_error_desc_regex should not contain newlines')
|
|
|
|
expected_fruit_error_regex = _replace_using_test_params(expected_fruit_error_regex, test_params)
|
|
expected_fruit_error_regex = expected_fruit_error_regex.replace(' ', '')
|
|
|
|
def check_error(e, error_message_lines, error_message_head):
|
|
normalized_error_message_lines = normalize_error_message_lines(error_message_lines)
|
|
|
|
for line_number, line in enumerate(normalized_error_message_lines):
|
|
match = re.search('fruit::impl::(.*Error<.*>)', line)
|
|
if match:
|
|
actual_fruit_error_line_number = line_number
|
|
actual_fruit_error = match.groups()[0]
|
|
actual_fruit_error = apply_any_error_context_replacements(actual_fruit_error, normalized_error_message_lines[line_number + 1:])
|
|
break
|
|
else:
|
|
raise Exception(textwrap.dedent('''\
|
|
Expected error {expected_error} but the compiler output did not contain user-facing Fruit errors.
|
|
Compiler command line: {compiler_command}
|
|
Error message was:
|
|
{error_message}
|
|
''').format(expected_error = expected_fruit_error_regex, compiler_command = e.command, error_message = error_message_head))
|
|
|
|
for line_number, line in enumerate(error_message_lines):
|
|
match = re.search(fruit_error_message_extraction_regex, line)
|
|
if match:
|
|
actual_static_assert_error_line_number = line_number
|
|
actual_static_assert_error = match.groups()[0]
|
|
break
|
|
else:
|
|
raise Exception(textwrap.dedent('''\
|
|
Expected error {expected_error} but the compiler output did not contain static_assert errors.
|
|
Compiler command line: {compiler_command}
|
|
Error message was:
|
|
{error_message}
|
|
''').format(expected_error = expected_fruit_error_regex, compiler_command=e.command, error_message = error_message_head))
|
|
|
|
try:
|
|
regex_search_result = re.search(expected_fruit_error_regex, actual_fruit_error)
|
|
except Exception as e:
|
|
raise Exception('re.search() failed for regex \'%s\'' % expected_fruit_error_regex) from e
|
|
if not regex_search_result:
|
|
raise Exception(textwrap.dedent('''\
|
|
The compilation failed as expected, but with a different error type.
|
|
Expected Fruit error type: {expected_fruit_error_regex}
|
|
Error type was: {actual_fruit_error}
|
|
Expected static assert error: {expected_fruit_error_desc_regex}
|
|
Static assert was: {actual_static_assert_error}
|
|
Error message was:
|
|
{error_message}
|
|
'''.format(
|
|
expected_fruit_error_regex = expected_fruit_error_regex,
|
|
actual_fruit_error = actual_fruit_error,
|
|
expected_fruit_error_desc_regex = expected_fruit_error_desc_regex,
|
|
actual_static_assert_error = actual_static_assert_error,
|
|
error_message = error_message_head)))
|
|
try:
|
|
regex_search_result = re.search(expected_fruit_error_desc_regex, actual_static_assert_error)
|
|
except Exception as e:
|
|
raise Exception('re.search() failed for regex \'%s\'' % expected_fruit_error_desc_regex) from e
|
|
if not regex_search_result:
|
|
raise Exception(textwrap.dedent('''\
|
|
The compilation failed as expected, but with a different error message.
|
|
Expected Fruit error type: {expected_fruit_error_regex}
|
|
Error type was: {actual_fruit_error}
|
|
Expected static assert error: {expected_fruit_error_desc_regex}
|
|
Static assert was: {actual_static_assert_error}
|
|
Error message:
|
|
{error_message}
|
|
'''.format(
|
|
expected_fruit_error_regex = expected_fruit_error_regex,
|
|
actual_fruit_error = actual_fruit_error,
|
|
expected_fruit_error_desc_regex = expected_fruit_error_desc_regex,
|
|
actual_static_assert_error = actual_static_assert_error,
|
|
error_message = error_message_head)))
|
|
|
|
# 6 is just a constant that works for both g++ (<=6.0.0 at least) and clang++ (<=4.0.0 at least).
|
|
# It might need to be changed.
|
|
if not disable_error_line_number_check and (actual_fruit_error_line_number > 6 or actual_static_assert_error_line_number > 6):
|
|
raise Exception(textwrap.dedent('''\
|
|
The compilation failed with the expected message, but the error message contained too many lines before the relevant ones.
|
|
The error type was reported on line {actual_fruit_error_line_number} of the message (should be <=6).
|
|
The static assert was reported on line {actual_static_assert_error_line_number} of the message (should be <=6).
|
|
Error message:
|
|
{error_message}
|
|
'''.format(
|
|
actual_fruit_error_line_number = actual_fruit_error_line_number,
|
|
actual_static_assert_error_line_number = actual_static_assert_error_line_number,
|
|
error_message = error_message_head)))
|
|
|
|
for line in error_message_lines[:max(actual_fruit_error_line_number, actual_static_assert_error_line_number)]:
|
|
if re.search('fruit::impl::meta', line):
|
|
raise Exception(
|
|
'The compilation failed with the expected message, but the error message contained some metaprogramming types in the output (besides Error). Error message:\n%s' + error_message_head)
|
|
|
|
expect_compile_error_helper(check_error, setup_source_code, source_code, test_params, ignore_deprecation_warnings, ignore_warnings)
|
|
|
|
|
|
def expect_runtime_error(
|
|
expected_error_regex,
|
|
setup_source_code,
|
|
source_code,
|
|
test_params={},
|
|
ignore_deprecation_warnings=False):
|
|
"""
|
|
Tests that the given source (compiles successfully and) produces the expected error at runtime.
|
|
|
|
:param expected_error_regex: A regex used to match the content of stderr.
|
|
Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
|
|
:param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
|
|
*not* subject to test_params, unlike source_code.
|
|
:param source_code: The second part of the source code. Any identifiers will be replaced using test_params
|
|
(where a replacement is defined). This will be dedented.
|
|
:param test_params: A dict containing the definition of some identifiers. Each identifier in
|
|
expected_error_regex and source_code will be replaced (textually) with its definition (if a definition
|
|
was provided).
|
|
"""
|
|
expected_error_regex = _replace_using_test_params(expected_error_regex, test_params)
|
|
source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
|
|
|
|
source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
|
|
executable_suffix = {'posix': '', 'nt': '.exe'}[os.name]
|
|
output_file_name = _create_temporary_file('', executable_suffix)
|
|
|
|
args = fruit_tests_linker_flags.copy()
|
|
if ignore_deprecation_warnings:
|
|
args += compiler.get_disable_deprecation_warning_flags()
|
|
compiler.compile_and_link(
|
|
source=source_file_name,
|
|
include_dirs=fruit_tests_include_dirs,
|
|
output_file_name=output_file_name,
|
|
args=args)
|
|
|
|
try:
|
|
run_compiled_executable(output_file_name)
|
|
raise Exception('The test should have failed at runtime, but it ran successfully')
|
|
except CommandFailedException as e1:
|
|
e = e1
|
|
|
|
stderr = e.stderr
|
|
stderr_head = _cap_to_lines(stderr, 40)
|
|
|
|
if '\n' in expected_error_regex:
|
|
regex_flags = re.MULTILINE
|
|
else:
|
|
regex_flags = 0
|
|
|
|
try:
|
|
regex_search_result = re.search(expected_error_regex, stderr, flags=regex_flags)
|
|
except Exception as e:
|
|
raise Exception('re.search() failed for regex \'%s\'' % expected_error_regex) from e
|
|
if not regex_search_result:
|
|
raise Exception(textwrap.dedent('''\
|
|
The test failed as expected, but with a different message.
|
|
Expected: {expected_error_regex}
|
|
Was:
|
|
{stderr}
|
|
'''.format(expected_error_regex = expected_error_regex, stderr = stderr_head)))
|
|
|
|
# Note that we don't delete the temporary files if the test failed. This is intentional, keeping them around helps debugging the failure.
|
|
if not ENABLE_COVERAGE:
|
|
try_remove_temporary_file(source_file_name)
|
|
try_remove_temporary_file(output_file_name)
|
|
|
|
|
|
def expect_success(setup_source_code, source_code, test_params={}, ignore_deprecation_warnings=False):
|
|
"""
|
|
Tests that the given source compiles and runs successfully.
|
|
|
|
:param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
|
|
*not* subject to test_params, unlike source_code.
|
|
:param source_code: The second part of the source code. Any identifiers will be replaced using test_params
|
|
(where a replacement is defined). This will be dedented.
|
|
:param test_params: A dict containing the definition of some identifiers. Each identifier in
|
|
source_code will be replaced (textually) with its definition (if a definition was provided).
|
|
"""
|
|
source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
|
|
|
|
if 'main(' not in source_code:
|
|
source_code += textwrap.dedent('''
|
|
int main() {
|
|
}
|
|
''')
|
|
|
|
source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
|
|
executable_suffix = {'posix': '', 'nt': '.exe'}[os.name]
|
|
output_file_name = _create_temporary_file('', executable_suffix)
|
|
|
|
args = fruit_tests_linker_flags.copy()
|
|
if ignore_deprecation_warnings:
|
|
args += compiler.get_disable_deprecation_warning_flags()
|
|
compiler.compile_and_link(
|
|
source=source_file_name,
|
|
include_dirs=fruit_tests_include_dirs,
|
|
output_file_name=output_file_name,
|
|
args=args)
|
|
|
|
run_compiled_executable(output_file_name)
|
|
|
|
# Note that we don't delete the temporary files if the test failed. This is intentional, keeping them around helps debugging the failure.
|
|
if not ENABLE_COVERAGE:
|
|
try_remove_temporary_file(source_file_name)
|
|
try_remove_temporary_file(output_file_name)
|
|
|
|
|
|
# Note: this is not the main function of this file, it's meant to be used as main function from test_*.py files.
|
|
def main():
|
|
absltest.main(*sys.argv)
|