44import pathlib
55import platform
66import pytest
7- from testutils import simplecpp , format_include_path_arg , format_include
7+ from testutils import (
8+ simplecpp ,
9+ format_include_path_arg ,
10+ format_framework_path_arg ,
11+ format_iframework_path_arg ,
12+ format_include ,
13+ )
14+
15+ def __test_create_header (dir , hdr_relpath , with_pragma_once = True , already_included_error_msg = None ):
16+ """
17+ Creates a C header file under `dir/hdr_relpath` with simple include guards.
18+
19+ The file contains:
20+ - optional `#pragma once` (when `with_pragma_once=True`)
21+ - a header guard derived from `hdr_relpath` (e.g. "test.h" -> TEST_H_INCLUDED)
22+ - optional `#error <already_included_error_msg>` if the guard is already defined
23+ - a dummy non-preprocessor declaration to force line emission
24+
25+ Return absolute path to the created header file.
26+ """
27+ inc_guard = hdr_relpath .upper ().replace ("." , "_" ) # "test.h" -> "TEST_H"
28+ header_file = os .path .join (dir , hdr_relpath )
29+ os .makedirs (os .path .dirname (header_file ), exist_ok = True )
30+ with open (header_file , 'wt' ) as f :
31+ f .write (f"""
32+ { "#pragma once" if with_pragma_once else "" }
33+ #ifndef { inc_guard } _INCLUDED
34+ #define { inc_guard } _INCLUDED
35+ #else
36+ { f"#error { already_included_error_msg } " if already_included_error_msg else "" }
37+ #endif
38+ int __force_line_emission; /* anything non-preprocessor */
39+ """ )
40+ return header_file
41+
42+ def __test_create_source (dir , include , is_include_sys = False ):
43+ """
44+ Creates a minimal C source file that includes a single header.
45+
46+ The generated `<dir>/test.c` contains one `#include` directive.
47+ If `is_include_sys` is True, the include is written as `<...>`;
48+ otherwise it is written as `"..."`.
49+
50+ Returns absolute path to the created header file.
51+ """
52+ src_file = os .path .join (dir , 'test.c' )
53+ with open (src_file , 'wt' ) as f :
54+ f .write (f"""
55+ #include { format_include (include , is_include_sys )}
56+ """ )
57+ return src_file
58+
59+ def __test_create_framework (dir , fw_name , hdr_relpath , content = "" , private = False ):
60+ """
61+ Creates a minimal Apple-style framework layout containing one header.
62+
63+ The generated structure is:
64+ `<dir>/<fw_name>.framework/{Headers|PrivateHeaders}/<hdr_relpath>`
65+
66+ The header file contains the given `content` followed by a dummy
67+ declaration to force line emission.
68+
69+ Returns absolute path to the created header file.
70+ """
71+ fwdir = os .path .join (dir , f"{ fw_name } .framework" , "PrivateHeaders" if private else "Headers" )
72+ header_file = os .path .join (fwdir , hdr_relpath )
73+ os .makedirs (os .path .dirname (header_file ), exist_ok = True )
74+ with open (header_file , "wt" , encoding = "utf-8" ) as f :
75+ f .write (f"""
76+ { content }
77+ int __force_line_emission; /* anything non-preprocessor */
78+ """ )
79+ return header_file
880
981def __test_relative_header_create_header (dir , with_pragma_once = True ):
1082 """
@@ -18,18 +90,10 @@ def __test_relative_header_create_header(dir, with_pragma_once=True):
1890 - absolute path to the created header file
1991 - expected compiler error substring for duplicate inclusion
2092 """
21- header_file = os .path .join (dir , 'test.h' )
22- with open (header_file , 'wt' ) as f :
23- f .write (f"""
24- { "#pragma once" if with_pragma_once else "" }
25- #ifndef TEST_H_INCLUDED
26- #define TEST_H_INCLUDED
27- #else
28- #error header_was_already_included
29- #endif
30- const int dummy = 1;
31- """ )
32- return header_file , "error: #error header_was_already_included"
93+ already_included_error_msg = "header_was_already_included"
94+ header_file = __test_create_header (
95+ dir , "test.h" , with_pragma_once = with_pragma_once , already_included_error_msg = already_included_error_msg )
96+ return header_file , f"error: #error { already_included_error_msg } "
3397
3498def __test_relative_header_create_source (dir , include1 , include2 , is_include1_sys = False , is_include2_sys = False , inv = False ):
3599 """
@@ -258,6 +322,175 @@ def test_same_name_header(record_property, tmpdir):
258322 assert "OK" in stdout
259323 assert stderr == ""
260324
325+ @pytest .mark .parametrize ("is_sys" , (False , True ))
326+ @pytest .mark .parametrize ("is_iframework" , (False , True ))
327+ @pytest .mark .parametrize ("is_private" , (False , True ))
328+ def test_framework_lookup (record_property , tmpdir , is_sys , is_iframework , is_private ):
329+ # Arrange framework: <tmp>/FwRoot/MyKit.framework/(Headers|PrivateHeaders)/Component.h
330+ fw_root = os .path .join (tmpdir , "FwRoot" )
331+ __test_create_framework (fw_root , "MyKit" , "Component.h" , private = is_private )
332+
333+ test_file = __test_create_source (tmpdir , "MyKit/Component.h" , is_include_sys = is_sys )
334+
335+ args = [format_iframework_path_arg (fw_root ) if is_iframework else format_framework_path_arg (fw_root ), test_file ]
336+ _ , stdout , stderr = simplecpp (args , cwd = tmpdir )
337+ record_property ("stdout" , stdout )
338+ record_property ("stderr" , stderr )
339+
340+ assert stderr == ""
341+ relative = "PrivateHeaders" if is_private else "Headers"
342+ assert f'#line 3 "{ pathlib .PurePath (tmpdir ).as_posix ()} /FwRoot/MyKit.framework/{ relative } /Component.h"' in stdout
343+
344+ @pytest .mark .parametrize ("is_sys" , (False , True ))
345+ @pytest .mark .parametrize (
346+ "order,expected" ,
347+ [
348+ # Note:
349+ # - `I1` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy).
350+ # - `I` / `F` / `IFW` point to directories that contain `Component.h`, which the
351+ # translation unit (TU) includes via `#include "MyKit/Component.h"`.
352+ #
353+ # This makes the winning flag (-I, -F, or -iframework) uniquely identifiable
354+ # in the preprocessor `#line` output.
355+
356+ # Sanity checks
357+ (("I" ,), "I" ),
358+ (("F" ,), "F" ),
359+ (("IFW" ,), "IFW" ),
360+
361+ # Includes (-I)
362+ (("I1" , "I" ), "I" ),
363+ (("I" , "I1" ), "I" ),
364+ # Includes (-I) duplicates
365+ (("I1" , "I" , "I1" ), "I" ),
366+ (("I" , "I1" , "I" ), "I" ),
367+
368+ # Framework (-F)
369+ (("F1" , "F" ), "F" ),
370+ (("F" , "F1" ), "F" ),
371+ # Framework (-F) duplicates
372+ (("F1" , "F" , "F1" ), "F" ),
373+ (("F" , "F1" , "F" ), "F" ),
374+
375+ # System framework (-iframework)
376+ (("IFW1" , "IFW" ), "IFW" ),
377+ (("IFW" , "IFW1" ), "IFW" ),
378+ # System framework (-iframework) duplicates
379+ (("IFW1" , "IFW" , "IFW1" ), "IFW" ),
380+ (("IFW" , "IFW1" , "IFW" ), "IFW" ),
381+
382+ # -I and -F are processed as specified (left-to-right)
383+ (("I" , "F" ), "I" ),
384+ (("I1" , "I" , "F" ), "I" ),
385+ (("F" , "I" ), "F" ),
386+ (("F1" , "F" , "I" ), "F" ),
387+
388+ # -I and -F beat system framework (-iframework)
389+ (("I" , "IFW" ), "I" ),
390+ (("F" , "IFW" ), "F" ),
391+ (("IFW" , "F" ), "F" ),
392+ (("IFW" , "I" , "F" ), "I" ),
393+ (("IFW" , "I1" , "F1" , "I" , "F" ), "I" ),
394+ (("IFW" , "I" ), "I" ),
395+ (("IFW" , "F" , "I" ), "F" ),
396+ (("IFW" , "F1" , "I1" , "F" , "I" ), "F" ),
397+ ],
398+ )
399+ def test_searchpath_order (record_property , tmpdir , is_sys , order , expected ):
400+ """
401+ Validate include resolution order across -I (user include), -F (user framework),
402+ and -iframework (system framework) using a minimal file layout, asserting which
403+ physical header path appears in the preprocessor #line output.
404+
405+ The test constructs three parallel trees (two entries per kind):
406+ - inc{,_1}/MyKit/Component{,_1}.h # for -I
407+ - Fw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -F
408+ - SysFw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -iframework
409+
410+ It then preprocesses a TU that includes "MyKit/Component.h" (or <...> when
411+ is_sys=True), assembles compiler args in the exact `order`, and asserts that
412+ only the expected path appears in #line. Distinct names (Component.h vs
413+ Component_1.h) ensure a unique winner per bucket.
414+
415+ References:
416+ - https://gcc.gnu.org/onlinedocs/cpp/Invocation.html#Invocation
417+ - https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html
418+ """
419+
420+ # Create two include dirs, two user framework dirs, and two system framework dirs
421+ inc_dirs , fw_dirs , sysfw_dirs = [], [], []
422+
423+ def _suffix (idx : int ) -> str :
424+ return f"_{ idx } " if idx > 0 else ""
425+
426+ for idx in range (2 ):
427+ # -I paths
428+ inc_dir = os .path .join (tmpdir , f"inc{ _suffix (idx )} " )
429+ __test_create_header (inc_dir , hdr_relpath = f"MyKit/Component{ _suffix (idx )} .h" )
430+ inc_dirs .append (inc_dir )
431+
432+ # -F paths (user frameworks)
433+ fw_dir = os .path .join (tmpdir , f"Fw{ _suffix (idx )} " )
434+ __test_create_framework (fw_dir , "MyKit" , f"Component{ _suffix (idx )} .h" )
435+ fw_dirs .append (fw_dir )
436+
437+ # -iframework paths (system frameworks)
438+ sysfw_dir = os .path .join (tmpdir , f"SysFw{ _suffix (idx )} " )
439+ __test_create_framework (sysfw_dir , "MyKit" , f"Component{ _suffix (idx )} .h" )
440+ sysfw_dirs .append (sysfw_dir )
441+
442+ # Translation unit under test: include MyKit/Component.h (quote or system form)
443+ test_file = __test_create_source (tmpdir , "MyKit/Component.h" , is_include_sys = is_sys )
444+
445+ def idx_from_flag (prefix : str , flag : str ) -> int :
446+ """Extract numeric suffix from tokens like 'I1', 'F1', 'IFW1'.
447+ Returns 0 when no suffix is present (e.g., 'I', 'F', 'IFW')."""
448+ return int (flag [len (prefix ):]) if len (flag ) > len (prefix ) else 0
449+
450+ # Build argv in the exact order requested by `order`
451+ args = []
452+ for flag in order :
453+ if flag in ["I" , "I1" ]:
454+ args .append (format_include_path_arg (inc_dirs [idx_from_flag ("I" , flag )]))
455+ elif flag in ["F" , "F1" ]:
456+ args .append (format_framework_path_arg (fw_dirs [idx_from_flag ("F" , flag )]))
457+ elif flag in ["IFW" , "IFW1" ]:
458+ args .append (format_iframework_path_arg (sysfw_dirs [idx_from_flag ("IFW" , flag )]))
459+ else :
460+ raise AssertionError (f"unknown flag in order: { flag } " )
461+ args .append (test_file )
462+
463+ # Run the preprocessor and capture outputs
464+ _ , stdout , stderr = simplecpp (args , cwd = tmpdir )
465+ record_property ("stdout" , stdout )
466+ record_property ("stderr" , stderr )
467+
468+ # Resolve the absolute expected/forbidden paths we want to see in #line output
469+ root = pathlib .PurePath (tmpdir ).as_posix ()
470+
471+ inc_paths = [f"{ root } /inc{ _suffix (idx )} /MyKit/Component{ _suffix (idx )} .h" for idx in range (2 )]
472+ fw_paths = [f"{ root } /Fw{ _suffix (idx )} /MyKit.framework/Headers/Component{ _suffix (idx )} .h" for idx in range (2 )]
473+ ifw_paths = [f"{ root } /SysFw{ _suffix (idx )} /MyKit.framework/Headers/Component{ _suffix (idx )} .h" for idx in range (2 )]
474+ all_candidate_paths = [* inc_paths , * fw_paths , * ifw_paths ]
475+
476+ # Compute the single path we expect to appear
477+ expected_path = None
478+ if expected in ["I" , "I1" ]:
479+ expected_path = inc_paths [idx_from_flag ("I" , expected )]
480+ elif expected in ["F" , "F1" ]:
481+ expected_path = fw_paths [idx_from_flag ("F" , expected )]
482+ elif expected in ["IFW" , "IFW1" ]:
483+ expected_path = ifw_paths [idx_from_flag ("IFW" , expected )]
484+ assert expected_path is not None , "test configuration error: expected token not recognized"
485+
486+ # Assert ONLY the expected path appears in the preprocessor #line output
487+ assert expected_path in stdout
488+ for p in (p for p in all_candidate_paths if p != expected_path ):
489+ assert p not in stdout
490+
491+ # No diagnostics expected
492+ assert stderr == ""
493+
261494def test_pragma_once_matching (record_property , tmpdir ):
262495 test_dir = os .path .join (tmpdir , "test_dir" )
263496 test_subdir = os .path .join (test_dir , "test_subdir" )
0 commit comments