Further tweak CPPDEFINES repleacement in scanner
[scons.git] / SCons / Scanner / CTests.py
blobb0fdb566e2ee4756ca06c0c93f61c15633009dd3
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 import collections
25 import os
26 import unittest
28 import TestCmd
29 import TestUnit
31 import SCons.compat
32 import SCons.Node.FS
33 import SCons.Warnings
35 import SCons.Scanner.C
37 test = TestCmd.TestCmd(workdir = '')
39 os.chdir(test.workpath(''))
41 # create some source files and headers:
43 test.write('f1.cpp',"""
44 #ifdef INCLUDE_F2 /* multi-line comment */
45 #include <f2.h>
46 #else
47 #include \"f1.h\"
48 #endif
50 int main(void)
52 return 0;
54 """)
56 test.write('f2.cpp',"""
57 #include \"d1/f1.h\"
59 #if 5UL < 10 && !defined(DUMMY_MACRO) // some comment
60 #if NESTED_CONDITION
61 #include <d2/f1.h>
62 #endif
63 #else
64 #include \"f1.h\"
65 #endif
67 #import <f4.h>
69 int main(void)
71 return 0;
73 """)
75 test.write('f3.cpp',"""
76 #include \t "f1.h"
77 \t #include "f2.h"
78 # \t include "f3-test.h"
80 #include \t <d1/f1.h>
81 \t #include <d1/f2.h>
82 # \t include <d1/f3-test.h>
84 // #include "never.h"
86 const char* x = "#include <never.h>"
88 int main(void)
90 return 0;
92 """)
94 # include using a macro, defined in source file
95 test.write('f9a.c', """\
96 #define HEADER "f9.h"
97 #include HEADER
99 int main(void)
101 return 0;
103 """)
105 # include using a macro, not defined in source file
106 test.write('f9b.c', """\
107 #include HEADER
109 int main(void)
111 return 0;
113 """)
116 # for Emacs -> "
118 test.subdir('d1', ['d1', 'd2'])
120 headers = ['f1.h','f2.h', 'f3-test.h', 'fi.h', 'fj.h', 'never.h',
121 'd1/f1.h', 'd1/f2.h', 'd1/f3-test.h', 'd1/fi.h', 'd1/fj.h',
122 'd1/d2/f1.h', 'd1/d2/f2.h', 'd1/d2/f3-test.h',
123 'd1/d2/f4.h', 'd1/d2/fi.h', 'd1/d2/fj.h', 'f9.h']
125 for h in headers:
126 test.write(h, " ")
128 test.write('f2.h',"""
129 #include "fi.h"
130 """)
132 test.write('f3-test.h',"""
133 #include <fj.h>
134 """)
137 test.subdir('include', 'subdir', ['subdir', 'include'])
139 test.write('fa.cpp',"""
140 #include \"fa.h\"
141 #include <fb.h>
143 int main(void)
145 return 0;
147 """)
149 test.write(['include', 'fa.h'], "\n")
150 test.write(['include', 'fb.h'], "\n")
151 test.write(['subdir', 'include', 'fa.h'], "\n")
152 test.write(['subdir', 'include', 'fb.h'], "\n")
155 test.subdir('repository', ['repository', 'include'],
156 ['repository', 'src' ])
157 test.subdir('work', ['work', 'src'])
159 test.write(['repository', 'include', 'iii.h'], "\n")
161 test.write(['work', 'src', 'fff.c'], """
162 #include <iii.h>
163 #include <jjj.h>
165 int main(void)
167 return 0;
169 """)
171 test.write([ 'work', 'src', 'aaa.c'], """
172 #include "bbb.h"
174 int main(void)
176 return 0;
178 """)
180 test.write([ 'work', 'src', 'bbb.h'], "\n")
182 test.write([ 'repository', 'src', 'ccc.c'], """
183 #include "ddd.h"
185 int main(void)
187 return 0;
189 """)
191 test.write([ 'repository', 'src', 'ddd.h'], "\n")
193 test.write('f5.c', """\
194 #include\"f5a.h\"
195 #include<f5b.h>
196 """)
198 test.write("f5a.h", "\n")
199 test.write("f5b.h", "\n")
201 # define some helpers:
203 class DummyEnvironment(collections.UserDict):
204 def __init__(self, **kwargs) -> None:
205 super().__init__()
206 self.data.update(kwargs)
207 self.fs = SCons.Node.FS.FS(test.workpath(''))
209 def Dictionary(self, *args):
210 return self.data
212 def subst(self, strSubst, target=None, source=None, conv=None):
213 if strSubst[0] == '$':
214 return self.data[strSubst[1:]]
215 return strSubst
217 def subst_list(self, strSubst, target=None, source=None, conv=None):
218 if strSubst[0] == '$':
219 return [self.data[strSubst[1:]]]
220 return [[strSubst]]
222 def subst_path(self, path, target=None, source=None, conv=None):
223 if not isinstance(path, list):
224 path = [path]
225 return list(map(self.subst, path))
227 def get_calculator(self):
228 return None
230 def get_factory(self, factory):
231 return factory or self.fs.File
233 def Dir(self, filename):
234 return self.fs.Dir(filename)
236 def File(self, filename):
237 return self.fs.File(filename)
239 if os.path.normcase('foo') == os.path.normcase('FOO'):
240 my_normpath = os.path.normcase
241 else:
242 my_normpath = os.path.normpath
244 def deps_match(self, deps, headers) -> None:
245 global my_normpath
246 scanned = list(map(my_normpath, list(map(str, deps))))
247 expect = list(map(my_normpath, headers))
248 self.assertTrue(scanned == expect, f"expect {expect} != scanned {scanned}")
250 # define some tests:
252 class CScannerTestCase1(unittest.TestCase):
253 def runTest(self) -> None:
254 """Find local files with no CPPPATH"""
255 env = DummyEnvironment(CPPPATH=[])
256 s = SCons.Scanner.C.CScanner()
257 path = s.path(env)
258 deps = s(env.File('f1.cpp'), env, path)
259 headers = ['f1.h', 'f2.h']
260 deps_match(self, deps, headers)
262 class CScannerTestCase2(unittest.TestCase):
263 def runTest(self) -> None:
264 """Find a file in a CPPPATH directory"""
265 env = DummyEnvironment(CPPPATH=[test.workpath("d1")])
266 s = SCons.Scanner.C.CScanner()
267 path = s.path(env)
268 deps = s(env.File('f1.cpp'), env, path)
269 headers = ['f1.h', 'd1/f2.h']
270 deps_match(self, deps, headers)
272 class CScannerTestCase3(unittest.TestCase):
273 def runTest(self) -> None:
274 """Find files in explicit subdirectories, ignore missing file"""
275 env = DummyEnvironment(CPPPATH=[test.workpath("d1")])
276 s = SCons.Scanner.C.CScanner()
277 path = s.path(env)
278 deps = s(env.File('f2.cpp'), env, path)
279 headers = ['d1/f1.h', 'f1.h', 'd1/d2/f1.h']
280 deps_match(self, deps, headers)
282 class CScannerTestCase4(unittest.TestCase):
283 def runTest(self) -> None:
284 """Find files in explicit subdirectories"""
285 env = DummyEnvironment(CPPPATH=[test.workpath("d1"), test.workpath("d1/d2")])
286 s = SCons.Scanner.C.CScanner()
287 path = s.path(env)
288 deps = s(env.File('f2.cpp'), env, path)
289 headers = ['d1/f1.h', 'f1.h', 'd1/d2/f1.h', 'd1/d2/f4.h']
290 deps_match(self, deps, headers)
292 class CScannerTestCase5(unittest.TestCase):
293 def runTest(self) -> None:
294 """Make sure files in repositories will get scanned"""
295 env = DummyEnvironment(CPPPATH=[])
296 s = SCons.Scanner.C.CScanner()
297 path = s.path(env)
299 n = env.File('f3.cpp')
300 def my_rexists(s):
301 s.Tag('rexists_called', 1)
302 return SCons.Node._rexists_map[s.GetTag('old_rexists')](s)
303 n.Tag('old_rexists', n._func_rexists)
304 SCons.Node._rexists_map[3] = my_rexists
305 n._func_rexists = 3
307 deps = s(n, env, path)
309 # Make sure rexists() got called on the file node being
310 # scanned, essential for cooperation with VariantDir functionality.
311 assert n.GetTag('rexists_called')
313 headers = ['f1.h', 'f2.h', 'f3-test.h',
314 'd1/f1.h', 'd1/f2.h', 'd1/f3-test.h']
315 deps_match(self, deps, headers)
317 class CScannerTestCase6(unittest.TestCase):
318 def runTest(self) -> None:
319 """Find a same-named file in different directories when CPPPATH changes"""
320 env1 = DummyEnvironment(CPPPATH=[test.workpath("d1")])
321 env2 = DummyEnvironment(CPPPATH=[test.workpath("d1/d2")])
322 s = SCons.Scanner.C.CScanner()
323 path1 = s.path(env1)
324 path2 = s.path(env2)
325 deps1 = s(env1.File('f1.cpp'), env1, path1)
326 deps2 = s(env2.File('f1.cpp'), env2, path2)
327 headers1 = ['f1.h', 'd1/f2.h']
328 headers2 = ['f1.h', 'd1/d2/f2.h']
329 deps_match(self, deps1, headers1)
330 deps_match(self, deps2, headers2)
332 class CScannerTestCase8(unittest.TestCase):
333 def runTest(self) -> None:
334 """Find files in a subdirectory relative to the current directory"""
335 env = DummyEnvironment(CPPPATH=["include"])
336 s = SCons.Scanner.C.CScanner()
337 path = s.path(env)
338 deps1 = s(env.File('fa.cpp'), env, path)
339 env.fs.chdir(env.Dir('subdir'))
340 dir = env.fs.getcwd()
341 env.fs.chdir(env.Dir(''))
342 path = s.path(env, dir)
343 deps2 = s(env.File('#fa.cpp'), env, path)
344 headers1 = list(map(test.workpath, ['include/fa.h', 'include/fb.h']))
345 headers2 = ['include/fa.h', 'include/fb.h']
346 deps_match(self, deps1, headers1)
347 deps_match(self, deps2, headers2)
349 class CScannerTestCase9(unittest.TestCase):
350 def runTest(self) -> None:
351 """Generate a warning when we can't find a #included file"""
352 SCons.Warnings.enableWarningClass(SCons.Warnings.DependencyWarning)
353 class TestOut:
354 def __call__(self, x) -> None:
355 self.out = x
357 to = TestOut()
358 to.out = None
359 SCons.Warnings._warningOut = to
360 test.write('fa.h','\n')
361 fs = SCons.Node.FS.FS(test.workpath(''))
362 env = DummyEnvironment(CPPPATH=[])
363 env.fs = fs
364 s = SCons.Scanner.C.CScanner()
365 path = s.path(env)
366 deps = s(fs.File('fa.cpp'), env, path)
368 # Did we catch the warning associated with not finding fb.h?
369 assert to.out
371 deps_match(self, deps, [ 'fa.h' ])
372 test.unlink('fa.h')
374 class CScannerTestCase10(unittest.TestCase):
375 def runTest(self) -> None:
376 """Find files in the local directory when the scanned file is elsewhere"""
377 fs = SCons.Node.FS.FS(test.workpath(''))
378 fs.chdir(fs.Dir('include'))
379 env = DummyEnvironment(CPPPATH=[])
380 env.fs = fs
381 s = SCons.Scanner.C.CScanner()
382 path = s.path(env)
383 test.write('include/fa.cpp', test.read('fa.cpp'))
384 fs.chdir(fs.Dir('..'))
385 deps = s(fs.File('#include/fa.cpp'), env, path)
386 deps_match(self, deps, [ 'include/fa.h', 'include/fb.h' ])
387 test.unlink('include/fa.cpp')
389 class CScannerTestCase11(unittest.TestCase):
390 def runTest(self) -> None:
391 """Handle dependencies on a derived .h file in a non-existent directory"""
392 os.chdir(test.workpath('work'))
393 fs = SCons.Node.FS.FS(test.workpath('work'))
394 fs.Repository(test.workpath('repository'))
396 # Create a derived file in a directory that does not exist yet.
397 # This was a bug at one time.
398 f1=fs.File('include2/jjj.h')
399 f1.builder=1
400 env = DummyEnvironment(CPPPATH=['include', 'include2'])
401 env.fs = fs
402 s = SCons.Scanner.C.CScanner()
403 path = s.path(env)
404 deps = s(fs.File('src/fff.c'), env, path)
405 deps_match(self, deps, [ test.workpath('repository/include/iii.h'),
406 'include2/jjj.h' ])
407 os.chdir(test.workpath(''))
409 class CScannerTestCase12(unittest.TestCase):
410 def runTest(self) -> None:
411 """Find files in VariantDir() directories"""
412 os.chdir(test.workpath('work'))
413 fs = SCons.Node.FS.FS(test.workpath('work'))
414 fs.VariantDir('build1', 'src', 1)
415 fs.VariantDir('build2', 'src', 0)
416 fs.Repository(test.workpath('repository'))
417 env = DummyEnvironment(CPPPATH=[])
418 env.fs = fs
419 s = SCons.Scanner.C.CScanner()
420 path = s.path(env)
421 deps1 = s(fs.File('build1/aaa.c'), env, path)
422 deps_match(self, deps1, [ 'build1/bbb.h' ])
423 deps2 = s(fs.File('build2/aaa.c'), env, path)
424 deps_match(self, deps2, [ 'src/bbb.h' ])
425 deps3 = s(fs.File('build1/ccc.c'), env, path)
426 deps_match(self, deps3, [ 'build1/ddd.h' ])
427 deps4 = s(fs.File('build2/ccc.c'), env, path)
428 deps_match(self, deps4, [ test.workpath('repository/src/ddd.h') ])
429 os.chdir(test.workpath(''))
431 class CScannerTestCase13(unittest.TestCase):
432 def runTest(self) -> None:
433 """Find files in directories named in a substituted environment variable"""
434 class SubstEnvironment(DummyEnvironment):
435 def subst(self, arg, target=None, source=None, conv=None, test=test):
436 if arg == "$blah":
437 return test.workpath("d1")
438 else:
439 return arg
440 env = SubstEnvironment(CPPPATH=["$blah"])
441 s = SCons.Scanner.C.CScanner()
442 path = s.path(env)
443 deps = s(env.File('f1.cpp'), env, path)
444 headers = ['f1.h', 'd1/f2.h']
445 deps_match(self, deps, headers)
447 class CScannerTestCase14(unittest.TestCase):
448 def runTest(self) -> None:
449 """Find files when there's no space between "#include" and the name"""
450 env = DummyEnvironment(CPPPATH=[])
451 s = SCons.Scanner.C.CScanner()
452 path = s.path(env)
453 deps = s(env.File('f5.c'), env, path)
454 headers = ['f5a.h', 'f5b.h']
455 deps_match(self, deps, headers)
457 class CScannerTestCase15(unittest.TestCase):
458 def runTest(self) -> None:
459 """Verify scanner initialization with the suffixes in $CPPSUFFIXES"""
460 suffixes = [".c", ".C", ".cxx", ".cpp", ".c++", ".cc",
461 ".h", ".H", ".hxx", ".hpp", ".hh",
462 ".F", ".fpp", ".FPP",
463 ".S", ".spp", ".SPP"]
464 env = DummyEnvironment(CPPSUFFIXES = suffixes)
465 s = SCons.Scanner.C.CScanner()
466 for suffix in suffixes:
467 assert suffix in s.get_skeys(env), f"{suffix} not in skeys"
470 class CConditionalScannerTestCase1(unittest.TestCase):
471 def runTest(self) -> None:
472 """Find local files with no CPPPATH"""
473 env = DummyEnvironment(CPPPATH=[])
474 s = SCons.Scanner.C.CConditionalScanner()
475 path = s.path(env)
476 deps = s(env.File('f1.cpp'), env, path)
477 headers = ['f1.h']
478 deps_match(self, deps, headers)
481 class CConditionalScannerTestCase2(unittest.TestCase):
482 def runTest(self) -> None:
483 """Find local files with no CPPPATH based on #ifdef"""
484 env = DummyEnvironment(CPPPATH=[], CPPDEFINES=["INCLUDE_F2"])
485 s = SCons.Scanner.C.CConditionalScanner()
486 path = s.path(env)
487 deps = s(env.File('f1.cpp'), env, path)
488 headers = ['f2.h', 'fi.h']
489 deps_match(self, deps, headers)
492 class CConditionalScannerTestCase3(unittest.TestCase):
493 def runTest(self) -> None:
494 """Find files in explicit subdirectories, ignore missing file"""
495 env = DummyEnvironment(
496 CPPPATH=[test.workpath("d1")],
497 CPPDEFINES=[("NESTED_CONDITION", 1)]
499 s = SCons.Scanner.C.CConditionalScanner()
500 deps = s(env.File('f2.cpp'), env, s.path(env))
501 headers = ['d1/f1.h', 'd1/d2/f1.h']
502 deps_match(self, deps, headers)
504 # disable nested conditions
505 env = DummyEnvironment(
506 CPPPATH=[test.workpath("d1")],
507 CPPDEFINES=[("NESTED_CONDITION", 0)]
509 s = SCons.Scanner.C.CConditionalScanner()
510 deps = s(env.File('f2.cpp'), env, s.path(env))
511 headers = ['d1/f1.h']
512 deps_match(self, deps, headers)
515 class CConditionalScannerTestCase4(unittest.TestCase):
516 def runTest(self) -> None:
517 """Test that dependency is detected if #include uses a macro."""
519 with self.subTest("macro defined in the source file"):
520 env = DummyEnvironment()
521 s = SCons.Scanner.C.CConditionalScanner()
522 deps = s(env.File('f9a.c'), env, s.path(env))
523 headers = ['f9.h']
524 deps_match(self, deps, headers)
526 with self.subTest("macro defined on the command line"):
527 env = DummyEnvironment(CPPDEFINES='HEADER=\\"f9.h\\"')
528 #env = DummyEnvironment(CPPDEFINES=['HEADER=\\"f9.h\\"'])
529 s = SCons.Scanner.C.CConditionalScanner()
530 deps = s(env.File('f9b.c'), env, s.path(env))
531 headers = ['f9.h']
532 deps_match(self, deps, headers)
535 class dictify_CPPDEFINESTestCase(unittest.TestCase):
536 def runTest(self) -> None:
537 """Make sure CPPDEFINES converts correctly.
539 Various types and combinations of types could fail if not handled
540 specifically by dictify_CPPDEFINES - this is a regression test.
542 with self.subTest("tuples in a sequence, including one without value"):
543 env = DummyEnvironment(CPPDEFINES=[("VALUED", 1), ("UNVALUED",)])
544 d = SCons.Scanner.C.dictify_CPPDEFINES(env)
545 expect = {"VALUED": 1, "UNVALUED": None}
546 self.assertEqual(d, expect)
548 with self.subTest("tuple-valued define by itself"):
549 env = DummyEnvironment(CPPDEFINES=("STRING", "VALUE"))
550 d = SCons.Scanner.C.dictify_CPPDEFINES(env)
551 expect = {"STRING": "VALUE"}
552 self.assertEqual(d, expect)
554 with self.subTest("string-valued define in a sequence"):
555 env = DummyEnvironment(CPPDEFINES=[("STRING=VALUE")])
556 d = SCons.Scanner.C.dictify_CPPDEFINES(env)
557 expect = {"STRING": "VALUE"}
558 self.assertEqual(d, expect)
560 with self.subTest("string-valued define by itself"):
561 env = DummyEnvironment(CPPDEFINES="STRING=VALUE")
562 d = SCons.Scanner.C.dictify_CPPDEFINES(env)
563 expect = {"STRING": "VALUE"}
564 self.assertEqual(d, expect)
566 from collections import deque
567 with self.subTest("compound CPPDEFINES in internal format"):
568 env = DummyEnvironment(
569 CPPDEFINES=deque([("STRING", "VALUE"), ("UNVALUED",)])
571 d = SCons.Scanner.C.dictify_CPPDEFINES(env)
572 expect = {"STRING": "VALUE", "UNVALUED": None}
573 self.assertEqual(d, expect)
575 with self.subTest("CPPDEFINES with macro replacement"):
576 env = DummyEnvironment(
577 CPPDEFINES=[
578 ("STRING", "VALUE"),
579 ("REPLACEABLE", "RVALUE"),
580 ("RVALUE", "AVALUE"),
583 d = SCons.Scanner.C.dictify_CPPDEFINES(env, replace=True)
584 expect = {"STRING": "VALUE", "REPLACEABLE": "AVALUE", "RVALUE": "AVALUE"}
585 self.assertEqual(d, expect)
588 if __name__ == "__main__":
589 unittest.main()
591 # Local Variables:
592 # tab-width:4
593 # indent-tabs-mode:nil
594 # End:
595 # vim: set expandtab tabstop=4 shiftwidth=4: