Skip to content

Commit ef233b8

Browse files
committed
feat(new vehicle): add a checkbox to copy vehicle.jpg from template
1 parent a6e7dca commit ef233b8

File tree

5 files changed

+364
-14
lines changed

5 files changed

+364
-14
lines changed

ardupilot_methodic_configurator/backend_filesystem.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def directory_exists(directory: str) -> bool:
554554
return os_path.exists(directory) and os_path.isdir(directory)
555555

556556
def copy_template_files_to_new_vehicle_dir(
557-
self, template_dir: str, new_vehicle_dir: str, blank_change_reason: bool
557+
self, template_dir: str, new_vehicle_dir: str, blank_change_reason: bool, copy_vehicle_image: bool
558558
) -> str:
559559
# Copy the template files to the new vehicle directory
560560
try:
@@ -570,14 +570,17 @@ def copy_template_files_to_new_vehicle_dir(
570570
logging_error(error_msg)
571571
return error_msg
572572

573+
skip_files = {
574+
"apm.pdef.xml",
575+
"last_uploaded_filename.txt",
576+
"tempcal_acc.png",
577+
"tempcal_gyro.png",
578+
}
579+
if not copy_vehicle_image:
580+
skip_files.add("vehicle.jpg")
581+
573582
for item in os_listdir(template_dir):
574-
if item in {
575-
"apm.pdef.xml",
576-
"vehicle.jpg",
577-
"last_uploaded_filename.txt",
578-
"tempcal_acc.png",
579-
"tempcal_gyro.png",
580-
}:
583+
if item in skip_files:
581584
continue
582585
source = os_path.join(template_dir, item)
583586
dest = os_path.join(new_vehicle_dir, item)

ardupilot_methodic_configurator/frontend_tkinter_directory_selection.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ def __init__(
251251
self.infer_comp_specs_and_conn_from_fc_params = tk.BooleanVar(value=False)
252252
self.use_fc_params = tk.BooleanVar(value=False)
253253
self.blank_change_reason = tk.BooleanVar(value=False)
254+
self.copy_vehicle_image = tk.BooleanVar(value=False)
254255
self.configuration_template: str = "" # will be set to a string if a template was used
255256

256257
# Explain why we are here
@@ -360,6 +361,19 @@ def create_option1_widgets( # pylint: disable=too-many-locals,too-many-argument
360361
blank_change_reason_checkbox,
361362
_("Do not use the parameters change reason from the template."),
362363
)
364+
copy_vehicle_image_checkbox = ttk.Checkbutton(
365+
option1_label_frame,
366+
variable=self.copy_vehicle_image,
367+
text=_("Copy vehicle image from template"),
368+
)
369+
copy_vehicle_image_checkbox.pack(anchor=tk.NW)
370+
show_tooltip(
371+
copy_vehicle_image_checkbox,
372+
_(
373+
"Copy the vehicle.jpg image file from the template directory to the new vehicle directory\n"
374+
"if it exists. This image helps identify the vehicle configuration."
375+
),
376+
)
363377
if not fc_connected:
364378
self.infer_comp_specs_and_conn_from_fc_params.set(False)
365379
infer_comp_specs_and_conn_from_fc_params_checkbox.config(state=tk.DISABLED)
@@ -491,7 +505,10 @@ def create_new_vehicle_from_template(self) -> None:
491505
return
492506

493507
error_msg = self.local_filesystem.copy_template_files_to_new_vehicle_dir(
494-
template_dir, new_vehicle_dir, self.blank_change_reason.get()
508+
template_dir,
509+
new_vehicle_dir,
510+
blank_change_reason=self.blank_change_reason.get(),
511+
copy_vehicle_image=self.copy_vehicle_image.get(),
495512
)
496513
if error_msg:
497514
messagebox.showerror(_("Copying template files"), error_msg)

tests/test_backend_filesystem.py

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from subprocess import SubprocessError
1717
from unittest.mock import MagicMock, mock_open, patch
1818

19+
import pytest
20+
1921
from ardupilot_methodic_configurator.annotate_params import Par
2022
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem, is_within_tolerance
2123

@@ -915,7 +917,9 @@ def test_copy_template_files_to_new_vehicle_dir( # pylint: disable=too-many-arg
915917
lfs = LocalFilesystem(
916918
"vehicle_dir", "vehicle_type", None, allow_editing_template_files=False, save_component_to_system_templates=False
917919
)
918-
lfs.copy_template_files_to_new_vehicle_dir("template_dir", "new_vehicle_dir", blank_change_reason=False)
920+
lfs.copy_template_files_to_new_vehicle_dir(
921+
"template_dir", "new_vehicle_dir", blank_change_reason=False, copy_vehicle_image=False
922+
)
919923

920924
# Verify all files and directories were processed
921925
assert mock_listdir.call_count >= 1
@@ -959,7 +963,9 @@ def test_copy_template_files_with_blank_change_reason( # pylint: disable=too-ma
959963
)
960964

961965
# Test with blank_change_reason=True
962-
lfs.copy_template_files_to_new_vehicle_dir("template_dir", "new_vehicle_dir", blank_change_reason=True)
966+
lfs.copy_template_files_to_new_vehicle_dir(
967+
"template_dir", "new_vehicle_dir", blank_change_reason=True, copy_vehicle_image=False
968+
)
963969

964970
# Verify file handling when blank_change_reason=True
965971
# First check if writelines was called at all
@@ -986,11 +992,154 @@ def test_copy_template_files_with_blank_change_reason( # pylint: disable=too-ma
986992
mock_copytree.reset_mock()
987993

988994
# Test with blank_change_reason=False
989-
lfs.copy_template_files_to_new_vehicle_dir("template_dir", "new_vehicle_dir", blank_change_reason=False)
995+
lfs.copy_template_files_to_new_vehicle_dir(
996+
"template_dir", "new_vehicle_dir", blank_change_reason=False, copy_vehicle_image=False
997+
)
990998

991999
# Verify param file was copied normally
9921000
mock_copy2.assert_any_call("template_dir/file1.param", "new_vehicle_dir/file1.param")
9931001

1002+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.exists")
1003+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_listdir")
1004+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.join")
1005+
@patch("ardupilot_methodic_configurator.backend_filesystem.shutil_copy2")
1006+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.isdir")
1007+
def test_user_can_copy_vehicle_image_from_template(
1008+
self, mock_isdir, mock_copy2, mock_join, mock_listdir, mock_exists
1009+
) -> None:
1010+
"""
1011+
User can copy vehicle image file when creating a new vehicle from template.
1012+
1013+
GIVEN: A template directory containing a vehicle.jpg file
1014+
WHEN: The user creates a new vehicle with copy_vehicle_image=True
1015+
THEN: The vehicle.jpg file should be copied to the new vehicle directory
1016+
"""
1017+
# Arrange: Set up template directory with vehicle.jpg
1018+
mock_exists.side_effect = lambda path: path in ["template_dir", "new_vehicle_dir"]
1019+
mock_listdir.return_value = ["vehicle.jpg", "config.param"]
1020+
mock_join.side_effect = lambda *args: "/".join(args)
1021+
mock_isdir.return_value = False
1022+
1023+
lfs = LocalFilesystem(
1024+
"vehicle_dir", "vehicle_type", None, allow_editing_template_files=False, save_component_to_system_templates=False
1025+
)
1026+
1027+
# Act: Copy template files with copy_vehicle_image=True
1028+
result = lfs.copy_template_files_to_new_vehicle_dir(
1029+
"template_dir", "new_vehicle_dir", blank_change_reason=False, copy_vehicle_image=True
1030+
)
1031+
1032+
# Assert: Vehicle image should be copied
1033+
assert result == "" # No error
1034+
mock_copy2.assert_any_call("template_dir/vehicle.jpg", "new_vehicle_dir/vehicle.jpg")
1035+
mock_copy2.assert_any_call("template_dir/config.param", "new_vehicle_dir/config.param")
1036+
1037+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.exists")
1038+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_listdir")
1039+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.join")
1040+
@patch("ardupilot_methodic_configurator.backend_filesystem.shutil_copy2")
1041+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.isdir")
1042+
def test_user_can_skip_copying_vehicle_image_from_template(
1043+
self, mock_isdir, mock_copy2, mock_join, mock_listdir, mock_exists
1044+
) -> None:
1045+
"""
1046+
User can choose not to copy vehicle image file when creating a new vehicle from template.
1047+
1048+
GIVEN: A template directory containing a vehicle.jpg file
1049+
WHEN: The user creates a new vehicle with copy_vehicle_image=False
1050+
THEN: The vehicle.jpg file should not be copied to the new vehicle directory
1051+
AND: Other files should still be copied normally
1052+
"""
1053+
# Arrange: Set up template directory with vehicle.jpg and other files
1054+
mock_exists.side_effect = lambda path: path in ["template_dir", "new_vehicle_dir"]
1055+
mock_listdir.return_value = ["vehicle.jpg", "config.param", "readme.txt"]
1056+
mock_join.side_effect = lambda *args: "/".join(args)
1057+
mock_isdir.return_value = False
1058+
1059+
lfs = LocalFilesystem(
1060+
"vehicle_dir", "vehicle_type", None, allow_editing_template_files=False, save_component_to_system_templates=False
1061+
)
1062+
1063+
# Act: Copy template files with copy_vehicle_image=False
1064+
result = lfs.copy_template_files_to_new_vehicle_dir(
1065+
"template_dir", "new_vehicle_dir", blank_change_reason=False, copy_vehicle_image=False
1066+
)
1067+
1068+
# Assert: Vehicle image should not be copied, but other files should be
1069+
assert result == "" # No error
1070+
1071+
# Verify vehicle.jpg was NOT copied
1072+
copy_calls = [call.args for call in mock_copy2.call_args_list]
1073+
vehicle_jpg_calls = [call for call in copy_calls if "vehicle.jpg" in str(call)]
1074+
assert len(vehicle_jpg_calls) == 0, "vehicle.jpg should not be copied when copy_vehicle_image=False"
1075+
1076+
# Verify other files were copied
1077+
mock_copy2.assert_any_call("template_dir/config.param", "new_vehicle_dir/config.param")
1078+
mock_copy2.assert_any_call("template_dir/readme.txt", "new_vehicle_dir/readme.txt")
1079+
1080+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.exists")
1081+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_listdir")
1082+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.join")
1083+
@patch("ardupilot_methodic_configurator.backend_filesystem.shutil_copy2")
1084+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.isdir")
1085+
def test_copy_vehicle_image_defaults_to_false(self, mock_isdir, mock_copy2, mock_join, mock_listdir, mock_exists) -> None:
1086+
"""
1087+
Vehicle image copying defaults to disabled.
1088+
1089+
GIVEN: A template directory containing a vehicle.jpg file
1090+
WHEN: The user creates a new vehicle without specifying copy_vehicle_image parameter
1091+
THEN: The vehicle.jpg file should not be copied by default
1092+
"""
1093+
# Arrange: Set up template directory with vehicle.jpg
1094+
mock_exists.side_effect = lambda path: path in ["template_dir", "new_vehicle_dir"]
1095+
mock_listdir.return_value = ["vehicle.jpg", "config.param"]
1096+
mock_join.side_effect = lambda *args: "/".join(args)
1097+
mock_isdir.return_value = False
1098+
1099+
lfs = LocalFilesystem(
1100+
"vehicle_dir", "vehicle_type", "", allow_editing_template_files=False, save_component_to_system_templates=False
1101+
)
1102+
1103+
# Act: Copy template files without specifying copy_vehicle_image
1104+
# (this should cause an error since parameter is required)
1105+
with pytest.raises(TypeError):
1106+
lfs.copy_template_files_to_new_vehicle_dir("template_dir", "new_vehicle_dir", blank_change_reason=False)
1107+
1108+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.exists")
1109+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_listdir")
1110+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.join")
1111+
@patch("ardupilot_methodic_configurator.backend_filesystem.shutil_copy2")
1112+
@patch("ardupilot_methodic_configurator.backend_filesystem.os_path.isdir")
1113+
def test_copy_vehicle_image_with_no_image_file_present(
1114+
self, mock_isdir, mock_copy2, mock_join, mock_listdir, mock_exists
1115+
) -> None:
1116+
"""
1117+
Vehicle image copying gracefully handles missing vehicle.jpg file.
1118+
1119+
GIVEN: A template directory without a vehicle.jpg file
1120+
WHEN: The user creates a new vehicle with copy_vehicle_image=True
1121+
THEN: No error should occur and other files should be copied normally
1122+
"""
1123+
# Arrange: Set up template directory without vehicle.jpg
1124+
mock_exists.side_effect = lambda path: path in ["template_dir", "new_vehicle_dir"]
1125+
mock_listdir.return_value = ["config.param", "readme.txt"] # No vehicle.jpg
1126+
mock_join.side_effect = lambda *args: "/".join(args)
1127+
mock_isdir.return_value = False
1128+
1129+
lfs = LocalFilesystem(
1130+
"vehicle_dir", "vehicle_type", None, allow_editing_template_files=False, save_component_to_system_templates=False
1131+
)
1132+
1133+
# Act: Copy template files with copy_vehicle_image=True (but no vehicle.jpg exists)
1134+
result = lfs.copy_template_files_to_new_vehicle_dir(
1135+
"template_dir", "new_vehicle_dir", blank_change_reason=False, copy_vehicle_image=True
1136+
)
1137+
1138+
# Assert: No error and other files copied normally
1139+
assert result == "" # No error
1140+
mock_copy2.assert_any_call("template_dir/config.param", "new_vehicle_dir/config.param")
1141+
mock_copy2.assert_any_call("template_dir/readme.txt", "new_vehicle_dir/readme.txt")
1142+
9941143

9951144
def test_merge_forced_or_derived_parameters_comprehensive() -> None:
9961145
"""Test merge_forced_or_derived_parameters with various scenarios."""

0 commit comments

Comments
 (0)