• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2011 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# This file is meant to be included into an target to create a unittest that
6# invokes a set of no-compile tests.  A no-compile test is a test that asserts
7# a particular construct will not compile.
8#
9# Usage:
10#
11# 1. Create a GN target:
12#
13#    import("//build/nocompile.gni")
14#
15#    nocompile_source_set("base_nocompile_tests") {
16#      sources = [
17#        "functional/one_not_equal_two_nocompile.nc",
18#      ]
19#      deps = [
20#        ":base"
21#      ]
22#    }
23#
24#    Note that by convention, nocompile tests use the `.nc` extension rather
25#    than the standard `.cc` extension: this is because the expectation lines
26#    often exceed 80 characters, which would make clang-format unhappy.
27#
28# 2. Add a dep from a related test binary to the nocompile source set:
29#
30#    test("base_unittests") {
31#      ...
32#      deps += [ ":base_nocompile_tests" ]
33#    }
34#
35# 3. Populate the .nc file with test cases. Expected compile failures should be
36#    annotated with a comment of the form:
37#
38#    // expected-error {{<expected error string here>}}
39#
40#    For example:
41#
42#    void OneDoesNotEqualTwo() {
43#      static_assert(1 == 2);  // expected-error {{static assertion failed due to requirement '1 == 2'}}
44#    }
45#
46#    The verification logic is built as part of clang; full documentation is at
47#    https://clang.llvm.org/doxygen/classclang_1_1VerifyDiagnosticConsumer.html.
48#
49# Also see:
50#   http://dev.chromium.org/developers/testing/no-compile-tests
51#
52import("//build/config/clang/clang.gni")
53if (is_win) {
54  import("//build/toolchain/win/win_toolchain_data.gni")
55}
56
57declare_args() {
58  enable_nocompile_tests = (is_linux || is_chromeos || is_apple || is_win) &&
59                           is_clang && host_cpu == target_cpu
60  enable_nocompile_tests_new = is_clang && !is_nacl
61}
62
63if (enable_nocompile_tests_new) {
64  template("nocompile_source_set") {
65    action_foreach(target_name) {
66      testonly = true
67
68      script = "//tools/nocompile/wrapper.py"
69      sources = invoker.sources
70      deps = invoker.deps
71
72      # An action is not a compiler, so configs is empty until it is explicitly set.
73      configs = default_compiler_configs
74
75      # Disable the checks that the Chrome style plugin normally enforces to
76      # reduce the amount of boilerplate needed in nocompile tests.
77      configs -= [ "//build/config/clang:find_bad_constructs" ]
78
79      if (is_win) {
80        result_path =
81            "$target_out_dir/$target_name/{{source_name_part}}_placeholder.obj"
82      } else {
83        result_path =
84            "$target_out_dir/$target_name/{{source_name_part}}_placeholder.o"
85      }
86      rebased_obj_path = rebase_path(result_path, root_build_dir)
87
88      depfile = "${result_path}.d"
89      rebased_depfile_path = rebase_path(depfile, root_build_dir)
90      outputs = [ result_path ]
91
92      if (is_win) {
93        if (host_os == "win") {
94          cxx = "clang-cl.exe"
95        } else {
96          cxx = "clang-cl"
97        }
98      } else {
99        cxx = "clang++"
100      }
101
102      args = []
103
104      if (is_win) {
105        # ninja normally parses /showIncludes output, but the depsformat
106        # variable can only be set in compiler tools, not for custom actions.
107        # Unfortunately, this means the clang wrapper needs to generate the
108        # depfile itself.
109        args += [ "--generate-depfile" ]
110      }
111
112      args += [
113        rebase_path("$clang_base_path/bin/$cxx", root_build_dir),
114        "{{source}}",
115        rebased_obj_path,
116        rebased_depfile_path,
117        "--",
118        "{{cflags}}",
119        "{{cflags_cc}}",
120        "{{defines}}",
121        "{{include_dirs}}",
122
123        # No need to generate an object file for nocompile tests.
124        "-Xclang",
125        "-fsyntax-only",
126
127        # Enable clang's VerifyDiagnosticConsumer:
128        # https://clang.llvm.org/doxygen/classclang_1_1VerifyDiagnosticConsumer.html
129        "-Xclang",
130        "-verify",
131
132        # But don't require expected-note comments since that is not the
133        # primary point of the nocompile tests.
134        "-Xclang",
135        "-verify-ignore-unexpected=note",
136
137        # Disable the error limit so that nocompile tests do not need to be
138        # arbitrarily split up when they hit the default error limit.
139        "-ferror-limit=0",
140
141        # So funny characters don't show up in error messages.
142        "-fno-color-diagnostics",
143
144        # Always treat warnings as errors.
145        "-Werror",
146      ]
147
148      if (!is_win) {
149        args += [
150          # On non-Windows platforms, clang can generate the depfile.
151          "-MMD",
152          "-MF",
153          rebased_depfile_path,
154          "-MT",
155          rebased_obj_path,
156
157          # Non-Windows clang uses file extensions to determine how to treat
158          # various inputs, so explicitly tell it to treat all inputs (even
159          # those with weird extensions like .nc) as C++ source files.
160          "-x",
161          "c++",
162        ]
163      } else {
164        # For some reason, the Windows includes are not part of the default
165        # compiler configs. Set it explicitly here, since things like libc++
166        # depend on the VC runtime.
167        if (target_cpu == "x86") {
168          win_toolchain_data = win_toolchain_data_x86
169        } else if (target_cpu == "x64") {
170          win_toolchain_data = win_toolchain_data_x64
171        } else if (target_cpu == "arm64") {
172          win_toolchain_data = win_toolchain_data_arm64
173        } else {
174          error("Unsupported target_cpu, add it to win_toolchain_data.gni")
175        }
176        args += win_toolchain_data.include_flags_imsvc_list
177        args += [ "/showIncludes:user" ]
178      }
179
180      # Note: for all platforms, the depfile only lists user includes, and not
181      # system includes. If system includes change, the compiler flags are
182      # expected to artificially change in some way to invalidate and force the
183      # nocompile tests to run again.
184    }
185  }
186}
187
188# TODO(https://crbug.com/1480969): this section remains for legacy
189# documentation. However, nocompile tests using these legacy templates are
190# migrated to the new-style tests. Please do not add more old-style tests.
191#
192# To use this, create a GN target with the following form:
193#
194# import("//build/nocompile.gni")
195# nocompile_test("my_module_nc_unittests") {
196#   sources = [
197#     'nc_testset_1.nc',
198#     'nc_testset_2.nc',
199#   ]
200#
201#   # optional extra include dirs:
202#   include_dirs = [ ... ]
203# }
204#
205# The tests are invoked by building the target named in the nocompile_test()
206# macro, for example:
207#
208# ninja -C out/Default my_module_nc_unittests
209#
210# The .nc files are C++ files that contain code we wish to assert will not
211# compile. Each individual test case in the file should be put in its own
212# #if defined(...) section specifying an unique preprocessor symbol beginning
213# with NCTEST which names the test. The set of tests in a file is automatically
214# determined by scanning the file for these #if blocks and no other explicit
215# definition of the symbol is required to register a test.
216#
217# The expected complier error message should be appended with a C++-style
218# comment that has a python list of regular expressions. This will likely be
219# greater than 80-characters. Giving a solid expected output test is important
220# so that random compile failures do not cause the test to pass.
221#
222# Example .nc file:
223#
224#   #if defined(NCTEST_NEEDS_SEMICOLON)  // [r"expected ',' or ';' at end of input"]
225#
226#   int a = 1
227#
228#   #elif defined(NCTEST_NEEDS_CAST)  // [r"invalid conversion from 'void*' to 'char*'"]
229#
230#   void* a = NULL;
231#   char* b = a;
232#
233#   #endif
234#
235# If we need to disable NCTEST_NEEDS_SEMICOLON, then change the #if to:
236#
237#   #if defined(DISABLED_NCTEST_NEEDS_SEMICOLON)
238#   ...
239#   #elif defined(NCTEST_NEEDS_CAST)
240#   ...
241#
242# The lines above are parsed by a regexp so avoid getting creative with the
243# formatting or ifdef logic; it will likely just not work.
244#
245# Implementation notes:
246# The .nc files are actually processed by a python script which executes the
247# compiler and generates a .cc file that is empty on success, or will have a
248# series of #error lines on failure, and a set of trivially passing gunit
249# TEST() functions on success. This allows us to fail at the compile step when
250# something goes wrong, and know during the unittest run that the test was at
251# least processed when things go right.
252
253if (enable_nocompile_tests) {
254  import("//build/config/c++/c++.gni")
255  import("//build/config/sysroot.gni")
256  import("//testing/test.gni")
257
258  if (is_mac) {
259    import("//build/config/mac/mac_sdk.gni")
260  }
261
262  template("nocompile_test") {
263    nocompile_target = target_name + "_run_nocompile"
264
265    action_foreach(nocompile_target) {
266      testonly = true
267      script = "//tools/nocompile/driver.py"
268      sources = invoker.sources
269      deps = invoker.deps
270      if (defined(invoker.public_deps)) {
271        public_deps = invoker.public_deps
272      }
273
274      result_path = "$target_gen_dir/{{source_name_part}}_nc.cc"
275      outputs = [ result_path ]
276      rebased_result_path = rebase_path(result_path, root_build_dir)
277      if (is_win) {
278        if (host_os == "win") {
279          cxx = "clang-cl.exe"
280          nulldevice = "nul"
281        } else {
282          cxx = "clang-cl"
283
284          # Unfortunately, clang-cl always wants to suffix the output file name
285          # with ".obj", and /dev/null.obj is not a valid file. As a bit of a
286          # hack, simply use the path to the generated .cc file, knowing:
287          # - that clang-cl will append ".obj" to the filename, so it will never
288          #   clash.
289          # - except when the nocompile test unexpectedly passes, the output
290          #   file will never actually be written.
291          nulldevice = rebased_result_path
292        }
293      } else {
294        cxx = "clang++"
295      }
296
297      depfile = "${result_path}.d"
298
299      args = []
300      if (is_win) {
301        args += [
302          "--depfile",
303          rebased_result_path + ".d",
304        ]
305      }
306      args += [
307        rebase_path("$clang_base_path/bin/$cxx", root_build_dir),
308        "4",  # number of compilers to invoke in parallel.
309        "{{source}}",
310        rebased_result_path,
311        "--",
312        "-Werror",
313        "-Wfatal-errors",
314        "-Wthread-safety",
315        "-I" + rebase_path("//", root_build_dir),
316        "-I" + rebase_path("//third_party/abseil-cpp/", root_build_dir),
317        "-I" + rebase_path("//buildtools/third_party/libc++/", root_build_dir),
318        "-I" + rebase_path(root_gen_dir, root_build_dir),
319        "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE",
320
321        # TODO(https://crbug.com/989932): Track build/config/compiler/BUILD.gn
322        "-Wno-implicit-int-float-conversion",
323      ]
324      if (is_win) {
325        # On Windows we fall back to using system headers from a sysroot from
326        # depot_tools. This is negotiated by python scripts and the result is
327        # available in //build/toolchain/win/win_toolchain_data.gni. From there
328        # we get the `include_flags_imsvc` which point to the system headers.
329        if (host_cpu == "x86") {
330          win_toolchain_data = win_toolchain_data_x86
331        } else if (host_cpu == "x64") {
332          win_toolchain_data = win_toolchain_data_x64
333        } else if (host_cpu == "arm64") {
334          win_toolchain_data = win_toolchain_data_arm64
335        } else {
336          error("Unsupported host_cpu, add it to win_toolchain_data.gni")
337        }
338        args += win_toolchain_data.include_flags_imsvc_list
339
340        args += [
341          "/W4",
342          "-Wno-unused-parameter",
343          "-I" + rebase_path("$libcxx_prefix/include", root_build_dir),
344          "/std:c++20",
345          "/showIncludes",
346          "/Fo" + nulldevice,
347          "/c",
348          "/Tp",
349        ]
350      } else {
351        args += [
352          "-Wall",
353          "-nostdinc++",
354          "-isystem" + rebase_path("$libcxx_prefix/include", root_build_dir),
355          "-isystem" + rebase_path("$libcxxabi_prefix/include", root_build_dir),
356          "-std=c++20",
357          "-MMD",
358          "-MF",
359          rebased_result_path + ".d",
360          "-MT",
361          rebased_result_path,
362          "-o",
363          "/dev/null",
364          "-c",
365          "-x",
366          "c++",
367        ]
368      }
369      args += [ "{{source}}" ]
370
371      if (is_mac && host_os != "mac") {
372        args += [
373          "--target=x86_64-apple-macos",
374          "-mmacos-version-min=$mac_deployment_target",
375        ]
376      }
377
378      # Iterate over any extra include dirs and append them to the command line.
379      if (defined(invoker.include_dirs)) {
380        foreach(include_dir, invoker.include_dirs) {
381          args += [ "-I" + rebase_path(include_dir, root_build_dir) ]
382        }
383      }
384
385      if (sysroot != "") {
386        assert(!is_win)
387        sysroot_path = rebase_path(sysroot, root_build_dir)
388        args += [
389          "--sysroot",
390          sysroot_path,
391        ]
392      }
393
394      if (!is_nacl) {
395        args += [
396          # TODO(crbug.com/1343975) Evaluate and possibly enable.
397          "-Wno-deprecated-builtins",
398        ]
399      }
400    }
401
402    test(target_name) {
403      deps = invoker.deps + [ ":$nocompile_target" ]
404      sources = get_target_outputs(":$nocompile_target")
405      forward_variables_from(invoker, [ "bundle_deps" ])
406    }
407  }
408}
409