| #!/usr/bin/env python3 | |
| """ | |
| Unit tests for LLVM and Subproject CMake Formatter Utility (`libc/utils/cmake_format.py`). | |
| Verifies all 9 formatting rules, dynamic schema learning, codebase idempotency, | |
| and golden output snapshots. | |
| """ | |
| import unittest | |
| import os | |
| import sys | |
| # Add the directory containing cmake_format.py to sys.path | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, SCRIPT_DIR) | |
| import cmake_format | |
| class TestRule1_CommandCasing(unittest.TestCase): | |
| """Rule 1: Command Casing.""" | |
| def test_1_1_builtin_lowercase(self): | |
| """Rule 1.1: Built-in language commands are cased in lowercase.""" | |
| code = "SET(FOO bar)\nIF(FOO)\n ADD_LIBRARY(lib STATIC)\nENDIF()\n" | |
| expected = "set(FOO bar)\nif(FOO)\n add_library(lib STATIC)\nendif()\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_1_2_module_canonical_casing(self): | |
| """Rule 1.2: Module commands retain canonical declared casing.""" | |
| code = "externalproject_add(\n proj\n PREFIX\n dir\n)\n" | |
| expected = "ExternalProject_Add(\n proj\n PREFIX\n dir\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_1_3_custom_function_casing(self): | |
| """Rule 1.3: Custom user functions/macros retain their original casing.""" | |
| code = "add_libc_unittest(\n my_test\n SRCS\n my_test.cpp\n)\n" | |
| expected = "add_libc_unittest(\n my_test\n SRCS\n my_test.cpp\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule2_ParenthesisSpacingAndWhitespace(unittest.TestCase): | |
| """Rule 2: Parenthesis Spacing & Whitespace Collapsing.""" | |
| def test_2_1_zero_space_before_opening_paren(self): | |
| """Rule 2.1: Zero space between command identifier and opening '('.""" | |
| code = "set (FOO bar)\n" | |
| expected = "set(FOO bar)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_2_2_collapse_arg_whitespace(self): | |
| """Rule 2.2: Collapses multiple spaces between single-line arguments down to one.""" | |
| code = "add_entrypoint_object(target_name DEPENDS dep1 dep2)\n" | |
| expected = "add_entrypoint_object(target_name DEPENDS dep1 dep2)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule3_QuotedAndBracketImmutability(unittest.TestCase): | |
| """Rule 3: Quoted String & Bracket Immutability.""" | |
| def test_3_1_quoted_and_bracket_tokens(self): | |
| """Rule 3.1: Quoted arguments and bracket arguments are single immutable tokens.""" | |
| code = 'set(MSG "Hello \\"World\\"")\nset(CODE [=[raw string\n content]=])\n' | |
| expected = ( | |
| 'set(MSG "Hello \\"World\\"")\nset(CODE [=[raw string\n content]=])\n' | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_3_2_multiline_string_preservation(self): | |
| """Rule 3.2: Multi-line quoted strings are preserved 100% untouched.""" | |
| code = 'set(DOC "\n Line 1\n Line 2\n")\n' | |
| expected = 'set(DOC "\n Line 1\n Line 2\n")\n' | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule4_EmptyClosures(unittest.TestCase): | |
| """Rule 4: Empty Closures.""" | |
| def test_4_1_empty_closing_parens(self): | |
| """Rule 4.1: Control block closures strip old condition arguments down to empty ().""" | |
| code = "if(FOO)\n set(BAR 1)\nelse(FOO)\n set(BAR 0)\nendif(FOO)\n" | |
| expected = "if(FOO)\n set(BAR 1)\nelse()\n set(BAR 0)\nendif()\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule5_SchemaAwareKeywordCasing(unittest.TestCase): | |
| """Rule 5: Schema-Aware Keyword Casing.""" | |
| def test_5_1_keyword_uppercasing(self): | |
| """Rule 5.1: Schema keywords are upper-cased.""" | |
| code = "add_entrypoint_object(\n target\n srcs\n src.cpp\n depends\n dep\n)\n" | |
| expected = "add_entrypoint_object(\n target\n SRCS\n src.cpp\n DEPENDS\n dep\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_5_2_positional_args_untouched(self): | |
| """Rule 5.2: Positional arguments, file paths, and target names are left untouched.""" | |
| code = "add_header_macro(\n string\n ../libc/include/string.yaml\n string.h\n DEPENDS\n .llvm_libc_common_h\n)\n" | |
| expected = "add_header_macro(\n string\n ../libc/include/string.yaml\n string.h\n DEPENDS\n .llvm_libc_common_h\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_5_3_custom_function_positional_args_untouched(self): | |
| """Rule 5.3: Positional output_var parameters in custom functions retain casing.""" | |
| code = '_get_common_compile_options(compile_options "${ADD_OBJECT_FLAGS}")\n' | |
| expected = ( | |
| '_get_common_compile_options(compile_options "${ADD_OBJECT_FLAGS}")\n' | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule6_MultilineArgumentLayout(unittest.TestCase): | |
| """Rule 6: Multi-line Argument Layout & Keyword Schema Mechanics.""" | |
| def test_6_1_option_keyword_scope(self): | |
| """Rule 6.1: Option keywords (0 values) close immediately.""" | |
| code = "add_library(\n target\n ALIAS\n real_target\n)\n" | |
| expected = "add_library(\n target\n ALIAS\n real_target\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_6_2_single_value_keyword_scope(self): | |
| """Rule 6.2: Single-value keywords consume 1 argument then close.""" | |
| code = "execute_process(\n WORKING_DIRECTORY\n /tmp\n OUTPUT_VARIABLE\n out\n)\n" | |
| expected = "execute_process(\n WORKING_DIRECTORY\n /tmp\n OUTPUT_VARIABLE\n out\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_6_3a_multivalue_separate_line(self): | |
| """Rule 6.3a: Multi-value keyword on new line indents items +4 spaces (+2 relative to keyword).""" | |
| code = "add_entrypoint_object(\n target\n SRCS\n s1.cpp\n s2.cpp\n)\n" | |
| expected = ( | |
| "add_entrypoint_object(\n target\n SRCS\n s1.cpp\n s2.cpp\n)\n" | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_6_3b_multivalue_header_line(self): | |
| """Rule 6.3b: Multi-value keyword on header line indents items +2 spaces relative to call base.""" | |
| code = "add_entrypoint_object(target SRCS s1.cpp s2.cpp)\n" | |
| expected = "add_entrypoint_object(target SRCS s1.cpp s2.cpp)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_6_4_argument_forwarding_vars(self): | |
| """Rule 6.4: Argument forwarding variables (${ARGN}, ${ARGV}) align at +2 spaces and reset active keyword.""" | |
| code = "add_entrypoint_object(\n target\n DEPENDS\n dep1\n ${ARGN}\n)\n" | |
| expected = ( | |
| "add_entrypoint_object(\n target\n DEPENDS\n dep1\n ${ARGN}\n)\n" | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_6_5_closing_paren_indent(self): | |
| """Rule 6.5: Closing parenthesis ')' aligns at +0 spaces (call base).""" | |
| code = "add_entrypoint_object(\n target\n SRCS\n s1.cpp\n)\n" | |
| expected = "add_entrypoint_object(\n target\n SRCS\n s1.cpp\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule7_ControlBlockIndentation(unittest.TestCase): | |
| """Rule 7: Control Block Indentation.""" | |
| def test_7_1_nested_control_blocks(self): | |
| """Rule 7.1: Nests 2 spaces per control block level.""" | |
| code = "if(A)\nif(B)\nset(C 1)\nendif()\nendif()\n" | |
| expected = "if(A)\n if(B)\n set(C 1)\n endif()\nendif()\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_7_2_middle_blocks(self): | |
| """Rule 7.2: else() and elseif() align at block base level.""" | |
| code = "if(A)\nset(X 1)\nelseif(B)\nset(X 2)\nelse()\nset(X 3)\nendif()\n" | |
| expected = ( | |
| "if(A)\n set(X 1)\nelseif(B)\n set(X 2)\nelse()\n set(X 3)\nendif()\n" | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule8_CommentFormatting(unittest.TestCase): | |
| """Rule 8: Comment Formatting & Lookahead Buffering.""" | |
| def test_8_1_lookahead_comment_buffering(self): | |
| """Rule 8.1: Line comment aligns with the indentation of the upcoming code line.""" | |
| code = "add_entrypoint_object(\n target\n SRCS\n # Comment for s1\n s1.cpp\n)\n" | |
| expected = "add_entrypoint_object(\n target\n SRCS\n # Comment for s1\n s1.cpp\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_8_2_standalone_comments_blank_line(self): | |
| """Rule 8.2: Standalone comments followed by a blank line flush at current base indent.""" | |
| code = "# Standalone comment\n\nadd_library(target s1.cpp)\n" | |
| expected = "# Standalone comment\n\nadd_library(target s1.cpp)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_8_3a_closing_paren_keyword_comment(self): | |
| """Rule 8.3a: Comment following a keyword before closing ')' indents under keyword (+4 spaces). | |
| In practice, UNIT_TEST_ONLY is learned as an option from cmake_parse_arguments in | |
| LLVMLibCTestRules.cmake via pre_scan_workspace_modules. We supply an equivalent | |
| inline definition so the test is self-contained. | |
| """ | |
| # Inline definition matching the real LLVMLibCTestRules.cmake declaration. | |
| schema_stub = """ | |
| function(add_libc_test test_name) | |
| cmake_parse_arguments( | |
| "LIBC_TEST" | |
| "UNIT_TEST_ONLY;HERMETIC_TEST_ONLY" | |
| "" | |
| "" | |
| ${ARGN} | |
| ) | |
| endfunction() | |
| """ | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.scan_dynamic_schemas(schema_stub, ctx=ctx) | |
| code = "add_libc_test(\n hash_test\n UNIT_TEST_ONLY\n # Explanation for option\n)\n" | |
| expected = "add_libc_test(\n hash_test\n UNIT_TEST_ONLY\n # Explanation for option\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code, ctx=ctx), expected) | |
| def test_8_3b_closing_paren_list_comment(self): | |
| """Rule 8.3b: Comment following a list item before closing ')' aligns at top-level call (+2 spaces).""" | |
| code = "add_libc_test(\n stdbit_test\n DEPENDS\n dep1\n # Top level call comment\n)\n" | |
| expected = "add_libc_test(\n stdbit_test\n DEPENDS\n dep1\n # Top level call comment\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_8_4_comment_space_hygiene(self): | |
| """Rule 8.4: Missing space after '#' is added; special markers (#---, #!) preserved.""" | |
| code = "#no space\n#--- header ---\n#!shebang\n" | |
| expected = "# no space\n#--- header ---\n#!shebang\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestRule9_CleanlinessAndFileHygiene(unittest.TestCase): | |
| """Rule 9: Cleanliness & File Hygiene.""" | |
| def test_9_1_strip_trailing_whitespace(self): | |
| """Rule 9.1: Trailing whitespace on all lines is stripped.""" | |
| code = "set(FOO bar) \nset(BAR baz) \n" | |
| expected = "set(FOO bar)\nset(BAR baz)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_9_2_single_trailing_newline(self): | |
| """Rule 9.2: Non-empty files end with exactly one trailing newline.""" | |
| code = "set(FOO bar)\n\n\n" | |
| expected = "set(FOO bar)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_9_3_empty_file_preservation(self): | |
| """Rule 9.3: Empty 0-byte files are preserved as 0-byte.""" | |
| self.assertEqual(cmake_format.format_cmake_content(""), "") | |
| self.assertEqual(cmake_format.format_cmake_content(" \n\n \t "), "") | |
| class TestDynamicSchemaLearning(unittest.TestCase): | |
| """Dynamic Schema Learning Tests.""" | |
| def test_function_block_parser(self): | |
| """Verifies cmake_parse_arguments extraction inside function/macro AST blocks.""" | |
| code = """ | |
| function(custom_target_func target) | |
| cmake_parse_arguments( | |
| "FUNC" | |
| "OPTION1;OPTION2" | |
| "SINGLE1" | |
| "MULTI1;MULTI2" | |
| ${ARGN} | |
| ) | |
| endfunction() | |
| """ | |
| cmake_format.scan_dynamic_schemas(code) | |
| schema = cmake_format.get_schema_for_cmd("custom_target_func") | |
| self.assertIn("OPTION1", schema.options) | |
| self.assertIn("SINGLE1", schema.one_value) | |
| self.assertIn("MULTI1", schema.multi_value) | |
| def test_variable_list_scanner(self): | |
| """Verifies dynamic learning from set(*_OPTION_ARGS), set(*_SINGLE_VALUE_ARGS), set(*_MULTI_VALUE_ARGS).""" | |
| code = """ | |
| set(CUSTOM_OPTION_ARGS OPT_A OPT_B) | |
| set(CUSTOM_SINGLE_VALUE_ARGS SING_A) | |
| set(CUSTOM_MULTI_VALUE_ARGS MULT_A MULT_B) | |
| """ | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.scan_dynamic_schemas(code, ctx=ctx) | |
| self.assertIn("OPT_A", ctx.learned_options) | |
| self.assertIn("SING_A", ctx.learned_one_value) | |
| self.assertIn("MULT_A", ctx.learned_multi_value) | |
| def test_cache_variable_exclusion(self): | |
| """Verifies cache variable declarations like set(FOO "" CACHE STRING "...") are excluded.""" | |
| code = 'set(LIBC_COMPILE_OPTIONS_DEFAULT "" CACHE STRING "Docstring")\n' | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.scan_dynamic_schemas(code, ctx=ctx) | |
| self.assertNotIn("STRING", ctx.learned_options) | |
| class TestCodebaseIdempotency(unittest.TestCase): | |
| """Codebase Idempotency Test.""" | |
| def test_libc_codebase_idempotency(self): | |
| """Verifies format(format(content)) == format(content) over libc/ files.""" | |
| libc_dir = os.path.join(SCRIPT_DIR, "..") | |
| cmake_files = cmake_format.find_cmake_files([libc_dir]) | |
| self.assertGreater(len(cmake_files), 0, "No CMake files found under libc/") | |
| cmake_format.pre_scan_workspace_modules([libc_dir]) | |
| for fpath in cmake_files: | |
| with self.subTest(file=fpath): | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| original = f.read() | |
| formatted = cmake_format.format_cmake_content(original) | |
| reformatted = cmake_format.format_cmake_content(formatted) | |
| self.assertEqual( | |
| formatted, reformatted, f"Formatting is not idempotent for {fpath}" | |
| ) | |
| class TestEdgeCasesAndIsolation(unittest.TestCase): | |
| """Edge cases for parenthesis counting in comments/strings and state isolation.""" | |
| def test_parens_in_comments_and_strings(self): | |
| """Verifies parentheses inside comments and strings do not corrupt paren_balance or indentation.""" | |
| code = ( | |
| "add_custom_target(\n" | |
| " mytarget\n" | |
| " # Comment with parentheses () and (extra)\n" | |
| ' "file_with_(parens).cpp"\n' | |
| " DEPENDS\n" | |
| " dep1\n" | |
| ")\n" | |
| ) | |
| expected = ( | |
| "add_custom_target(\n" | |
| " mytarget\n" | |
| " # Comment with parentheses () and (extra)\n" | |
| ' "file_with_(parens).cpp"\n' | |
| " DEPENDS\n" | |
| " dep1\n" | |
| ")\n" | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_bracket_comment_with_equals(self): | |
| """Verifies bracket comments with '=' delimiters (#[=[...]=]) are properly preserved.""" | |
| code = ( | |
| "#[=[\n" | |
| " Multi-line comment with equals\n" | |
| " set(FOO bar)\n" | |
| "]=]\n" | |
| "set(BAR baz)\n" | |
| ) | |
| expected = ( | |
| "#[=[\n" | |
| " Multi-line comment with equals\n" | |
| " set(FOO bar)\n" | |
| "]=]\n" | |
| "set(BAR baz)\n" | |
| ) | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_unterminated_quoted_argument_raises_lexerror(self): | |
| """Verifies unterminated quoted string arguments raise LexError.""" | |
| code_quote = 'set(FOO "unterminated string' | |
| with self.assertRaises(cmake_format.LexError) as cm: | |
| cmake_format.format_cmake_content(code_quote) | |
| self.assertIn("unterminated quoted argument", str(cm.exception)) | |
| def test_unterminated_bracket_comment_raises_lexerror(self): | |
| """Verifies unterminated bracket comments raise LexError.""" | |
| code_bc = "#[=[ unterminated bracket comment" | |
| with self.assertRaises(cmake_format.LexError) as cm: | |
| cmake_format.format_cmake_content(code_bc) | |
| self.assertIn("unterminated bracket comment", str(cm.exception)) | |
| def test_unterminated_bracket_argument_raises_lexerror(self): | |
| """Verifies unterminated bracket arguments raise LexError.""" | |
| code_ba = "set(FOO [=[unterminated bracket arg)" | |
| with self.assertRaises(cmake_format.LexError) as cm: | |
| cmake_format.format_cmake_content(code_ba) | |
| self.assertIn("unterminated bracket argument", str(cm.exception)) | |
| def test_global_state_isolation(self): | |
| """Verifies custom schemas learned in File A do not leak into formatting of File B when using isolated contexts.""" | |
| file_a = ( | |
| "set(MY_CUSTOM_OPTION_ARGS OPT_ISOLATED_A OPT_ISOLATED_B)\n" | |
| "my_custom_cmd(OPT_ISOLATED_A valA OPT_ISOLATED_B valB)\n" | |
| ) | |
| file_b = "my_other_cmd(opt_isolated_a vala opt_isolated_b valb)\n" | |
| ctx_a = cmake_format.FormatterContext() | |
| ctx_b = cmake_format.FormatterContext() | |
| cmake_format.format_cmake_content(file_a, ctx=ctx_a) | |
| result_b = cmake_format.format_cmake_content(file_b, ctx=ctx_b) | |
| expected_b = "my_other_cmd(opt_isolated_a vala opt_isolated_b valb)\n" | |
| self.assertEqual(result_b, expected_b) | |
| def test_pre_scan_workspace_modules(self): | |
| """Verifies pre_scan_workspace_modules discovers and scans modules in cmake/modules/.""" | |
| import tempfile | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| mod_dir = os.path.join(tmpdir, "cmake", "modules") | |
| os.makedirs(mod_dir, exist_ok=True) | |
| mod_file = os.path.join(mod_dir, "CustomModule.cmake") | |
| with open(mod_file, "w", encoding="utf-8") as f: | |
| f.write("set(MY_MOD_OPTION_ARGS MOD_OPT1 MOD_OPT2)\n") | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.pre_scan_workspace_modules([tmpdir], ctx=ctx) | |
| self.assertIn("MOD_OPT1", ctx.learned_options) | |
| self.assertIn("MOD_OPT2", ctx.learned_options) | |
| def test_pre_scan_workspace_modules_fails_fast_on_error(self): | |
| """Verifies pre_scan_workspace_modules exits with code 1 if a module contains invalid syntax.""" | |
| import contextlib | |
| import io | |
| import tempfile | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| mod_dir = os.path.join(tmpdir, "cmake", "modules") | |
| os.makedirs(mod_dir, exist_ok=True) | |
| mod_file = os.path.join(mod_dir, "BrokenModule.cmake") | |
| with open(mod_file, "w", encoding="utf-8") as f: | |
| f.write('message("unterminated string\n') | |
| ctx = cmake_format.FormatterContext() | |
| err_buf = io.StringIO() | |
| with contextlib.redirect_stderr(err_buf): | |
| with self.assertRaises(SystemExit) as cm: | |
| cmake_format.pre_scan_workspace_modules([tmpdir], ctx=ctx) | |
| self.assertEqual(cm.exception.code, 1) | |
| self.assertIn("Error pre-scanning module", err_buf.getvalue()) | |
| def test_find_cmake_files_excludes_build_directories(self): | |
| """Verifies find_cmake_files excludes directories starting with 'build' and '.'.""" | |
| import tempfile | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| for d in [ | |
| "src", | |
| "build", | |
| "build-ninja", | |
| "build_debug", | |
| ".hidden", | |
| ]: | |
| sub = os.path.join(tmpdir, d) | |
| os.makedirs(sub, exist_ok=True) | |
| with open( | |
| os.path.join(sub, "CMakeLists.txt"), "w", encoding="utf-8" | |
| ) as f: | |
| f.write("set(X 1)\n") | |
| found = cmake_format.find_cmake_files([tmpdir]) | |
| rel_found = [os.path.relpath(p, tmpdir) for p in found] | |
| self.assertEqual(rel_found, [os.path.join("src", "CMakeLists.txt")]) | |
| def test_stdin_pipe_formatting(self): | |
| """Verifies formatting unformatted CMake code via stdin.""" | |
| code = "add_library(\n foo\n STATIC\n s1.cpp\n )\n" | |
| expected = "add_library(\n foo\n STATIC\n s1.cpp\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestParsingBugs(unittest.TestCase): | |
| """Regression tests for parsing and formatting bugs.""" | |
| def test_bug_inline_comment_on_empty_closures(self): | |
| """Empty closure commands (endif, else, endfunction, etc.) preserve inline comments.""" | |
| code = "if(FOO)\n set(BAR 1)\nelse() # else comment\n set(BAR 0)\nendif() # endif comment\n" | |
| expected = "if(FOO)\n set(BAR 1)\nelse() # else comment\n set(BAR 0)\nendif() # endif comment\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_bug_escaped_chars_in_unquoted_args(self): | |
| """Unquoted args containing escaped characters (e.g. \\#, \\(, \\), \\", \\ ) are preserved as single tokens.""" | |
| code = "add_entrypoint_object(\n target\n SRCS\n bar\\#baz\n bar\\(baz\\)\n C:\\Program\\ Files\\Foo\n)\n" | |
| expected = "add_entrypoint_object(\n target\n SRCS\n bar\\#baz\n bar\\(baz\\)\n C:\\Program\\ Files\\Foo\n)\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_bug_continuation_line_starting_with_paren(self): | |
| """Continuation lines starting with ')' when paren balance > 0 do not prematurely close call state.""" | |
| code = "if(\n (A AND B)\n ) AND (C AND D)\n)\n set(X 1)\nendif()\n" | |
| expected = "if(\n (A AND B)\n ) AND (C AND D)\n)\n set(X 1)\nendif()\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| def test_bug_multiline_set_schema_learning(self): | |
| """Multi-line set(*_MULTI_VALUE_ARGS ...) definitions are parsed correctly by schema scanner.""" | |
| code = "set(CUSTOM_MULTI_VALUE_ARGS\n MULT_LINE1\n MULT_LINE2\n)\n" | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.scan_dynamic_schemas(code, ctx=ctx) | |
| self.assertIn("MULT_LINE1", ctx.learned_multi_value) | |
| self.assertIn("MULT_LINE2", ctx.learned_multi_value) | |
| def test_bug_cmake_parse_arguments_var_refs(self): | |
| """Variable references like ${MY_OPTIONS} in cmake_parse_arguments are not registered as literal keywords.""" | |
| code = """ | |
| function(my_func) | |
| cmake_parse_arguments("ARG" "${MY_OPTIONS}" "${MY_ONE_VAL}" "${MY_MULTI_VAL}" ${ARGN}) | |
| endfunction() | |
| """ | |
| ctx = cmake_format.FormatterContext() | |
| cmake_format.scan_dynamic_schemas(code, ctx=ctx) | |
| schema = cmake_format.get_schema_for_cmd("my_func", ctx=ctx) | |
| self.assertNotIn("MY_OPTIONS", schema.options) | |
| self.assertNotIn("MY_ONE_VAL", schema.one_value) | |
| self.assertNotIn("MY_MULTI_VAL", schema.multi_value) | |
| def test_bug_directive_comment_preservation(self): | |
| """Directives like #nolint, #pragma, #include, # cmake-lint, # cmake-format in comments are preserved without forced spaces.""" | |
| code = "#nolint\n#pragma once\n#include <foo>\n# cmake-lint: disable=syntax-error\n# cmake-format: off\n#cmake-lint: disable=syntax-error\n" | |
| expected = "#nolint\n#pragma once\n#include <foo>\n# cmake-lint: disable=syntax-error\n# cmake-format: off\n#cmake-lint: disable=syntax-error\n" | |
| self.assertEqual(cmake_format.format_cmake_content(code), expected) | |
| class TestFileWriting(unittest.TestCase): | |
| """Regression tests for the bytes that land on disk, not the formatted string.""" | |
| def test_inplace_write_keeps_lf_endings(self): | |
| """In-place formatting writes exactly the bytes format_cmake_content produced. | |
| A text-mode write substitutes the platform line separator, which | |
| rewrites every line of an LF file as CRLF on Windows. | |
| """ | |
| import contextlib | |
| import io | |
| import tempfile | |
| code = "add_library(foo STATIC\n a.c\n)\n" | |
| expected = cmake_format.format_cmake_content(code).encode("utf-8") | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| path = os.path.join(tmpdir, "CMakeLists.txt") | |
| with open(path, "w", encoding="utf-8", newline="") as f: | |
| f.write(code) | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| self.assertTrue(cmake_format.process_file(path, inplace=True)) | |
| with open(path, "rb") as f: | |
| self.assertEqual(f.read(), expected) | |
| def test_stdout_write_keeps_lf_endings(self): | |
| """Formatting through stdin/stdout does not rewrite the line endings either.""" | |
| import subprocess | |
| code = "add_library(foo STATIC\n a.c\n)\n" | |
| expected = cmake_format.format_cmake_content(code).encode("utf-8") | |
| proc = subprocess.run( | |
| [sys.executable, os.path.join(SCRIPT_DIR, "cmake_format.py")], | |
| input=code.encode("utf-8"), | |
| stdout=subprocess.PIPE, | |
| check=True, | |
| ) | |
| self.assertEqual(proc.stdout, expected) | |
| if __name__ == "__main__": | |
| unittest.main() |