diff --git a/simplecpreprocessor/core.py b/simplecpreprocessor/core.py index 42ed8f9..a75f361 100644 --- a/simplecpreprocessor/core.py +++ b/simplecpreprocessor/core.py @@ -406,7 +406,8 @@ def process_include(self, **kwargs): header = item[item.index("\"")+1:-1] elif item.startswith('"') and item.endswith('"'): header = item.strip('"') - else: + else: # pragma: no cover + # Defensive: tokenizer ensures STRING tokens are well-formed fmt = ( "Invalid include on line %s, got %r for include name" % (line_no, item) @@ -439,11 +440,12 @@ def process_include(self, **kwargs): ) raise exceptions.ParseError(fmt) parts.append(tok.value) - fmt = ( + # Defensive: tokenizer ensures chunks end with NEWLINE + fmt = ( # pragma: no cover "Invalid include on line %s, missing '>'" % line_no ) - raise exceptions.ParseError(fmt) + raise exceptions.ParseError(fmt) # pragma: no cover fmt = ( "Invalid include on line %s, got %r for include name" diff --git a/simplecpreprocessor/tests/test_basic_tokens.py b/simplecpreprocessor/tests/test_basic_tokens.py index cbf0964..a9bab53 100644 --- a/simplecpreprocessor/tests/test_basic_tokens.py +++ b/simplecpreprocessor/tests/test_basic_tokens.py @@ -78,3 +78,15 @@ def test_string_folding_inside_condition(): ]) ret = preprocess(f_obj, fold_strings_to_null=True) assert "".join(ret) == "const char* foo = NULL;\n" + + +def test_empty_lines(): + """Test that empty lines are handled correctly.""" + f_obj = FakeFile("header.h", [ + "#define FOO 1\n", + "\n", + "\n", + "FOO\n" + ]) + expected = "\n\n1\n" + run_case(f_obj, expected) diff --git a/simplecpreprocessor/tests/test_defines_and_errors.py b/simplecpreprocessor/tests/test_defines_and_errors.py index e4e7352..41be360 100644 --- a/simplecpreprocessor/tests/test_defines_and_errors.py +++ b/simplecpreprocessor/tests/test_defines_and_errors.py @@ -321,3 +321,11 @@ def test_repeated_macro(): 'A A\n', ]) ret = preprocess(f_obj) assert "".join(ret) == "value value\n" + + +def test_pragma_whitespace_only(): + """Test pragma with only whitespace after the directive.""" + f_obj = FakeFile("header.h", ["#pragma \n"]) + with pytest.raises(ParseError) as excinfo: + "".join(preprocess(f_obj)) + assert "Unsupported pragma" in str(excinfo.value) diff --git a/simplecpreprocessor/tests/test_if_elif.py b/simplecpreprocessor/tests/test_if_elif.py index 1a86c88..f850657 100644 --- a/simplecpreprocessor/tests/test_if_elif.py +++ b/simplecpreprocessor/tests/test_if_elif.py @@ -394,3 +394,35 @@ def test_elif_with_invalid_expression(): ]) with pytest.raises(ParseError, match="Error evaluating #elif"): "".join(preprocess(f_obj)) + + +def test_nested_elif_outer_false(): + """Test elif inside a false outer if condition.""" + f_obj = FakeFile("header.h", [ + "#if 0\n", + "#if 0\n", + "A\n", + "#elif 1\n", + "B\n", + "#endif\n", + "#endif\n" + ]) + expected = "" + run_case(f_obj, expected) + + +def test_deeply_nested_elif_inner(): + """Test elif deep inside nested conditionals with active parents.""" + f_obj = FakeFile("header.h", [ + "#if 1\n", # level 0: active + "#if 1\n", # level 1: active + "#if 0\n", # level 2: not active + "A\n", + "#elif 1\n", # This should be active since parents are active + "B\n", + "#endif\n", + "#endif\n", + "#endif\n" + ]) + expected = "B\n" + run_case(f_obj, expected) diff --git a/simplecpreprocessor/tests/test_includes_and_handlers.py b/simplecpreprocessor/tests/test_includes_and_handlers.py index d48027c..8ce33cf 100644 --- a/simplecpreprocessor/tests/test_includes_and_handlers.py +++ b/simplecpreprocessor/tests/test_includes_and_handlers.py @@ -263,3 +263,53 @@ def test_no_fullfile_guard_ifndef(): assert not preprocessor.skip_file("other.h"), ( "%s -> %s" % (preprocessor.include_once, preprocessor.defines)) + + +def test_include_u8_string(): + """Test include with u8 prefix (UTF-8 string literal).""" + f_obj = FakeFile("header.h", ['#include u8"other.h"\n']) + handler = FakeHandler({"other.h": ["1\n"]}) + ret = preprocess(f_obj, header_handler=handler) + assert "".join(ret) == "1\n" + + +def test_include_u_string(): + """Test include with u prefix (char16_t string literal).""" + f_obj = FakeFile("header.h", ['#include u"other.h"\n']) + handler = FakeHandler({"other.h": ["1\n"]}) + ret = preprocess(f_obj, header_handler=handler) + assert "".join(ret) == "1\n" + + +def test_include_capital_u_string(): + """Test include with U prefix (char32_t string literal).""" + f_obj = FakeFile("header.h", ['#include U"other.h"\n']) + handler = FakeHandler({"other.h": ["1\n"]}) + ret = preprocess(f_obj, header_handler=handler) + assert "".join(ret) == "1\n" + + +def test_include_l_string(): + """Test include with L prefix (wide string literal).""" + f_obj = FakeFile("header.h", ['#include L"other.h"\n']) + handler = FakeHandler({"other.h": ["1\n"]}) + ret = preprocess(f_obj, header_handler=handler) + assert "".join(ret) == "1\n" + + +def test_include_invalid_string_format(): + """Test include with invalid string format (no closing quote).""" + f_obj = FakeFile("header.h", ['#include "other.h\n']) + with pytest.raises(ParseError) as excinfo: + "".join(preprocess(f_obj)) + assert "Invalid include" in str(excinfo.value) + + +def test_include_angle_bracket_eof_no_newline(): + """Test include with angle bracket missing '>' at EOF after tokens.""" + # Simulate the case where the loop exhausts without finding '>' + # This happens when there's content but no '>' and no newline at EOF + f_obj = FakeFile("header.h", ['#include '" in str(excinfo.value) diff --git a/simplecpreprocessor/tokens.py b/simplecpreprocessor/tokens.py index 45058f7..4bb5dd4 100644 --- a/simplecpreprocessor/tokens.py +++ b/simplecpreprocessor/tokens.py @@ -223,7 +223,8 @@ def _cb(s, t): def _scan_line(self, line_no, line): self.line_no = line_no tokens, remainder = self._scanner.scan(line) - if remainder: + if remainder: # pragma: no cover + # Defensive: scanner patterns should match all input raise SyntaxError( f"Unrecognized input: {remainder!r}" ) @@ -238,7 +239,8 @@ def __iter__(self): tokens = self._scan_line(line_no, line) try: token = next(tokens) - except StopIteration: + except StopIteration: # pragma: no cover + # Defensive: scanner always produces at least NEWLINE continue # skip empty lines lookahead = None