Skip to content

Commit 4521cda

Browse files
committed
feat(software_update): Add scrollable changelog
remove PR and author information add pytests increase window size
1 parent 291dca8 commit 4521cda

File tree

4 files changed

+233
-9
lines changed

4 files changed

+233
-9
lines changed

ardupilot_methodic_configurator/frontend_tkinter_software_update.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import Callable, Optional
1414

1515
from ardupilot_methodic_configurator import _, __version__
16+
from ardupilot_methodic_configurator.frontend_tkinter_base import ScrollFrame
1617

1718

1819
class UpdateDialog: # pylint: disable=too-many-instance-attributes
@@ -21,31 +22,55 @@ class UpdateDialog: # pylint: disable=too-many-instance-attributes
2122
def __init__(self, version_info: str, download_callback: Optional[Callable[[], bool]] = None) -> None:
2223
self.root = tk.Tk()
2324
self.root.title(_("Amilcar Lucas's - ArduPilot methodic configurator ") + __version__ + _(" - New version available"))
25+
self.root.geometry("700x700")
2426
self.download_callback = download_callback
2527
self.root.protocol("WM_DELETE_WINDOW", self.on_cancel)
2628

29+
self.root.grid_rowconfigure(0, weight=1)
30+
self.root.grid_columnconfigure(0, weight=1)
31+
2732
self.frame = ttk.Frame(self.root, padding="20")
2833
self.frame.grid(sticky="nsew")
2934

30-
self.msg = ttk.Label(self.frame, text=version_info, wraplength=650, justify="left")
31-
self.msg.grid(row=0, column=0, columnspan=2, pady=20)
35+
# Configure main frame to expand
36+
self.frame.grid_rowconfigure(0, weight=1)
37+
self.frame.grid_columnconfigure(0, weight=1)
38+
self.frame.grid_columnconfigure(1, weight=1)
39+
40+
version_info_scroll_frame = ScrollFrame(self.frame)
41+
version_info_scroll_frame.grid(row=0, column=0, columnspan=2, pady=20, sticky="nsew")
42+
43+
viewport = version_info_scroll_frame.view_port
44+
viewport.grid_columnconfigure(0, weight=1)
45+
viewport.grid_rowconfigure(0, weight=1)
46+
47+
self.msg = ttk.Label(viewport, text=version_info, justify="left")
48+
self.msg.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
3249

3350
self.progress = ttk.Progressbar(self.frame, orient="horizontal", length=400, mode="determinate")
34-
self.progress.grid(row=1, column=0, columnspan=2, pady=10, padx=10)
51+
self.progress.grid(row=1, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
3552
self.progress.grid_remove()
3653

3754
self.status_label = ttk.Label(self.frame, text="")
38-
self.status_label.grid(row=2, column=0, columnspan=2)
55+
self.status_label.grid(row=2, column=0, columnspan=2, sticky="ew")
3956

4057
self.result: Optional[bool] = None
4158
self._setup_buttons()
4259

60+
self.root.bind("<Configure>", self._on_window_resize)
61+
4362
def _setup_buttons(self) -> None:
4463
self.yes_btn = ttk.Button(self.frame, text=_("Update Now"), command=self.on_yes)
4564
self.no_btn = ttk.Button(self.frame, text=_("Not Now"), command=self.on_no)
4665
self.yes_btn.grid(row=3, column=0, padx=5)
4766
self.no_btn.grid(row=3, column=1, padx=5)
4867

68+
def _on_window_resize(self, event: tk.Event) -> None:
69+
"""Update label wraplength when window is resized."""
70+
# Get window width and account for padding
71+
window_width = event.width - 50
72+
self.msg.configure(wraplength=window_width)
73+
4974
def update_progress(self, value: float, status: str = "") -> None:
5075
"""Update progress directly."""
5176
self.progress["value"] = value

ardupilot_methodic_configurator/middleware_software_updates.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import platform
14+
import re
1415
from argparse import ArgumentParser
1516
from logging import basicConfig as logging_basicConfig
1617
from logging import debug as logging_error
@@ -34,15 +35,23 @@
3435
from ardupilot_methodic_configurator.frontend_tkinter_software_update import UpdateDialog
3536

3637

37-
def format_version_info(_current_version: str, _latest_release: str, _changes: str) -> str:
38+
def format_version_info(_current_version: str, _latest_release: str, changes: str) -> str:
39+
# remove pull request information from the changelog as PRs are not relevant for the end user.
40+
# PRs start with "[#" and end with ")", use a non-greedy match to remove them.
41+
changes = re.sub(r"\[#.*?\)", "", changes)
42+
43+
# remove author information from the changelog as authors are not relevant for the end user.
44+
changes = re.sub(r"\(\[.*?\)\)", "", changes)
45+
46+
# Clean up multiple spaces within each line while preserving newlines
47+
changes = "\n".join(re.sub(r"\s+", " ", line).strip() for line in changes.splitlines())
48+
3849
return (
39-
_("New version available!")
40-
+ "\n\n"
41-
+ _("Current version: {_current_version}")
50+
_("Current version: {_current_version}")
4251
+ "\n"
4352
+ _("Latest version: {_latest_release}")
4453
+ "\n\n"
45-
+ _("Changes:\n{_changes}")
54+
+ _("Changes:\n{changes}")
4655
).format(**locals())
4756

4857

tests/test_frontend_tkinter_software_update.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,112 @@ def test_multiple_updates(self) -> None:
9595

9696
def test_version_info_display(self) -> None:
9797
"""Test version info display in dialog."""
98+
99+
def test_init_window_config(self) -> None:
100+
"""Test window configuration during initialization."""
101+
dialog = UpdateDialog(self.version_info)
102+
103+
# Window configuration
104+
dialog.root.title.assert_called_once()
105+
dialog.root.geometry.assert_called_with("700x700")
106+
107+
# Grid configuration
108+
dialog.root.grid_rowconfigure.assert_called_with(0, weight=1)
109+
dialog.root.grid_columnconfigure.assert_called_with(0, weight=1)
110+
111+
def test_init_scroll_frame(self) -> None:
112+
"""Test ScrollFrame setup during initialization."""
113+
with patch("ardupilot_methodic_configurator.frontend_tkinter_software_update.ScrollFrame") as mock_scroll:
114+
UpdateDialog(self.version_info)
115+
116+
# ScrollFrame creation and configuration
117+
mock_scroll.assert_called_once()
118+
mock_scroll_instance = mock_scroll.return_value
119+
mock_scroll_instance.grid.assert_called_with(row=0, column=0, columnspan=2, pady=20, sticky="nsew")
120+
121+
# Viewport configuration
122+
viewport = mock_scroll_instance.view_port
123+
viewport.grid_columnconfigure.assert_called_with(0, weight=1)
124+
viewport.grid_rowconfigure.assert_called_with(0, weight=1)
125+
126+
def test_init_progress_bar(self) -> None:
127+
"""Test progress bar initial state."""
128+
mock_progress = MagicMock()
129+
130+
with patch("tkinter.ttk.Progressbar", return_value=mock_progress):
131+
dialog = UpdateDialog(self.version_info)
132+
dialog.progress = mock_progress
133+
134+
# Verify progress bar is created and hidden
135+
assert hasattr(dialog, "progress")
136+
mock_progress.grid_remove.assert_called_once()
137+
138+
def test_window_resize(self) -> None:
139+
"""Test window resize event handler."""
140+
mock_msg = MagicMock()
141+
142+
with patch("tkinter.ttk.Label", return_value=mock_msg):
143+
dialog = UpdateDialog(self.version_info)
144+
dialog.msg = mock_msg
145+
146+
# Create mock event
147+
mock_event = MagicMock()
148+
mock_event.width = 800
149+
150+
# Configure the mock
151+
mock_msg.configure(wraplength=750)
152+
153+
# Trigger resize event
154+
dialog._on_window_resize(mock_event)
155+
156+
# Verify label wraplength update
157+
mock_msg.configure.assert_called_with(wraplength=750)
158+
159+
def test_init_status_label(self) -> None:
160+
"""Test status label initialization."""
161+
# Create mocks
162+
mock_version_label = MagicMock()
163+
mock_status_label = MagicMock()
164+
mock_scroll_frame = MagicMock()
165+
mock_viewport = MagicMock()
166+
167+
# Configure ScrollFrame mock
168+
mock_scroll_frame.view_port = mock_viewport
169+
170+
with (
171+
patch("tkinter.ttk.Label") as mock_label_class,
172+
patch(
173+
"ardupilot_methodic_configurator.frontend_tkinter_software_update.ScrollFrame", return_value=mock_scroll_frame
174+
),
175+
):
176+
# Configure label mock
177+
mock_label_class.side_effect = [mock_version_label, mock_status_label]
178+
179+
# Create dialog
180+
dialog = UpdateDialog(self.version_info)
181+
182+
# Verify label creation count
183+
assert mock_label_class.call_count == 2
184+
185+
# Verify version info label
186+
mock_label_class.assert_any_call(mock_viewport, text=self.version_info, justify="left")
187+
188+
# Verify status label
189+
mock_label_class.assert_any_call(dialog.frame, text="")
190+
191+
def test_button_configuration(self) -> None:
192+
"""Test button creation and configuration."""
193+
mock_yes_btn = MagicMock()
194+
mock_no_btn = MagicMock()
195+
196+
with patch("tkinter.ttk.Button") as mock_button:
197+
mock_button.side_effect = [mock_yes_btn, mock_no_btn]
198+
dialog = UpdateDialog(self.version_info)
199+
200+
# Configure the mocks
201+
mock_yes_btn.configure(text="Update Now", command=dialog.on_yes)
202+
mock_no_btn.configure(text="Not Now", command=dialog.on_no)
203+
204+
# Verify configurations
205+
mock_yes_btn.configure.assert_called_with(text="Update Now", command=dialog.on_yes)
206+
mock_no_btn.configure.assert_called_with(text="Not Now", command=dialog.on_no)

tests/test_middleware_software_updates.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616

17+
from ardupilot_methodic_configurator import _
1718
from ardupilot_methodic_configurator.middleware_software_updates import UpdateManager, format_version_info
1819

1920

@@ -66,3 +67,83 @@ def test_check_and_update_value_error(self, update_manager) -> None: # pylint:
6667
):
6768
assert not update_manager.check_and_update(latest_release, current_version)
6869
mock_logging_error.assert_called_once()
70+
71+
def test_format_version_info_pr_removal() -> None:
72+
changes = "Feature [#123) Added test\nBug [#456) Fixed issue"
73+
result = format_version_info("1.0.0", "2.0.0", changes)
74+
assert "[#123)" not in result
75+
assert "[#456)" not in result
76+
assert "Added test" in result
77+
assert "Fixed issue" in result
78+
79+
def test_format_version_info_author_removal() -> None:
80+
changes = "Feature ([author)) Added test\nBug ([contributor)) Fixed issue"
81+
result = format_version_info("1.0.0", "2.0.0", changes)
82+
assert "([author))" not in result
83+
assert "([contributor))" not in result
84+
85+
def test_format_version_info_complex_changes() -> None:
86+
changes = "Feature [#123)([author)) Multiple tags\nBug [#456)([contributor)) Mixed content"
87+
result = format_version_info("1.0.0", "2.0.0", changes)
88+
assert "[#123)" not in result
89+
assert "[#456)" not in result
90+
assert "([author))" not in result
91+
assert "([contributor))" not in result
92+
assert "Multiple tags" in result
93+
assert "Mixed content" in result
94+
95+
def test_format_version_info_empty_changes() -> None:
96+
result = format_version_info("1.0.0", "2.0.0", "")
97+
assert "Current version: 1.0.0" in result
98+
assert "Latest version: 2.0.0" in result
99+
assert "Changes:" in result
100+
101+
def test_format_version_info_special_chars() -> None:
102+
changes = "Feature ([#123]) Added *special* characters\nBug ([#456]) with $symbols%"
103+
result = format_version_info("1.0.0", "2.0.0", changes)
104+
assert "*special*" in result
105+
assert "$symbols%" in result
106+
107+
def test_format_version_info_basic() -> None:
108+
result = format_version_info("1.0.0", "2.0.0", "Simple change")
109+
expected = (
110+
_("Current version: {_current_version}")
111+
+ "\n"
112+
+ _("Latest version: {_latest_release}")
113+
+ "\n\n"
114+
+ _("Changes:\n{changes}")
115+
).format(_current_version="1.0.0", _latest_release="2.0.0", changes="Simple change")
116+
assert result == expected
117+
118+
def test_format_version_info_newlines() -> None:
119+
result = format_version_info("1.0.0", "2.0.0", "Change 1\nChange 2")
120+
expected = (
121+
_("Current version: {_current_version}")
122+
+ "\n"
123+
+ _("Latest version: {_latest_release}")
124+
+ "\n\n"
125+
+ _("Changes:\n{changes}")
126+
).format(_current_version="1.0.0", _latest_release="2.0.0", changes="Change 1\nChange 2")
127+
assert result == expected
128+
129+
def test_format_version_info_empty() -> None:
130+
result = format_version_info("1.0.0", "2.0.0", "")
131+
assert "Current version: 1.0.0" in result
132+
assert "Latest version: 2.0.0" in result
133+
assert "Changes:" in result
134+
135+
def test_format_version_info_pr_references() -> None:
136+
changes = "Feature [#123) Test\nBug [#456) Fix"
137+
result = format_version_info("1.0.0", "2.0.0", changes)
138+
assert "[#123)" not in result
139+
assert "[#456)" not in result
140+
assert "Feature Test" in result
141+
assert "Bug Fix" in result
142+
143+
def test_format_version_info_malformed_refs() -> None:
144+
changes = "Feature [#123 Test\nBug (#456)] Fix"
145+
result = format_version_info("1.0.0", "2.0.0", changes)
146+
assert "Feature" in result
147+
assert "Bug" in result
148+
assert "Test" in result
149+
assert "Fix" in result

0 commit comments

Comments
 (0)