From 50d17fac5ed317007003e52a74b3e95a5aa654c6 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 15:19:00 -0500 Subject: [PATCH] Add UI checks in headless mode. --- .github/workflows/lint.yml | 2 +- .github/workflows/ui.yml | 22 ++++++++++++ example/shared.py | 2 ++ scripts/headless.sh | 57 +++++++++++++++++++++++++++++ test/ui.py | 73 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ui.yml create mode 100755 scripts/headless.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 13f87e6..10c7bf9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: lint-cpp: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] # TODO: Restore macos-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml new file mode 100644 index 0000000..f7a341f --- /dev/null +++ b/.github/workflows/ui.yml @@ -0,0 +1,22 @@ +name: Ui + +on: [push] + +jobs: + ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip + pip install PySide2 PySide6 PyQt5 PyQt6 + sudo apt-get ^Cstall xvfb + - name: Checking our Python imports. + run: | + scripts/headless.sh diff --git a/example/shared.py b/example/shared.py index a305385..709de21 100644 --- a/example/shared.py +++ b/example/shared.py @@ -1017,6 +1017,8 @@ def exec_app(args, app, window): '''Show and execute the Qt application.''' window.show() + if os.environ.get('QT_QPA_PLATFORM') == 'offscreen': + return app.quit() return execute(args, app) diff --git a/scripts/headless.sh b/scripts/headless.sh new file mode 100755 index 0000000..eaf797a --- /dev/null +++ b/scripts/headless.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2086,2068 +# +# Run each configure for all supported frameworks, and store them in `dist/ci`. +# This requires the correct frameworks to be installed: +# - PyQt5 +# - PyQt6 +# - PySide6 +# And if using Python 3.10 or earlier: +# - PySide2 + +set -eux pipefail + +scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +project_home="$(dirname "${scripts_home}")" +mkdir -p "${project_home}/dist/ci" +cd "${project_home}" +# shellcheck source=/dev/null +. "${scripts_home}/shared.sh" + +# we xcb installed for our headless running, so exit if we don't have it +if ! hash xvfb-run &>/dev/null; then + >&2 echo "Do not have xvfb installed..." + exit 1 +fi + +# pop them into dist since it's ignored anyway +if ! is-set PYTHON; then + PYTHON=python +fi +frameworks=("pyqt5" "pyqt6" "pyside6") +have_pyside=$(${PYTHON} -c 'import sys; print(sys.version_info < (3, 11))') +if [[ "${have_pyside}" == "True" ]]; then + frameworks+=("pyside2") +fi + +# need to run everything in headless mode. +# note: our shared libraries can be run without issues +export QT_QPA_PLATFORM=offscreen +for script in example/*.py; do + if [[ "${script}" == "example/advanced-dock.py" ]]; then + continue + fi + for framework in "${frameworks[@]}"; do + echo "Running '${script}' for framework '${framework}'." + xvfb-run -a "${PYTHON}" "${script}" --qt-framework "${framework}" + done +done + +# now we need to run our tests +widgets=$(${PYTHON} -c "import os; os.chdir('test'); import ui; print(' '.join([i[5:] for i in dir(ui) if i.startswith('test_')]))") +for widget in ${widgets[@]}; do + for framework in "${frameworks[@]}"; do + echo "Running test for widget '${widget}' for framework '${framework}'." + xvfb-run -a "${PYTHON}" test/ui.py --widget "${widget}" --qt-framework "${framework}" + done +done diff --git a/test/ui.py b/test/ui.py index 3b35c4f..df876eb 100644 --- a/test/ui.py +++ b/test/ui.py @@ -99,6 +99,11 @@ } +def is_headless(): + '''Get if the scripts are running in test mode, that is offscreen.''' + return os.environ.get('QT_QPA_PLATFORM') == 'offscreen' + + def add_widgets(layout, children): '''Add 1 or more widgets to the layout.''' @@ -1731,6 +1736,8 @@ def test_file_icon_provider(widget, *_): def test_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) shared.execute(args, dialog) @@ -1739,6 +1746,8 @@ def test_dialog(_, window, *__): def test_modal_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) dialog.setModal(True) @@ -1748,6 +1757,8 @@ def test_modal_dialog(_, window, *__): def test_sizegrip_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QDialog(window) dialog.setMinimumSize(100, 100) dialog.setSizeGripEnabled(True) @@ -1757,6 +1768,8 @@ def test_sizegrip_dialog(_, window, *__): def test_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial) @@ -1764,6 +1777,8 @@ def test_colordialog(*_): def test_alpha_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorShowAlphaChannel) @@ -1771,6 +1786,8 @@ def test_alpha_colordialog(*_): def test_nobuttons_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorNoButtons) @@ -1778,6 +1795,8 @@ def test_nobuttons_colordialog(*_): def test_qt_colordialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QColor() QtWidgets.QColorDialog.getColor(initial, options=compat.ColorDontUseNativeDialog) @@ -1785,6 +1804,8 @@ def test_qt_colordialog(*_): def test_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial) @@ -1792,6 +1813,8 @@ def test_fontdialog(*_): def test_nobuttons_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial, options=compat.FontNoButtons) @@ -1799,6 +1822,8 @@ def test_nobuttons_fontdialog(*_): def test_qt_fontdialog(*_): + if is_headless(): + return None, None, False, False initial = QtGui.QFont() QtWidgets.QFontDialog.getFont(initial, options=compat.FontDontUseNativeDialog) @@ -1806,6 +1831,8 @@ def test_qt_fontdialog(*_): def test_filedialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QFileDialog(window) dialog.setFileMode(compat.Directory) shared.execute(args, dialog) @@ -1814,6 +1841,8 @@ def test_filedialog(_, window, *__): def test_qt_filedialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QFileDialog(window) dialog.setOption(compat.FileDontUseNativeDialog) shared.execute(args, dialog) @@ -1822,6 +1851,8 @@ def test_qt_filedialog(_, window, *__): def test_error_message(widget, *_): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QErrorMessage(widget) dialog.showMessage('Error message') shared.execute(args, dialog) @@ -1834,7 +1865,8 @@ def test_progress_dialog(_, window, __, ___, ____, app): dialog.setMinimumDuration(0) dialog.setMinimumSize(300, 100) dialog.show() - for i in range(1, 101): + count = 5 if is_headless() else 100 + for i in range(1, count + 1): dialog.setValue(i) app.processEvents() time.sleep(0.02) @@ -1846,6 +1878,8 @@ def test_progress_dialog(_, window, __, ___, ____, app): def test_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) shared.execute(args, dialog) @@ -1853,6 +1887,8 @@ def test_input_dialog(_, window, *__): def test_int_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setInputMode(compat.IntInput) shared.execute(args, dialog) @@ -1861,6 +1897,8 @@ def test_int_input_dialog(_, window, *__): def test_double_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setInputMode(compat.DoubleInput) shared.execute(args, dialog) @@ -1869,6 +1907,8 @@ def test_double_input_dialog(_, window, *__): def test_combobox_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) shared.execute(args, dialog) @@ -1877,6 +1917,8 @@ def test_combobox_input_dialog(_, window, *__): def test_list_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) dialog.setOption(compat.UseListViewForComboBoxItems) @@ -1886,6 +1928,8 @@ def test_list_input_dialog(_, window, *__): def test_nobuttons_input_dialog(_, window, *__): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QInputDialog(window) dialog.setComboBoxItems(['Item 1', 'Item 2']) dialog.setOption(compat.InputNoButtons) @@ -1939,6 +1983,8 @@ def _wizard(widget): def test_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) shared.execute(args, wizard) @@ -1946,6 +1992,8 @@ def test_wizard(widget, *_): def test_classic_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.ClassicStyle) shared.execute(args, wizard) @@ -1954,6 +2002,8 @@ def test_classic_wizard(widget, *_): def test_modern_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.ModernStyle) shared.execute(args, wizard) @@ -1962,6 +2012,8 @@ def test_modern_wizard(widget, *_): def test_mac_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.MacStyle) shared.execute(args, wizard) @@ -1970,6 +2022,8 @@ def test_mac_wizard(widget, *_): def test_aero_wizard(widget, *_): + if is_headless(): + return None, None, False, False wizard = _wizard(widget) wizard.setWizardStyle(compat.AeroStyle) shared.execute(args, wizard) @@ -1978,6 +2032,8 @@ def test_aero_wizard(widget, *_): def test_system_tray(widget, window, *_): + if is_headless(): + return None, None, False, False dialog = QtWidgets.QErrorMessage(widget) dialog.showMessage('Hey! System tray icon.') @@ -1993,6 +2049,8 @@ def test_system_tray(widget, window, *_): def _test_standard_button(window, app, button): + if is_headless(): + return None, None, False, False message = QtWidgets.QMessageBox(window) message.addButton(button) message.setMinimumSize(100, 100) @@ -2065,6 +2123,8 @@ def test_discard_button(_, window, __, ___, ____, app): def _test_standard_icon(window, app, icon): + if is_headless(): + return None, None, False, False message = QtWidgets.QMessageBox(window) message.setIcon(icon) message.setMinimumSize(100, 100) @@ -2190,6 +2250,8 @@ def test_disabled_menubar(widget, window, font, width, *_): def test_issue25(widget, window, font, width, *_): + if is_headless(): + return None, None, False, False def launch_filedialog(folder): dialog = QtWidgets.QFileDialog() @@ -2303,6 +2365,8 @@ def launch_fontdialog(value): def test_issue28(_, window, *__): + if is_headless(): + return dialog = QtWidgets.QFileDialog(window) dialog.setFileMode(compat.Directory) shared.execute(args, dialog) @@ -2369,9 +2433,12 @@ def test(args, test_widget): # run if show_window: window.show() - if quit: + if is_headless(): + window.close() + if quit or is_headless(): return app.quit() - return shared.execute(args, app) + elif not is_headless(): + return shared.execute(args, app) def main():