diff --git a/AGENTS.md b/AGENTS.md index ed915c7..9080c69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ lua test/run_tests.lua # Run all tests lua test/test_udt.lua # Run UDT parser tests lua test/test_db.lua # Run DB parser tests lua test/test_fb.lua # Run FB parser tests +lua test/test_formatter.lua # Run formatter tests lua test/test_plc_json.lua # Run plc_json tests lua test/test_integration.lua # Run integration tests diff --git a/test/run_tests.lua b/test/run_tests.lua index ff5f701..4f622ee 100644 --- a/test/run_tests.lua +++ b/test/run_tests.lua @@ -28,6 +28,7 @@ local function run_all_tests() run_test_file("test_udt.lua") run_test_file("test_db.lua") run_test_file("test_fb.lua") + run_test_file("test_formatter.lua") run_test_file("test_plc_json.lua") run_test_file("test_integration.lua") diff --git a/test/test_formatter.lua b/test/test_formatter.lua new file mode 100644 index 0000000..872a446 --- /dev/null +++ b/test/test_formatter.lua @@ -0,0 +1,296 @@ +local test = dofile("test/test_harness.lua") + +package.path = package.path .. ";src/?.lua" + +local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or "." +script_path = script_path:gsub("^test/", ""):gsub("/test$", "") +if script_path == "" then script_path = "." end + +local formatter = dofile(script_path .. "/src/formatter.lua") + +local function test_formatter_basic() + local content = [[FUNCTION "TestFunc" : Int +VAR_INPUT + a : Int; +END_VAR +BEGIN + +END_FUNCTION +]] + local result = formatter.format_document(content, {}) + test.assert_not_nil(result, "should return formatting result") + test.assert(result[1].newText, "should have newText") +end + +local function test_formatter_function_block() + local content = [[FUNCTION_BLOCK "TestFB" +VAR_INPUT + enable : Bool; + value : Int; +END_VAR + +VAR_OUTPUT + done : Bool; +END_VAR + +BEGIN + #done := #enable; +END_FUNCTION_BLOCK +]] + local result = formatter.format_document(content, {}) + test.assert_not_nil(result, "should return formatting result") + local formatted = result[1].newText + test.assert(formatted:match("FUNCTION_BLOCK"), "should preserve FUNCTION_BLOCK") + test.assert(formatted:match("VAR_INPUT"), "should preserve VAR_INPUT") +end + +local function test_formatter_if_statement() + local content = [[IF #x > 5 THEN +#y := 1; +ELSIF #x > 3 THEN +#y := 2; +ELSE +#y := 0; +END_IF;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("IF"), "should preserve IF") + test.assert(formatted:match("THEN"), "should preserve THEN") + test.assert(formatted:match("END_IF"), "should preserve END_IF") +end + +local function test_formatter_for_loop() + local content = [[FOR #i := 0 TO 10 BY 1 DO +#arr[#i] := #i; +END_FOR;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("FOR"), "should preserve FOR") + test.assert(formatted:match("END_FOR"), "should preserve END_FOR") +end + +local function test_formatter_case_statement() + local content = [[CASE #x OF +1: #y := 10; +2: #y := 20; +ELSE +#y := 0; +END_CASE;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("CASE"), "should preserve CASE") + test.assert(formatted:match("END_CASE"), "should preserve END_CASE") +end + +local function test_formatter_comments() + local content = [[// This is a comment +(* This is a block comment *) +(* Another block comment *) +// Another line comment +#x := 1;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("This is a comment"), "should preserve line comments") +end + +local function test_formatter_multiline_assignment_simple() + local content = [[#x := 1 AND +2 AND +3;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#x := 1 AND"), "should preserve assignment start") +end + +local function test_formatter_multiline_assignment_with_hash() + local content = [[#statWarning := #instTurn.warning OR +#instPusher.warning OR +#instClamp.warning;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#statWarning :="), "should preserve assignment start") + test.assert(formatted:match("#instClamp.warning;$"), "should have continuation") +end + +local function test_formatter_multiline_assignment_complex() + local content = [[#interface.status.main.inBasePosition := #equipmentData.turn.status.homePos AND +#equipmentData.pusher.status.homePos AND +NOT #equipmentData.conveyor.status.forward AND +NOT #equipmentData.conveyor.status.backward;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#interface.status.main.inBasePosition :="), "should preserve first line") + test.assert(formatted:match("NOT #equipmentData"), "should preserve NOT continuation") +end + +local function test_formatter_multiline_assignment_ends_properly() + local content = [[#first := 1 AND +2 AND +3; + +#second := 10;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#first := 1"), "should preserve first assignment") + test.assert(formatted:match("#second := 10"), "should start new assignment properly") +end + +local function test_formatter_fb_call_single_line() + local content = [[#instMyFB(enable := #x, done => #y);]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#instMyFB"), "should preserve FB call") +end + +local function test_formatter_fb_call_multiline() + local content = [[#instMyFB( +enable := #x, +done => #y +);]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#instMyFB"), "should preserve FB call") +end + +local function test_formatter_fb_call_with_long_params() + local content = [[#instPartCounter(enable := #unitStatus.currentSubMode <> "DRY_RUN", +incPartIn := #equipmentData.partCounter.commands.incPartIn, +incGoodPart := #equipmentData.partCounter.commands.incGoodPart, +resetCntr := #equipmentData.partCounter.commands.resetCntr, +status => #equipmentData.partCounter.status);]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#instPartCounter"), "should preserve FB call") + test.assert(formatted:match("status =>"), "should preserve output param") +end + +local function test_formatter_region() + local content = [[REGION MyRegion +// code +#x := 1; +END_REGION;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("REGION"), "should preserve REGION") + test.assert(formatted:match("END_REGION"), "should preserve END_REGION") +end + +local function test_formatter_struct() + local content = [[TYPE MyStruct +STRUCT + x : Int; + y : Bool; +END_STRUCT; +END_TYPE +]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("TYPE"), "should preserve TYPE") + test.assert(formatted:match("STRUCT"), "should preserve STRUCT") +end + +local function test_formatter_var_sections() + local content = [[VAR_INPUT + in1 : Bool; + in2 : Int; +END_VAR + +VAR_OUTPUT + out1 : Bool; +END_VAR + +VAR + stat1 : Int; +END_VAR +]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("VAR_INPUT"), "should preserve VAR_INPUT") + test.assert(formatted:match("VAR_OUTPUT"), "should preserve VAR_OUTPUT") + test.assert(formatted:match("\nVAR\n"), "should preserve VAR") +end + +local function test_formatter_attribute_block() + local content = [[VAR + stat1{ S7_SetPoint := 'True' } : Int; + stat2 : Bool; +END_VAR +]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("stat1"), "should preserve variable name") +end + +local function test_formatter_while_loop() + local content = [[WHILE #x < 10 DO +#x := #x + 1; +END_WHILE;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("WHILE"), "should preserve WHILE") + test.assert(formatted:match("END_WHILE"), "should preserve END_WHILE") +end + +local function test_formatter_repeat_loop() + local content = [[REPEAT +#x := #x + 1; +UNTIL #x >= 10;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("REPEAT"), "should preserve REPEAT") + test.assert(formatted:match("UNTIL"), "should preserve UNTIL") +end + +local function test_formatter_assignment_with_string() + local content = [[#str := "Hello World";]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match('"Hello World"'), "should preserve string") +end + +local function test_formatter_assignment_with_array() + local content = [[#arr[0] := 1; +#arr[1] := 2;]] + local result = formatter.format_document(content, {}) + local formatted = result[1].newText + test.assert(formatted:match("#arr%[0%]"), "should preserve array access") +end + +local function run_formatter_tests() + test.run_suite("Formatter Tests", function() + print("\n--- Basic Tests ---") + test_formatter_basic() + test_formatter_function_block() + test_formatter_if_statement() + test_formatter_for_loop() + test_formatter_case_statement() + test_formatter_comments() + test_formatter_while_loop() + test_formatter_repeat_loop() + test_formatter_region() + test_formatter_struct() + test_formatter_var_sections() + test_formatter_attribute_block() + test_formatter_assignment_with_string() + test_formatter_assignment_with_array() + + print("\n--- Multiline Assignment Tests ---") + test_formatter_multiline_assignment_simple() + test_formatter_multiline_assignment_with_hash() + test_formatter_multiline_assignment_complex() + test_formatter_multiline_assignment_ends_properly() + + print("\n--- FB Call Tests ---") + test_formatter_fb_call_single_line() + test_formatter_fb_call_multiline() + test_formatter_fb_call_with_long_params() + end) +end + +if vim then + vim.api.nvim_create_user_command("TestFormatter", function() + run_formatter_tests() + end, {}) +end + +run_formatter_tests()