Skip to content
Open
310 changes: 301 additions & 9 deletions integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,110 @@
import pathlib
import platform
import pytest
from testutils import simplecpp, format_include_path_arg, format_include

def __test_relative_header_create_header(dir, with_pragma_once=True):
header_file = os.path.join(dir, 'test.h')
from testutils import (
simplecpp,
format_include_path_arg,
format_isystem_path_arg,
format_framework_path_arg,
format_iframework_path_arg,
format_include,
)

def __test_create_header(dir, hdr_relpath, with_pragma_once=True, already_included_error_msg=None):
"""
Creates a C header file under `dir/hdr_relpath` with simple include guards.

The file contains:
- optional `#pragma once` (when `with_pragma_once=True`)
- a header guard derived from `hdr_relpath` (e.g. "test.h" -> TEST_H_INCLUDED)
- optional `#error <already_included_error_msg>` if the guard is already defined
- a dummy non-preprocessor declaration to force line emission

Return absolute path to the created header file.
"""
inc_guard = hdr_relpath.upper().replace(".", "_") # "test.h" -> "TEST_H"
header_file = os.path.join(dir, hdr_relpath)
os.makedirs(os.path.dirname(header_file), exist_ok=True)
with open(header_file, 'wt') as f:
f.write(f"""
{"#pragma once" if with_pragma_once else ""}
#ifndef TEST_H_INCLUDED
#define TEST_H_INCLUDED
#ifndef {inc_guard}_INCLUDED
#define {inc_guard}_INCLUDED
#else
#error header_was_already_included
{f"#error {already_included_error_msg}" if already_included_error_msg else ""}
#endif
const int dummy = 1;
int __force_line_emission; /* anything non-preprocessor */
""")
return header_file

def __test_create_source(dir, include, is_include_sys=False):
"""
Creates a minimal C source file that includes a single header.

The generated `<dir>/test.c` contains one `#include` directive.
If `is_include_sys` is True, the include is written as `<...>`;
otherwise it is written as `"..."`.

Returns absolute path to the created header file.
"""
src_file = os.path.join(dir, 'test.c')
with open(src_file, 'wt') as f:
f.write(f"""
#include {format_include(include, is_include_sys)}
""")
return header_file, "error: #error header_was_already_included"
return src_file

def __test_create_framework(dir, fw_name, hdr_relpath, content="", private=False):
"""
Creates a minimal Apple-style framework layout containing one header.

The generated structure is:
`<dir>/<fw_name>.framework/{Headers|PrivateHeaders}/<hdr_relpath>`

The header file contains the given `content` followed by a dummy
declaration to force line emission.

Returns absolute path to the created header file.
"""
fwdir = os.path.join(dir, f"{fw_name}.framework", "PrivateHeaders" if private else "Headers")
header_file = os.path.join(fwdir, hdr_relpath)
os.makedirs(os.path.dirname(header_file), exist_ok=True)
with open(header_file, "wt", encoding="utf-8") as f:
f.write(f"""
{content}
int __force_line_emission; /* anything non-preprocessor */
""")
return header_file

def __test_relative_header_create_header(dir, with_pragma_once=True):
"""
Creates a local `test.h` header with both `#pragma once` (optional)
and a macro guard.

The header emits `#error header_was_already_included` if it is
re-included past the guard.

Returns tuple of:
- absolute path to the created header file
- expected compiler error substring for duplicate inclusion
"""
already_included_error_msg="header_was_already_included"
header_file = __test_create_header(
dir, "test.h", with_pragma_once=with_pragma_once, already_included_error_msg=already_included_error_msg)
return header_file, f"error: #error {already_included_error_msg}"

def __test_relative_header_create_source(dir, include1, include2, is_include1_sys=False, is_include2_sys=False, inv=False):
"""
Creates a C source file that includes two headers in order.

The generated `<dir>/test.c`:
- `#undef TEST_H_INCLUDED` to reset the guard in `test.h`
- includes `include1` then `include2`
- if `inv=True`, the order is swapped (`include2` then `include1`)
- each include can be written as `<...>` or `"..."`

Returns absolute path to the created source file.
"""
if inv:
return __test_relative_header_create_source(dir, include1=include2, include2=include1, is_include1_sys=is_include2_sys, is_include2_sys=is_include1_sys)
## otherwise
Expand Down Expand Up @@ -236,6 +323,211 @@ def test_same_name_header(record_property, tmpdir):
assert "OK" in stdout
assert stderr == ""

@pytest.mark.parametrize("is_sys", (False, True))
@pytest.mark.parametrize("is_iframework", (False, True))
@pytest.mark.parametrize("is_private", (False, True))
def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_private):
# Arrange framework: <tmp>/FwRoot/MyKit.framework/(Headers|PrivateHeaders)/Component.h
fw_root = os.path.join(tmpdir, "FwRoot")
__test_create_framework(fw_root, "MyKit", "Component.h", private=is_private)

test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys)

args = [format_iframework_path_arg(fw_root) if is_iframework else format_framework_path_arg(fw_root), test_file]
_, stdout, stderr = simplecpp(args, cwd=tmpdir)
record_property("stdout", stdout)
record_property("stderr", stderr)

assert stderr == ""
relative = "PrivateHeaders" if is_private else "Headers"
assert f'#line 3 "{pathlib.PurePath(tmpdir).as_posix()}/FwRoot/MyKit.framework/{relative}/Component.h"' in stdout

@pytest.mark.parametrize("is_sys", (False, True))
@pytest.mark.parametrize(
"order,expected",
[
# Note:
# - `I1` / `ISYS1` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy).
# - `I` / `ISYS` / `F` / `IFW` point to directories that contain `Component.h`, which the
# translation unit (TU) includes via `#include "MyKit/Component.h"`.
#
# This makes the winning flag (-I, -isystem, -F, or -iframework) uniquely identifiable
# in the preprocessor `#line` output.

# Sanity checks
(("I",), "I"),
(("ISYS",), "ISYS"),
(("F",), "F"),
(("IFW",), "IFW"),

# Includes (-I)
(("I1", "I"), "I"),
(("I", "I1"), "I"),
# Includes (-I) duplicates
(("I1", "I", "I1"), "I"),
(("I", "I1", "I"), "I"),

# System includes (-isystem)
(("ISYS1", "ISYS"), "ISYS"),
(("ISYS", "ISYS1"), "ISYS"),
# System includes (-isystem) duplicates
(("ISYS1", "ISYS", "ISYS1"), "ISYS"),
(("ISYS", "ISYS1", "ISYS"), "ISYS"),

# Framework (-F)
(("F1", "F"), "F"),
(("F", "F1"), "F"),
# Framework (-F) duplicates
(("F1", "F", "F1"), "F"),
(("F", "F1", "F"), "F"),

# System framework (-iframework)
(("IFW1", "IFW"), "IFW"),
(("IFW", "IFW1"), "IFW"),
# System framework (-iframework) duplicates
(("IFW1", "IFW", "IFW1"), "IFW"),
(("IFW", "IFW1", "IFW"), "IFW"),

# -I and -F are processed as specified (left-to-right)
(("I", "F"), "I"),
(("I1", "I", "F"), "I"),
(("F", "I"), "F"),
(("F1", "F", "I"), "F"),

# -I and -F takes precedence over -isystem
(("I", "ISYS"), "I"),
(("F", "ISYS"), "F"),
(("ISYS", "F"), "F"),
(("ISYS", "I", "F"), "I"),
(("ISYS", "I1", "F1", "I", "F"), "I"),
(("ISYS", "I"), "I"),
(("ISYS", "F", "I"), "F"),
(("ISYS", "F1", "I1", "F", "I"), "F"),

# -I and -F beat system framework (-iframework)
(("I", "IFW"), "I"),
(("F", "IFW"), "F"),
(("IFW", "F"), "F"),
(("IFW", "I", "F"), "I"),
(("IFW", "I1", "F1", "I", "F"), "I"),
(("IFW", "I"), "I"),
(("IFW", "F", "I"), "F"),
(("IFW", "F1", "I1", "F", "I"), "F"),

# system include (-isystem) beats system framework (-iframework)
(("ISYS", "IFW"), "ISYS"),
(("IFW", "ISYS"), "ISYS"),
(("IFW1", "ISYS1", "IFW", "ISYS"), "ISYS"),
(("I1", "F1", "IFW1", "ISYS1", "IFW", "ISYS"), "ISYS"),
],
)
def test_searchpath_order(record_property, tmpdir, is_sys, order, expected):
"""
Validate include resolution order across -I (user include),
-isystem (system include), -F (user framework), and
-iframework (system framework) using a minimal file layout,
asserting which physical header path appears in the preprocessor #line output.

The test constructs four parallel trees (two entries per kind):
- inc{,_1}/MyKit/Component{,_1}.h # for -I
- isys{,_1}/MyKit/Component{,_1}.h # for -isystem
- Fw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -F
- SysFw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -iframework

It then preprocesses a TU that includes "MyKit/Component.h" (or <...> when
is_sys=True), assembles compiler args in the exact `order`, and asserts that
only the expected path appears in #line. Distinct names (Component.h vs
Component_1.h) ensure a unique winner per bucket.

References:
- https://gcc.gnu.org/onlinedocs/cpp/Invocation.html#Invocation
- https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html
"""

# Create two include dirs, two user framework dirs, and two system framework dirs
inc_dirs, isys_dirs, fw_dirs, sysfw_dirs = [], [], [], []

def _suffix(idx: int) -> str:
return f"_{idx}" if idx > 0 else ""

for idx in range(2):
# -I paths
inc_dir = os.path.join(tmpdir, f"inc{_suffix(idx)}")
__test_create_header(inc_dir, hdr_relpath=f"MyKit/Component{_suffix(idx)}.h")
inc_dirs.append(inc_dir)

# -isystem paths (system includes)
isys_dir = os.path.join(tmpdir, f"isys{_suffix(idx)}")
__test_create_header(isys_dir, hdr_relpath=f"MyKit/Component{_suffix(idx)}.h")
isys_dirs.append(isys_dir)

# -F paths (user frameworks)
fw_dir = os.path.join(tmpdir, f"Fw{_suffix(idx)}")
__test_create_framework(fw_dir, "MyKit", f"Component{_suffix(idx)}.h")
fw_dirs.append(fw_dir)

# -iframework paths (system frameworks)
sysfw_dir = os.path.join(tmpdir, f"SysFw{_suffix(idx)}")
__test_create_framework(sysfw_dir, "MyKit", f"Component{_suffix(idx)}.h")
sysfw_dirs.append(sysfw_dir)

# Translation unit under test: include MyKit/Component.h (quote or system form)
test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys)

def idx_from_flag(prefix: str, flag: str) -> int:
"""Extract numeric suffix from tokens like 'I1', 'ISYS1', 'F1', 'IFW1'.
Returns 0 when no suffix is present (e.g., 'I', 'ISYS', 'F', 'IFW')."""
return int(flag[len(prefix):]) if len(flag) > len(prefix) else 0

# Build argv in the exact order requested by `order`
args = []
for flag in order:
if flag in ["I", "I1"]:
args.append(format_include_path_arg(inc_dirs[idx_from_flag("I", flag)]))
elif flag in ["ISYS", "ISYS1"]:
args.append(format_isystem_path_arg(isys_dirs[idx_from_flag("ISYS", flag)]))
elif flag in ["F", "F1"]:
args.append(format_framework_path_arg(fw_dirs[idx_from_flag("F", flag)]))
elif flag in ["IFW", "IFW1"]:
args.append(format_iframework_path_arg(sysfw_dirs[idx_from_flag("IFW", flag)]))
else:
raise AssertionError(f"unknown flag in order: {flag}")
args.append(test_file)

# Run the preprocessor and capture outputs
_, stdout, stderr = simplecpp(args, cwd=tmpdir)
record_property("stdout", stdout)
record_property("stderr", stderr)

# Resolve the absolute expected/forbidden paths we want to see in #line output
root = pathlib.PurePath(tmpdir).as_posix()

inc_paths = [f"{root}/inc{_suffix(idx)}/MyKit/Component{_suffix(idx)}.h" for idx in range(2)]
isys_paths = [f"{root}/isys{_suffix(idx)}/MyKit/Component{_suffix(idx)}.h" for idx in range(2)]
fw_paths = [f"{root}/Fw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)]
ifw_paths = [f"{root}/SysFw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)]
all_candidate_paths = [*inc_paths, *isys_paths, *fw_paths, *ifw_paths]

# Compute the single path we expect to appear
expected_path = None
if expected in ["I", "I1"]:
expected_path = inc_paths[idx_from_flag("I", expected)]
elif expected in ["ISYS", "ISYS1"]:
expected_path = isys_paths[idx_from_flag("ISYS", expected)]
elif expected in ["F", "F1"]:
expected_path = fw_paths[idx_from_flag("F", expected)]
elif expected in ["IFW", "IFW1"]:
expected_path = ifw_paths[idx_from_flag("IFW", expected)]
assert expected_path is not None, "test configuration error: expected token not recognized"

# Assert ONLY the expected path appears in the preprocessor #line output
assert expected_path in stdout
for p in (p for p in all_candidate_paths if p != expected_path):
assert p not in stdout

# No diagnostics expected
assert stderr == ""

def test_pragma_once_matching(record_property, tmpdir):
test_dir = os.path.join(tmpdir, "test_dir")
test_subdir = os.path.join(test_dir, "test_subdir")
Expand Down
17 changes: 16 additions & 1 deletion main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,29 @@ int main(int argc, char **argv)
}
case 'I': { // include path
const char * const value = arg[2] ? (argv[i] + 2) : argv[++i];
dui.includePaths.push_back(value);
dui.addIncludePath(value, /* legacy= */ false);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if "legacy" is used simplecpp will not just skip over the new handling it will copy all the include paths (and as far as I see this is repeated). This solution will introduce a performance penalty.

We have had major performance problems and we managed to improve the situation but this I fear adds cpu overhead.

I envision that in Cppcheck many users will only use -I flags and I would like to be able to get optimal performance by choosing "legacy=true"

found = true;
break;
}
case 'F': { // framework path
const char * const value = arg[2] ? (argv[i] + 2) : argv[++i];
dui.addFrameworkPath(value);
found = true;
break;
}
case 'i':
if (std::strncmp(arg, "-include=",9)==0) {
dui.includes.push_back(arg+9);
found = true;
} else if (std::strncmp(arg, "-isystem", 8) == 0) {
dui.searchPaths.push_back({arg + 8, simplecpp::DUI::PathKind::SystemInclude});
found = true;
} else if (std::strncmp(arg, "-is",3)==0) {
use_istream = true;
found = true;
} else if (std::strncmp(arg, "-iframework", 11) == 0) {
dui.addSystemFrameworkPath(arg + 11);
found = true;
}
break;
case 's':
Expand Down Expand Up @@ -100,6 +112,9 @@ int main(int argc, char **argv)
std::cout << "simplecpp [options] filename" << std::endl;
std::cout << " -DNAME Define NAME." << std::endl;
std::cout << " -IPATH Include path." << std::endl;
std::cout << " -isystemPATH System include path." << std::endl;
std::cout << " -FPATH Framework path." << std::endl;
std::cout << " -iframeworkPATH System framework path." << std::endl;
std::cout << " -include=FILE Include FILE." << std::endl;
std::cout << " -UNAME Undefine NAME." << std::endl;
std::cout << " -std=STD Specify standard." << std::endl;
Expand Down
Loading