diff --git a/python/requirements.txt b/python/requirements.txt index 9d53b550cd..cb48193733 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -4,3 +4,4 @@ cython pandas scipy setuptools>=60.0.0 +telnetlib3 diff --git a/src/input_output/FGInputSocket.cpp b/src/input_output/FGInputSocket.cpp index 89de037d7f..eb7de7b1cf 100644 --- a/src/input_output/FGInputSocket.cpp +++ b/src/input_output/FGInputSocket.cpp @@ -259,7 +259,7 @@ void FGInputSocket::Read(bool Holding) } else if (command == "help") { // HELP socket->Reply( - " JSBSim Server commands:\n\r\n" + " JSBSim Server commands:\r\n\r\n" " get {property name}\r\n" " set {property name} {value}\r\n" " hold\r\n" @@ -267,7 +267,7 @@ void FGInputSocket::Read(bool Holding) " iterate {value}\r\n" " help\r\n" " quit\r\n" - " info\n\r\n"); + " info\r\n\r\n"); } else { socket->Reply(string("Unknown command: ") + command + "\r\n"); diff --git a/src/input_output/FGfdmSocket.cpp b/src/input_output/FGfdmSocket.cpp index 536ae49367..535b3d10a1 100644 --- a/src/input_output/FGfdmSocket.cpp +++ b/src/input_output/FGfdmSocket.cpp @@ -269,7 +269,7 @@ string FGfdmSocket::Receive(void) int flags = fcntl(sckt_in, F_GETFL, 0); fcntl(sckt_in, F_SETFL, flags | O_NONBLOCK); #endif - if (send(sckt_in, "Connected to JSBSim server\n\rJSBSim> ", 36, 0) == SOCKET_ERROR) + if (send(sckt_in, "Connected to JSBSim server\r\nJSBSim> ", 36, 0) == SOCKET_ERROR) LogSocketError("Receive - TCP connection acknowledgement"); } } diff --git a/tests/TestInputSocket.py b/tests/TestInputSocket.py index c4762ac07e..13b0828444 100644 --- a/tests/TestInputSocket.py +++ b/tests/TestInputSocket.py @@ -3,7 +3,7 @@ # A test case that checks that providing commands to JSBSim via an input socket # is working. # -# Copyright (c) 2015-2022 Bertrand Coconnier +# Copyright (c) 2015-2024 Bertrand Coconnier # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -19,74 +19,86 @@ # this program; if not, see # -import telnetlib, socket, time +import asyncio import xml.etree.ElementTree as et -from JSBSim_utils import JSBSimTestCase, CopyAircraftDef, RunTest + +import telnetlib3 +from JSBSim_utils import CopyAircraftDef, JSBSimTestCase, RunTest class TelnetInterface: - def __init__(self, fdm, port): - self.tn = telnetlib.Telnet("localhost", port) - self.fdm = fdm - fdm.run() - - def __del__(self): - if ( - "tn" in self.__dict__.keys() - ): # Check if the Telnet session has been succesfully open - self.tn.close() - self.fdm = None - - def sendCommand(self, command): - self.tn.write("{}\n".format(command).encode()) - for _ in range(50): - self.fdm.run() - self.fdm.check_incremental_hold() - return self.getOutput() - - def getOutput(self): - time.sleep(1.0) # Wait for the socket to process all the data. - return self.tn.read_very_eager().decode() - - def getPropertyValue(self, property): - msg = self.sendCommand(f"get {property}").split("\n") + reader = None + writer = None + + async def run(self, port, shell): + self.reader, self.writer = await telnetlib3.open_connection( + "localhost", port, shell=shell + ) + await self.writer.protocol.waiter_closed + + async def get_output(self): + msg = await self.reader.read(1024) + lines = msg.split("\r\n") + if lines[-1] == "JSBSim> ": + return "\n".join(lines[:-1]) + else: + prompt = await self.reader.read(1024) + assert prompt == "JSBSim> " + return "\n".join(lines) + + async def send_command(self, command): + self.writer.write(f"{command}\n") + await self.writer.drain() + return await self.get_output() + + async def get_property_value(self, property_name): + msg = (await self.send_command(f"get {property_name}")).split("\n") return float(msg[0].split("=")[1]) class TestInputSocket(JSBSimTestCase): - def setUp(self): - JSBSimTestCase.setUp(self) + def setUp(self, *args): + super().setUp(*args) + self._fdm = self.create_fdm() + self.telnet = TelnetInterface() self.script_path = self.sandbox.path_to_jsbsim_file("scripts", "c1722.xml") - - def sanityCheck(self, tn): - # Check that the connection has been established - out = tn.getOutput() - self.assertTrue( - out.split("\n")[0] == "Connected to JSBSim server", - msg="Not connected to the JSBSim server.\nGot message '%s' instead" - % (out,), - ) - - # Check that "help" returns the minimum set of commands that will be - # tested - self.assertEqual( - sorted( - map( - lambda x: x.split("{")[0].strip(), - tn.sendCommand("help").split("\n")[2:-2], - ) - ), - ["get", "help", "hold", "info", "iterate", "quit", "resume", "set"], - ) + self.assertion_failed = False + + def tearDown(self): + self.assertFalse(self.assertion_failed) + super().tearDown() + self._fdm = None + self.telnet = None + + def assertEqual(self, first, second, msg=None): + try: + super().assertEqual(first, second, msg) + except AssertionError as e: + print(e, flush=True) + self.assertion_failed = True + + def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None): + try: + super().assertAlmostEqual(first, second, places, msg, delta) + except AssertionError as e: + print(e, flush=True) + self.assertion_failed = True + + async def run_fdm(self): + while True: + for _ in range(50): + if not self._fdm.run(): + return + self._fdm.check_incremental_hold() + await asyncio.sleep(0.1) def test_no_input(self): - fdm = self.create_fdm() - fdm.load_script(self.script_path) - fdm.run_ic() - fdm.hold() + self._fdm.load_script(self.script_path) + self._fdm.run_ic() + self._fdm.hold() - with self.assertRaises(socket.error): - TelnetInterface(fdm, 2222) + with self.assertRaises(OSError): + asyncio.run(self.run_test(2222, self.sanity_check)) def test_input_socket(self): # First, extract the time step from the script file @@ -95,93 +107,143 @@ def test_input_socket(self): # The aircraft c172x does not contain an tag so we need # to add one. - tree, aircraft_name, b = CopyAircraftDef(self.script_path, self.sandbox) + tree, aircraft_name, _ = CopyAircraftDef(self.script_path, self.sandbox) root = tree.getroot() input_tag = et.SubElement(root, "input") input_tag.attrib["port"] = "1137" tree.write(self.sandbox("aircraft", aircraft_name, aircraft_name + ".xml")) - fdm = self.create_fdm() - fdm.set_aircraft_path("aircraft") - fdm.load_script(self.script_path) - fdm.run_ic() - fdm.hold() + self._fdm.set_aircraft_path("aircraft") + self._fdm.load_script(self.script_path) + self._fdm.run_ic() + self._fdm.hold() + + asyncio.run(self.run_test(1137, lambda r, w: self.shell(root, dt, r, w))) - tn = TelnetInterface(fdm, 1137) - self.sanityCheck(tn) + async def shell(self, root, dt, reader, writer): + await self.sanity_check(reader, writer) + msg = (await self.telnet.send_command("info")).split("\n") # Check the aircraft name and its version - msg = tn.sendCommand("info").split("\n") self.assertEqual(msg[2].split(":")[1].strip(), root.attrib["name"].strip()) self.assertEqual(msg[1].split(":")[1].strip(), root.attrib["version"].strip()) # Check that the simulation time is 0.0 self.assertEqual(float(msg[3].split(":")[1].strip()), 0.0) - self.assertEqual(fdm.get_sim_time(), 0.0) - self.assertEqual(tn.getPropertyValue("simulation/sim-time-sec"), 0.0) + self.assertEqual(self._fdm.get_sim_time(), 0.0) + self.assertEqual( + await self.telnet.get_property_value("simulation/sim-time-sec"), + 0.0, + ) # Check that 'iterate' iterates the correct number of times - tn.sendCommand("iterate 19") - self.assertEqual(fdm.get_sim_time(), 19.0 * dt) + await self.telnet.send_command("iterate 19") + self.assertEqual(self._fdm.get_sim_time(), 19.0 * dt) self.assertAlmostEqual( - tn.getPropertyValue("simulation/sim-time-sec"), - fdm.get_sim_time(), + await self.telnet.get_property_value("simulation/sim-time-sec"), + self._fdm.get_sim_time(), delta=1e-5, ) - self.assertTrue(fdm.holding()) + self.assertTrue(self._fdm.holding()) # Wait a little bit and make sure that the simulation time has not # changed meanwhile thus confirming that the simulation is on hold. - for _ in range(40): - fdm.run() - self.assertEqual(fdm.get_sim_time(), 19.0 * dt) + await asyncio.sleep(0.5) + self.assertEqual(self._fdm.get_sim_time(), 19.0 * dt) self.assertAlmostEqual( - tn.getPropertyValue("simulation/sim-time-sec"), - fdm.get_sim_time(), + await self.telnet.get_property_value("simulation/sim-time-sec"), + self._fdm.get_sim_time(), delta=1e-5, ) # Modify the tank[0] contents via the "send" command - half_contents = 0.5 * tn.getPropertyValue("propulsion/tank/contents-lbs") - tn.sendCommand("set propulsion/tank/contents-lbs " + str(half_contents)) + half_contents = 0.5 * ( + await self.telnet.get_property_value("propulsion/tank/contents-lbs") + ) + await self.telnet.send_command( + "set propulsion/tank/contents-lbs " + str(half_contents) + ) self.assertEqual( - tn.getPropertyValue("propulsion/tank/contents-lbs"), half_contents + await self.telnet.get_property_value("propulsion/tank/contents-lbs"), + half_contents, ) # Check the resume/hold commands - t = fdm.get_sim_time() - tn.sendCommand("resume") - self.assertNotEqual(fdm.get_sim_time(), t) - self.assertFalse(fdm.holding()) - tn.sendCommand("hold") - t = fdm.get_sim_time() - self.assertTrue(fdm.holding()) + t = self._fdm.get_sim_time() + await self.telnet.send_command("resume") + self.assertNotEqual(self._fdm.get_sim_time(), t) + self.assertFalse(self._fdm.holding()) + await self.telnet.send_command("hold") + t = self._fdm.get_sim_time() + self.assertTrue(self._fdm.holding()) self.assertAlmostEqual( - tn.getPropertyValue("simulation/sim-time-sec"), t, delta=1e-5 + await self.telnet.get_property_value("simulation/sim-time-sec"), + t, + delta=1e-5, ) # Wait a little bit and make sure that the simulation time has not # changed meanwhile thus confirming that the simulation is on hold. - for _ in range(40): - fdm.run() - self.assertEqual(fdm.get_sim_time(), t) + await asyncio.sleep(0.5) + self.assertEqual(self._fdm.get_sim_time(), t) self.assertAlmostEqual( - tn.getPropertyValue("simulation/sim-time-sec"), t, delta=1e-5 + await self.telnet.get_property_value("simulation/sim-time-sec"), + t, + delta=1e-5, + ) + + writer.close() + + async def sanity_check(self, _, __): + out = await self.telnet.get_output() + + self.assertTrue( + out == "Connected to JSBSim server", + msg=f"Not connected to the JSBSim server.\nGot message '{out}' instead", ) + out = await self.telnet.send_command("help") + + # Check that "help" returns the minimum set of commands that will be + # tested + self.assertEqual( + sorted( + map( + lambda x: x.split("{")[0].strip(), + out.split("\n")[2:-1], + ) + ), + ["get", "help", "hold", "info", "iterate", "quit", "resume", "set"], + ) + + async def run_test(self, port, shell): + telnet_task = asyncio.create_task(self.telnet.run(port, shell)) + fdm_task = asyncio.create_task(self.run_fdm()) + + done, pending = await asyncio.wait( + [telnet_task, fdm_task], return_when=asyncio.FIRST_COMPLETED + ) + + # Cancel the fdm_task if it is still pending + for task in pending: + task.cancel() + + # Handle exceptions if any + for task in done: + if task.exception(): + raise task.exception() + def test_script_input(self): tree = et.parse(self.script_path) input_tag = et.SubElement(tree.getroot(), "input") input_tag.attrib["port"] = "1138" tree.write("c1722_1.xml") - fdm = self.create_fdm() - fdm.load_script("c1722_1.xml") - fdm.run_ic() - fdm.hold() + self._fdm.load_script("c1722_1.xml") + self._fdm.run_ic() + self._fdm.hold() - tn = TelnetInterface(fdm, 1138) - self.sanityCheck(tn) + asyncio.run(self.run_test(1138, self.sanity_check)) RunTest(TestInputSocket)