Skip to content

Commit baa0c52

Browse files
committed
test(import bin): added pytests
1 parent 48269e5 commit baa0c52

2 files changed

Lines changed: 481 additions & 0 deletions

File tree

tests/test_data_model_vehicle_project.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pytest
2020

2121
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
22+
from ardupilot_methodic_configurator.data_model_par_dict import ParDict
2223
from ardupilot_methodic_configurator.data_model_vehicle_project import VehicleProjectManager
2324
from ardupilot_methodic_configurator.data_model_vehicle_project_creator import (
2425
NewVehicleProjectSettings,
@@ -1027,3 +1028,298 @@ def test_user_can_complete_vehicle_opening_workflow(self) -> None:
10271028
assert vehicle_path == "/opened/vehicle/path"
10281029
mock_open.assert_called_once_with("/vehicle/path")
10291030
mock_store.assert_called_once_with("/opened/vehicle/path")
1031+
1032+
1033+
class TestCreateNewVehicleFromBinLog:
1034+
"""Test the create_new_vehicle_from_bin_log orchestration method."""
1035+
1036+
def _make_manager(self, with_fc: bool = False) -> "VehicleProjectManager":
1037+
mock_filesystem = MagicMock(spec=LocalFilesystem)
1038+
mock_flight_controller = MagicMock() if with_fc else None
1039+
return VehicleProjectManager(mock_filesystem, mock_flight_controller)
1040+
1041+
def test_user_can_create_project_from_bin_log_successfully(self) -> None:
1042+
"""
1043+
User can create a new vehicle project from a valid .bin log file.
1044+
1045+
GIVEN: A project manager and a valid .bin log file
1046+
WHEN: create_new_vehicle_from_bin_log is called
1047+
THEN: The vehicle directory is created, defaults replaced, and the path returned
1048+
"""
1049+
# Arrange
1050+
manager = self._make_manager()
1051+
1052+
fake_defaults = ParDict.from_float_dict({"PARAM_A": 1.0})
1053+
fake_current = ParDict.from_float_dict({"PARAM_A": 1.0, "PARAM_B": 2.0})
1054+
empty_compound = ParDict.from_float_dict({})
1055+
1056+
with (
1057+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl/ArduCopter/empty_4.6.x"),
1058+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="my_flight"),
1059+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(fake_defaults, fake_current)),
1060+
patch.object(
1061+
manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/my_flight"
1062+
) as mock_create,
1063+
patch.object(manager._creator, "next_import_filename", return_value="02_imported_bin_log_parameters.param"),
1064+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1065+
patch.object(manager, "store_recently_used_template_dirs"),
1066+
patch.object(manager, "open_vehicle_directory") as mock_open,
1067+
patch.object(manager._local_filesystem, "write_param_default_values_to_file") as mock_write,
1068+
patch.object(manager._local_filesystem, "compound_params", return_value=(empty_compound, "00_default.param")),
1069+
patch.object(manager._local_filesystem, "export_to_param"),
1070+
patch.object(manager._local_filesystem, "re_init"),
1071+
):
1072+
# Act
1073+
result = manager.create_new_vehicle_from_bin_log("/logs/my_flight.bin")
1074+
1075+
# Assert: correct path returned
1076+
assert result == "/vehicles/my_flight"
1077+
# Assert: template creation called with fc_connected=False (key difference from normal flow)
1078+
_args, kwargs = mock_create.call_args
1079+
assert kwargs.get("fc_connected") is False
1080+
# Assert: vehicle directory opened immediately after creation
1081+
mock_open.assert_called_once_with("/vehicles/my_flight")
1082+
# Assert: extracted defaults written (replacing the template's 00_default.param)
1083+
mock_write.assert_called_once_with(fake_defaults)
1084+
1085+
def test_bin_log_defaults_are_written_to_vehicle_directory(self) -> None:
1086+
"""
1087+
The defaults extracted from the .bin log replace the template's 00_default.param.
1088+
1089+
GIVEN: A valid .bin log file with a known defaults snapshot
1090+
WHEN: create_new_vehicle_from_bin_log is called
1091+
THEN: write_param_default_values_to_file is called with the extracted defaults ParDict
1092+
"""
1093+
# Arrange
1094+
manager = self._make_manager()
1095+
1096+
fake_defaults = ParDict.from_float_dict({"BARO_ALT_OFFSET": 0.0})
1097+
fake_current = ParDict.from_float_dict({"BARO_ALT_OFFSET": 0.5})
1098+
empty_compound = ParDict.from_float_dict({})
1099+
1100+
with (
1101+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1102+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1103+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(fake_defaults, fake_current)),
1104+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight"),
1105+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1106+
patch.object(manager, "store_recently_used_template_dirs"),
1107+
patch.object(manager, "open_vehicle_directory"),
1108+
patch.object(manager._local_filesystem, "compound_params", return_value=(empty_compound, "00_default.param")),
1109+
patch.object(manager._creator, "next_import_filename", return_value="02_imported_bin_log_parameters.param"),
1110+
patch.object(manager._local_filesystem, "export_to_param"),
1111+
patch.object(manager._local_filesystem, "re_init"),
1112+
patch.object(manager._local_filesystem, "write_param_default_values_to_file") as mock_write,
1113+
):
1114+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1115+
1116+
# Assert: the extracted defaults — not the template's — are written
1117+
mock_write.assert_called_once_with(fake_defaults)
1118+
1119+
def test_missing_params_exported_to_import_file(self) -> None:
1120+
"""
1121+
Parameters present in the .bin log but absent from the AMC files are exported.
1122+
1123+
GIVEN: A .bin log where current params include entries not covered by AMC files
1124+
WHEN: create_new_vehicle_from_bin_log is called
1125+
THEN: export_to_param is called for the difference, and the filesystem is re-initialised
1126+
"""
1127+
# Arrange
1128+
manager = self._make_manager()
1129+
1130+
fake_defaults = ParDict.from_float_dict({"PARAM_A": 1.0})
1131+
fake_current = ParDict.from_float_dict({"PARAM_A": 1.0, "EXTRA_PARAM": 99.0})
1132+
# compound_params covers only PARAM_A — EXTRA_PARAM is missing
1133+
compound = ParDict.from_float_dict({"PARAM_A": 1.0})
1134+
1135+
with (
1136+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1137+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1138+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(fake_defaults, fake_current)),
1139+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight"),
1140+
patch.object(manager._creator, "next_import_filename", return_value="02_imported_bin_log_parameters.param"),
1141+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1142+
patch.object(manager, "store_recently_used_template_dirs"),
1143+
patch.object(manager, "open_vehicle_directory"),
1144+
patch.object(manager._local_filesystem, "write_param_default_values_to_file"),
1145+
patch.object(manager._local_filesystem, "compound_params", return_value=(compound, "00_default.param")),
1146+
patch.object(manager._local_filesystem, "export_to_param") as mock_export,
1147+
patch.object(manager._local_filesystem, "re_init") as mock_reinit,
1148+
):
1149+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1150+
1151+
# Assert: the import file is created and the filesystem is re-initialised
1152+
mock_export.assert_called_once()
1153+
exported_params, export_filename = mock_export.call_args.args[:2]
1154+
assert export_filename == "02_imported_bin_log_parameters.param"
1155+
assert "EXTRA_PARAM" in exported_params
1156+
assert "PARAM_A" not in exported_params
1157+
assert mock_export.call_args.kwargs.get("annotate_doc") is False
1158+
mock_reinit.assert_called_once()
1159+
1160+
def test_no_import_file_when_all_params_covered_by_amc_files(self) -> None:
1161+
"""
1162+
No extra import file is created when all current params are already in AMC files.
1163+
1164+
GIVEN: A .bin log where all current params match the AMC param files
1165+
WHEN: create_new_vehicle_from_bin_log is called
1166+
THEN: export_to_param and re_init are NOT called
1167+
"""
1168+
# Arrange
1169+
manager = self._make_manager()
1170+
1171+
params = ParDict.from_float_dict({"PARAM_A": 1.0, "PARAM_B": 2.0})
1172+
compound = ParDict.from_float_dict({"PARAM_A": 1.0, "PARAM_B": 2.0})
1173+
1174+
with (
1175+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1176+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1177+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(params, params)),
1178+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight"),
1179+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1180+
patch.object(manager, "store_recently_used_template_dirs"),
1181+
patch.object(manager, "open_vehicle_directory"),
1182+
patch.object(manager._local_filesystem, "write_param_default_values_to_file"),
1183+
patch.object(manager._local_filesystem, "compound_params", return_value=(compound, "00_default.param")),
1184+
patch.object(manager._local_filesystem, "export_to_param") as mock_export,
1185+
patch.object(manager._local_filesystem, "re_init") as mock_reinit,
1186+
):
1187+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1188+
1189+
# Assert: no import file written, no re-init
1190+
mock_export.assert_not_called()
1191+
mock_reinit.assert_not_called()
1192+
1193+
def test_fc_parameters_synced_when_flight_controller_connected(self) -> None:
1194+
"""
1195+
When a flight controller is connected, its fc_parameters are updated.
1196+
1197+
GIVEN: A project manager with an active flight controller
1198+
WHEN: create_new_vehicle_from_bin_log completes successfully
1199+
THEN: The flight controller's fc_parameters are set to the current log params
1200+
"""
1201+
# Arrange
1202+
manager = self._make_manager(with_fc=True)
1203+
1204+
fake_defaults = ParDict.from_float_dict({"PARAM_A": 1.0})
1205+
fake_current = ParDict.from_float_dict({"PARAM_A": 1.0})
1206+
compound = ParDict.from_float_dict({"PARAM_A": 1.0})
1207+
1208+
with (
1209+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1210+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1211+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(fake_defaults, fake_current)),
1212+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight"),
1213+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1214+
patch.object(manager, "store_recently_used_template_dirs"),
1215+
patch.object(manager, "open_vehicle_directory"),
1216+
patch.object(manager._local_filesystem, "write_param_default_values_to_file"),
1217+
patch.object(manager._local_filesystem, "compound_params", return_value=(compound, "00_default.param")),
1218+
patch.object(manager._local_filesystem, "export_to_param"),
1219+
patch.object(manager._local_filesystem, "re_init"),
1220+
):
1221+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1222+
1223+
# Assert: FC parameters updated to the values extracted from the log
1224+
assert manager._flight_controller.fc_parameters == {"PARAM_A": 1.0}
1225+
1226+
def test_creation_error_propagates_to_caller(self) -> None:
1227+
"""
1228+
VehicleProjectCreationError from extraction propagates unchanged.
1229+
1230+
GIVEN: A .bin log file that cannot be parsed
1231+
WHEN: create_new_vehicle_from_bin_log is called
1232+
THEN: VehicleProjectCreationError is raised with the original title/message
1233+
"""
1234+
# Arrange
1235+
manager = self._make_manager()
1236+
1237+
with (
1238+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1239+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="bad"),
1240+
patch.object(
1241+
manager._creator,
1242+
"extract_param_files_from_bin_log",
1243+
side_effect=VehicleProjectCreationError(".bin log import", "Corrupt log"),
1244+
),
1245+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1246+
pytest.raises(VehicleProjectCreationError) as exc_info,
1247+
):
1248+
manager.create_new_vehicle_from_bin_log("/logs/bad.bin")
1249+
1250+
assert exc_info.value.title == ".bin log import"
1251+
assert exc_info.value.message == "Corrupt log"
1252+
1253+
def test_template_creation_always_called_with_fc_connected_false(self) -> None:
1254+
"""
1255+
create_new_vehicle_from_template is always called with fc_connected=False.
1256+
1257+
This is the key difference from the normal template-creation flow: the vehicle
1258+
is scaffolded without a live FC connection, using log-extracted params instead.
1259+
1260+
GIVEN: A project manager that even has a flight controller connected
1261+
WHEN: create_new_vehicle_from_bin_log is called
1262+
THEN: create_new_vehicle_from_template receives fc_connected=False
1263+
"""
1264+
# Arrange: manager WITH a connected flight controller
1265+
manager = self._make_manager(with_fc=True)
1266+
1267+
params = ParDict.from_float_dict({"PARAM_A": 1.0})
1268+
compound = ParDict.from_float_dict({"PARAM_A": 1.0})
1269+
1270+
with (
1271+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl"),
1272+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1273+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(params, params)),
1274+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight") as mock_create,
1275+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1276+
patch.object(manager, "store_recently_used_template_dirs"),
1277+
patch.object(manager, "open_vehicle_directory"),
1278+
patch.object(manager._local_filesystem, "write_param_default_values_to_file"),
1279+
patch.object(manager._local_filesystem, "compound_params", return_value=(compound, "00_default.param")),
1280+
patch.object(manager._local_filesystem, "export_to_param"),
1281+
patch.object(manager._local_filesystem, "re_init"),
1282+
):
1283+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1284+
1285+
# Assert: regardless of FC connection, fc_connected must be False
1286+
_args, kwargs = mock_create.call_args
1287+
assert kwargs.get("fc_connected") is False
1288+
1289+
def test_manager_state_updated_after_bin_log_import(self) -> None:
1290+
"""
1291+
Manager internal state is updated correctly after a successful .bin log import.
1292+
1293+
GIVEN: A project manager in its initial state
1294+
WHEN: create_new_vehicle_from_bin_log completes successfully
1295+
THEN: _settings carries the bin-log import options and configuration_template
1296+
is set to the template directory name
1297+
"""
1298+
# Arrange
1299+
manager = self._make_manager()
1300+
1301+
params = ParDict.from_float_dict({"PARAM_A": 1.0})
1302+
compound = ParDict.from_float_dict({"PARAM_A": 1.0})
1303+
1304+
with (
1305+
patch.object(manager._creator, "template_dir_for_bin_import", return_value="/tpl/ArduCopter/empty_4.6.x"),
1306+
patch.object(manager._creator, "vehicle_name_from_bin_log", return_value="flight"),
1307+
patch.object(manager._creator, "extract_param_files_from_bin_log", return_value=(params, params)),
1308+
patch.object(manager._creator, "create_new_vehicle_from_template", return_value="/vehicles/flight"),
1309+
patch.object(LocalFilesystem, "get_vehicles_default_dir", return_value="/vehicles"),
1310+
patch.object(manager, "store_recently_used_template_dirs"),
1311+
patch.object(manager, "open_vehicle_directory"),
1312+
patch.object(manager._local_filesystem, "write_param_default_values_to_file"),
1313+
patch.object(manager._local_filesystem, "compound_params", return_value=(compound, "00_default.param")),
1314+
patch.object(manager._local_filesystem, "export_to_param"),
1315+
patch.object(manager._local_filesystem, "re_init"),
1316+
):
1317+
manager.create_new_vehicle_from_bin_log("/logs/flight.bin")
1318+
1319+
# Assert: settings reflect the bin-log import defaults
1320+
assert manager._settings is not None
1321+
assert manager._settings.blank_change_reason is True
1322+
assert manager._settings.infer_comp_specs_and_conn_from_fc_params is True
1323+
assert manager._settings.use_fc_params is True
1324+
# Assert: configuration_template is the leaf directory name of the template path
1325+
assert manager.configuration_template == "empty_4.6.x"

0 commit comments

Comments
 (0)