Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Callable, Optional

from ardupilot_methodic_configurator import _, __version__
from ardupilot_methodic_configurator.frontend_tkinter_base import ScrollFrame


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

self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)

self.frame = ttk.Frame(self.root, padding="20")
self.frame.grid(sticky="nsew")

self.msg = ttk.Label(self.frame, text=version_info, wraplength=650, justify="left")
self.msg.grid(row=0, column=0, columnspan=2, pady=20)
# Configure main frame to expand
self.frame.grid_rowconfigure(0, weight=1)
self.frame.grid_columnconfigure(0, weight=1)
self.frame.grid_columnconfigure(1, weight=1)

version_info_scroll_frame = ScrollFrame(self.frame)
version_info_scroll_frame.grid(row=0, column=0, columnspan=2, pady=20, sticky="nsew")

viewport = version_info_scroll_frame.view_port
viewport.grid_columnconfigure(0, weight=1)
viewport.grid_rowconfigure(0, weight=1)

self.msg = ttk.Label(viewport, text=version_info, justify="left")
self.msg.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)

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

self.status_label = ttk.Label(self.frame, text="")
self.status_label.grid(row=2, column=0, columnspan=2)
self.status_label.grid(row=2, column=0, columnspan=2, sticky="ew")

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

self.root.bind("<Configure>", self._on_window_resize)

def _setup_buttons(self) -> None:
self.yes_btn = ttk.Button(self.frame, text=_("Update Now"), command=self.on_yes)
self.no_btn = ttk.Button(self.frame, text=_("Not Now"), command=self.on_no)
self.yes_btn.grid(row=3, column=0, padx=5)
self.no_btn.grid(row=3, column=1, padx=5)

def _on_window_resize(self, event: tk.Event) -> None:
"""Update label wraplength when window is resized."""
# Get window width and account for padding
window_width = event.width - 50
self.msg.configure(wraplength=window_width)

def update_progress(self, value: float, status: str = "") -> None:
"""Update progress directly."""
self.progress["value"] = value
Expand Down
19 changes: 14 additions & 5 deletions ardupilot_methodic_configurator/middleware_software_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import platform
import re
from argparse import ArgumentParser
from logging import basicConfig as logging_basicConfig
from logging import debug as logging_error
Expand All @@ -34,15 +35,23 @@
from ardupilot_methodic_configurator.frontend_tkinter_software_update import UpdateDialog


def format_version_info(_current_version: str, _latest_release: str, _changes: str) -> str:
def format_version_info(_current_version: str, _latest_release: str, changes: str) -> str:
# remove pull request information from the changelog as PRs are not relevant for the end user.
# PRs start with "[#" and end with ")", use a non-greedy match to remove them.
changes = re.sub(r"\[#.*?\)", "", changes)

# remove author information from the changelog as authors are not relevant for the end user.
changes = re.sub(r"\(\[.*?\)\)", "", changes)

# Clean up multiple spaces within each line while preserving newlines
changes = "\n".join(re.sub(r"\s+", " ", line).strip() for line in changes.splitlines())

return (
_("New version available!")
+ "\n\n"
+ _("Current version: {_current_version}")
_("Current version: {_current_version}")
+ "\n"
+ _("Latest version: {_latest_release}")
+ "\n\n"
+ _("Changes:\n{_changes}")
+ _("Changes:\n{changes}")
).format(**locals())


Expand Down
65 changes: 65 additions & 0 deletions tests/test_frontend_tkinter_software_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,68 @@ def test_multiple_updates(self) -> None:

def test_version_info_display(self) -> None:
"""Test version info display in dialog."""

def test_init_window_config(self) -> None:
"""Test window configuration during initialization."""
dialog = UpdateDialog(self.version_info)

# Window configuration
dialog.root.title.assert_called_once() # pylint: disable=no-member
dialog.root.geometry.assert_called_with("700x700") # pylint: disable=no-member

# Grid configuration
dialog.root.grid_rowconfigure.assert_called_with(0, weight=1) # pylint: disable=no-member
dialog.root.grid_columnconfigure.assert_called_with(0, weight=1) # pylint: disable=no-member

def test_init_scroll_frame(self) -> None:
"""Test ScrollFrame setup during initialization."""
with patch("ardupilot_methodic_configurator.frontend_tkinter_software_update.ScrollFrame") as mock_scroll:
UpdateDialog(self.version_info)

# ScrollFrame creation and configuration
mock_scroll.assert_called_once()
mock_scroll_instance = mock_scroll.return_value
mock_scroll_instance.grid.assert_called_with(row=0, column=0, columnspan=2, pady=20, sticky="nsew")

# Viewport configuration
viewport = mock_scroll_instance.view_port
viewport.grid_columnconfigure.assert_called_with(0, weight=1)
viewport.grid_rowconfigure.assert_called_with(0, weight=1)

def test_window_resize(self) -> None:
"""Test window resize event handler."""
mock_msg = MagicMock()

with patch("tkinter.ttk.Label", return_value=mock_msg):
dialog = UpdateDialog(self.version_info)
dialog.msg = mock_msg

# Create mock event
mock_event = MagicMock()
mock_event.width = 800

# Configure the mock
mock_msg.configure(wraplength=750)

# Trigger resize event
dialog._on_window_resize(mock_event) # pylint: disable=protected-access

# Verify label wraplength update
mock_msg.configure.assert_called_with(wraplength=750)

def test_button_configuration(self) -> None:
"""Test button creation and configuration."""
mock_yes_btn = MagicMock()
mock_no_btn = MagicMock()

with patch("tkinter.ttk.Button") as mock_button:
mock_button.side_effect = [mock_yes_btn, mock_no_btn]
dialog = UpdateDialog(self.version_info)

# Configure the mocks
mock_yes_btn.configure(text="Update Now", command=dialog.on_yes)
mock_no_btn.configure(text="Not Now", command=dialog.on_no)

# Verify configurations
mock_yes_btn.configure.assert_called_with(text="Update Now", command=dialog.on_yes)
mock_no_btn.configure.assert_called_with(text="Not Now", command=dialog.on_no)
95 changes: 95 additions & 0 deletions tests/test_middleware_software_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import pytest

from ardupilot_methodic_configurator import _
from ardupilot_methodic_configurator.middleware_software_updates import UpdateManager, format_version_info


Expand Down Expand Up @@ -66,3 +67,97 @@ def test_check_and_update_value_error(self, update_manager) -> None: # pylint:
):
assert not update_manager.check_and_update(latest_release, current_version)
mock_logging_error.assert_called_once()


def test_format_version_info_pr_removal() -> None:
changes = "Feature [#123) Added test\nBug [#456) Fixed issue"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "[#123)" not in result
assert "[#456)" not in result
assert "Added test" in result
assert "Fixed issue" in result


def test_format_version_info_author_removal() -> None:
changes = "Feature ([author)) Added test\nBug ([contributor)) Fixed issue"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "([author))" not in result
assert "([contributor))" not in result


def test_format_version_info_complex_changes() -> None:
changes = "Feature [#123)([author)) Multiple tags\nBug [#456)([contributor)) Mixed content"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "[#123)" not in result
assert "[#456)" not in result
assert "([author))" not in result
assert "([contributor))" not in result
assert "Multiple tags" in result
assert "Mixed content" in result


def test_format_version_info_empty_changes() -> None:
result = format_version_info("1.0.0", "2.0.0", "")
assert "Current version: 1.0.0" in result
assert "Latest version: 2.0.0" in result
assert "Changes:" in result


def test_format_version_info_special_chars() -> None:
changes = "Feature ([#123]) Added *special* characters\nBug ([#456]) with $symbols%"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "*special*" in result
assert "$symbols%" in result


def test_format_version_info_basic() -> None:
result = format_version_info("1.0.0", "2.0.0", "Simple change")
expected = (
# pylint: disable=duplicate-code
_("Current version: {_current_version}")
+ "\n"
+ _("Latest version: {_latest_release}")
+ "\n\n"
+ _("Changes:\n{changes}")
# pylint: enable=duplicate-code
).format(_current_version="1.0.0", _latest_release="2.0.0", changes="Simple change")
assert result == expected


def test_format_version_info_newlines() -> None:
result = format_version_info("1.0.0", "2.0.0", "Change 1\nChange 2")
expected = (
# pylint: disable=duplicate-code
_("Current version: {_current_version}")
+ "\n"
+ _("Latest version: {_latest_release}")
+ "\n\n"
+ _("Changes:\n{changes}")
# pylint: enable=duplicate-code
).format(_current_version="1.0.0", _latest_release="2.0.0", changes="Change 1\nChange 2")
assert result == expected


def test_format_version_info_empty() -> None:
result = format_version_info("1.0.0", "2.0.0", "")
assert "Current version: 1.0.0" in result
assert "Latest version: 2.0.0" in result
assert "Changes:" in result


def test_format_version_info_pr_references() -> None:
changes = "Feature [#123) Test\nBug [#456) Fix"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "[#123)" not in result
assert "[#456)" not in result
assert "Feature Test" in result
assert "Bug Fix" in result


def test_format_version_info_malformed_refs() -> None:
changes = "Feature [#123 Test\nBug (#456)] Fix"
result = format_version_info("1.0.0", "2.0.0", changes)
assert "Feature" in result
assert "Bug" in result
assert "Test" in result
assert "Fix" in result
Loading