# Copyright 2011 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # This file is meant to be included into an target to create a unittest that # invokes a set of no-compile tests. A no-compile test is a test that asserts # a particular construct will not compile. # # Usage: # # 1. Create a GN target: # # import("//build/nocompile.gni") # # nocompile_source_set("base_nocompile") { # sources = [ # "functional/one_not_equal_two_nocompile.nc", # ] # deps = [ # ":base" # ] # } # # Note that by convention, nocompile tests use the `.nc` extension rather # than the standard `.cc` extension: this is because the expectation lines # often exceed 80 characters, which would make clang-format unhappy. # # 2. Add a dep from a related test binary to the nocompile source set: # # test("base_unittests") { # ... # deps += [ ":base_nocompile_tests" ] # } # # 3. Populate the .nc file with test cases. Expected compile failures should be # annotated with a comment of the form: # # // expected-error {{}} # # For example: # # void OneDoesNotEqualTwo() { # static_assert(1 == 2); // expected-error {{static assertion failed due to requirement '1 == 2'}} # } # # The verification logic is built as part of clang; full documentation is at # https://clang.llvm.org/doxygen/classclang_1_1VerifyDiagnosticConsumer.html. # # Also see: # http://dev.chromium.org/developers/testing/no-compile-tests # import("//build/config/clang/clang.gni") if (is_win) { import("//build/toolchain/win/win_toolchain_data.gni") } declare_args() { enable_nocompile_tests = (is_linux || is_chromeos || is_apple || is_win) && is_clang && host_cpu == target_cpu enable_nocompile_tests_new = is_clang && !is_nacl && !is_tizen } if (enable_nocompile_tests_new) { template("nocompile_source_set") { action_foreach(target_name) { testonly = true script = "//tools/nocompile/wrapper.py" sources = invoker.sources deps = invoker.deps # An action is not a compiler, so configs is empty until it is explicitly set. configs = default_compiler_configs # Disable the checks that the Chrome style plugin normally enforces to # reduce the amount of boilerplate needed in nocompile tests. configs -= [ "//build/config/clang:find_bad_constructs" ] if (is_win) { result_path = "$target_out_dir/$target_name/{{source_name_part}}_placeholder.obj" } else { result_path = "$target_out_dir/$target_name/{{source_name_part}}_placeholder.o" } rebased_obj_path = rebase_path(result_path, root_build_dir) depfile = "${result_path}.d" rebased_depfile_path = rebase_path(depfile, root_build_dir) outputs = [ result_path ] if (is_win) { if (host_os == "win") { cxx = "clang-cl.exe" } else { cxx = "clang-cl" } } else { cxx = "clang++" } args = [] if (is_win) { # ninja normally parses /showIncludes output, but the depsformat # variable can only be set in compiler tools, not for custom actions. # Unfortunately, this means the clang wrapper needs to generate the # depfile itself. args += [ "--generate-depfile" ] } args += [ rebase_path("$clang_base_path/bin/$cxx", root_build_dir), "{{source}}", rebased_obj_path, rebased_depfile_path, "--", "{{cflags}}", "{{cflags_cc}}", "{{defines}}", "{{include_dirs}}", # No need to generate an object file for nocompile tests. "-Xclang", "-fsyntax-only", # Enable clang's VerifyDiagnosticConsumer: # https://clang.llvm.org/doxygen/classclang_1_1VerifyDiagnosticConsumer.html "-Xclang", "-verify", # But don't require expected-note comments since that is not the # primary point of the nocompile tests. "-Xclang", "-verify-ignore-unexpected=note", # Disable the error limit so that nocompile tests do not need to be # arbitrarily split up when they hit the default error limit. "-ferror-limit=0", # So funny characters don't show up in error messages. "-fno-color-diagnostics", # Always treat warnings as errors. "-Werror", ] if (!is_win) { args += [ # On non-Windows platforms, clang can generate the depfile. "-MMD", "-MF", rebased_depfile_path, "-MT", rebased_obj_path, # Non-Windows clang uses file extensions to determine how to treat # various inputs, so explicitly tell it to treat all inputs (even # those with weird extensions like .nc) as C++ source files. "-x", "c++", ] } else { # For some reason, the Windows includes are not part of the default # compiler configs. Set it explicitly here, since things like libc++ # depend on the VC runtime. if (target_cpu == "x86") { win_toolchain_data = win_toolchain_data_x86 } else if (target_cpu == "x64") { win_toolchain_data = win_toolchain_data_x64 } else if (target_cpu == "arm64") { win_toolchain_data = win_toolchain_data_arm64 } else { error("Unsupported target_cpu, add it to win_toolchain_data.gni") } args += win_toolchain_data.include_flags_imsvc_list args += [ "/showIncludes:user" ] } # Note: for all platforms, the depfile only lists user includes, and not # system includes. If system includes change, the compiler flags are # expected to artificially change in some way to invalidate and force the # nocompile tests to run again. } } } # TODO(https://crbug.com/1480969): this section remains for legacy # documentation. However, nocompile tests using these legacy templates are # migrated to the new-style tests. Please do not add more old-style tests. # # To use this, create a GN target with the following form: # # import("//build/nocompile.gni") # nocompile_test("my_module_nc_unittests") { # sources = [ # 'nc_testset_1.nc', # 'nc_testset_2.nc', # ] # # # optional extra include dirs: # include_dirs = [ ... ] # } # # The tests are invoked by building the target named in the nocompile_test() # macro, for example: # # ninja -C out/Default my_module_nc_unittests # # The .nc files are C++ files that contain code we wish to assert will not # compile. Each individual test case in the file should be put in its own # #if defined(...) section specifying an unique preprocessor symbol beginning # with NCTEST which names the test. The set of tests in a file is automatically # determined by scanning the file for these #if blocks and no other explicit # definition of the symbol is required to register a test. # # The expected complier error message should be appended with a C++-style # comment that has a python list of regular expressions. This will likely be # greater than 80-characters. Giving a solid expected output test is important # so that random compile failures do not cause the test to pass. # # Example .nc file: # # #if defined(NCTEST_NEEDS_SEMICOLON) // [r"expected ',' or ';' at end of input"] # # int a = 1 # # #elif defined(NCTEST_NEEDS_CAST) // [r"invalid conversion from 'void*' to 'char*'"] # # void* a = NULL; # char* b = a; # # #endif # # If we need to disable NCTEST_NEEDS_SEMICOLON, then change the #if to: # # #if defined(DISABLED_NCTEST_NEEDS_SEMICOLON) # ... # #elif defined(NCTEST_NEEDS_CAST) # ... # # The lines above are parsed by a regexp so avoid getting creative with the # formatting or ifdef logic; it will likely just not work. # # Implementation notes: # The .nc files are actually processed by a python script which executes the # compiler and generates a .cc file that is empty on success, or will have a # series of #error lines on failure, and a set of trivially passing gunit # TEST() functions on success. This allows us to fail at the compile step when # something goes wrong, and know during the unittest run that the test was at # least processed when things go right. if (enable_nocompile_tests) { import("//build/config/c++/c++.gni") import("//build/config/sysroot.gni") import("//testing/test.gni") if (is_mac) { import("//build/config/mac/mac_sdk.gni") } template("nocompile_test") { nocompile_target = target_name + "_run_nocompile" action_foreach(nocompile_target) { testonly = true script = "//tools/nocompile/driver.py" sources = invoker.sources deps = invoker.deps if (defined(invoker.public_deps)) { public_deps = invoker.public_deps } result_path = "$target_gen_dir/{{source_name_part}}_nc.cc" outputs = [ result_path ] rebased_result_path = rebase_path(result_path, root_build_dir) if (is_win) { if (host_os == "win") { cxx = "clang-cl.exe" nulldevice = "nul" } else { cxx = "clang-cl" # Unfortunately, clang-cl always wants to suffix the output file name # with ".obj", and /dev/null.obj is not a valid file. As a bit of a # hack, simply use the path to the generated .cc file, knowing: # - that clang-cl will append ".obj" to the filename, so it will never # clash. # - except when the nocompile test unexpectedly passes, the output # file will never actually be written. nulldevice = rebased_result_path } } else { cxx = "clang++" } depfile = "${result_path}.d" args = [] if (is_win) { args += [ "--depfile", rebased_result_path + ".d", ] } args += [ rebase_path("$clang_base_path/bin/$cxx", root_build_dir), "4", # number of compilers to invoke in parallel. "{{source}}", rebased_result_path, "--", "-Werror", "-Wfatal-errors", "-Wthread-safety", "-I" + rebase_path("//", root_build_dir), "-I" + rebase_path("//third_party/abseil-cpp/", root_build_dir), "-I" + rebase_path("//buildtools/third_party/libc++/", root_build_dir), "-I" + rebase_path(root_gen_dir, root_build_dir), # TODO(https://crbug.com/989932): Track build/config/compiler/BUILD.gn "-Wno-implicit-int-float-conversion", ] if (is_win) { # On Windows we fall back to using system headers from a sysroot from # depot_tools. This is negotiated by python scripts and the result is # available in //build/toolchain/win/win_toolchain_data.gni. From there # we get the `include_flags_imsvc` which point to the system headers. if (host_cpu == "x86") { win_toolchain_data = win_toolchain_data_x86 } else if (host_cpu == "x64") { win_toolchain_data = win_toolchain_data_x64 } else if (host_cpu == "arm64") { win_toolchain_data = win_toolchain_data_arm64 } else { error("Unsupported host_cpu, add it to win_toolchain_data.gni") } args += win_toolchain_data.include_flags_imsvc_list args += [ "/W4", "-Wno-unused-parameter", "-I" + rebase_path("$libcxx_prefix/include", root_build_dir), "/std:c++20", "/showIncludes", "/Fo" + nulldevice, "/c", "/Tp", ] } else { args += [ "-Wall", "-nostdinc++", "-isystem" + rebase_path("$libcxx_prefix/include", root_build_dir), "-isystem" + rebase_path("$libcxxabi_prefix/include", root_build_dir), "-std=c++20", "-MMD", "-MF", rebased_result_path + ".d", "-MT", rebased_result_path, "-o", "/dev/null", "-c", "-x", "c++", ] } args += [ "{{source}}" ] if (is_mac && host_os != "mac") { args += [ "--target=x86_64-apple-macos", "-mmacos-version-min=$mac_deployment_target", ] } # Iterate over any extra include dirs and append them to the command line. if (defined(invoker.include_dirs)) { foreach(include_dir, invoker.include_dirs) { args += [ "-I" + rebase_path(include_dir, root_build_dir) ] } } if (sysroot != "") { assert(!is_win) sysroot_path = rebase_path(sysroot, root_build_dir) args += [ "--sysroot", sysroot_path, ] } if (!is_nacl) { args += [ # TODO(crbug.com/1343975) Evaluate and possibly enable. "-Wno-deprecated-builtins", ] } } test(target_name) { deps = invoker.deps + [ ":$nocompile_target" ] sources = get_target_outputs(":$nocompile_target") forward_variables_from(invoker, [ "bundle_deps" ]) } } }