Skip to content

Commit

Permalink
Add UI checks in headless mode.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexhuszagh committed Sep 7, 2024
1 parent 02bfbea commit 50d17fa
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ui.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions example/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
57 changes: 57 additions & 0 deletions scripts/headless.sh
Original file line number Diff line number Diff line change
@@ -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
73 changes: 70 additions & 3 deletions test/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'''

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -1757,55 +1768,71 @@ def test_sizegrip_dialog(_, window, *__):


def test_colordialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QColor()
QtWidgets.QColorDialog.getColor(initial)

return None, None, False, True


def test_alpha_colordialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QColor()
QtWidgets.QColorDialog.getColor(initial, options=compat.ColorShowAlphaChannel)

return None, None, False, True


def test_nobuttons_colordialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QColor()
QtWidgets.QColorDialog.getColor(initial, options=compat.ColorNoButtons)

return None, None, False, True


def test_qt_colordialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QColor()
QtWidgets.QColorDialog.getColor(initial, options=compat.ColorDontUseNativeDialog)

return None, None, False, True


def test_fontdialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QFont()
QtWidgets.QFontDialog.getFont(initial)

return None, None, False, True


def test_nobuttons_fontdialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QFont()
QtWidgets.QFontDialog.getFont(initial, options=compat.FontNoButtons)

return None, None, False, True


def test_qt_fontdialog(*_):
if is_headless():
return None, None, False, False
initial = QtGui.QFont()
QtWidgets.QFontDialog.getFont(initial, options=compat.FontDontUseNativeDialog)

return None, None, False, True


def test_filedialog(_, window, *__):
if is_headless():
return None, None, False, False
dialog = QtWidgets.QFileDialog(window)
dialog.setFileMode(compat.Directory)
shared.execute(args, dialog)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -1846,13 +1878,17 @@ 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)

return None, None, False, True


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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -1939,13 +1983,17 @@ def _wizard(widget):


def test_wizard(widget, *_):
if is_headless():
return None, None, False, False
wizard = _wizard(widget)
shared.execute(args, wizard)

return None, None, False, True


def test_classic_wizard(widget, *_):
if is_headless():
return None, None, False, False
wizard = _wizard(widget)
wizard.setWizardStyle(compat.ClassicStyle)
shared.execute(args, wizard)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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.')

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down

0 comments on commit 50d17fa

Please sign in to comment.