Repository navigation
Require the compiler's rejection in reject tests, and run them concurrently #464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Bronek
wants to merge
2
commits into
main
Choose a base branch
from
bronek/concurrent_reject_tests
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # Copyright (c) 2026 Bronek Kozicki | ||
| # | ||
| # Distributed under the ISC License. See accompanying file LICENSE.md | ||
| # or copy at https://opensource.org/licenses/ISC | ||
|
|
||
| # Compiler launcher for reject tests: runs the compile command following `--` and fails whenever it | ||
| # fails, printing the marker a reject test requires, "The compiler rejected the source", only if the | ||
| # compiler reported an error at a source line. | ||
| # Usage: cmake -P RejectLauncher.cmake -- <compiler> [<argument>...] | ||
|
|
||
| cmake_minimum_required(VERSION 3.25) | ||
|
|
||
| # Bracket arguments forward each argument verbatim; expanding a list would split an argument | ||
| # containing `;` and join arguments following an unbalanced `[` | ||
| set(command "") | ||
| set(forwarding FALSE) | ||
| math(EXPR last "${CMAKE_ARGC} - 1") | ||
| foreach(index RANGE ${last}) | ||
| set(argument "${CMAKE_ARGV${index}}") | ||
| if(forwarding) | ||
| string(FIND "${argument}" "]=]" clash) | ||
| if(NOT clash EQUAL -1) | ||
| message(FATAL_ERROR "Cannot forward an argument containing `]=]`: ${argument}") | ||
| endif() | ||
| string(APPEND command " [=[${argument}]=]") | ||
| elseif(argument STREQUAL "--") | ||
| set(forwarding TRUE) | ||
| endif() | ||
| endforeach() | ||
|
|
||
| cmake_language(EVAL CODE "execute_process(COMMAND ${command} RESULT_VARIABLE result | ||
| OUTPUT_VARIABLE output ERROR_VARIABLE output ECHO_OUTPUT_VARIABLE ECHO_ERROR_VARIABLE)") | ||
| if(NOT result MATCHES "^[0-9]+$") | ||
| message(FATAL_ERROR "The compiler did not run to completion: ${result}") | ||
| elseif(result EQUAL 0) | ||
| return() | ||
| endif() | ||
|
|
||
| # A diagnosed error names its source line, as `file:line[:column]: error` (GCC, Clang) or | ||
| # `file(line[,column]): error` (MSVC, clang-cl). A failing driver, such as GCC reporting its killed | ||
| # front end, names no line. Lines reporting an internal compiler error are dropped first, since they | ||
| # name a source line too, and MSVC's would otherwise match. | ||
| string(ASCII 27 escape) | ||
| string(REGEX REPLACE "${escape}\\[[0-9;]*[A-Za-z]" "" output "${output}") | ||
| string(REGEX REPLACE "[^\n]*[Ii]nternal (compiler )?error[^\n]*" "" output "${output}") | ||
| if(output MATCHES "(^|\n)[^\n]+(:[0-9]+(:[0-9]+)?: |\\([0-9]+(,[0-9]+)?\\) ?: )(fatal )?error[: ]") | ||
| message(FATAL_ERROR "The compiler rejected the source") | ||
| endif() | ||
| message(FATAL_ERROR "The compiler failed without reporting an error at a source line") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Copyright (c) 2026 Bronek Kozicki | ||
| # | ||
| # Distributed under the ISC License. See accompanying file LICENSE.md | ||
| # or copy at https://opensource.org/licenses/ISC | ||
|
|
||
| include(CompilationOptions) | ||
|
|
||
| set(REJECT_LAUNCHER "${CMAKE_CURRENT_LIST_DIR}/RejectLauncher.cmake") | ||
| # Only Makefile and Ninja generators run compiler launchers. Targets using this one replace any | ||
| # global launcher, such as ccache, whose own failure would pass as a rejection. | ||
| if(CMAKE_GENERATOR MATCHES "Makefiles|Ninja") | ||
| set(REJECT_LAUNCH "${CMAKE_COMMAND}" -P "${REJECT_LAUNCHER}" --) | ||
| endif() | ||
| # RejectLauncher.cmake prints this exact text | ||
| set(REJECT_MARKER "The compiler rejected the source") | ||
|
|
||
| add_custom_target(tests_reject_controls) | ||
| add_test( | ||
| NAME tests_reject_controls | ||
| COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --target tests_reject_controls | ||
| ) | ||
|
|
||
| # Scenario tests require this fixture, which other setup tests may join. A rejection counts only | ||
| # once each reject source compiles without a scenario, through the same launcher, if any, as its | ||
| # scenarios. Building the controls first also regenerates a stale build system once, rather than in | ||
| # each scenario build. | ||
| set_tests_properties(tests_reject_controls PROPERTIES FIXTURES_SETUP reject_setup) | ||
|
|
||
| # Generate an object target for SOURCE, which must compile, and for each | ||
| # `#ifdef LIBFN_REJECT_<scenario>` guard in it a test building SOURCE with only that guard defined, | ||
| # which the compiler must reject | ||
| function(add_reject_tests) | ||
| set(oneValueArgs SOURCE MODE) | ||
| set(multiValueArgs DEPENDENCIES LABELS) | ||
| cmake_parse_arguments(Reject "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) | ||
|
|
||
| set(path "${CMAKE_CURRENT_SOURCE_DIR}/${Reject_SOURCE}") | ||
| set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${path}") | ||
| file(STRINGS "${path}" guards REGEX "^#ifdef LIBFN_REJECT_[A-Za-z0-9_]+$") | ||
| file(STRINGS "${path}" directives REGEX "^[ \t]*#.*LIBFN_REJECT_") | ||
| if(NOT guards OR NOT guards STREQUAL directives) | ||
| message(FATAL_ERROR "${Reject_SOURCE}: needs at least one guard, each spelled `#ifdef LIBFN_REJECT_<scenario>`") | ||
| endif() | ||
| list(TRANSFORM guards REPLACE "^#ifdef LIBFN_REJECT_" "" OUTPUT_VARIABLE scenarios) | ||
|
|
||
| string(REGEX REPLACE "\\.cpp$" "" root_name "${Reject_SOURCE}") | ||
| string(REPLACE "/" "_" root_name "${root_name}") | ||
| set(control "tests_${root_name}_cxx${Reject_MODE}") | ||
| add_library("${control}" OBJECT "${Reject_SOURCE}") | ||
| add_dependencies("cxx${Reject_MODE}" "${control}") | ||
| add_dependencies("tests" "${control}") | ||
| set(targets "${control}") | ||
|
|
||
| if(REJECT_LAUNCH) | ||
| # Positive control for the scenarios, compiled through their launcher; the plain control | ||
| # above keeps any global launcher instead | ||
| set(launched "${control}_launched") | ||
| add_library("${launched}" OBJECT EXCLUDE_FROM_ALL "${Reject_SOURCE}") | ||
| set_property(TARGET "${launched}" PROPERTY CXX_COMPILER_LAUNCHER ${REJECT_LAUNCH}) | ||
| add_dependencies(tests_reject_controls "${launched}") | ||
| # The build tracks the launcher's path, not its content; as a source property, this also | ||
| # recompiles the plain control and the scenarios when the launcher changes | ||
| set_property(SOURCE "${Reject_SOURCE}" PROPERTY OBJECT_DEPENDS "${REJECT_LAUNCHER}") | ||
| list(APPEND targets "${launched}") | ||
| else() | ||
| add_dependencies(tests_reject_controls "${control}") | ||
| endif() | ||
|
|
||
| foreach(scenario IN LISTS scenarios) | ||
| set(target "tests_${root_name}_${scenario}_cxx${Reject_MODE}") | ||
| add_library("${target}" OBJECT EXCLUDE_FROM_ALL "${Reject_SOURCE}") | ||
| target_compile_definitions("${target}" PRIVATE "LIBFN_REJECT_${scenario}") | ||
|
|
||
| add_test( | ||
| NAME "${target}" | ||
| COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --target "${target}" | ||
| ) | ||
| set_tests_properties("${target}" PROPERTIES | ||
| FIXTURES_REQUIRED reject_setup | ||
| LABELS "${Reject_LABELS};cxx${Reject_MODE};${root_name}") | ||
| if(REJECT_LAUNCH) | ||
| set_property(TARGET "${target}" PROPERTY CXX_COMPILER_LAUNCHER ${REJECT_LAUNCH}) | ||
| set_tests_properties("${target}" PROPERTIES PASS_REGULAR_EXPRESSION "${REJECT_MARKER}") | ||
| else() | ||
| # Any failed build passes here, even one failed by a concurrent build in the same tree | ||
| set_tests_properties("${target}" PROPERTIES WILL_FAIL TRUE RESOURCE_LOCK build_tree) | ||
| endif() | ||
| list(APPEND targets "${target}") | ||
|
|
||
| unset(target) | ||
| endforeach() | ||
|
|
||
| foreach(target IN LISTS targets) | ||
| target_link_libraries("${target}" PRIVATE ${Reject_DEPENDENCIES}) | ||
| append_compilation_options("${target}" WARNINGS) | ||
| set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${Reject_MODE}") | ||
| endforeach() | ||
| endfunction() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright (c) 2026 Bronek Kozicki | ||
| # | ||
| # Distributed under the ISC License. See accompanying file LICENSE.md | ||
| # or copy at https://opensource.org/licenses/ISC | ||
|
|
||
| # Usage: cmake -DLAUNCHER=<RejectLauncher.cmake> -DMARKER=<marker> -P reject_launcher.cmake | ||
|
|
||
| cmake_minimum_required(VERSION 3.25) | ||
|
|
||
| # With LIBFN_REJECT_DIAGNOSTIC in its environment, this script stands in for a compiler which prints | ||
| # it and fails; CMake's own error line does not take the shape of a diagnostic. The environment | ||
| # carries the diagnostic because a -D argument, if the launcher mangled it, could start a configure | ||
| # run in the current directory. | ||
| if(DEFINED ENV{LIBFN_REJECT_DIAGNOSTIC}) | ||
| message(NOTICE "$ENV{LIBFN_REJECT_DIAGNOSTIC}") | ||
| message(FATAL_ERROR "") | ||
| endif() | ||
|
|
||
| set(launch "${CMAKE_COMMAND}" -P "${LAUNCHER}" --) | ||
|
|
||
| function(launch_failing diagnostic) | ||
| set(ENV{LIBFN_REJECT_DIAGNOSTIC} "${diagnostic}") | ||
| execute_process( | ||
| COMMAND ${launch} "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_FUNCTION_LIST_FILE}" | ||
| RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output) | ||
| unset(ENV{LIBFN_REJECT_DIAGNOSTIC}) | ||
| set(result "${result}" PARENT_SCOPE) | ||
| set(output "${output}" PARENT_SCOPE) | ||
| endfunction() | ||
|
|
||
| string(ASCII 27 escape) | ||
| foreach(diagnostic IN ITEMS | ||
| "source.cpp:12:3: error: rejected" | ||
| "source.cpp:12: error: rejected" | ||
| "source.cpp:3:10: fatal error: missing.hpp: No such file or directory" | ||
| "source.cpp(12): error C2672: rejected" | ||
| "source.cpp(12,3): error: rejected" | ||
| "source.cpp:12:3: error: rejected\nsource.cpp:20:1: internal compiler error: Segmentation fault" | ||
| "${escape}[01m${escape}[Ksource.cpp:12:3:${escape}[m${escape}[K ${escape}[01;31m${escape}[Kerror: ${escape}[m${escape}[Krejected") | ||
| launch_failing("${diagnostic}") | ||
| if(result EQUAL 0 OR NOT output MATCHES "${MARKER}") | ||
| message(SEND_ERROR "An error at a source line must fail the launch with the marker, got ${result}:\n${output}") | ||
| endif() | ||
| endforeach() | ||
|
|
||
| foreach(diagnostic IN ITEMS | ||
| "g++: fatal error: Killed signal terminated program cc1plus" | ||
| "source.cpp:12:3: internal compiler error: Segmentation fault" | ||
| "source.cpp(61,1): fatal error C1001: Internal compiler error.\n(compiler file 'msc1.cpp', line 1589)" | ||
| "source.cpp(12): fatal error C1001: An internal error has occurred in the compiler." | ||
| "cc1plus: error: unrecognized command-line option '-fnone'" | ||
| "source.cpp:12:3: warning: unused variable 'error'") | ||
| launch_failing("${diagnostic}") | ||
| if(result EQUAL 0 OR output MATCHES "${MARKER}") | ||
| message(SEND_ERROR "A failure without an error at a source line must fail the launch without the marker, got ${result}:\n${output}") | ||
| endif() | ||
| endforeach() | ||
|
|
||
| execute_process(COMMAND ${launch} "${CMAKE_COMMAND}" -E echo "a;b" "[c" "d]" | ||
| RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output OUTPUT_STRIP_TRAILING_WHITESPACE) | ||
| if(NOT result EQUAL 0 OR NOT output STREQUAL "a;b [c d]") | ||
| message(SEND_ERROR "A succeeding command must succeed with its arguments intact, got ${result}:\n${output}") | ||
| endif() | ||
|
|
||
| execute_process(COMMAND ${launch} "${CMAKE_CURRENT_LIST_DIR}/no-such-compiler" | ||
| RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output) | ||
| if(result EQUAL 0 OR output MATCHES "${MARKER}") | ||
| message(SEND_ERROR "A command that cannot start must fail the launch without the marker, got ${result}:\n${output}") | ||
| endif() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.