Skip to content

Commit 5339f33

Browse files
committed
fix(recent project): update the recent project history when opening projects
1 parent 991ddb2 commit 5339f33

9 files changed

+197
-118
lines changed

ARCHITECTURE_3_directory_selection.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,13 @@ The data flow follows the layered architecture pattern with clear separation of
262262
- Clean separation between project opening and project creation concerns
263263

264264
6. **Recent Directories History Flow**
265-
- On application startup, `ProgramSettings.get_recent_vehicle_dirs()` loads history from settings.json
265+
- On application startup, `ProgramSettings.get_recent_vehicle_dirs()` loads history from settings.json; the history is passed through the manager
266266
- History is passed to VehicleProjectOpenerWindow to populate the combobox widget
267267
- User selects a directory from the combobox dropdown
268268
- Frontend calls `project_manager.open_vehicle_directory(selected_dir)`
269-
- If successful, `ProgramSettings.store_recently_used_vehicle_dir(selected_dir)` is called
269+
- The `VehicleProjectManager` (business logic) stores the path in the recent‑vehicle history
270+
using `LocalFilesystem`/`ProgramSettings` abstractions; the frontend never touches
271+
the persistence layer directly
270272
- History is updated: selected directory moved/added to position [0], oldest removed if > 5 entries
271273
- Updated history is persisted to settings.json
272274
- History order maintained: most recent [0] to oldest [4]
@@ -327,7 +329,7 @@ The recent vehicle directories are stored in the `settings.json` file in the use
327329
- Template directory paths are excluded from migration (not user selections)
328330
- Legacy `vehicle_dir` setting is always removed after migration attempt (prevents repeated migrations)
329331
- Once migrated, `vehicle_dir` is no longer used or written
330-
- The `get_recently_used_dirs()` method returns `recent_vehicle_history[0]` as the last opened directory, or defaults if empty
332+
- The `get_recently_used_dirs()` method (invoked via the manager) returns `recent_vehicle_history[0]` as the last opened directory, or defaults if empty
331333

332334
### Integration Points
333335

ardupilot_methodic_configurator/backend_filesystem_program_settings.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,14 +469,6 @@ def get_recent_vehicle_dirs() -> list[str]:
469469
settings = ProgramSettings._get_settings_as_dict()
470470
return ProgramSettings._recent_vehicle_history.get_items(settings)
471471

472-
@staticmethod
473-
def store_vehicle_dir_to_history_safe(vehicle_dir: str) -> None:
474-
"""Store vehicle directory to history with error handling (logs errors, doesn't raise)."""
475-
try:
476-
ProgramSettings.store_recently_used_vehicle_dir(vehicle_dir)
477-
except ValueError as e:
478-
logging_warning(_("Failed to store vehicle directory to history: %s"), e)
479-
480472
@staticmethod
481473
def get_templates_base_dir() -> str:
482474
package_path = importlib_files("ardupilot_methodic_configurator")

ardupilot_methodic_configurator/data_model_vehicle_project.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ def create_new_vehicle_from_template(
150150
if new_path:
151151
self._settings = settings
152152
self.configuration_template = self.get_directory_name_from_path(template_dir)
153+
# History updates belong in the manager/facade layer so they are
154+
# performed consistently for both project creation and opening.
155+
self.store_recently_used_template_dirs(template_dir, new_base_dir)
153156
return new_path
154157

155158
# Vehicle project opening operations
@@ -167,7 +170,10 @@ def open_vehicle_directory(self, vehicle_dir: str) -> str:
167170
VehicleProjectOpenError: If opening fails for any reason
168171
169172
"""
170-
return self._opener.open_vehicle_directory(vehicle_dir)
173+
result = self._opener.open_vehicle_directory(vehicle_dir)
174+
# update history whenever a directory is opened successfully
175+
self.store_recently_used_vehicle_dir(result)
176+
return result
171177

172178
def open_last_vehicle_directory(self, last_vehicle_dir: str) -> str:
173179
"""
@@ -183,7 +189,10 @@ def open_last_vehicle_directory(self, last_vehicle_dir: str) -> str:
183189
VehicleProjectOpenError: If opening fails for any reason
184190
185191
"""
186-
return self._opener.open_last_vehicle_directory(last_vehicle_dir)
192+
result = self._opener.open_last_vehicle_directory(last_vehicle_dir)
193+
# update history whenever a directory is opened successfully
194+
self.store_recently_used_vehicle_dir(result)
195+
return result
187196

188197
# Filesystem state management
189198
def get_vehicle_directory(self) -> str:

ardupilot_methodic_configurator/data_model_vehicle_project_creator.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -470,11 +470,6 @@ def create_new_vehicle_from_template( # pylint: disable=too-many-arguments, too
470470
_("No parameter files found"), _("No intermediate parameter files found after creating vehicle from template")
471471
)
472472

473-
# Store the successfully used directories for future use
474-
LocalFilesystem.store_recently_used_template_dirs(template_dir, new_base_dir)
475-
476-
LocalFilesystem.store_vehicle_dir_to_history_safe(new_vehicle_dir)
477-
478473
return new_vehicle_dir
479474

480475
def _validate_template_directory(self, template_dir: str) -> None:

ardupilot_methodic_configurator/data_model_vehicle_project_opener.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,6 @@ def open_vehicle_directory(self, vehicle_dir: str) -> str:
157157
),
158158
)
159159

160-
# Store to history - non-critical operation, log errors but don't fail
161-
LocalFilesystem.store_vehicle_dir_to_history_safe(vehicle_dir)
162-
163160
return vehicle_dir
164161

165162
def _validate_existing_directory(self, vehicle_dir: str) -> None:

tests/test_data_model_vehicle_project.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,54 @@ def test_user_can_get_recently_used_directories(self) -> None:
161161
assert vehicle_dir == "/vehicle"
162162
mock_get_dirs.assert_called_once()
163163

164+
def test_creation_updates_recent_history(self) -> None:
165+
"""Creating a new vehicle via the manager must automatically update the template."""
166+
mock_filesystem = MagicMock(spec=LocalFilesystem)
167+
manager = VehicleProjectManager(mock_filesystem)
168+
169+
with (
170+
patch.object(manager._creator, "create_new_vehicle_from_template") as mock_create,
171+
patch.object(LocalFilesystem, "store_recently_used_template_dirs") as mock_store_template,
172+
):
173+
mock_create.return_value = "/new/vehicle/path"
174+
settings = MagicMock(spec=NewVehicleProjectSettings)
175+
176+
# Act
177+
result = manager.create_new_vehicle_from_template("/templates/ArduCopter", "/vehicles", "Name", settings)
178+
179+
# Assert history updated
180+
assert result == "/new/vehicle/path"
181+
mock_store_template.assert_called_once_with("/templates/ArduCopter", "/vehicles")
182+
183+
def test_creation_does_not_store_vehicle_dir_in_history(self) -> None:
184+
"""
185+
Creating a vehicle must NOT store the vehicle directory in recent-vehicle history directly.
186+
187+
The vehicle dir is persisted to history by the subsequent open_vehicle_directory call in
188+
__main__, so the manager must not write it a second time during creation (which would
189+
result in duplicate entries and wrong history order).
190+
191+
GIVEN: A project manager performing vehicle creation
192+
WHEN: create_new_vehicle_from_template succeeds
193+
THEN: store_recently_used_vehicle_dir must not be called
194+
"""
195+
mock_filesystem = MagicMock(spec=LocalFilesystem)
196+
manager = VehicleProjectManager(mock_filesystem)
197+
198+
with (
199+
patch.object(manager._creator, "create_new_vehicle_from_template") as mock_create,
200+
patch.object(LocalFilesystem, "store_recently_used_template_dirs"),
201+
patch.object(manager, "store_recently_used_vehicle_dir") as mock_store_vehicle,
202+
):
203+
mock_create.return_value = "/new/vehicle/path"
204+
settings = MagicMock(spec=NewVehicleProjectSettings)
205+
206+
# Act
207+
manager.create_new_vehicle_from_template("/templates/ArduCopter", "/vehicles", "Name", settings)
208+
209+
# Assert: vehicle dir history must not be written during creation
210+
mock_store_vehicle.assert_not_called()
211+
164212
def test_user_can_get_current_working_directory(self) -> None:
165213
"""
166214
User can get current working directory.
@@ -318,86 +366,104 @@ def test_user_can_open_vehicle_directory_successfully(self) -> None:
318366
319367
GIVEN: A project manager with valid vehicle directory
320368
WHEN: User opens vehicle directory
321-
THEN: Should open directory and return path
369+
THEN: Should open directory, update history and return path
322370
"""
323371
# Arrange: Mock filesystem
324372
mock_filesystem = MagicMock(spec=LocalFilesystem)
325373
manager = VehicleProjectManager(mock_filesystem)
326374

327-
# Mock the opener
328-
with patch.object(manager._opener, "open_vehicle_directory") as mock_open:
375+
# Mock the opener and history store
376+
with (
377+
patch.object(manager._opener, "open_vehicle_directory") as mock_open,
378+
patch.object(manager, "store_recently_used_vehicle_dir") as mock_store,
379+
):
329380
mock_open.return_value = "/opened/vehicle/path"
330381

331382
# Act: Open vehicle directory
332383
result = manager.open_vehicle_directory("/vehicle/path")
333384

334-
# Assert: Directory opened successfully
385+
# Assert: Directory opened successfully and history recorded
335386
assert result == "/opened/vehicle/path"
336387
mock_open.assert_called_once_with("/vehicle/path")
388+
mock_store.assert_called_once_with("/opened/vehicle/path")
337389

338390
def test_user_sees_error_when_vehicle_directory_opening_fails(self) -> None:
339391
"""
340392
User sees error when vehicle directory opening fails.
341393
342394
GIVEN: A project manager with invalid vehicle directory
343395
WHEN: User attempts to open vehicle directory
344-
THEN: Should raise VehicleProjectOpenError
396+
THEN: Should raise VehicleProjectOpenError and not update history
345397
"""
346398
# Arrange: Mock filesystem
347399
mock_filesystem = MagicMock(spec=LocalFilesystem)
348400
manager = VehicleProjectManager(mock_filesystem)
349401

350402
# Mock the opener to raise an exception
351-
with patch.object(manager._opener, "open_vehicle_directory") as mock_open:
403+
with (
404+
patch.object(manager._opener, "open_vehicle_directory") as mock_open,
405+
patch.object(manager, "store_recently_used_vehicle_dir") as mock_store,
406+
):
352407
mock_open.side_effect = VehicleProjectOpenError("Open Error", "Opening failed")
353408

354409
# Act & Assert: Opening should raise error
355410
with pytest.raises(VehicleProjectOpenError, match="Opening failed"):
356411
manager.open_vehicle_directory("/invalid/path")
357412

413+
mock_store.assert_not_called()
414+
358415
def test_user_can_open_last_vehicle_directory_successfully(self) -> None:
359416
"""
360417
User can open last used vehicle directory successfully.
361418
362419
GIVEN: A project manager with last used vehicle directory
363420
WHEN: User opens last vehicle directory
364-
THEN: Should open directory and return path
421+
THEN: Should open directory, update history and return path
365422
"""
366423
# Arrange: Mock filesystem
367424
mock_filesystem = MagicMock(spec=LocalFilesystem)
368425
manager = VehicleProjectManager(mock_filesystem)
369426

370-
# Mock the opener
371-
with patch.object(manager._opener, "open_last_vehicle_directory") as mock_open:
427+
# Mock the opener and history storage
428+
with (
429+
patch.object(manager._opener, "open_last_vehicle_directory") as mock_open,
430+
patch.object(manager, "store_recently_used_vehicle_dir") as mock_store,
431+
):
372432
mock_open.return_value = "/last/vehicle/path"
373433

374434
# Act: Open last vehicle directory
375435
result = manager.open_last_vehicle_directory("/last/path")
376436

377-
# Assert: Directory opened successfully
437+
# Assert: Directory opened successfully and history recorded
378438
assert result == "/last/vehicle/path"
379439
mock_open.assert_called_once_with("/last/path")
440+
mock_store.assert_called_once_with("/last/vehicle/path")
380441

381442
def test_user_sees_error_when_opening_last_vehicle_directory_fails(self) -> None:
382443
"""
383444
User sees error when opening last vehicle directory fails.
384445
385446
GIVEN: A project manager with invalid last vehicle directory
386447
WHEN: User attempts to open last vehicle directory
387-
THEN: Should raise VehicleProjectOpenError
448+
THEN: Should raise VehicleProjectOpenError and not update history
388449
"""
389450
# Arrange: Mock filesystem
390451
mock_filesystem = MagicMock(spec=LocalFilesystem)
391452
manager = VehicleProjectManager(mock_filesystem)
392453

393454
# Mock the opener to raise an exception
394-
with patch.object(manager._opener, "open_last_vehicle_directory") as mock_open:
455+
with (
456+
patch.object(manager._opener, "open_last_vehicle_directory") as mock_open,
457+
patch.object(manager, "store_recently_used_vehicle_dir") as mock_store,
458+
):
395459
mock_open.side_effect = VehicleProjectOpenError("Last Open Error", "Last directory opening failed")
396460

397461
# Act & Assert: Opening should raise error
398462
with pytest.raises(VehicleProjectOpenError, match="Last directory opening failed"):
399463
manager.open_last_vehicle_directory("/invalid/last/path")
400464

465+
mock_store.assert_not_called()
466+
401467

402468
class TestFilesystemStateManagement:
403469
"""Test filesystem state management operations."""
@@ -865,20 +931,21 @@ def test_user_can_complete_new_vehicle_creation_workflow(self) -> None:
865931
mock_create.return_value = "/new/vehicle/MyVehicle"
866932
mock_settings = MagicMock(spec=NewVehicleProjectSettings)
867933

868-
# Act: Complete workflow
934+
# Act: Complete workflow - manager should take care of history updates
869935
vehicle_path = manager.create_new_vehicle_from_template(
870936
"/templates/ArduCopter", "/vehicles", "MyVehicle", mock_settings
871937
)
872-
manager.store_recently_used_template_dirs("/templates/ArduCopter", "/vehicles")
873-
manager.store_recently_used_vehicle_dir(vehicle_path)
874938

875-
# Assert: Complete workflow executed
939+
# Assert: Complete workflow executed and history recorded
876940
assert vehicle_path == "/new/vehicle/MyVehicle"
877941
assert manager._settings is mock_settings
878942
assert manager.configuration_template == "ArduCopter"
879943
mock_create.assert_called_once()
880944
mock_store_template.assert_called_once_with("/templates/ArduCopter", "/vehicles")
881-
mock_store_vehicle.assert_called_once_with("/new/vehicle/MyVehicle")
945+
# the manager does not store the vehicle directory here; the
946+
# __main__ path opens the new project which causes the history write
947+
# later, so the mock should not be invoked as part of creation.
948+
mock_store_vehicle.assert_not_called()
882949

883950
def test_user_can_complete_vehicle_opening_workflow(self) -> None:
884951
"""
@@ -898,9 +965,10 @@ def test_user_can_complete_vehicle_opening_workflow(self) -> None:
898965
):
899966
mock_open.return_value = "/opened/vehicle/path"
900967

901-
# Act: Complete workflow
968+
# Act: Complete workflow - the manager method is responsible for
969+
# updating the history, so we don't call store_recently_used_vehicle_dir
970+
# explicitly here.
902971
vehicle_path = manager.open_vehicle_directory("/vehicle/path")
903-
manager.store_recently_used_vehicle_dir(vehicle_path)
904972

905973
# Assert: Complete workflow executed
906974
assert vehicle_path == "/opened/vehicle/path"

tests/test_data_model_vehicle_project_creator.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ def test_user_can_create_vehicle_project_from_template_successfully(
290290
patch.object(LocalFilesystem, "valid_directory_name", return_value=True),
291291
patch.object(LocalFilesystem, "new_vehicle_dir", return_value=expected_vehicle_dir),
292292
patch.object(LocalFilesystem, "store_recently_used_template_dirs"),
293-
patch.object(LocalFilesystem, "store_vehicle_dir_to_history_safe"),
294293
patch.object(LocalFilesystem, "get_directory_name_from_full_path", return_value="QuadCopter_Template"),
295294
):
296295
# Act: Create new vehicle project
@@ -472,7 +471,6 @@ def test_user_can_create_project_with_custom_settings(self, project_creator, moc
472471
patch.object(LocalFilesystem, "valid_directory_name", return_value=True),
473472
patch.object(LocalFilesystem, "new_vehicle_dir", return_value=expected_vehicle_dir),
474473
patch.object(LocalFilesystem, "store_recently_used_template_dirs"),
475-
patch.object(LocalFilesystem, "store_vehicle_dir_to_history_safe"),
476474
patch.object(LocalFilesystem, "get_directory_name_from_full_path", return_value="Custom_Template"),
477475
):
478476
# Act: Create vehicle project with custom settings
@@ -519,7 +517,6 @@ def test_user_can_create_project_with_fc_connection(
519517
patch.object(LocalFilesystem, "valid_directory_name", return_value=True),
520518
patch.object(LocalFilesystem, "new_vehicle_dir", return_value=expected_vehicle_dir),
521519
patch.object(LocalFilesystem, "store_recently_used_template_dirs"),
522-
patch.object(LocalFilesystem, "store_vehicle_dir_to_history_safe"),
523520
patch.object(LocalFilesystem, "get_directory_name_from_full_path", return_value="FC_Template"),
524521
):
525522
# Act: Create vehicle project with FC connection
@@ -531,16 +528,13 @@ def test_user_can_create_project_with_fc_connection(
531528
assert result_dir == expected_vehicle_dir
532529
mock_local_filesystem.create_new_vehicle_dir.assert_called_once_with(expected_vehicle_dir)
533530

534-
def test_recently_used_directories_are_stored_after_successful_creation(
535-
self, project_creator, mock_local_filesystem, default_settings
536-
) -> None:
531+
def test_creator_does_not_store_history_itself(self, project_creator, mock_local_filesystem, default_settings) -> None:
537532
"""
538-
User's recently used directories are stored after successful project creation.
533+
The creator class should not modify the recent-vehicle history; that is the responsibility of the manager.
539534
540535
GIVEN: A user successfully creates a vehicle project
541536
WHEN: The creation process completes
542-
THEN: The template and vehicle directories should be stored as recently used
543-
AND: The configuration template name should be available for reference
537+
THEN: No calls to history storage APIs should occur inside the creator
544538
"""
545539
# Arrange: Valid inputs for successful creation
546540
template_dir = "C:\\valid\\template\\dir"
@@ -553,14 +547,11 @@ def test_recently_used_directories_are_stored_after_successful_creation(
553547
patch.object(LocalFilesystem, "valid_directory_name", return_value=True),
554548
patch.object(LocalFilesystem, "new_vehicle_dir", return_value=expected_vehicle_dir),
555549
patch.object(LocalFilesystem, "store_recently_used_template_dirs") as mock_store_template,
556-
patch.object(LocalFilesystem, "store_vehicle_dir_to_history_safe") as mock_store_vehicle,
557550
):
558551
# Act: Create vehicle project
559552
project_creator.create_new_vehicle_from_template(template_dir, new_base_dir, new_vehicle_name, default_settings)
560553

561-
# Assert: Recently used directories were stored
562-
mock_store_template.assert_called_once_with(template_dir, new_base_dir)
563-
mock_store_vehicle.assert_called_once_with(expected_vehicle_dir)
554+
mock_store_template.assert_not_called()
564555

565556

566557
class TestNewVehicleProjectSettingsMetadata:

0 commit comments

Comments
 (0)