Skip to content

Commit 6b36ca5

Browse files
MacOs Test
Signed-off-by: Omkar Sarkar <omkarsarkar24@gmail.com>
1 parent e72707f commit 6b36ca5

8 files changed

Lines changed: 66 additions & 46 deletions

.github/workflows/pytest.yml

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@ jobs:
2929
strategy:
3030
fail-fast: false
3131
matrix:
32-
# os: [ubuntu-latest, macos-latest, windows-latest]
33-
# python-version: ["3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"]
34-
os: [ubuntu-latest]
35-
python-version: ["3.9", "3.14", "3.14t"]
32+
include:
33+
- os: ubuntu-latest
34+
python-version: "3.9"
35+
- os: ubuntu-latest
36+
python-version: "3.14"
37+
- os: ubuntu-latest
38+
python-version: "3.14t"
39+
- os: macos-latest
40+
python-version: "3.9"
3641

3742
steps:
3843
- name: Harden the runner (Audit all outbound calls)
44+
if: matrix.os != 'windows-latest'
3945
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
4046
with:
4147
egress-policy: audit
@@ -50,37 +56,30 @@ jobs:
5056
python-version: ${{ matrix.python-version }}
5157
activate-environment: true
5258

53-
- name: Install system dependencies for GUI testing
59+
- name: Install system dependencies for GUI testing on linux
60+
if: matrix.os == 'ubuntu-latest'
5461
run: |
5562
sudo apt-get update
56-
# Only install system Tcl/Tk for Python < 3.13 (newer versions bundle their own)
57-
if [[ "${{ matrix.python-version }}" < "3.13" ]]; then
58-
echo "Installing system Tcl/Tk 8.6 for Python ${{ matrix.python-version }}"
59-
sudo apt-get install -y python3-tk tcl8.6 tk8.6 libtcl8.6 libtk8.6
60-
else
61-
echo "Python ${{ matrix.python-version }} uses bundled Tcl/Tk, skipping system packages"
62-
# Only install tools, not Tcl/Tk libraries
63-
sudo apt-get install -y scrot xdotool x11-utils gnome-screenshot
64-
fi
65-
python3 --version && python3 -c "import tkinter; print(tkinter.TclVersion, tkinter.TkVersion)"
63+
sudo apt-get install -y xvfb python3-tk tcl tk tcl-dev tk-dev libx11-6 libxrender1 libxext6 libsm6 scrot xdotool x11-utils
64+
python3 --version
65+
python3 -c "import tkinter; print('Tk OK:', tkinter.TclVersion, tkinter.TkVersion)"
6666
67-
- name: Ensure Tcl/Tk search paths
67+
- name: Install system dependencies for GUI testing on macOS
68+
if: matrix.os == 'macos-latest'
6869
run: |
69-
# Only set Tcl/Tk paths for Python < 3.13
70-
if [[ "${{ matrix.python-version }}" < "3.13" ]]; then
71-
echo "Setting Tcl/Tk paths for Python ${{ matrix.python-version }}"
72-
echo "TCL_LIBRARY=/usr/share/tcltk/tcl8.6" >> $GITHUB_ENV
73-
echo "TK_LIBRARY=/usr/share/tcltk/tk8.6" >> $GITHUB_ENV
74-
else
75-
echo "Python ${{ matrix.python-version }} uses bundled Tcl/Tk, no custom paths needed"
76-
fi
70+
brew install tcl-tk
71+
echo "LDFLAGS=-L$(brew --prefix tcl-tk)/lib" >> $GITHUB_ENV
72+
echo "CPPFLAGS=-I$(brew --prefix tcl-tk)/include" >> $GITHUB_ENV
73+
echo "PKG_CONFIG_PATH=$(brew --prefix tcl-tk)/lib/pkgconfig" >> $GITHUB_ENV
74+
python3 -c "import tkinter; print('Tk OK:', tkinter.TclVersion, tkinter.TkVersion)"
7775
7876
- name: Install dependencies and application
7977
# without --editable, the coverage report is not generated correctly
8078
run: |
8179
uv pip install --editable .[dev,ci_headless_tests]
8280
8381
- name: Download ArduCopter SITL (if available)
82+
if: matrix.os == 'ubuntu-latest'
8483
run: |
8584
# Create cache key based on current quarter (YYYY-Q)
8685
CURRENT_YEAR=$(date +%Y)
@@ -132,21 +131,19 @@ jobs:
132131
continue-on-error: false
133132
run: |
134133
export LIBGL_ALWAYS_SOFTWARE=1
135-
export DISPLAY=:99
136-
# disable X authentication
137-
export XAUTHORITY=/dev/null
138-
# disable access control restrictions
139-
Xvfb :99 -screen 0 1024x768x16 -ac &
140-
# ensure Xvfb is fully started before running tests
141-
sleep 2
142-
if [ "$SITL_AVAILABLE" = "true" ]; then
143-
echo "Running tests with SITL support"
144-
uv run pytest --cov=ardupilot_methodic_configurator --cov-report=xml:tests/coverage.xml --md=tests/results-${{ matrix.python-version }}.md --junit-xml=tests/results-junit.xml -m "sitl or not sitl"
134+
135+
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
136+
export DISPLAY=:99
137+
export XAUTHORITY=/dev/null
138+
Xvfb :99 -screen 0 1024x768x16 -ac &
139+
sleep 2
140+
FINAL_MARKER="sitl or not sitl"
145141
else
146-
echo "Running tests without SITL (mocked tests only)"
147-
uv run pytest --cov=ardupilot_methodic_configurator --cov-report=xml:tests/coverage.xml --md=tests/results-${{ matrix.python-version }}.md --junit-xml=tests/results-junit.xml -m "not sitl"
142+
FINAL_MARKER="not gui and not acceptance"
148143
fi
149144
145+
echo "Running tests with markers: $FINAL_MARKER"
146+
uv run pytest --cov=ardupilot_methodic_configurator --cov-report=xml:tests/coverage.xml --md=tests/results-${{ matrix.python-version }}.md --junit-xml=tests/results-junit.xml -m "$FINAL_MARKER"
150147
- name: Fix coverage paths
151148
run: |
152149
sed -i 's|<package name="." |<package name="ardupilot_methodic_configurator" |' tests/coverage.xml
@@ -160,13 +157,13 @@ jobs:
160157
if: ${{ always() }}
161158

162159
- name: Upload coverage xml report
160+
# Use always() to always run this step to publish test results when there are test failures
161+
if: matrix.os == 'ubuntu-latest' && always()
163162
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
164163
with:
165164
name: coverage-${{ matrix.python-version }}-xml
166165
path: tests/*.xml
167166
retention-days: 1
168-
# Use always() to always run this step to publish test results when there are test failures
169-
if: ${{ always() }}
170167

171168
- name: Upload coverage report
172169
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
@@ -175,8 +172,6 @@ jobs:
175172
path: .coverage
176173
include-hidden-files: true
177174
retention-days: 1
178-
# Use always() to always run this step to publish test results when there are test failures
179-
if: ${{ always() }}
180175

181176
upload_coverage_to_coveralls:
182177
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') && (success() || failure())

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ markers =
1414
integration: mark a test as an integration test
1515
slow: mark a test as slow running
1616
sitl: mark a test as requiring SITL (real ArduPilot simulation)
17+
gui: requires real GUI (tkinter, windows, cocoa)

tests/acceptance_battery_monitor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
)
3131
from ardupilot_methodic_configurator.plugin_factory import plugin_factory
3232

33+
pytestmark = pytest.mark.gui
34+
3335
# pylint: disable=redefined-outer-name,protected-access,too-many-lines
3436

3537

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,11 @@ def sitl_flight_controller(sitl_manager: SITLManager) -> Generator[FlightControl
498498

499499
# Cleanup connection but keep SITL running for subsequent tests
500500
fc.disconnect()
501+
502+
503+
@pytest.fixture(autouse=True)
504+
def mock_center_window_for_macos_only() -> None:
505+
"""Only mock center_window on macOS to prevent Tkinter crashes. Let Linux run normally."""
506+
if platform.system() == "Darwin":
507+
patcher = patch("ardupilot_methodic_configurator.frontend_tkinter_base_window.BaseWindow.center_window")
508+
patcher.start()

tests/gui_frontend_tkinter_parameter_editor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def test_show_about_window(self, mocker) -> None: # pylint: disable=too-many-lo
106106
# Check window properties
107107
assert about_window.title() == "About"
108108
# Check that geometry contains the expected size (position may vary)
109+
about_window.update_idletasks()
109110
geometry = about_window.geometry()
110111
assert "650x340" in geometry
111112

tests/test_backend_mavftp.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717

1818
# from unittest.mock import patch
1919
from io import StringIO
20-
21-
from pymavlink import mavutil
20+
from unittest.mock import MagicMock
2221

2322
# from ardupilot_methodic_configurator.backend_mavftp import ERR_NoErrorCodeInPayload
2423
# from ardupilot_methodic_configurator.backend_mavftp import ERR_NoErrorCodeInNack
@@ -65,10 +64,10 @@ def setUp(self) -> None:
6564
logger.setLevel(logging.DEBUG)
6665

6766
# Mock mavutil.mavlink_connection to simulate a connection
68-
self.mock_master = mavutil.mavlink_connection(device="udp:localhost:14550", source_system=1)
67+
self.mock_master = MagicMock()
6968

7069
# Initialize MAVFTP instance for testing
71-
self.mav_ftp = MAVFTP(self.mock_master, target_system=1, target_component=1)
70+
self.mav_ftp = MAVFTP(self.mock_master, 1, 0)
7271

7372
def tearDown(self) -> None:
7473
self.log_stream.seek(0)

tests/test_frontend_tkinter_rich_text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,8 @@ def tearDown(self) -> None:
184184
def test_get_widget_font_family_and_size(self) -> None:
185185
label = ttk.Label(self.root, text="Test")
186186
family, size = get_widget_font_family_and_size(label)
187-
expected_family = ["Segoe UI"] if platform_system() == "Windows" else ["Helvetica", "sans-serif"]
188-
expected_size = [9] if platform_system() == "Windows" else [-12, 10]
187+
expected_family = ["Segoe UI"] if platform_system() == "Windows" else ["Helvetica", "sans-serif", ".AppleSystemUIFont"]
188+
expected_size = [9] if platform_system() == "Windows" else [-12, 10, 13]
189189
assert isinstance(family, str)
190190
assert isinstance(size, int)
191191
assert family in expected_family

tests/test_frontend_tkinter_usage_popup_window.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ def popup_window(tk_root) -> BaseWindow:
4545
window.root.destroy()
4646

4747

48+
@pytest.fixture(autouse=True)
49+
def mock_center_window_for_macos() -> None:
50+
"""Mock center_window globally for all tests in this file to prevent macOS Tkinter segfaults/hangs."""
51+
patcher = patch("ardupilot_methodic_configurator.frontend_tkinter_base_window.BaseWindow.center_window")
52+
patcher.start()
53+
54+
4855
@pytest.fixture
4956
def rich_text_widget(popup_window) -> RichText:
5057
"""Fixture providing a RichText widget for popup instructions."""
@@ -140,6 +147,7 @@ def test_show_again_checkbox_updates_user_preferences(self, popup_window, mock_p
140147
# Assert: Settings updated correctly
141148
mock_program_settings.set_display_usage_popup.assert_called_with("test_popup", show=False)
142149

150+
@patch("ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window.sys_platform", "linux")
143151
def test_closing_popup_returns_focus_to_parent_window(
144152
self,
145153
popup_window,
@@ -182,6 +190,7 @@ def test_popup_shows_correct_title_and_content(self, tk_root, popup_window, rich
182190
with (
183191
patch("tkinter.BooleanVar") as mock_bool_var,
184192
patch.object(popup_window.root, "grab_set"),
193+
patch.object(tk_root, "wait_window"),
185194
):
186195
mock_var_instance = MagicMock()
187196
mock_bool_var.return_value = mock_var_instance
@@ -226,6 +235,7 @@ def test_user_can_dismiss_popup_with_button(self, tk_root, popup_window, rich_te
226235
with (
227236
patch("tkinter.BooleanVar"),
228237
patch.object(popup_window.root, "grab_set"),
238+
patch.object(tk_root, "wait_window"),
229239
):
230240
UsagePopupWindow.display(
231241
parent=tk_root,
@@ -249,6 +259,7 @@ def test_user_can_dismiss_popup_with_button(self, tk_root, popup_window, rich_te
249259
# Assert: Window closes as expected
250260
mock_destroy.assert_called_once()
251261

262+
@patch("ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window.sys_platform", "linux")
252263
def test_popup_prevents_interaction_with_other_windows(self, tk_root, popup_window, rich_text_widget) -> None:
253264
"""
254265
Popup window prevents user interaction with other application windows.
@@ -262,7 +273,9 @@ def test_popup_prevents_interaction_with_other_windows(self, tk_root, popup_wind
262273
patch.object(popup_window.root, "grab_set") as mock_grab_set,
263274
patch.object(popup_window.root, "withdraw"),
264275
patch.object(popup_window.root, "deiconify"),
276+
patch.object(popup_window.root, "update_idletasks"),
265277
patch("tkinter.BooleanVar"),
278+
patch.object(tk_root, "wait_window"),
266279
):
267280
UsagePopupWindow.display(
268281
parent=tk_root,
@@ -276,6 +289,7 @@ def test_popup_prevents_interaction_with_other_windows(self, tk_root, popup_wind
276289
# Assert: Popup becomes modal to focus user attention
277290
mock_grab_set.assert_called_once()
278291

292+
@patch("ardupilot_methodic_configurator.frontend_tkinter_usage_popup_window.sys_platform", "linux")
279293
def test_closing_popup_returns_focus_to_parent_window(self, tk_root, popup_window) -> None:
280294
"""
281295
Closing popup window properly releases focus and returns control to parent.

0 commit comments

Comments
 (0)