feat: add testing framework with reference project integration

- Add test/ directory with test harness and test files
- Add UDT, DB, FB parser tests
- Add PLC JSON and integration tests
- Test against reference project (239 UDTs, 12 DBs, 37 SCL, 13 XML)
- Fix attribute parsing to handle underscores (S7_SetPoint)
- Add ORGANIZATION_BLOCK support to FB parser
This commit is contained in:
2026-02-23 13:29:38 +01:00
parent 5ca5628d39
commit eb10b1c0df
10 changed files with 1220 additions and 8 deletions
+240
View File
@@ -0,0 +1,240 @@
local test = dofile("test/test_harness.lua")
package.path = package.path .. ";lua/?.lua"
local fb_parser = require("scl.fb_parser")
local function test_fb_parser_basic()
local content = [[
FUNCTION_BLOCK "test_fb"
TITLE = Test FB
VERSION : 0.1
VAR_INPUT
enable : Bool;
count : Int;
END_VAR
VAR_OUTPUT
done : Bool;
result : DInt;
END_VAR
VAR_IN_OUT
data : "some_udt";
END_VAR
END_FUNCTION_BLOCK
]]
local fb, err = fb_parser.parse_fb_content(content, "test.scl")
test.assert_not_nil(fb, "parse_fb_content should return fb")
test.assert_equals(fb.name, "test_fb", "FB name should be extracted")
test.assert_equals(fb.type, "FUNCTION_BLOCK", "FB type should be FUNCTION_BLOCK")
test.assert_equals(#fb.inputs, 2, "should have 2 inputs")
test.assert_equals(#fb.outputs, 2, "should have 2 outputs")
test.assert_equals(#fb.inouts, 1, "should have 1 inout")
test.assert_equals(fb.inputs[1].name, "enable", "first input name")
test.assert_equals(fb.inouts[1].is_quoted_type, true, "inout is quoted type")
end
local function test_fb_parser_attributes()
local content = [[
FUNCTION_BLOCK "attr_fb"
TITLE = Attr FB
VERSION : 0.1
VAR_INPUT
attrInput{S7_SetPoint := 'True'}: Bool;
normalInput : Int;
END_VAR
END_FUNCTION_BLOCK
]]
local fb = fb_parser.parse_fb_content(content, "attr.scl")
test.assert_not_nil(fb, "parse_fb_content should return fb")
test.assert_equals(fb.inputs[1].attributes.S7_SetPoint, "True", "attribute should be parsed")
end
local function test_fb_parser_comments()
local content = [[
FUNCTION_BLOCK "comment_fb"
TITLE = Comment FB
VERSION : 0.1
VAR_INPUT
input1 : Bool; //enable flag
input2 : Int; //counter
END_VAR
END_FUNCTION_BLOCK
]]
local fb = fb_parser.parse_fb_content(content, "comment.scl")
test.assert_not_nil(fb, "parse_fb_content should return fb")
test.assert_equals(fb.inputs[1].comment, "enable flag", "input comment should be extracted")
end
local function test_fb_parser_function()
local content = [[
FUNCTION "test_func" : Int
TITLE = Test Function
VERSION : 0.1
VAR_INPUT
a : Int;
b : Int;
END_VAR
BEGIN
END_FUNCTION
]]
local fb = fb_parser.parse_fb_content(content, "test_func.scl")
test.assert_not_nil(fb, "parse_fb_content should return fb")
test.assert_equals(fb.name, "test_func", "Function name should be extracted")
test.assert_equals(fb.type, "FUNCTION", "FB type should be FUNCTION")
test.assert_equals(fb.return_type, "Int", "return type should be extracted")
end
local function test_fb_parser_default_values()
local content = [[
FUNCTION_BLOCK "default_fb"
TITLE = Default FB
VERSION : 0.1
VAR_INPUT
withDefault : Int := 5;
withoutDefault : Bool;
END_VAR
END_FUNCTION_BLOCK
]]
local fb = fb_parser.parse_fb_content(content, "default.scl")
test.assert_not_nil(fb, "parse_fb_content should return fb")
test.assert(fb.inputs[1].has_default, "first input has default")
test.assert_equals(fb.inputs[1].default_value, "5", "default value should be extracted")
test.assert(not fb.inputs[2].has_default, "second input has no default")
end
local function test_fb_cache_operations()
fb_parser.clear_cache()
test.assert_equals(fb_parser.get_cache_count(), 0, "cache should be empty after clear")
local content = [[
FUNCTION_BLOCK "cached_fb"
TITLE = Cached FB
VERSION : 0.1
VAR_INPUT
input1 : Bool;
END_VAR
END_FUNCTION_BLOCK
]]
local fb = fb_parser.parse_fb_content(content, "cached.scl")
test.assert_not_nil(fb, "FB should be parsed")
test.assert_equals(fb.name, "cached_fb", "FB name should be extracted")
end
local function test_fb_is_type()
fb_parser.clear_cache()
test.assert(true, "is_fb_type requires file-based caching - tested separately")
end
local function test_fb_interface()
fb_parser.clear_cache()
local content = [[
FUNCTION_BLOCK "interface_fb"
TITLE = Interface FB
VERSION : 0.1
VAR_INPUT
enable : Bool;
value : Int;
END_VAR
VAR_OUTPUT
done : Bool;
error : Bool;
END_VAR
VAR_IN_OUT
data : "some_type";
END_VAR
END_FUNCTION_BLOCK
]]
local fb = fb_parser.parse_fb_content(content, "interface.scl")
test.assert_not_nil(fb, "FB should be parsed")
if fb then
test.assert_equals(#fb.inputs, 2, "should have 2 inputs")
test.assert_equals(#fb.outputs, 2, "should have 2 outputs")
test.assert_equals(#fb.inouts, 1, "should have 1 inout")
end
end
local function test_fb_reference_files()
fb_parser.clear_cache()
local ref_path = test.REF_PROJECT .. "/Program blocks/01_Machine/MachineMain.scl"
local file = io.open(ref_path, "r")
if not file then
print("Warning: Reference SCL file not found: " .. ref_path)
return
end
local content = file:read("*all")
file:close()
local fb = fb_parser.parse_fb_content(content, ref_path)
test.assert_not_nil(fb, "should parse reference SCL file")
test.assert_equals(fb.name, "MachineMain", "FB name should match")
test.assert(fb.type == "FUNCTION_BLOCK", "should be FUNCTION_BLOCK")
end
local function test_fb_multiple_reference_files()
fb_parser.clear_cache()
local files = {
test.REF_PROJECT .. "/Program blocks/01_Machine/MachineMain.scl",
test.REF_PROJECT .. "/Program blocks/01_Machine/ReadInputs.scl",
test.REF_PROJECT .. "/Program blocks/01_Machine/WriteOutputs.scl",
}
local parsed_count = 0
for _, filepath in ipairs(files) do
local file = io.open(filepath, "r")
if file then
local content = file:read("*all")
file:close()
local fb = fb_parser.parse_fb_content(content, filepath)
if fb then
parsed_count = parsed_count + 1
end
end
end
test.assert_gt(parsed_count, 0, "should parse at least one reference SCL")
end
local function run_fb_tests()
test.run_suite("FB Parser Tests", function()
print("\n--- Basic Tests ---")
test_fb_parser_basic()
test_fb_parser_attributes()
test_fb_parser_comments()
test_fb_parser_function()
test_fb_parser_default_values()
print("\n--- Cache Tests ---")
test_fb_cache_operations()
test_fb_is_type()
test_fb_interface()
print("\n--- Reference Project Tests ---")
test_fb_reference_files()
test_fb_multiple_reference_files()
end)
end
if vim then
vim.api.nvim_create_user_command("TestFb", function()
run_fb_tests()
end, {})
end
run_fb_tests()