From 989589d7c8d4eb6bc81743b932dbb0e118dba158 Mon Sep 17 00:00:00 2001 From: Charlie <30303272+charliefoxtwo@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:40:03 -0700 Subject: [PATCH] Extract core state machine to lua module (#362) --- Scripts/DCS-BIOS/BIOS.lua | 52 +++--- Scripts/DCS-BIOS/BIOSConfig.lua | 50 +++-- Scripts/DCS-BIOS/lib/BIOSStateMachine.lua | 174 ++++++++++++++++++ Scripts/DCS-BIOS/lib/ConnectionManager.lua | 68 +++++++ Scripts/DCS-BIOS/lib/Protocol.lua | 150 --------------- Scripts/DCS-BIOS/lib/ProtocolIO.lua | 46 ----- .../DCS-BIOS/test/ConnectionManagerTest.lua | 52 ++++++ Scripts/DCS-BIOS/test/ProtocolIOTest.lua | 57 ------ Scripts/DCS-BIOS/test/StateMachineTest.lua | 79 ++++++++ Scripts/DCS-BIOS/test/TestSuite.lua | 3 +- .../DCS-BIOS/test/compile/LocalCompile.lua | 1 - Scripts/DCS-BIOS/test/controls/MockDevice.lua | 3 + 12 files changed, 442 insertions(+), 293 deletions(-) create mode 100644 Scripts/DCS-BIOS/lib/BIOSStateMachine.lua create mode 100644 Scripts/DCS-BIOS/lib/ConnectionManager.lua delete mode 100644 Scripts/DCS-BIOS/lib/ProtocolIO.lua create mode 100644 Scripts/DCS-BIOS/test/ConnectionManagerTest.lua delete mode 100644 Scripts/DCS-BIOS/test/ProtocolIOTest.lua create mode 100644 Scripts/DCS-BIOS/test/StateMachineTest.lua diff --git a/Scripts/DCS-BIOS/BIOS.lua b/Scripts/DCS-BIOS/BIOS.lua index f9f55b11d..b20610a64 100644 --- a/Scripts/DCS-BIOS/BIOS.lua +++ b/Scripts/DCS-BIOS/BIOS.lua @@ -22,8 +22,12 @@ package.path = lfs.writedir() .. [[Scripts/DCS-BIOS/lib/modules/documentation/?. package.path = lfs.writedir() .. [[Scripts/DCS-BIOS/lib/modules/memory_map/?.lua;]] .. package.path -- all requires must come after updates to package.path -local ProtocolIO = require("ProtocolIO") - +local BIOSConfig = require("BIOSConfig") +local BIOSStateMachine = require("BIOSStateMachine") +local ConnectionManager= require("ConnectionManager") +local TCPServer = require("TCPServer") +local UDPServer = require("UDPServer") +local socket = require("socket") --[[@as Socket]] local json = loadfile([[Scripts/JSON.lua]]) -- try to load json from dcs BIOS.json = json and json() or require "JSON" -- if that fails, fall back to module that we can define @@ -175,8 +179,6 @@ BIOS.protocol.writeNewModule(VNAO_T_45) local Yak_52 = require "Yak-52" BIOS.protocol.writeNewModule(Yak_52) ----------------------------------------------------------------------------Modules End-------------------------------------- -dofile(lfs.writedir()..[[Scripts/DCS-BIOS/BIOSConfig.lua]]) - --Saves aliases for each aircraft for external programs local function saveAliases() local JSON = BIOS.json @@ -188,6 +190,8 @@ local function saveAliases() end end pcall(saveAliases) +-- save constants for arduino devs to a header file +pcall(BIOS.protocol.saveAddresses) -- Prev Export functions. local PrevExport = {} @@ -196,12 +200,26 @@ PrevExport.LuaExportStop = LuaExportStop PrevExport.LuaExportBeforeNextFrame = LuaExportBeforeNextFrame PrevExport.LuaExportAfterNextFrame = LuaExportAfterNextFrame +local connection_manager = ConnectionManager:new({}) + +local state_machine = BIOSStateMachine:new(BIOS.dbg.aircraftNameToModules, MetadataStart, MetadataEnd, 11000, connection_manager) + +local function process_input_line(line) + state_machine:processInputLine(line) +end + +for _, udp in ipairs(BIOSConfig.udp_config) do + connection_manager:addConnection(UDPServer:new(udp.send_address, udp.send_port, udp.receive_address, udp.receive_port, socket, process_input_line)) +end + +for _, tcp in ipairs(BIOSConfig.tcp_config) do + connection_manager:addConnection(TCPServer:new(tcp.address, tcp.port, socket, process_input_line)) +end + -- Lua Export Functions LuaExportStart = function() - - for _, v in pairs(ProtocolIO.connections) do v:init() end - BIOS.protocol.init() - + state_machine:init() + -- Chain previously-included export as necessary if PrevExport.LuaExportStart then PrevExport.LuaExportStart() @@ -209,11 +227,8 @@ LuaExportStart = function() end LuaExportStop = function() - - BIOS.protocol.shutdown() - ProtocolIO.flush() - for _, v in pairs(ProtocolIO.connections) do v:close() end - + state_machine:shutdown() + -- Chain previously-included export as necessary if PrevExport.LuaExportStop then PrevExport.LuaExportStop() @@ -221,11 +236,8 @@ LuaExportStop = function() end function LuaExportBeforeNextFrame() - - for _, v in pairs(ProtocolIO.connections) do - if v.step then v:step() end - end - + state_machine:receive() + -- Chain previously-included export as necessary if PrevExport.LuaExportBeforeNextFrame then PrevExport.LuaExportBeforeNextFrame() @@ -234,9 +246,7 @@ function LuaExportBeforeNextFrame() end function LuaExportAfterNextFrame() - - BIOS.protocol.step() - ProtocolIO.flush() + state_machine:step() -- Chain previously-included export as necessary if PrevExport.LuaExportAfterNextFrame then diff --git a/Scripts/DCS-BIOS/BIOSConfig.lua b/Scripts/DCS-BIOS/BIOSConfig.lua index 5433b5acf..707f993c0 100644 --- a/Scripts/DCS-BIOS/BIOSConfig.lua +++ b/Scripts/DCS-BIOS/BIOSConfig.lua @@ -1,17 +1,33 @@ -local ProtocolIO = require("ProtocolIO") -local TCPServer = require("TCPServer") -local UDPServer = require("UDPServer") -local socket = require("socket") --[[@as Socket]] - -local udp_send_address = "239.255.50.10" -local udp_send_port = 5010 -local udp_receive_address = "*" -local udp_receive_port = 7778 - -local tcp_address = "*" -local tcp_port = 7778 - -ProtocolIO.connections = { - UDPServer:new(udp_send_address, udp_send_port, udp_receive_address, udp_receive_port, socket, BIOS.protocol.processInputLine), - TCPServer:new(tcp_address, tcp_port, socket, BIOS.protocol.processInputLine), -} +module("BIOSConfig", package.seeall) + +--- @class TCPConnectionConfig +--- @field address string +--- @field port integer + +--- @class UDPConnectionConfig +--- @field send_address string +--- @field send_port integer +--- @field receive_address string +--- @field receive_port integer + +--- @class BIOSConfig +--- @field tcp_config TCPConnectionConfig[] +--- @field udp_config UDPConnectionConfig[] +local BIOSConfig = { + tcp_config = { + { + address = "*", + port = 7778 + }, + }, + udp_config = { + { + send_address = "239.255.50.10", + send_port = 5010, + receive_address = "*", + receive_port = 7778 + }, + }, +} + +return BIOSConfig diff --git a/Scripts/DCS-BIOS/lib/BIOSStateMachine.lua b/Scripts/DCS-BIOS/lib/BIOSStateMachine.lua new file mode 100644 index 000000000..f4363e16e --- /dev/null +++ b/Scripts/DCS-BIOS/lib/BIOSStateMachine.lua @@ -0,0 +1,174 @@ +local Log = require "Log" +module("BIOSStateMachine", package.seeall) + +--- @class BIOSStateMachine +--- @field private modules_by_name {[string]: Module[]} a map of module names to the modules to send data for +--- @field private metadata_start MetadataStart the MetadataStart module +--- @field private metadata_end MetadataEnd the MetadataEnd module +--- @field private max_bytes_per_second integer the maximum amount of data per second to send +--- @field private connection_manager ConnectionManager a connection manager with all active connections +--- @field private active_aircraft_name string? the name of the current aircraft +--- @field private bytes_in_transit integer the number of bytes currently being sent over the wire +--- @field private active_modules Module[] modules which are currently being exported (i.e. modules associated with the active aircraft) +--- @field private update_counter integer a frame counter which ticks with every frame +--- @field private update_skip_counter integer a counter which increments for every skipped frame due to too much data being sent +--- @field private next_step_time number the time at which the next frame tick should occur +--- @field private last_frame_time number the time at which the last frame tick occurred +local BIOSStateMachine = {} + +--- Constructs a new BIOS state machine +--- @param modules_by_name {[string]: Module[]} a map of module names to the modules to send data for +--- @param metadata_start MetadataStart the MetadataStart module +--- @param metadata_end MetadataEnd the MetadataEnd module +--- @param max_bytes_per_second integer the maximum amount of data per second to send +--- @param connection_manager ConnectionManager a connection manager with all active connections +--- @return BIOSStateMachine +function BIOSStateMachine:new(modules_by_name, metadata_start, metadata_end, max_bytes_per_second, connection_manager) + --- @type BIOSStateMachine + local o = { + modules_by_name = modules_by_name, + metadata_start = metadata_start, + metadata_end = metadata_end, + max_bytes_per_second = max_bytes_per_second, + connection_manager = connection_manager, + bytes_in_transit = 0, + active_modules = {}, + update_counter = 0, + update_skip_counter = 0, + next_step_time = 0, + last_frame_time = LoGetModelTime(), + } + setmetatable(o, self) + self.__index = self + return o +end + +function BIOSStateMachine:processInputLine(line) + local cmd, args = line:match("^([^ ]+) (.*)") + + if cmd then + for _, module in ipairs(self.active_modules) do + local processor = module.inputProcessors[cmd] + if processor then + processor(args) + end + end + end +end + +--- @private +--- @param module Module +--- @param dev0 CockpitDevice +function BIOSStateMachine:queue_module_data(module, dev0) + for _, hook in ipairs(module.exportHooks) do + hook(dev0) + end + -- legacy behavior - for some reason, we seem to typically call this twice. Is this because modules are getting too big? + module.memoryMap:autosyncStep() + module.memoryMap:autosyncStep() + local data = module.memoryMap:flushData() + self.bytes_in_transit = self.bytes_in_transit + data:len() + self.connection_manager:queue(data) +end + +function BIOSStateMachine:init() + for _, connection in ipairs(self.connection_manager.connections) do + connection:init() + end +end + +function BIOSStateMachine:receive() + for _, connection in ipairs(self.connection_manager.connections) do + if connection.step then + connection:step() + end + end +end + +local frame_sync_sequence = string.char(0x55, 0x55, 0x55, 0x55) + +function BIOSStateMachine:step() + -- rate limiting + local curTime = LoGetModelTime() + self.bytes_in_transit = self.bytes_in_transit - ((curTime - self.last_frame_time) * self.max_bytes_per_second) + self.last_frame_time = curTime + if self.bytes_in_transit < 0 then self.bytes_in_transit = 0 end + + -- determine active aircraft + local self_data = LoGetSelfData() + local current_aircraft_name = self_data and self_data["Name"] or "NONE" + + self.metadata_start:setAircraftName(current_aircraft_name) + + self.active_modules = self.modules_by_name[current_aircraft_name] or {} + if self.active_aircraft_name ~= current_aircraft_name then + for _, acftModule in ipairs(self.active_modules) do + acftModule.memoryMap:clearValues() + end + self.active_aircraft_name = current_aircraft_name + end + + -- export data + if curTime < self.next_step_time then + return -- runs 30 times per second + end + + self.update_counter = (self.update_counter + 1) % 256 + self.metadata_end:setUpdateCounter(self.update_counter) + + -- if the last frame update has not been completely transmitted, skip a frame + if self.bytes_in_transit > 0 then + -- TODO: increase a frame skip counter for logging purposes + self.update_skip_counter = (self.update_skip_counter + 1) % 256 + return + end + self.metadata_end:setUpdateSkipCounter(self.update_skip_counter) + self.next_step_time = curTime + .033 + + -- send frame sync sequence + self.bytes_in_transit = self.bytes_in_transit + 4 + self.connection_manager:queue(frame_sync_sequence) + + local dev0 = GetDevice(0) + if dev0 and type(dev0) ~= "number" then -- this type check is legacy code - unclear if this is still possible + dev0:update_arguments() + end + + -- export aircraft-independent data + self:queue_module_data(self.metadata_start, dev0) + + -- Export aircraft data + for _, module in ipairs(self.active_modules) do + self:queue_module_data(module, dev0) + end + + self:queue_module_data(self.metadata_end, dev0) + + self.connection_manager:send_queue() +end + +function BIOSStateMachine:shutdown() + local dev0 = GetDevice(0) + + -- Nullify the aircraft name and publish one last frame to identify end of mission. + self.metadata_start:setAircraftName("") + + -- send frame sync sequence + self.connection_manager:queue(frame_sync_sequence) + + -- export aircraft-independent data: MetadataStart + self:queue_module_data(self.metadata_start, dev0) + + -- export aircraft-independent data: MetadataEnd + self:queue_module_data(self.metadata_end, dev0) + + self.connection_manager:send_queue() + + -- close any open connections + for _, connection in ipairs(self.connection_manager.connections) do + connection:close() + end +end + + +return BIOSStateMachine diff --git a/Scripts/DCS-BIOS/lib/ConnectionManager.lua b/Scripts/DCS-BIOS/lib/ConnectionManager.lua new file mode 100644 index 000000000..bb42c1501 --- /dev/null +++ b/Scripts/DCS-BIOS/lib/ConnectionManager.lua @@ -0,0 +1,68 @@ +module("ConnectionManager", package.seeall) + +--- @class ConnectionManager +--- @field connections Server[] the connections to send messages to +--- @field private msg_buf string[] the buffer of messages to send +--- @field private MAX_PAYLOAD_SIZE integer the maximum payload that can be accepted and sent +local ConnectionManager = { +} + +--- Constructs a new connection handler +--- @param connections Server[] the connections to send messages to +--- @return ConnectionManager +function ConnectionManager:new(connections) + --- @type ConnectionManager + local o = { + connections = connections, + msg_buf = {}, + MAX_PAYLOAD_SIZE = 2048 + } + setmetatable(o, self) + self.__index = self + return o +end + +--- Adds a new connection +--- @param server Server +function ConnectionManager:addConnection(server) + table.insert(self.connections, server) +end + +--- Queues a message to be sent to any connections +---@param msg string the message to send +function ConnectionManager:queue(msg) + if (msg:len() > self.MAX_PAYLOAD_SIZE) then + error("Message exceeded max buffer size! " + msg) + end + + table.insert(self.msg_buf, msg) +end + +--- Flushes the message buffer, sending any queued messages +function ConnectionManager:send_queue() + local packet = "" + while #self.msg_buf > 0 do + local msg = table.remove(self.msg_buf, 1) + if packet:len() + msg:len() > self.MAX_PAYLOAD_SIZE then + -- packet would be too big, so send what we have now + self:send_packet(packet) + packet = "" + end + packet = packet .. msg + end + + if packet:len() > 0 then + self:send_packet(packet) + end +end + +--- @private +--- Sends a packet to all open connections +--- @param packet string +function ConnectionManager:send_packet(packet) + for _, conn in ipairs(self.connections) do + if conn.send then conn:send(packet) end + end +end + +return ConnectionManager diff --git a/Scripts/DCS-BIOS/lib/Protocol.lua b/Scripts/DCS-BIOS/lib/Protocol.lua index 13123bc3e..deeda9788 100644 --- a/Scripts/DCS-BIOS/lib/Protocol.lua +++ b/Scripts/DCS-BIOS/lib/Protocol.lua @@ -1,14 +1,9 @@ -local ProtocolIO = require("ProtocolIO") - BIOS.protocol = {} -BIOS.protocol.maxBytesPerSecond = BIOS.protocol.maxBytesPerSecond or 11000 -BIOS.protocol.maxBytesInTransit = BIOS.protocol.maxBytesPerSecond or 4000 --- @type Module[] local exportModules = {} local aircraftNameToModuleNames = {} local aircraftNameToModules = {} -local lastAcftName = "" function BIOS.protocol.setExportModuleAircrafts(acftList) -- first, delete moduleName from all mappings @@ -46,8 +41,6 @@ function BIOS.protocol.setExportModuleAircrafts(acftList) BIOS.dbg.aircraftNameToModuleNames = aircraftNameToModuleNames BIOS.dbg.aircraftNameToModules = aircraftNameToModules BIOS.dbg.exportModules = exportModules - - lastAcftName = "" end function BIOS.protocol.beginModule(name, baseAddress) @@ -151,146 +144,3 @@ function BIOS.protocol.writeNewModule(mod) BIOS.protocol.setExportModuleAircrafts(mod.aircraftList) BIOS.protocol.endModule() end - -local metadataStartModule = nil -local metadataEndModule = nil -function BIOS.protocol.init() - -- called after all aircraft modules have been loaded - metadataStartModule = exportModules["MetadataStart"] - metadataEndModule = exportModules["MetadataEnd"] - - BIOS.protocol.saveAddresses() -end - -local acftModules = nil -bytesInTransit = 0 - -function BIOS.protocol.processInputLine(line) - local cmd, args = line:match("^([^ ]+) (.*)") - if cmd == "SYNC" and args == "E" then - argumentCache = {} - end - if cmd then - if acftModules then - for _, acftModule in pairs(acftModules) do - if acftModule.inputProcessors[cmd] then - acftModule.inputProcessors[cmd](args) - end - end - end - end -end - -local nextLowFreqStepTime = 0 -local nextHighFreqStepTime = 0 - -local lastFrameTime = LoGetModelTime() - -local updateCounter = 0 -local updateSkipCounter = 0 -function BIOS.protocol.step() - - if( metadataStartModule == nil or metadataEndModule == nil) then - error("Either MetadataStart or MetadataEnd was nil.", 1) -- this should never happen since init() is being called but it removes intellisense warnings - end - - -- rate limiting - local curTime = LoGetModelTime() - bytesInTransit = bytesInTransit - ((curTime - lastFrameTime) * BIOS.protocol.maxBytesPerSecond) - lastFrameTime = curTime - if bytesInTransit < 0 then bytesInTransit = 0 end - - -- determine active aircraft - local acftName = "NONE" - local selfData = LoGetSelfData() - if selfData then - acftName = selfData["Name"] - end - - metadataStartModule:setAircraftName(acftName) - - acftModules = aircraftNameToModules[acftName] - if lastAcftName ~= acftName then - if acftModules then - for _, acftModule in pairs(acftModules) do - acftModule.memoryMap:clearValues() - end - end - lastAcftName = acftName - end - - -- export data - if curTime >= nextLowFreqStepTime then - -- runs 30 times per second - updateCounter = (updateCounter + 1) % 256 - metadataEndModule:setUpdateCounter(updateCounter) - - -- if the last frame update has not been completely transmitted, skip a frame - if bytesInTransit > 0 then - -- TODO: increase a frame skip counter for logging purposes - updateSkipCounter = (updateSkipCounter + 1) % 256 - return - end - metadataEndModule:setUpdateSkipCounter(updateSkipCounter) - nextLowFreqStepTime = curTime + .033 - - -- send frame sync sequence - bytesInTransit = bytesInTransit + 4 - ProtocolIO.queue(string.char(0x55, 0x55, 0x55, 0x55)) - - -- export aircraft-independent data - for k, v in pairs(metadataStartModule.exportHooks) do v() end - metadataStartModule.memoryMap:autosyncStep() - local data = metadataStartModule.memoryMap:flushData() - bytesInTransit = bytesInTransit + data:len() - ProtocolIO.queue(data) - - -- Export aircraft data - if acftModules then - for _, acftModule in pairs(acftModules) do - local dev0 = GetDevice(0) - if dev0 ~= nil and type(dev0) ~= "number" then - dev0:update_arguments() - end - - for k, v in pairs(acftModule.exportHooks) do - v(dev0) - end - - acftModule.memoryMap:autosyncStep() - acftModule.memoryMap:autosyncStep() - local data = acftModule.memoryMap:flushData() - bytesInTransit = bytesInTransit + data:len() - ProtocolIO.queue(data) - end - end - - for k, v in pairs(metadataEndModule.exportHooks) do v() end - metadataEndModule.memoryMap:autosyncStep() - local data = metadataEndModule.memoryMap:flushData() - bytesInTransit = bytesInTransit + data:len() - ProtocolIO.queue(data) - end - -end - -function BIOS.protocol.shutdown() - -- Nullify the aircraft name and publish one last frame to identify end of mission. - metadataStartModule:setAircraftName("") - - -- send frame sync sequence - ProtocolIO.queue(string.char(0x55, 0x55, 0x55, 0x55)) - - -- export aircraft-independent data: MetadataStart - for k, v in pairs(metadataStartModule.exportHooks) do v() end - metadataStartModule.memoryMap:autosyncStep() - local data = metadataStartModule.memoryMap:flushData() - ProtocolIO.queue(data) - - -- export aircraft-independent data: MetadataEnd - for k, v in pairs(metadataEndModule.exportHooks) do v() end - metadataEndModule.memoryMap:autosyncStep() - local data = metadataEndModule.memoryMap:flushData() - ProtocolIO.queue(data) -end - diff --git a/Scripts/DCS-BIOS/lib/ProtocolIO.lua b/Scripts/DCS-BIOS/lib/ProtocolIO.lua deleted file mode 100644 index fcbbaf103..000000000 --- a/Scripts/DCS-BIOS/lib/ProtocolIO.lua +++ /dev/null @@ -1,46 +0,0 @@ -module("ProtocolIO", package.seeall) - ---- @class ProtocolIO ---- @field connections Server[] the connections to send messages to ---- @field private msg_buf string[] the buffer of messages to send ---- @field private MAX_PAYLOAD_SIZE integer the maximum payload that can be accepted and sent -local ProtocolIO = { - connections = {}, - msg_buf = {}, - MAX_PAYLOAD_SIZE = 2048 -} - ---- Queues a message to be sent to any connections ----@param msg string the message to send -function ProtocolIO.queue(msg) - assert(msg:len() <= ProtocolIO.MAX_PAYLOAD_SIZE, "Message exceeded max buffer size! " .. msg) - table.insert(ProtocolIO.msg_buf, msg) -end - ---- Flushes the message buffer, sending any queued messages -function ProtocolIO.flush() - local packet = "" - for _, msg in ipairs(ProtocolIO.msg_buf) do - if packet:len() + msg:len() > ProtocolIO.MAX_PAYLOAD_SIZE then - -- packet would be too big, so send what we have now - ProtocolIO.send_packet(packet) - packet = "" - end - packet = packet .. msg - end - if packet:len() > 0 then - ProtocolIO.send_packet(packet) - end - ProtocolIO.msg_buf = {} -end - ---- @private ---- Sends a packet to all open connections ---- @param packet string -function ProtocolIO.send_packet(packet) - for _, conn in ipairs(ProtocolIO.connections) do - if conn.send then conn:send(packet) end - end -end - -return ProtocolIO diff --git a/Scripts/DCS-BIOS/test/ConnectionManagerTest.lua b/Scripts/DCS-BIOS/test/ConnectionManagerTest.lua new file mode 100644 index 000000000..d053627ce --- /dev/null +++ b/Scripts/DCS-BIOS/test/ConnectionManagerTest.lua @@ -0,0 +1,52 @@ +local ConnectionManager = require("ConnectionManager") +local MockServer = require("MockServer") + +local lu = require("luaunit") + +--- @class TestConnectionManager +--- @field connection_manager ConnectionManager +TestConnectionManager = {} + +function TestConnectionManager:setUp() + self.connection_manager = ConnectionManager:new({}) +end + +function TestConnectionManager:testFlush() + local testBuffer = "test" + local server = MockServer:new() + self.connection_manager:addConnection(server) + + self.connection_manager:queue(testBuffer) + self.connection_manager:send_queue() + + lu.assertEquals(#server.sent_messages, 1) + lu.assertEquals(server.sent_messages[1], testBuffer) +end + +function TestConnectionManager:testFlushEmptyBuffer() + local server = MockServer:new() + self.connection_manager:addConnection(server) + + self.connection_manager:send_queue() + + lu.assertEquals(#server.sent_messages, 0) +end + +function TestConnectionManager:testFlushOverMaxBuffer() + local testBuffer = "" + for _ = 1, 200, 1 do + testBuffer = testBuffer .. "test" -- build testbuffer to be 800 bytes long + end + + local server = MockServer:new() + self.connection_manager:addConnection(server) + + self.connection_manager:queue(testBuffer) + self.connection_manager:queue(testBuffer) + self.connection_manager:queue(testBuffer) + self.connection_manager:send_queue() + + lu.assertEquals(#server.sent_messages, 2) + lu.assertEquals(server.sent_messages[1], testBuffer .. testBuffer) -- max buffer size is 2048 bytes, so two should be concatentated + lu.assertEquals(server.sent_messages[2], testBuffer) -- but the third should overflow +end diff --git a/Scripts/DCS-BIOS/test/ProtocolIOTest.lua b/Scripts/DCS-BIOS/test/ProtocolIOTest.lua deleted file mode 100644 index cb4ea9d7b..000000000 --- a/Scripts/DCS-BIOS/test/ProtocolIOTest.lua +++ /dev/null @@ -1,57 +0,0 @@ -local MockServer = require("MockServer") -local ProtocolIO = require("ProtocolIO") - -local lu = require("luaunit") - ---- @class TestProtocolIO -TestProtocolIO = {} - -function TestProtocolIO:setUp() - ProtocolIO.connections = {} -end - -function TestProtocolIO:testFlush() - local testBuffer = "test" - local server = MockServer:new() - ProtocolIO.connections = { - server, - } - - ProtocolIO.queue(testBuffer) - ProtocolIO.flush() - - lu.assertEquals(#server.sent_messages, 1) - lu.assertEquals(server.sent_messages[1], testBuffer) -end - -function TestProtocolIO:testFlushEmptyBuffer() - local server = MockServer:new() - ProtocolIO.connections = { - server, - } - - ProtocolIO.flush() - - lu.assertEquals(#server.sent_messages, 0) -end - -function TestProtocolIO:testFlushOverMaxBuffer() - local testBuffer = "" - for _ = 1, 200, 1 do - testBuffer = testBuffer .. "test" -- build testbuffer to be 800 bytes long - end - - local server = MockServer:new() - ProtocolIO.connections = { - server, - } - - ProtocolIO.queue(testBuffer) - ProtocolIO.queue(testBuffer) - ProtocolIO.queue(testBuffer) - ProtocolIO.flush() - - lu.assertEquals(#server.sent_messages, 2) - lu.assertEquals(server.sent_messages[1], testBuffer .. testBuffer) -- max buffer size is 2048 bytes, so two should be concatentated - lu.assertEquals(server.sent_messages[2], testBuffer) -- but the third should overflow -end diff --git a/Scripts/DCS-BIOS/test/StateMachineTest.lua b/Scripts/DCS-BIOS/test/StateMachineTest.lua new file mode 100644 index 000000000..c266b50ad --- /dev/null +++ b/Scripts/DCS-BIOS/test/StateMachineTest.lua @@ -0,0 +1,79 @@ +local BIOSStateMachine = require("BIOSStateMachine") +local ConnectionManager = require("ConnectionManager") +local MockDevice = require("MockDevice") +local MockServer = require("MockServer") +local Module = require("Module") + +local lu = require("luaunit") + +--- @class TestState +--- @field aircraft_name string? +--- @field lines_processed string[] +--- @field update_counter integer +--- @field update_skip_counter integer + +-- Unit testing starts +--- @class TestStateMachine +--- @field state_machine BIOSStateMachine +--- @field state TestState +TestStateMachine = {} + +-- global function used to retrieve the aircraft name +function LoGetSelfData() + return { + ["Name"] = "My Module", + } +end + +Input_Processor_Device = MockDevice:new(0) + +function GetDevice() + return Input_Processor_Device +end + +local model_time = 0 + +function LoGetModelTime() + model_time = model_time + 0.01 + return model_time +end + +function TestStateMachine:setUp() + self.state = { + lines_processed = {}, + update_counter = 0, + update_skip_counter = 0, + } + + model_time = 0 + + local my_module = Module:new("My Module", 0x4200, { "My Module" }) + my_module:addInputProcessor("MY_CONTROL", function(value) + table.insert(self.state.lines_processed, "MY_CONTROL " .. value) + end) + + local metadata_start = Module:new("MetadataStart", 0x0000, {}) --[[@as MetadataStart]] + metadata_start.setAircraftName = function(name) + self.state.aircraft_name = name + end + + local metadata_end = Module:new("MetadataEnd", 0xfffe, {}) --[[@as MetadataEnd]] + metadata_end.setUpdateCounter = function(value) + self.state.update_counter = value + end + metadata_end.setUpdateSkipCounter = function(value) + self.state.update_skip_counter = value + end + + self.state_machine = BIOSStateMachine:new({ ["My Module"] = { my_module } }, metadata_start, metadata_end, 11000, ConnectionManager:new(MockServer:new())) + + self.state_machine:init() +end + +function TestStateMachine:testProcessInputLine() + local state_machine = self.state_machine + state_machine:step() -- sets the active module + state_machine:processInputLine("MY_CONTROL 1") + lu.assertEquals(#self.state.lines_processed, 1) + lu.assertEquals("MY_CONTROL 1", self.state.lines_processed[1]) +end diff --git a/Scripts/DCS-BIOS/test/TestSuite.lua b/Scripts/DCS-BIOS/test/TestSuite.lua index e0f489b58..77a4e74a9 100644 --- a/Scripts/DCS-BIOS/test/TestSuite.lua +++ b/Scripts/DCS-BIOS/test/TestSuite.lua @@ -19,12 +19,13 @@ BIOS = {} lfs = require("lfs") require("AircraftTest") -- high-level tests for specific aircraft +require("ConnectionManagerTest") -- unit tests for send/receive logic require("MemoryMapTest") -- unit tests for the memory map require("MemoryMapEntryTest") -- unit tests for memory map entries require("ModuleTest") -- unit tests for core aircraft module functionality require("ParseIndicationTest") -- unit tests for the parse_indication function -require("ProtocolIOTest") -- unit tests for send/receive logic require("ServerTest") -- unit tests for tcp/udp server code +require("StateMachineTest") -- unit tests for the core state machine loop local lu = require("luaunit") os.exit(lu.LuaUnit:run()) diff --git a/Scripts/DCS-BIOS/test/compile/LocalCompile.lua b/Scripts/DCS-BIOS/test/compile/LocalCompile.lua index e07e1835a..17b40b81b 100644 --- a/Scripts/DCS-BIOS/test/compile/LocalCompile.lua +++ b/Scripts/DCS-BIOS/test/compile/LocalCompile.lua @@ -18,4 +18,3 @@ lfs = require("lfs") -- Include these that will mock the DCS APIs and the socket. dofile([[Scripts/DCS-BIOS/BIOS.lua]]) -BIOS.protocol.init() diff --git a/Scripts/DCS-BIOS/test/controls/MockDevice.lua b/Scripts/DCS-BIOS/test/controls/MockDevice.lua index dc97333eb..ba009503b 100644 --- a/Scripts/DCS-BIOS/test/controls/MockDevice.lua +++ b/Scripts/DCS-BIOS/test/controls/MockDevice.lua @@ -35,6 +35,9 @@ function MockDevice:get_argument_value(argument_id) return self.value end +-- noop +function MockDevice:update_arguments() end + --- Makes the device perform an action --- @param command_id integer --- @param argument number