diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea2ed6e..496d18b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,13 @@ jobs: - name: Install dependencies run: pip install -r requirements-dev.txt + - name: Install Xvfb for the UI tests + run: sudo apt-get update && sudo apt-get install -y xvfb + - name: Run tests - run: pytest + run: xvfb-run -a pytest + env: + REQUIRE_UI_TESTS: "1" lint: runs-on: ubuntu-latest diff --git a/readme.md b/readme.md index b7c1503..524c7ca 100644 --- a/readme.md +++ b/readme.md @@ -168,5 +168,6 @@ pip install -r requirements-dev.txt | Type checks | `mypy` | The UI tests are skipped when no display is available. On Linux they can be run with `xvfb-run -a pytest`. +CI sets `REQUIRE_UI_TESTS=1`, which makes them fail instead of being skipped. Pull requests target the `dev` branch. diff --git a/renovate.json b/renovate.json index 1bc58cd..b6eb8bc 100644 --- a/renovate.json +++ b/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "rebaseWhen": "conflicted", "labels": [ diff --git a/test/tkinter_floor_test.py b/test/tkinter_floor_test.py index 5904b5e..d5c8dfb 100644 --- a/test/tkinter_floor_test.py +++ b/test/tkinter_floor_test.py @@ -1,6 +1,5 @@ """Unit tests for the TkinterFloor class.""" -import importlib.util import unittest import numpy @@ -8,11 +7,10 @@ from floors.floor import Floor from generators.generator import Generator from rooms.teleport_room import TeleportRoom +from ui_test_support import requires_tkinter -_HAS_TKINTER = importlib.util.find_spec("tkinter") is not None - -@unittest.skipUnless(_HAS_TKINTER, "tkinter is not available") +@requires_tkinter() class TkinterFloorTest(unittest.TestCase): def setUp(self) -> None: # pylint: disable=import-outside-toplevel diff --git a/test/tkinter_generator_test.py b/test/tkinter_generator_test.py index a584e0a..8478b1b 100644 --- a/test/tkinter_generator_test.py +++ b/test/tkinter_generator_test.py @@ -1,30 +1,15 @@ """Unit tests for the TkinterGenerator class.""" -import importlib.util import tempfile import time import unittest from unittest import mock +from ui_test_support import requires_display from utils import globals as my_globals -_HAS_TKINTER = importlib.util.find_spec("tkinter") is not None - -def _has_display() -> bool: - if not _HAS_TKINTER: - return False - # pylint: disable=import-outside-toplevel - import tkinter - - try: - tkinter.Tk().destroy() - except tkinter.TclError: - return False - return True - - -@unittest.skipUnless(_has_display(), "tkinter or a display is not available") +@requires_display() class TkinterGeneratorTest(unittest.TestCase): def setUp(self) -> None: # pylint: disable=import-outside-toplevel diff --git a/test/tkinter_teleport_room_test.py b/test/tkinter_teleport_room_test.py index aaff632..c753596 100644 --- a/test/tkinter_teleport_room_test.py +++ b/test/tkinter_teleport_room_test.py @@ -1,14 +1,12 @@ """Unit tests for the TkinterTeleportRoom class.""" -import importlib.util import unittest +from ui_test_support import requires_tkinter from utils.room_type import RoomType -_HAS_TKINTER = importlib.util.find_spec("tkinter") is not None - -@unittest.skipUnless(_HAS_TKINTER, "tkinter is not available") +@requires_tkinter() class TkinterTeleportRoomTest(unittest.TestCase): def setUp(self) -> None: # pylint: disable=import-outside-toplevel diff --git a/test/ui_test_support.py b/test/ui_test_support.py new file mode 100644 index 0000000..80b1dd4 --- /dev/null +++ b/test/ui_test_support.py @@ -0,0 +1,60 @@ +""" +Helpers to skip the UI tests when tkinter or a display is not available. + +Set the environment variable REQUIRE_UI_TESTS=1 to make these tests fail +instead of being skipped, e.g. in CI where they must always run. +""" + +import importlib.util +import os +import unittest +from typing import Callable, TypeVar + +T = TypeVar("T") + +_REQUIRED = os.environ.get("REQUIRE_UI_TESTS") == "1" + + +def has_tkinter() -> bool: + """ + Returns if tkinter can be imported. + """ + return importlib.util.find_spec("tkinter") is not None + + +def has_display() -> bool: + """ + Returns if tkinter can open a window. + """ + if not has_tkinter(): + return False + # pylint: disable=import-outside-toplevel + import tkinter + + try: + tkinter.Tk().destroy() + except tkinter.TclError: + return False + return True + + +def _require(available: bool, reason: str) -> Callable[[T], T]: + if available: + return lambda test: test + if _REQUIRED: + raise RuntimeError(f"UI tests are required but {reason}") + return unittest.skip(reason) + + +def requires_tkinter() -> Callable[[T], T]: + """ + Skips the decorated tests if tkinter is not available. + """ + return _require(has_tkinter(), "tkinter is not available") + + +def requires_display() -> Callable[[T], T]: + """ + Skips the decorated tests if tkinter cannot open a window. + """ + return _require(has_display(), "tkinter or a display is not available")