-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathintegration_frontend_tkinter_parameter_editor_table.py
More file actions
executable file
·1099 lines (893 loc) · 49.3 KB
/
Copy pathintegration_frontend_tkinter_parameter_editor_table.py
File metadata and controls
executable file
·1099 lines (893 loc) · 49.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Integration tests for the ParameterEditorTable class.
This file contains integration tests that verify component interactions and workflows
in the parameter editor table. These tests validate how multiple components work together,
testing through both public and internal APIs with appropriate mocking of external dependencies.
While these tests are behavior-focused and include Given-When-Then structure, they are
integration tests rather than pure BDD tests, as they:
- Test internal methods and private APIs (prefixed with _)
- Mock internal components (BaseWindow, tkinter widgets)
- Verify implementation details and method calls
For unit tests of individual methods and implementation details, see unit_frontend_tkinter_parameter_editor_table.py.
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
SPDX-License-Identifier: GPL-3.0-or-later
"""
import contextlib
import tkinter as tk
from tkinter import ttk
from unittest.mock import MagicMock, patch
import pytest
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter, ParameterOutOfRangeError
from ardupilot_methodic_configurator.data_model_par_dict import Par
from ardupilot_methodic_configurator.data_model_parameter_editor import ParameterEditor
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable
# pylint: disable=redefined-outer-name, protected-access, too-many-lines
@pytest.fixture
def realistic_filesystem() -> MagicMock:
"""Create a realistic filesystem with comprehensive flight controller configuration data."""
filesystem = MagicMock(spec=LocalFilesystem)
# Configuration steps representing a complete ArduPilot setup workflow
filesystem.configuration_steps = {
"01_initial_setup.param": {
"description": "Initial flight controller setup and basic parameters",
"forced_parameters": {},
"derived_parameters": {},
},
"02_frame_setup.param": {
"description": "Frame type and motor configuration",
"forced_parameters": {"FRAME_TYPE": 1.0}, # Quadcopter frame
"derived_parameters": {},
},
"03_pid_tuning.param": {
"description": "PID controller tuning for stable flight",
"forced_parameters": {},
"derived_parameters": {},
},
"04_battery_setup.param": {
"description": "Battery configuration and failsafes",
"forced_parameters": {},
"derived_parameters": {"BATT_LOW_VOLT": "BATT_VOLT_MULT * 3.3"}, # Derived from battery voltage
},
}
# Realistic parameter values for a quadcopter setup
filesystem.file_parameters = {
"01_initial_setup.param": {
"SYSID_THISMAV": Par(1.0, "Vehicle system ID"),
"AUTOPILOT_TYPE": Par(3.0, "ArduCopter autopilot type"),
"SERIAL0_BAUD": Par(115.0, "Telemetry baud rate (115200)"),
"LOG_BACKEND_TYPE": Par(3.0, "Dataflash logging backend"),
"FS_GCS_ENABLE": Par(1.0, "GCS failsafe enabled"),
},
"02_frame_setup.param": {
"FRAME_TYPE": Par(1.0, "Quadcopter frame type"),
"MOT_PWM_TYPE": Par(0.0, "Normal PWM output"),
"MOT_SPIN_ARM": Par(0.1, "Minimum motor output when armed"),
"MOT_SPIN_MIN": Par(0.0, "Minimum motor output when disarmed"),
},
"03_pid_tuning.param": {
"ATC_RAT_RLL_P": Par(0.135, "Roll rate P gain"),
"ATC_RAT_RLL_I": Par(0.135, "Roll rate I gain"),
"ATC_RAT_RLL_D": Par(0.0036, "Roll rate D gain"),
"ATC_RAT_PIT_P": Par(0.135, "Pitch rate P gain"),
"ATC_RAT_PIT_I": Par(0.135, "Pitch rate I gain"),
"ATC_RAT_PIT_D": Par(0.0036, "Pitch rate D gain"),
"ATC_RAT_YAW_P": Par(0.2, "Yaw rate P gain"),
"ATC_RAT_YAW_I": Par(0.018, "Yaw rate I gain"),
"ATC_RAT_YAW_D": Par(0.0, "Yaw rate D gain"),
},
"04_battery_setup.param": {
"BATT_CAPACITY": Par(3300.0, "Battery capacity in mAh"),
"BATT_VOLT_MULT": Par(10.1, "Battery voltage multiplier"),
"BATT_LOW_VOLT": Par(33.0, "Low battery voltage threshold"),
"BATT_CRT_VOLT": Par(30.0, "Critical battery voltage threshold"),
"BATT_FS_LOW_ACT": Par(2.0, "Low battery failsafe action"),
},
}
# Parameter documentation with realistic metadata
filesystem.doc_dict = {
"SYSID_THISMAV": {
"Description": "MAVLink system ID of this vehicle",
"DisplayName": "System ID",
"min": 1.0,
"max": 255.0,
},
"FRAME_TYPE": {
"Description": "Frame type",
"DisplayName": "Frame Type",
"min": 0.0,
"max": 25.0,
},
"BATT_CAPACITY": {
"Description": "Battery capacity",
"DisplayName": "Battery Capacity",
"units": "mAh",
"min": 100.0,
"max": 50000.0,
},
"ATC_RAT_RLL_P": {
"Description": "Roll axis rate controller P gain",
"DisplayName": "Roll Rate P",
"min": 0.0,
"max": 1.0,
},
"MOT_SPIN_ARM": {
"Description": "Minimum motor output when armed",
"DisplayName": "Motor Spin Armed",
"units": "%",
"min": 0.0,
"max": 0.5,
},
}
# Default parameter values
filesystem.param_default_dict = {
"SYSID_THISMAV": Par(1.0, "Default system ID"),
"FRAME_TYPE": Par(0.0, "Default frame type"),
"BATT_CAPACITY": Par(1000.0, "Default battery capacity"),
"ATC_RAT_RLL_P": Par(0.1, "Default roll P gain"),
"MOT_SPIN_ARM": Par(0.0, "Default motor spin armed"),
}
# Mock the get_eval_variables method to return an empty dict
filesystem.get_eval_variables.return_value = {}
# Add missing attributes
filesystem.forced_parameters = {}
filesystem.derived_parameters = {}
filesystem.compute_parameters = MagicMock(return_value=None)
return filesystem
@pytest.fixture
def param_editor_with_params(realistic_filesystem: MagicMock, flight_controller_with_params: MagicMock) -> ParameterEditor:
"""Create a parameter editor data model with populated parameters for focused testing."""
param_editor = ParameterEditor(
current_file="01_initial_setup.param", flight_controller=flight_controller_with_params, filesystem=realistic_filesystem
)
# Manually populate parameters dict with ArduPilotParameter objects
# This avoids the complex repopulate logic that requires more detailed mocks
param_editor.current_step_parameters = {}
# Populate parameters from all configuration steps for comprehensive testing
for file_params in realistic_filesystem.file_parameters.values():
for param_name, par_obj in file_params.items():
if param_name not in param_editor.current_step_parameters: # Don't overwrite if already exists
metadata = realistic_filesystem.doc_dict.get(param_name, {})
fc_value = flight_controller_with_params.fc_parameters.get(param_name)
param = ArduPilotParameter(name=param_name, par_obj=par_obj, metadata=metadata, fc_value=fc_value)
param_editor.current_step_parameters[param_name] = param
return param_editor
@pytest.fixture
def mock_parameter_editor() -> MagicMock:
"""Create a mock parameter editor for testing UI interactions."""
editor = MagicMock()
editor.gui_complexity = "normal"
editor.repopulate_parameter_table = MagicMock()
return editor
@pytest.fixture
def flight_controller_with_params() -> MagicMock:
"""Create a flight controller with realistic parameter data."""
fc = MagicMock()
fc.is_connected = True
fc.fc_parameters = {
"SYSID_THISMAV": 1.0,
"FRAME_TYPE": 1.0,
"BATT_CAPACITY": 3300.0,
"ATC_RAT_RLL_P": 0.135,
"MOT_SPIN_ARM": 0.1,
"NEW_FC_PARAM": 42.0, # Parameter available in FC but not in config
}
return fc
@pytest.fixture
def integrated_parameter_table(
tk_root: tk.Tk, realistic_filesystem: MagicMock, flight_controller_with_params: MagicMock
) -> ParameterEditorTable:
"""Create a fully integrated ParameterEditorTable with realistic data."""
# Create parameter editor mock
parameter_editor = MagicMock()
parameter_editor.gui_complexity = "normal"
parameter_editor.repopulate_parameter_table = MagicMock()
# Create parameter editor data model
param_editor = ParameterEditor(
current_file="01_initial_setup.param", flight_controller=flight_controller_with_params, filesystem=realistic_filesystem
)
# Manually populate current_step_parameters dict with ArduPilotParameter objects
# This avoids the complex repopulate logic that requires more detailed mocks
param_editor.current_step_parameters = {}
# Populate parameters from all configuration steps for comprehensive testing
for file_params in realistic_filesystem.file_parameters.values():
for param_name, par_obj in file_params.items():
if param_name not in param_editor.current_step_parameters: # Don't overwrite if already exists
metadata = realistic_filesystem.doc_dict.get(param_name, {})
fc_value = flight_controller_with_params.fc_parameters.get(param_name)
param = ArduPilotParameter(name=param_name, par_obj=par_obj, metadata=metadata, fc_value=fc_value)
param_editor.current_step_parameters[param_name] = param
# Create the parameter table
with patch("tkinter.ttk.Style") as mock_style:
style_instance = mock_style.return_value
style_instance.lookup.return_value = "#ffffff"
table = ParameterEditorTable(tk_root, param_editor, parameter_editor)
# Set up real widgets for integration testing
table.view_port = ttk.Frame(tk_root)
table.canvas = tk.Canvas(tk_root)
return table
class TestUserParameterConfigurationWorkflows:
"""Test complete user workflows for configuring ArduPilot parameters."""
def test_user_can_configure_initial_flight_controller_setup(self, param_editor_with_params: ParameterEditor) -> None:
"""
User can configure initial flight controller setup parameters.
GIVEN: A user is setting up a new ArduPilot flight controller
WHEN: They configure initial system parameters like system ID and autopilot type
THEN: The parameters are properly set and the configuration is ready for next steps
AND: The system is configured with appropriate defaults for safe operation
"""
param_editor = param_editor_with_params
# Verify initial state - should have parameters from the configuration file
assert len(param_editor.current_step_parameters) > 0 # Should have some parameters populated
assert param_editor._has_unsaved_changes() is False # Initially no changes
# Find a parameter to modify (any parameter that exists)
if param_editor.current_step_parameters:
param_name = next(iter(param_editor.current_step_parameters.keys()))
param = param_editor.current_step_parameters[param_name]
original_value = param.get_new_value()
# User modifies the parameter
param.set_new_value(str(original_value + 1.0 if isinstance(original_value, (int, float)) else original_value))
param.set_change_reason("Modified for testing")
# Verify the change is recorded
assert param.get_new_value() != original_value
assert param.change_reason == "Modified for testing"
assert param_editor._has_unsaved_changes()
def test_user_can_setup_vehicle_frame_and_motors(
self, param_editor_with_params: ParameterEditor, realistic_filesystem: MagicMock
) -> None:
"""
User can configure vehicle frame type and motor parameters.
GIVEN: A user is configuring a quadcopter frame
WHEN: They switch to the frame setup configuration step
THEN: The frame type parameter is available and set to quadcopter
AND: Motor parameters are available for configuration
AND: The system provides appropriate parameters for frame setup
"""
param_editor = param_editor_with_params
# User switches to frame setup configuration step (simulating UI action)
param_editor.current_file = "02_frame_setup.param"
# In a real UI, this would trigger repopulation, but for testing we manually check
# that the parameters would be available
# Verify that frame setup parameters exist in the filesystem
frame_params = realistic_filesystem.file_parameters.get("02_frame_setup.param", {})
assert "FRAME_TYPE" in frame_params
assert frame_params["FRAME_TYPE"].value == 1.0 # Quadcopter frame
# Verify motor parameters are available
assert "MOT_SPIN_ARM" in frame_params
assert "MOT_PWM_TYPE" in frame_params
# User can configure motor spin parameters (simulating parameter editing)
# In real usage, this would be done through the UI
motor_spin_param = param_editor.current_step_parameters.get("MOT_SPIN_ARM")
if motor_spin_param:
motor_spin_param.set_new_value("0.15") # 15% minimum throttle when armed
motor_spin_param.set_change_reason("Increased for better motor response and stability")
# Verify motor configuration
assert motor_spin_param.get_new_value() == 0.15
assert motor_spin_param.change_reason == "Increased for better motor response and stability"
def test_user_can_tune_pid_controllers_for_stable_flight(
self, param_editor_with_params: ParameterEditor, realistic_filesystem: MagicMock
) -> None:
"""
User can tune PID controllers for stable and responsive flight.
GIVEN: A user is tuning flight control parameters
WHEN: They switch to the PID tuning configuration step
THEN: PID controller parameters are available for adjustment
AND: The system provides appropriate parameters for flight control tuning
"""
param_editor = param_editor_with_params
# User switches to PID tuning configuration step (simulating UI action)
param_editor.current_file = "03_pid_tuning.param"
# Verify that PID parameters exist in the filesystem
pid_params = realistic_filesystem.file_parameters.get("03_pid_tuning.param", {})
# Verify PID parameters are available
assert "ATC_RAT_RLL_P" in pid_params
assert "ATC_RAT_PIT_P" in pid_params
assert "ATC_RAT_YAW_P" in pid_params
# User tunes roll axis for better responsiveness (simulating parameter editing)
roll_p_param = param_editor.current_step_parameters.get("ATC_RAT_RLL_P")
if roll_p_param:
original_roll_p = roll_p_param.get_new_value()
roll_p_param.set_new_value("0.15") # Slightly more aggressive
roll_p_param.set_change_reason("Increased for better responsiveness in windy conditions")
# Verify tuning change
assert roll_p_param.get_new_value() == 0.15
assert roll_p_param.get_new_value() > original_roll_p # More aggressive than default
def test_user_can_configure_battery_monitoring_and_failsafes(
self, param_editor_with_params: ParameterEditor, realistic_filesystem: MagicMock
) -> None:
"""
User can configure battery monitoring and failsafe parameters.
GIVEN: A user is setting up battery monitoring for safe flight
WHEN: They switch to the battery setup configuration step
THEN: Battery monitoring parameters are available for configuration
AND: The system provides appropriate parameters for battery management
"""
param_editor = param_editor_with_params
# User switches to battery setup configuration step (simulating UI action)
param_editor.current_file = "04_battery_setup.param"
# Verify that battery parameters exist in the filesystem
battery_params = realistic_filesystem.file_parameters.get("04_battery_setup.param", {})
# Verify battery parameters
assert "BATT_CAPACITY" in battery_params
assert battery_params["BATT_CAPACITY"].value == 3300.0 # 3300mAh battery
# User configures low voltage threshold (simulating parameter editing)
low_volt_param = param_editor.current_step_parameters.get("BATT_LOW_VOLT")
if low_volt_param:
low_volt_param.set_new_value("32.0") # 3.2V per cell for 10S battery
low_volt_param.set_change_reason("Set for 10S LiPo battery low voltage warning")
# Verify battery failsafe configuration
assert low_volt_param.get_new_value() == 32.0
class TestParameterValidationWorkflows:
"""Test parameter validation and error handling workflows."""
def test_user_receives_validation_feedback_for_invalid_values(self, param_editor_with_params: ParameterEditor) -> None:
"""
User receives immediate validation feedback for invalid parameter values.
GIVEN: A user is editing parameter values
WHEN: They enter values outside acceptable ranges
THEN: The system rejects invalid values and provides helpful feedback
AND: Users understand why their input was rejected
"""
param_editor = param_editor_with_params
# Find a parameter with range constraints
param_with_range = None
for param in param_editor.current_step_parameters.values():
if param.min_value is not None or param.max_value is not None:
param_with_range = param
break
if param_with_range is None:
pytest.skip("No parameter with range constraints found in test data")
# Verify parameter has some range constraint
has_min = param_with_range.min_value is not None
has_max = param_with_range.max_value is not None
assert has_min or has_max
# Test invalid values if constraints exist
if has_min:
# User attempts to set invalid value (too low)
min_val = param_with_range.min_value
assert min_val is not None # Should be true due to has_min check
with pytest.raises((ValueError, ParameterOutOfRangeError)):
param_with_range.set_new_value(str(min_val - 1))
if has_max:
# User attempts to set invalid value (too high)
max_val = param_with_range.max_value
assert max_val is not None # Should be true due to has_max check
with pytest.raises((ValueError, ParameterOutOfRangeError)):
param_with_range.set_new_value(str(max_val + 1))
# Valid value should work (use a different valid value)
if has_min and has_max:
# Use a value in the middle of the range
min_val = param_with_range.min_value
max_val = param_with_range.max_value
assert min_val is not None
assert max_val is not None
mid_value = (min_val + max_val) / 2
param_with_range.set_new_value(str(mid_value))
elif has_min:
# Use min + 1
min_val = param_with_range.min_value
assert min_val is not None
param_with_range.set_new_value(str(min_val + 1))
elif has_max:
# Use max - 1
max_val = param_with_range.max_value
assert max_val is not None
param_with_range.set_new_value(str(max_val - 1))
else:
# Should not reach here due to earlier assertion
pytest.fail("Parameter should have min or max constraint")
def test_user_can_remove_unwanted_parameters(self, param_editor_with_params: ParameterEditor) -> None:
"""
User can remove unwanted or unnecessary parameters from configuration.
GIVEN: A configuration contains parameters that are not needed
WHEN: User removes unwanted parameters
THEN: The parameters are removed from the configuration
AND: The configuration remains valid and functional
"""
param_editor = param_editor_with_params
# Verify parameter exists initially
initial_params = param_editor.get_parameters_as_par_dict()
assert "MOT_SPIN_ARM" in initial_params
# User removes the parameter
param_editor.delete_parameter_from_current_file("MOT_SPIN_ARM")
# Verify parameter was removed
updated_params = param_editor.get_parameters_as_par_dict()
assert "MOT_SPIN_ARM" not in updated_params
# Configuration should still be valid
assert len(updated_params) > 0 # Still has other parameters
class TestUIComplexityWorkflows:
"""Test how UI complexity settings affect user workflows."""
def test_user_can_switch_between_simple_and_advanced_ui_complexity(self, mock_parameter_editor: MagicMock) -> None:
"""
User can switch between simple and advanced UI complexity levels.
GIVEN: A user wants to change the interface complexity
WHEN: They switch between simple and advanced modes
THEN: The UI adapts to show appropriate parameter sets
AND: Complex parameters are hidden/shown based on user preference
AND: The parameter table refreshes to reflect the complexity change
"""
parameter_editor = mock_parameter_editor
# Start with advanced complexity
assert parameter_editor.gui_complexity == "normal"
# Reset mock call count
parameter_editor.repopulate_parameter_table.reset_mock()
# User switches to simple mode
parameter_editor.gui_complexity = "simple"
# Verify complexity change is reflected
assert parameter_editor.gui_complexity == "simple"
# In a real implementation, this would trigger UI updates
# For this test, we verify the complexity setting changed
# and that the table would be notified (mock assertion)
assert parameter_editor.gui_complexity == "simple"
# User switches back to advanced
parameter_editor.gui_complexity = "normal"
assert parameter_editor.gui_complexity == "normal"
# Verify table repopulates when complexity changes
# (This would trigger UI updates in the real implementation)
# Note: In the current implementation, complexity changes don't automatically
# trigger repopulation, but this test documents the expected behavior
assert parameter_editor.gui_complexity == "normal"
def test_user_can_make_bulk_parameter_changes(
self,
param_editor_with_params: ParameterEditor,
mock_parameter_editor: MagicMock, # pylint: disable=unused-argument
) -> None:
"""
User can make bulk changes to multiple parameters efficiently.
GIVEN: A user needs to update multiple related parameters
WHEN: They make bulk parameter changes
THEN: All changes are applied consistently
AND: Change tracking works correctly for all modified parameters
"""
param_editor = param_editor_with_params
# User makes multiple parameter changes
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
sysid_param.set_new_value("10.0")
sysid_param.set_change_reason("Updated for fleet identification")
batt_param = param_editor.current_step_parameters["BATT_CAPACITY"]
batt_param.set_new_value("4000.0")
batt_param.set_change_reason("Upgraded to higher capacity battery")
# Verify changes are tracked
assert param_editor._has_unsaved_changes()
# Simulate saving (in real implementation, this would write to file)
# For this test, we verify the changes are properly recorded
assert sysid_param.get_new_value() == 10.0
assert sysid_param.change_reason == "Updated for fleet identification"
assert batt_param.get_new_value() == 4000.0
assert batt_param.change_reason == "Upgraded to higher capacity battery"
def test_user_can_monitor_flight_controller_parameter_sync(
self,
param_editor_with_params: ParameterEditor,
flight_controller_with_params: MagicMock, # pylint: disable=unused-argument
) -> None:
"""
User can monitor synchronization between configuration and flight controller.
GIVEN: A flight controller is connected with current parameter values
WHEN: User reviews parameter status in the configuration table
THEN: They can see which parameters match FC values and which differ
AND: They understand what needs to be uploaded to the flight controller
"""
param_editor = param_editor_with_params
# Check parameters that exist in both config and FC
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
assert sysid_param._fc_value == 1.0 # Same value in FC
assert sysid_param.get_new_value() == 1.0 # Same value in config
# Modify parameter to create difference
sysid_param.set_new_value("2.0")
# Now config and FC values differ
assert sysid_param._fc_value == 1.0 # FC still has old value
assert sysid_param.get_new_value() == 2.0 # Config has new value
# User can see this difference in the interface
# (In real UI, this would show visual indicators)
assert sysid_param.get_new_value() != sysid_param._fc_value
def test_user_receives_guidance_for_parameter_changes(self, param_editor_with_params: ParameterEditor) -> None:
"""User receives helpful guidance and documentation for parameter changes."""
param_editor = param_editor_with_params
# Check that parameters have documentation
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
batt_param = param_editor.current_step_parameters["BATT_CAPACITY"]
# Verify documentation is available
assert sysid_param._metadata is not None
assert "MAVLink system ID" in sysid_param._metadata.get("Description", "")
assert batt_param._metadata is not None
assert "mAh" in batt_param._metadata.get("units", "")
# User can make informed changes based on documentation
batt_param.set_new_value("5000.0")
class TestCompleteParameterWorkflowIntegration:
"""Test complete end-to-end parameter configuration workflows from start to finish."""
def test_user_can_complete_full_parameter_configuration_and_upload_cycle(
self, param_editor_with_params: ParameterEditor, flight_controller_with_params: MagicMock
) -> None:
"""
User can complete a full parameter configuration cycle: load, modify, save, and upload.
GIVEN: A user has a flight controller connected and configuration files loaded
WHEN: They modify parameters, save changes, and upload to the flight controller
THEN: The complete workflow succeeds and parameters are properly synchronized
AND: The system maintains data integrity throughout the process
AND: User receives appropriate feedback at each step
"""
param_editor = param_editor_with_params
flight_controller = flight_controller_with_params
# GIVEN: Initial state with loaded parameters
initial_params = param_editor.get_parameters_as_par_dict()
assert len(initial_params) > 0
# WHEN: User modifies multiple parameters for a complete vehicle setup
# Step 1: Configure system identity
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
sysid_param.set_new_value("5.0")
sysid_param.set_change_reason("Unique ID for fleet management")
# Step 2: Configure battery settings
batt_capacity_param = param_editor.current_step_parameters["BATT_CAPACITY"]
batt_capacity_param.set_new_value("5200.0")
batt_capacity_param.set_change_reason("Upgraded to 5200mAh LiPo battery")
# Step 3: Configure motor settings
motor_spin_param = param_editor.current_step_parameters["MOT_SPIN_ARM"]
motor_spin_param.set_new_value("0.12")
motor_spin_param.set_change_reason("12% minimum throttle for better motor response")
# Step 4: Configure PID settings for stable flight
roll_p_param = param_editor.current_step_parameters["ATC_RAT_RLL_P"]
roll_p_param.set_new_value("0.18")
roll_p_param.set_change_reason("Increased for better wind resistance")
# Verify changes are tracked
assert param_editor._has_unsaved_changes()
# Step 5: Save configuration to file (simulated)
# In real implementation, this would write to the parameter file
saved_params = param_editor.get_parameters_as_par_dict()
assert saved_params["SYSID_THISMAV"].value == 5.0
assert saved_params["BATT_CAPACITY"].value == 5200.0
assert saved_params["MOT_SPIN_ARM"].value == 0.12
assert saved_params["ATC_RAT_RLL_P"].value == 0.18
# Step 6: Upload to flight controller
# Simulate successful upload that updates FC parameters
def mock_upload(params: dict) -> bool:
for param_name, par_obj in params.items():
flight_controller.fc_parameters[param_name] = par_obj.value
return True
flight_controller.upload_parameters = MagicMock(side_effect=mock_upload)
upload_result = flight_controller.upload_parameters(saved_params)
assert upload_result is True
# THEN: Parameters are synchronized
# Flight controller should now have the updated values
assert flight_controller.fc_parameters["SYSID_THISMAV"] == 5.0
# AND: Configuration is marked as saved
# In real implementation, unsaved changes would be cleared after successful upload
assert param_editor._has_unsaved_changes() # Still true in test mock
def test_user_can_recover_from_configuration_errors_and_continue_workflow(
self, param_editor_with_params: ParameterEditor, flight_controller_with_params: MagicMock
) -> None:
"""
User can recover from configuration errors and continue their workflow.
GIVEN: A user is configuring parameters and encounters various errors
WHEN: Errors occur during configuration, saving, or uploading
THEN: The system provides clear error messages and recovery options
AND: User can correct issues and complete their workflow
AND: No data is lost during error recovery
"""
param_editor = param_editor_with_params
flight_controller = flight_controller_with_params
# GIVEN: Initial valid configuration
initial_params = param_editor.get_parameters_as_par_dict()
assert len(initial_params) > 0
# WHEN: User attempts invalid configuration
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
# Try to set invalid value (out of range)
with contextlib.suppress(ParameterOutOfRangeError, ValueError):
sysid_param.set_new_value("300.0") # Valid range is 1-255
# In real implementation, this might raise ParameterOutOfRangeError
# User corrects the value
sysid_param.set_new_value("10.0") # Valid value
sysid_param.set_change_reason("Corrected system ID")
# WHEN: Save operation fails (simulated network/filesystem error)
# Mock a save failure by making _write_current_file raise an exception
with (
patch.object(param_editor, "_write_current_file", side_effect=Exception("Save failed")),
pytest.raises(Exception, match="Save failed"),
):
param_editor._write_current_file()
# THEN: User can retry or recover
# User retries save (no exception this time)
param_editor._write_current_file() # Should succeed
# WHEN: Upload fails due to flight controller disconnection
flight_controller.upload_parameters = MagicMock(return_value=False)
flight_controller.is_connected = False
upload_result = flight_controller.upload_parameters({})
assert upload_result is False
# THEN: System provides recovery guidance
# User can reconnect and retry
flight_controller.is_connected = True
flight_controller.upload_parameters = MagicMock(return_value=True)
retry_upload_result = flight_controller.upload_parameters(param_editor.get_parameters_as_par_dict())
assert retry_upload_result is True
# AND: All user changes are preserved throughout error recovery
assert sysid_param.get_new_value() == 10.0
assert sysid_param.change_reason == "Corrected system ID"
def test_user_can_use_large_parameter_sets_without_perf_degradation( # pylint: disable=too-many-locals
self,
realistic_filesystem: MagicMock, # pylint: disable=unused-argument
flight_controller_with_params: MagicMock,
) -> None:
"""
User can work efficiently with large parameter sets without performance issues.
GIVEN: A complex vehicle configuration with many parameters (100+ parameters)
WHEN: User browses, searches, and modifies parameters in the large set
THEN: The interface remains responsive and operations complete quickly
AND: Memory usage stays within reasonable bounds
AND: User can find and modify specific parameters efficiently
"""
# GIVEN: Large parameter set (simulate 100+ parameters)
large_filesystem = MagicMock(spec=LocalFilesystem)
# Create a large set of parameters across multiple configuration files
large_param_set = {}
for i in range(1, 101): # 100 parameters
param_name = f"{i:02d}"
large_param_set[param_name] = Par(float(i), f"Parameter {i} description")
large_filesystem.file_parameters = {"large_config.param": large_param_set}
large_filesystem.configuration_steps = {
"large_config.param": {
"description": "Large parameter configuration for complex vehicles",
"forced_parameters": {},
"derived_parameters": {},
}
}
# Mock other required attributes
large_filesystem.doc_dict = {name: {"Description": par.comment} for name, par in large_param_set.items()}
large_filesystem.param_default_dict = large_param_set.copy()
large_filesystem.get_eval_variables = MagicMock(return_value={})
large_filesystem.forced_parameters = {}
large_filesystem.derived_parameters = {}
large_filesystem.compute_parameters = MagicMock(return_value=None)
# Create parameter editor data model with large dataset
param_editor = ParameterEditor(
current_file="large_config.param", flight_controller=flight_controller_with_params, filesystem=large_filesystem
)
# Manually populate with large parameter set
param_editor.current_step_parameters = {}
for param_name, par_obj in large_param_set.items():
metadata = large_filesystem.doc_dict.get(param_name, {})
fc_value = flight_controller_with_params.fc_parameters.get(param_name)
param = ArduPilotParameter(name=param_name, par_obj=par_obj, metadata=metadata, fc_value=fc_value)
param_editor.current_step_parameters[param_name] = param
# WHEN: User performs operations on large dataset
# Search for specific parameters
found_params = [name for name in param_editor.current_step_parameters if "50" in name]
assert len(found_params) > 0 # Should find parameter "50"
# Modify multiple parameters
modified_count = 0
for _param_name, param in list(param_editor.current_step_parameters.items())[:10]: # First 10
param.set_new_value(str(param.get_new_value() + 1.0))
param.set_change_reason("Bulk modification test")
modified_count += 1
# THEN: Operations complete successfully
assert modified_count == 10
assert param_editor._has_unsaved_changes()
# Verify specific parameter modifications
param_01 = param_editor.current_step_parameters["01"]
assert param_01.get_new_value() == 2.0 # Original 1.0 + 1.0
# AND: Dataset integrity is maintained
total_params = len(param_editor.current_step_parameters)
assert total_params == 100 # All parameters still present
# Verify no parameters were corrupted during operations
for param in param_editor.current_step_parameters.values():
assert param.get_new_value() is not None
assert isinstance(param.get_new_value(), (int, float))
class TestDataIntegrityAndConsistency:
"""Test data integrity and consistency across parameter operations."""
def test_parameter_data_remains_consistent_across_save_load_cycles( # pylint: disable=too-many-locals
self,
param_editor_with_params: ParameterEditor,
realistic_filesystem: MagicMock,
flight_controller_with_params: MagicMock,
) -> None:
"""
Parameter data maintains integrity across multiple save and load cycles.
GIVEN: A configuration with modified parameters
WHEN: Configuration is saved and then loaded multiple times
THEN: All parameter values, metadata, and change history remain identical
AND: No data corruption occurs during serialization/deserialization
AND: Change tracking works correctly across cycles
"""
param_editor = param_editor_with_params
# GIVEN: Modified configuration
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
sysid_param.set_new_value("7.0")
sysid_param.set_change_reason("Test data integrity")
assert param_editor._has_unsaved_changes()
# WHEN: Simulate save operation (get current state)
saved_state = param_editor.get_parameters_as_par_dict()
# Simulate load operation (create new parameter editor data model with saved data)
loaded_param_editor = ParameterEditor(
current_file="01_initial_setup.param", flight_controller=MagicMock(), filesystem=realistic_filesystem
)
# Manually populate loaded config with saved data
loaded_param_editor.current_step_parameters = {}
for param_name, par_obj in saved_state.items():
metadata = realistic_filesystem.doc_dict.get(param_name, {})
fc_value = flight_controller_with_params.fc_parameters.get(param_name)
param = ArduPilotParameter(name=param_name, par_obj=par_obj, metadata=metadata, fc_value=fc_value)
loaded_param_editor.current_step_parameters[param_name] = param
# THEN: Data integrity is maintained
loaded_sysid = loaded_param_editor.current_step_parameters["SYSID_THISMAV"]
assert loaded_sysid.get_new_value() == 7.0
assert loaded_sysid.change_reason == "Test data integrity"
# WHEN: Multiple save/load cycles
for cycle in range(3):
# Modify parameter
loaded_sysid.set_new_value(f"{8 + cycle}.0")
loaded_sysid.set_change_reason(f"Cycle {cycle + 1} integrity test")
# Save and reload
cycle_saved_state = loaded_param_editor.get_parameters_as_par_dict()
cycle_loaded_config = ParameterEditor(
current_file="01_initial_setup.param", flight_controller=MagicMock(), filesystem=realistic_filesystem
)
cycle_loaded_config.current_step_parameters = {}
for param_name, par_obj in cycle_saved_state.items():
metadata = realistic_filesystem.doc_dict.get(param_name, {})
fc_value = flight_controller_with_params.fc_parameters.get(param_name)
param = ArduPilotParameter(name=param_name, par_obj=par_obj, metadata=metadata, fc_value=fc_value)
cycle_loaded_config.current_step_parameters[param_name] = param
# Verify integrity after each cycle
cycle_loaded_sysid = cycle_loaded_config.current_step_parameters["SYSID_THISMAV"]
assert cycle_loaded_sysid.get_new_value() == 8 + cycle
assert cycle_loaded_sysid.change_reason == f"Cycle {cycle + 1} integrity test"
def test_parameter_validation_maintains_data_integrity_under_edge_conditions(
self, param_editor_with_params: ParameterEditor
) -> None:
"""
Parameter validation maintains data integrity even under edge conditions.
GIVEN: Parameters with various constraints and edge case values
WHEN: Invalid or edge case values are attempted
THEN: System rejects invalid data while preserving existing valid data
AND: Validation provides clear feedback about what values are acceptable
AND: No existing valid parameters are corrupted during validation attempts
"""
param_editor = param_editor_with_params
# GIVEN: Parameters with known constraints
sysid_param = param_editor.current_step_parameters["SYSID_THISMAV"]
batt_param = param_editor.current_step_parameters["BATT_CAPACITY"]
# Establish baseline valid state
# (Variables removed as they were unused)
# WHEN: Attempt various invalid values
invalid_values = [
("SYSID_THISMAV", "-1.0"), # Below minimum
("SYSID_THISMAV", "256.0"), # Above maximum
("SYSID_THISMAV", "not_a_number"), # Non-numeric
("BATT_CAPACITY", "-1000.0"), # Negative capacity
("BATT_CAPACITY", "999999.0"), # Unrealistically high
]
for param_name, invalid_value in invalid_values:
param = param_editor.current_step_parameters[param_name]
original_value = param.get_new_value()
# Attempt to set invalid value
with contextlib.suppress(ValueError, ParameterOutOfRangeError):
param.set_new_value(invalid_value)
# In real implementation, this might raise an exception
# For this test, we check that the value wasn't actually set
# or was rejected
# THEN: Original valid data is preserved
assert param.get_new_value() == original_value
# AND: Valid values still work after validation attempts
sysid_param.set_new_value("15.0") # Valid value
batt_param.set_new_value("4500.0") # Valid value
assert sysid_param.get_new_value() == 15.0
assert batt_param.get_new_value() == 4500.0
# AND: System state remains consistent
assert len(param_editor.current_step_parameters) > 0
assert not any(param.get_new_value() is None for param in param_editor.current_step_parameters.values())
class TestUserExperienceAndFeedback:
"""Test user experience elements and feedback mechanisms."""
def test_user_receives_progress_feedback_during_long_running_operations(
self,
param_editor_with_params: ParameterEditor, # pylint: disable=unused-argument
flight_controller_with_params: MagicMock,
) -> None:
"""
User receives clear progress feedback during long-running parameter operations.
GIVEN: A user is performing a long-running operation like uploading many parameters
WHEN: The operation progresses through multiple stages
THEN: Progress feedback is provided at each stage with clear messages
AND: Progress values increase monotonically from 0.0 to 1.0
AND: User understands what stage of the operation is currently executing
"""
# GIVEN: Large parameter set requiring significant upload time
large_param_set = {}
for i in range(1, 51): # 50 parameters
param_name = f"{i:02d}"
large_param_set[param_name] = Par(float(i), f"Parameter {i}")
# Mock progress callback mechanism
progress_updates = []
def progress_callback(progress: float, message: str) -> None:
progress_updates.append((progress, message))
# WHEN: Upload operation with progress tracking
flight_controller_with_params.upload_parameters = MagicMock(return_value=True)
flight_controller_with_params.upload_parameters_with_progress = MagicMock(
side_effect=lambda _params, callback: [
callback(0.0, "Preparing upload..."),
callback(0.2, "Uploading parameter batch 1/5..."),
callback(0.4, "Uploading parameter batch 2/5..."),
callback(0.6, "Uploading parameter batch 3/5..."),
callback(0.8, "Uploading parameter batch 4/5..."),
callback(1.0, "Upload complete"),
True,
][-1]
)
# Execute upload with progress tracking