• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#  Copyright 2016 Google Inc. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS-IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import tempfile
18import unittest
19import textwrap
20import re
21import sys
22import shlex
23
24import itertools
25
26import subprocess
27
28from absl.testing import parameterized
29
30from fruit_test_config import *
31
32from absl.testing import absltest
33
34run_under_valgrind = RUN_TESTS_UNDER_VALGRIND.lower() not in ('false', 'off', 'no', '0', '')
35
36def pretty_print_command(command, env):
37    return 'cd %s; env -i %s %s' % (
38        shlex.quote(os.getcwd()),
39        ' '.join('%s=%s' % (var_name, shlex.quote(value)) for var_name, value in env.items() if var_name != 'PWD'),
40        ' '.join(shlex.quote(x) for x in command))
41
42def multiple_parameters(*param_lists):
43    param_lists = [[params if isinstance(params, tuple) else (params,)
44                    for params in param_list]
45                   for param_list in param_lists]
46    result = param_lists[0]
47    for param_list in param_lists[1:]:
48        result = [(*args1, *args2)
49                  for args1 in result
50                  for args2 in param_list]
51    return parameterized.parameters(*result)
52
53def multiple_named_parameters(*param_lists):
54    result = param_lists[0]
55    for param_list in param_lists[1:]:
56        result = [(name1 + ', ' + name2, *args1, *args2)
57                  for name1, *args1 in result
58                  for name2, *args2 in param_list]
59    return parameterized.named_parameters(*result)
60
61class CommandFailedException(Exception):
62    def __init__(self, command, env, stdout, stderr, error_code):
63        self.command = command
64        self.env = env
65        self.stdout = stdout
66        self.stderr = stderr
67        self.error_code = error_code
68
69    def __str__(self):
70        return textwrap.dedent('''\
71        Ran command: {command}
72        Exit code {error_code}
73        Stdout:
74        {stdout}
75
76        Stderr:
77        {stderr}
78        ''').format(command=pretty_print_command(self.command, self.env), error_code=self.error_code, stdout=self.stdout, stderr=self.stderr)
79
80def run_command(executable, args=[], modify_env=lambda env: env):
81    command = [executable] + args
82    modified_env = modify_env(os.environ)
83    print('Executing command:', pretty_print_command(command, modified_env))
84    try:
85        p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, env=modified_env)
86        (stdout, stderr) = p.communicate()
87    except Exception as e:
88        raise Exception("While executing: %s" % command)
89    if p.returncode != 0:
90        raise CommandFailedException(command, modified_env, stdout, stderr, p.returncode)
91    print('Execution successful.')
92    print('stdout:')
93    print(stdout)
94    print('')
95    print('stderr:')
96    print(stderr)
97    print('')
98    return (stdout, stderr)
99
100def run_compiled_executable(executable):
101    if run_under_valgrind:
102        args = VALGRIND_FLAGS.split() + [executable]
103        run_command('valgrind', args = args, modify_env = modify_env_for_compiled_executables)
104    else:
105        run_command(executable, modify_env = modify_env_for_compiled_executables)
106
107class CompilationFailedException(Exception):
108    def __init__(self, command, env, error_message):
109        self.command = command
110        self.env = env
111        self.error_message = error_message
112
113    def __str__(self):
114        return textwrap.dedent('''\
115        Ran command: {command}
116        Error message:
117        {error_message}
118        ''').format(command=pretty_print_command(self.command, self.env), error_message=self.error_message)
119
120class PosixCompiler:
121    def __init__(self):
122        self.executable = CXX
123        self.name = CXX_COMPILER_NAME
124
125    def compile_discarding_output(self, source, include_dirs, args=[]):
126        try:
127            args = args + ['-c', source, '-o', os.path.devnull]
128            self._compile(include_dirs, args=args)
129        except CommandFailedException as e:
130            raise CompilationFailedException(e.command, e.env, e.stderr)
131
132    def compile_and_link(self, source, include_dirs, output_file_name, args=[]):
133        self._compile(
134            include_dirs,
135            args = (
136                [source]
137                + ADDITIONAL_LINKER_FLAGS.split()
138                + args
139                + ['-o', output_file_name]
140            ))
141
142    def _compile(self, include_dirs, args):
143        include_flags = ['-I%s' % include_dir for include_dir in include_dirs]
144        args = (
145            FRUIT_COMPILE_FLAGS.split()
146            + include_flags
147            + ['-g0', '-Werror']
148            + args
149        )
150        run_command(self.executable, args)
151
152    def get_disable_deprecation_warning_flags(self):
153        return ['-Wno-deprecated-declarations']
154
155    def get_disable_all_warnings_flags(self):
156        return ['-Wno-error']
157
158class MsvcCompiler:
159    def __init__(self):
160        self.executable = CXX
161        self.name = CXX_COMPILER_NAME
162
163    def compile_discarding_output(self, source, include_dirs, args=[]):
164        try:
165            args = args + ['/c', source]
166            self._compile(include_dirs, args = args)
167        except CommandFailedException as e:
168            # Note that we use stdout here, unlike above. MSVC reports compilation warnings and errors on stdout.
169            raise CompilationFailedException(e.command, e.env, e.stdout)
170
171    def compile_and_link(self, source, include_dirs, output_file_name, args=[]):
172        self._compile(
173            include_dirs,
174            args = (
175                [source]
176                + ADDITIONAL_LINKER_FLAGS.split()
177                + args
178                + ['/Fe' + output_file_name]
179            ))
180
181    def _compile(self, include_dirs, args):
182        include_flags = ['-I%s' % include_dir for include_dir in include_dirs]
183        args = (
184            FRUIT_COMPILE_FLAGS.split()
185            + include_flags
186            + ['/WX']
187            + args
188        )
189        run_command(self.executable, args)
190
191    def get_disable_deprecation_warning_flags(self):
192        return ['/wd4996']
193
194    def get_disable_all_warnings_flags(self):
195        return ['/WX:NO']
196
197if CXX_COMPILER_NAME == 'MSVC':
198    compiler = MsvcCompiler()
199    if PATH_TO_COMPILED_FRUIT_LIB.endswith('.dll'):
200        path_to_fruit_lib = PATH_TO_COMPILED_FRUIT_LIB[:-4] + '.lib'
201    else:
202        path_to_fruit_lib = PATH_TO_COMPILED_FRUIT_LIB
203    if PATH_TO_COMPILED_TEST_HEADERS_LIB.endswith('.dll'):
204        path_to_test_headers_lib = PATH_TO_COMPILED_TEST_HEADERS_LIB[:-4] + '.lib'
205    else:
206        path_to_test_headers_lib = PATH_TO_COMPILED_TEST_HEADERS_LIB
207    fruit_tests_linker_flags = [path_to_fruit_lib, path_to_test_headers_lib]
208    fruit_error_message_extraction_regex = 'error C2338: (.*)'
209else:
210    compiler = PosixCompiler()
211    fruit_tests_linker_flags = [
212        '-lfruit',
213        '-ltest_headers_copy',
214        '-L' + PATH_TO_COMPILED_FRUIT,
215        '-Wl,-rpath,' + PATH_TO_COMPILED_FRUIT,
216        '-L' + PATH_TO_COMPILED_TEST_HEADERS,
217        '-Wl,-rpath,' + PATH_TO_COMPILED_TEST_HEADERS,
218    ]
219    fruit_error_message_extraction_regex = 'static.assert(.*)'
220
221fruit_tests_include_dirs = ADDITIONAL_INCLUDE_DIRS.splitlines() + [
222    PATH_TO_FRUIT_TEST_HEADERS,
223    PATH_TO_FRUIT_STATIC_HEADERS,
224    PATH_TO_FRUIT_GENERATED_HEADERS,
225]
226
227_assert_helper = unittest.TestCase()
228
229def modify_env_for_compiled_executables(env):
230    env = env.copy()
231    path_to_fruit_lib_dir = os.path.dirname(PATH_TO_COMPILED_FRUIT_LIB)
232    path_to_fruit_test_headers_dir = os.path.dirname(PATH_TO_COMPILED_TEST_HEADERS_LIB)
233    print('PATH_TO_COMPILED_FRUIT_LIB:', PATH_TO_COMPILED_FRUIT_LIB)
234    print('PATH_TO_COMPILED_TEST_HEADERS_LIB:', PATH_TO_COMPILED_TEST_HEADERS_LIB)
235    print('Adding directory to PATH:', path_to_fruit_lib_dir)
236    print('Adding directory to PATH:', path_to_fruit_test_headers_dir)
237    env["PATH"] += os.pathsep + path_to_fruit_lib_dir + os.pathsep + path_to_fruit_test_headers_dir
238    return env
239
240def _create_temporary_file(file_content, file_name_suffix=''):
241    file_descriptor, file_name = tempfile.mkstemp(text=True, suffix=file_name_suffix)
242    file = os.fdopen(file_descriptor, mode='w')
243    file.write(file_content)
244    file.close()
245    return file_name
246
247def _cap_to_lines(s, n):
248    lines = s.splitlines()
249    if len(lines) <= n:
250        return s
251    else:
252        return '\n'.join(lines[0:n] + ['...'])
253
254def _replace_using_test_params(s, test_params):
255    for var_name, value in test_params.items():
256        if isinstance(value, str):
257            s = re.sub(r'\b%s\b' % var_name, value, s)
258    return s
259
260def _construct_final_source_code(setup_source_code, source_code, test_params):
261    setup_source_code = textwrap.dedent(setup_source_code)
262    source_code = textwrap.dedent(source_code)
263    source_code = _replace_using_test_params(source_code, test_params)
264    return setup_source_code + source_code
265
266def try_remove_temporary_file(filename):
267    try:
268        os.remove(filename)
269    except:
270        # When running Fruit tests on Windows using Appveyor, the remove command fails for temporary files sometimes.
271        # This shouldn't cause the tests to fail, so we ignore the exception and go ahead.
272        pass
273
274def normalize_error_message_lines(lines):
275    # Different compilers output a different number of spaces when pretty-printing types.
276    # When using libc++, sometimes std::foo identifiers are reported as std::__1::foo.
277    return [line.replace(' ', '').replace('std::__1::', 'std::') for line in lines]
278
279def expect_compile_error_helper(
280        check_error_fun,
281        setup_source_code,
282        source_code,
283        test_params={},
284        ignore_deprecation_warnings=False,
285        ignore_warnings=False):
286    source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
287
288    source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
289
290    try:
291        args = []
292        if ignore_deprecation_warnings:
293            args += compiler.get_disable_deprecation_warning_flags()
294        if ignore_warnings:
295            args += compiler.get_disable_all_warnings_flags()
296        if ENABLE_COVERAGE:
297            # When collecting coverage these arguments are enabled by default; however we must disable them in tests
298            # expected to fail at compile-time because GCC would otherwise fail with an error like:
299            # /tmp/tmp4m22cey7.cpp:1:0: error: cannot open /dev/null.gcno
300            args += ['-fno-profile-arcs', '-fno-test-coverage']
301        compiler.compile_discarding_output(
302            source=source_file_name,
303            include_dirs=fruit_tests_include_dirs,
304            args=args)
305        raise Exception('The test should have failed to compile, but it compiled successfully')
306    except CompilationFailedException as e1:
307        e = e1
308
309    error_message = e.error_message
310    error_message_lines = error_message.splitlines()
311    error_message_head = _cap_to_lines(error_message, 40)
312
313    check_error_fun(e, error_message_lines, error_message_head)
314
315    try_remove_temporary_file(source_file_name)
316
317def apply_any_error_context_replacements(error_string, following_lines):
318    if CXX_COMPILER_NAME == 'MSVC':
319        # MSVC errors are of the form:
320        #
321        # C:\Path\To\header\foo.h(59): note: see reference to class template instantiation 'fruit::impl::NoBindingFoundError<fruit::Annotated<Annotation,U>>' being compiled
322        #         with
323        #         [
324        #              Annotation=Annotation1,
325        #              U=std::function<std::unique_ptr<ScalerImpl,std::default_delete<ScalerImpl>> (double)>
326        #         ]
327        #
328        # So we need to parse the following few lines and use them to replace the placeholder types in the Fruit error type.
329        replacement_lines = []
330        if len(following_lines) >= 4 and following_lines[0].strip() == 'with':
331            assert following_lines[1].strip() == '[', 'Line was: ' + following_lines[1]
332            for line in itertools.islice(following_lines, 2, None):
333                line = line.strip()
334                if line == ']':
335                    break
336                if line.endswith(','):
337                    line = line[:-1]
338                replacement_lines.append(line)
339
340        for replacement_line in replacement_lines:
341            match = re.search('([A-Za-z0-9_-]*)=(.*)', replacement_line)
342            if not match:
343                raise Exception('Failed to parse replacement line: %s' % replacement_line)
344            (type_variable, type_expression) = match.groups()
345            error_string = re.sub(r'\b' + type_variable + r'\b', type_expression, error_string)
346    return error_string
347
348def expect_generic_compile_error(expected_error_regex, setup_source_code, source_code, test_params={}):
349    """
350    Tests that the given source produces the expected error during compilation.
351
352    :param expected_fruit_error_regex: A regex used to match the Fruit error type,
353           e.g. 'NoBindingFoundForAbstractClassError<ScalerImpl>'.
354           Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
355    :param expected_fruit_error_desc_regex: A regex used to match the Fruit error description,
356           e.g. 'No explicit binding was found for C, and C is an abstract class'.
357    :param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
358           *not* subject to test_params, unlike source_code.
359    :param source_code: The second part of the source code. Any identifiers will be replaced using test_params
360           (where a replacement is defined). This will be dedented.
361    :param test_params: A dict containing the definition of some identifiers. Each identifier in
362           expected_fruit_error_regex and source_code will be replaced (textually) with its definition (if a definition
363           was provided).
364    """
365
366    expected_error_regex = _replace_using_test_params(expected_error_regex, test_params)
367    expected_error_regex = expected_error_regex.replace(' ', '')
368
369    def check_error(e, error_message_lines, error_message_head):
370        error_message_lines_with_replacements = [
371            apply_any_error_context_replacements(line, error_message_lines[line_number + 1:])
372            for line_number, line in enumerate(error_message_lines)]
373
374        normalized_error_message_lines = normalize_error_message_lines(error_message_lines_with_replacements)
375
376        for line in normalized_error_message_lines:
377            if re.search(expected_error_regex, line):
378                return
379        raise Exception(textwrap.dedent('''\
380            Expected error {expected_error} but the compiler output did not contain that.
381            Compiler command line: {compiler_command}
382            Error message was:
383            {error_message}
384            ''').format(expected_error = expected_error_regex, compiler_command=e.command, error_message = error_message_head))
385
386    expect_compile_error_helper(check_error, setup_source_code, source_code, test_params)
387
388def expect_compile_error(
389        expected_fruit_error_regex,
390        expected_fruit_error_desc_regex,
391        setup_source_code,
392        source_code,
393        test_params={},
394        ignore_deprecation_warnings=False,
395        ignore_warnings=False,
396        disable_error_line_number_check=False):
397    """
398    Tests that the given source produces the expected error during compilation.
399
400    :param expected_fruit_error_regex: A regex used to match the Fruit error type,
401           e.g. 'NoBindingFoundForAbstractClassError<ScalerImpl>'.
402           Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
403    :param expected_fruit_error_desc_regex: A regex used to match the Fruit error description,
404           e.g. 'No explicit binding was found for C, and C is an abstract class'.
405    :param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
406           *not* subject to test_params, unlike source_code.
407    :param source_code: The second part of the source code. Any identifiers will be replaced using test_params
408           (where a replacement is defined). This will be dedented.
409    :param test_params: A dict containing the definition of some identifiers. Each identifier in
410           expected_fruit_error_regex and source_code will be replaced (textually) with its definition (if a definition
411           was provided).
412    :param ignore_deprecation_warnings: A boolean. If True, deprecation warnings will be ignored.
413    :param ignore_warnings: A boolean. If True, all warnings will be ignored.
414    :param disable_error_line_number_check: A boolean. If True, the test will not fail if there are other diagnostic
415           lines before the expected error.
416    """
417    if '\n' in expected_fruit_error_regex:
418        raise Exception('expected_fruit_error_regex should not contain newlines')
419    if '\n' in expected_fruit_error_desc_regex:
420        raise Exception('expected_fruit_error_desc_regex should not contain newlines')
421
422    expected_fruit_error_regex = _replace_using_test_params(expected_fruit_error_regex, test_params)
423    expected_fruit_error_regex = expected_fruit_error_regex.replace(' ', '')
424
425    def check_error(e, error_message_lines, error_message_head):
426        normalized_error_message_lines = normalize_error_message_lines(error_message_lines)
427
428        for line_number, line in enumerate(normalized_error_message_lines):
429            match = re.search('fruit::impl::(.*Error<.*>)', line)
430            if match:
431                actual_fruit_error_line_number = line_number
432                actual_fruit_error = match.groups()[0]
433                actual_fruit_error = apply_any_error_context_replacements(actual_fruit_error, normalized_error_message_lines[line_number + 1:])
434                break
435        else:
436            raise Exception(textwrap.dedent('''\
437                Expected error {expected_error} but the compiler output did not contain user-facing Fruit errors.
438                Compiler command line: {compiler_command}
439                Error message was:
440                {error_message}
441                ''').format(expected_error = expected_fruit_error_regex, compiler_command = e.command, error_message = error_message_head))
442
443        for line_number, line in enumerate(error_message_lines):
444            match = re.search(fruit_error_message_extraction_regex, line)
445            if match:
446                actual_static_assert_error_line_number = line_number
447                actual_static_assert_error = match.groups()[0]
448                break
449        else:
450            raise Exception(textwrap.dedent('''\
451                Expected error {expected_error} but the compiler output did not contain static_assert errors.
452                Compiler command line: {compiler_command}
453                Error message was:
454                {error_message}
455                ''').format(expected_error = expected_fruit_error_regex, compiler_command=e.command, error_message = error_message_head))
456
457        try:
458            regex_search_result = re.search(expected_fruit_error_regex, actual_fruit_error)
459        except Exception as e:
460            raise Exception('re.search() failed for regex \'%s\'' % expected_fruit_error_regex) from e
461        if not regex_search_result:
462            raise Exception(textwrap.dedent('''\
463                The compilation failed as expected, but with a different error type.
464                Expected Fruit error type:    {expected_fruit_error_regex}
465                Error type was:               {actual_fruit_error}
466                Expected static assert error: {expected_fruit_error_desc_regex}
467                Static assert was:            {actual_static_assert_error}
468                Error message was:
469                {error_message}
470                '''.format(
471                expected_fruit_error_regex = expected_fruit_error_regex,
472                actual_fruit_error = actual_fruit_error,
473                expected_fruit_error_desc_regex = expected_fruit_error_desc_regex,
474                actual_static_assert_error = actual_static_assert_error,
475                error_message = error_message_head)))
476        try:
477            regex_search_result = re.search(expected_fruit_error_desc_regex, actual_static_assert_error)
478        except Exception as e:
479            raise Exception('re.search() failed for regex \'%s\'' % expected_fruit_error_desc_regex) from e
480        if not regex_search_result:
481            raise Exception(textwrap.dedent('''\
482                The compilation failed as expected, but with a different error message.
483                Expected Fruit error type:    {expected_fruit_error_regex}
484                Error type was:               {actual_fruit_error}
485                Expected static assert error: {expected_fruit_error_desc_regex}
486                Static assert was:            {actual_static_assert_error}
487                Error message:
488                {error_message}
489                '''.format(
490                expected_fruit_error_regex = expected_fruit_error_regex,
491                actual_fruit_error = actual_fruit_error,
492                expected_fruit_error_desc_regex = expected_fruit_error_desc_regex,
493                actual_static_assert_error = actual_static_assert_error,
494                error_message = error_message_head)))
495
496        # 6 is just a constant that works for both g++ (<=6.0.0 at least) and clang++ (<=4.0.0 at least).
497        # It might need to be changed.
498        if not disable_error_line_number_check and (actual_fruit_error_line_number > 6 or actual_static_assert_error_line_number > 6):
499            raise Exception(textwrap.dedent('''\
500                The compilation failed with the expected message, but the error message contained too many lines before the relevant ones.
501                The error type was reported on line {actual_fruit_error_line_number} of the message (should be <=6).
502                The static assert was reported on line {actual_static_assert_error_line_number} of the message (should be <=6).
503                Error message:
504                {error_message}
505                '''.format(
506                actual_fruit_error_line_number = actual_fruit_error_line_number,
507                actual_static_assert_error_line_number = actual_static_assert_error_line_number,
508                error_message = error_message_head)))
509
510        for line in error_message_lines[:max(actual_fruit_error_line_number, actual_static_assert_error_line_number)]:
511            if re.search('fruit::impl::meta', line):
512                raise Exception(
513                    '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)
514
515    expect_compile_error_helper(check_error, setup_source_code, source_code, test_params, ignore_deprecation_warnings, ignore_warnings)
516
517
518def expect_runtime_error(
519        expected_error_regex,
520        setup_source_code,
521        source_code,
522        test_params={},
523        ignore_deprecation_warnings=False):
524    """
525    Tests that the given source (compiles successfully and) produces the expected error at runtime.
526
527    :param expected_error_regex: A regex used to match the content of stderr.
528           Any identifiers contained in the regex will be replaced using test_params (where a replacement is defined).
529    :param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
530           *not* subject to test_params, unlike source_code.
531    :param source_code: The second part of the source code. Any identifiers will be replaced using test_params
532           (where a replacement is defined). This will be dedented.
533    :param test_params: A dict containing the definition of some identifiers. Each identifier in
534           expected_error_regex and source_code will be replaced (textually) with its definition (if a definition
535           was provided).
536    """
537    expected_error_regex = _replace_using_test_params(expected_error_regex, test_params)
538    source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
539
540    source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
541    executable_suffix = {'posix': '', 'nt': '.exe'}[os.name]
542    output_file_name = _create_temporary_file('', executable_suffix)
543
544    args = fruit_tests_linker_flags.copy()
545    if ignore_deprecation_warnings:
546        args += compiler.get_disable_deprecation_warning_flags()
547    compiler.compile_and_link(
548        source=source_file_name,
549        include_dirs=fruit_tests_include_dirs,
550        output_file_name=output_file_name,
551        args=args)
552
553    try:
554        run_compiled_executable(output_file_name)
555        raise Exception('The test should have failed at runtime, but it ran successfully')
556    except CommandFailedException as e1:
557        e = e1
558
559    stderr = e.stderr
560    stderr_head = _cap_to_lines(stderr, 40)
561
562    if '\n' in expected_error_regex:
563        regex_flags = re.MULTILINE
564    else:
565        regex_flags = 0
566
567    try:
568        regex_search_result = re.search(expected_error_regex, stderr, flags=regex_flags)
569    except Exception as e:
570        raise Exception('re.search() failed for regex \'%s\'' % expected_error_regex) from e
571    if not regex_search_result:
572        raise Exception(textwrap.dedent('''\
573            The test failed as expected, but with a different message.
574            Expected: {expected_error_regex}
575            Was:
576            {stderr}
577            '''.format(expected_error_regex = expected_error_regex, stderr = stderr_head)))
578
579    # Note that we don't delete the temporary files if the test failed. This is intentional, keeping them around helps debugging the failure.
580    if not ENABLE_COVERAGE:
581        try_remove_temporary_file(source_file_name)
582        try_remove_temporary_file(output_file_name)
583
584
585def expect_success(setup_source_code, source_code, test_params={}, ignore_deprecation_warnings=False):
586    """
587    Tests that the given source compiles and runs successfully.
588
589    :param setup_source_code: The first part of the source code. This is dedented separately from source_code and it's
590           *not* subject to test_params, unlike source_code.
591    :param source_code: The second part of the source code. Any identifiers will be replaced using test_params
592           (where a replacement is defined). This will be dedented.
593    :param test_params: A dict containing the definition of some identifiers. Each identifier in
594           source_code will be replaced (textually) with its definition (if a definition was provided).
595    """
596    source_code = _construct_final_source_code(setup_source_code, source_code, test_params)
597
598    if 'main(' not in source_code:
599        source_code += textwrap.dedent('''
600            int main() {
601            }
602            ''')
603
604    source_file_name = _create_temporary_file(source_code, file_name_suffix='.cpp')
605    executable_suffix = {'posix': '', 'nt': '.exe'}[os.name]
606    output_file_name = _create_temporary_file('', executable_suffix)
607
608    args = fruit_tests_linker_flags.copy()
609    if ignore_deprecation_warnings:
610        args += compiler.get_disable_deprecation_warning_flags()
611    compiler.compile_and_link(
612        source=source_file_name,
613        include_dirs=fruit_tests_include_dirs,
614        output_file_name=output_file_name,
615        args=args)
616
617    run_compiled_executable(output_file_name)
618
619    # Note that we don't delete the temporary files if the test failed. This is intentional, keeping them around helps debugging the failure.
620    if not ENABLE_COVERAGE:
621        try_remove_temporary_file(source_file_name)
622        try_remove_temporary_file(output_file_name)
623
624
625# Note: this is not the main function of this file, it's meant to be used as main function from test_*.py files.
626def main():
627    absltest.main(*sys.argv)
628