Skip to content

Commit

Permalink
[tests] Check decoration rendering
Browse files Browse the repository at this point in the history
Render widget decorations and compare to reference images. Should be
useful to avoid any breakages!
  • Loading branch information
elParaguayo committed Sep 17, 2023
1 parent bd568db commit 9d3072a
Show file tree
Hide file tree
Showing 46 changed files with 480 additions and 5 deletions.
15 changes: 13 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,25 @@ jobs:
libdbus-1-dev libgirepository1.0-dev gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-gudev-1.0 \
imagemagick libpulse-dev git xserver-xephyr xterm xvfb dbus-x11 libnotify-bin \
libxcb-composite0-dev libxcb-icccm4-dev libxcb-res0-dev libxcb-render0-dev libxcb-res0-dev \
libxcb-xfixes0-dev libiw-dev
libxcb-xfixes0-dev libiw-dev fonts-noto
sudo pip -q install meson PyGObject
pip -q install "tox<4" tox-gh-actions
- name: Build wayland
run: bash -x ./scripts/ubuntu_wayland_setup
- name: run tests
- name: Run test suite
run: |
tox
- name: Test widget decoration output
if: ${{ matrix.python-version == '3.11' }}
run: |
tox -e decorations
- name: Upload generated images
if: ${{ always() && matrix.python-version == '3.11' }}
uses: actions/upload-artifact@v3
with:
name: generated-images
path: decoration_images/
retention-days: 5
- name: Push coverage to Coveralls
run: |
pip -q install coveralls
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
2023-09-17: [TESTS] Check decorations are rendered correctly during test suite
2023-09-16: [FEATURE] Add more popup templates for `Mpris2` widget
2023-09-16: [FEATURE] Add `PulseVolumeExtra` widget
2023-09-01: [BUGFIX] Fix two `RectDecoration` rendering bugs (issues #266 and #267)
Expand Down
9 changes: 9 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ def pytest_addoption(parser):
choices=("x11", "wayland"),
help="Test a specific backend. Can be passed more than once.",
)
parser.addoption(
"--generate", action="store_true", default=False, help="Generate widget decoration images"
)
parser.addoption(
"--generate-dir", action="store", default="", help="Location to save generated images"
)
parser.addoption(
"--generate-ci", action="store_true", default=False, help="Generate images for GitHub CI"
)


def pytest_cmdline_main(config):
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-single-E.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-single-N.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-single-S.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-single-W.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/border-stacked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/resources/test_images/powerline-zig_zag.png
Binary file added test/resources/test_images/rect-default-line.png
Binary file added test/resources/test_images/rect-default.png
Binary file added test/resources/test_images/rect-stacked.png
188 changes: 188 additions & 0 deletions test/widget/decorations/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Copyright (c) 2023 elParaguayo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import re
import subprocess
import tempfile
from pathlib import Path

import cairocffi
import pytest
from libqtile.bar import Bar
from libqtile.command.base import expose_command
from libqtile.config import Screen

from test.helpers import BareConfig

# Regex pattern for extracting difference value from compare output
# Matches a float including using scientific notation
COMPARE_RESULT = re.compile(r"\((-?[\d.]+(?:e-?\d+)?)\)")

RESOURCES = Path(__file__).parent / ".." / ".." / "resources" / "test_images"
TEST_FOLDER = Path(__file__).parent.resolve().as_posix()

# Required accuracy. Difference should be less than 0.5% (CI fonts render slightly differently)
TOLERANCE = 0.005


@pytest.fixture(scope="session")
def temp_images():
# with tempfile.TemporaryDirectory() as temp:
# yield temp
tempdir = tempfile.mkdtemp()
yield tempdir


@pytest.fixture(scope="function")
def camera(manager_nospawn, request, temp_images):
"""
This fixture provides a means to take a picture of the current bar.
The snapshot is then compared against a reference image to check that the
content is still being rendered correctly.
Reference images can be generated by passing the --generate option when
calling pytest. The images will be saved in `test/widgets/decorations` and,
if they are deemed correct (by manual verification), they should be moved to
test/resources/test_images.
"""
# Check if we're generating reference images
generate = request.config.getoption("--generate")
image_dir = request.config.getoption("--generate-dir")
ci = request.config.getoption("--generate-ci")

# Get the configuration
config = getattr(request, "param", False)
if not config:
assert False, "No configuration provided."

# Get name of the decoration scenario
name = config["name"]

# Generate output filepath
make_images = generate or ci
if make_images:
if ci:
output_dir = (
(Path(__file__).parent / ".." / ".." / ".." / "decoration_images")
.resolve()
.as_posix()
)
else:
output_dir = image_dir or TEST_FOLDER
else:
output_dir = temp_images

output_file = f"{output_dir}/{name}.png"

class ScreenshotBar(Bar):
"""
Subclass of Qtile's bar with additional methods to dump the
Drawer to a png file.
Both X11 and Wayland backends are supported.
"""

def x11_screenshot(self, name):
"""Dumps X11 Drawer to png file."""
# Create a temporary image surface
isurf = cairocffi.ImageSurface(
cairocffi.FORMAT_ARGB32, self.drawer.width, self.drawer.height
)

# Create context for image surface
with cairocffi.Context(isurf) as ctx:
# Loop over widgets and copy widget's drawer to image surface
# The context is translated to position contents correctly
for w in self.widgets:
ctx.save()
ctx.set_operator(cairocffi.OPERATOR_SOURCE)
ctx.translate(w.offsetx, w.offsety)
ctx.rectangle(0, 0, w.width, w.height)
ctx.set_source_surface(w.drawer._xcb_surface)
ctx.fill()
ctx.restore()

# Check if we need to fill the end of the bar
# (this happens if the bar doesn't have a SPACER widget)
last_widget = self.widgets[-1]
end = last_widget.offsetx + last_widget.width
if end < self.width:
ctx.save()
ctx.translate(end, 0)
ctx.set_source_surface(self.drawer._xcb_surface)
ctx.rectangle(0, 0, self.drawer.width - end, self.drawer.height)
ctx.fill()
ctx.restore()

# Dump surface to PNG file
isurf.write_to_png(name)

def wayland_screenshot(self, name):
"""Dumps Wayland Drawer to png file."""
# Much easier: Wayland has an ImageSurface which we can access directly
self.drawer._win.surface.write_to_png(name)

@expose_command
def take_screenshot(self, name):
"""Exposed command to save bar contents to png file."""
if self.qtile.core.name == "x11":
self.x11_screenshot(name)
else:
self.wayland_screenshot(name)

class DecorationConfig(BareConfig):
"""Simple configuration to add widgets to ScreenshotBar."""

fake_screens = [Screen(top=ScreenshotBar(config["widgets"], 50), width=400, height=400)]

def screenshot():
"""Convenience function to call screenshot."""
manager_nospawn.c.bar["top"].take_screenshot(output_file)

def assert_similar():
"""Check if generated images are similar to reference images."""
# If we're generating images then we don't need to compare them
if generate:
return

# Build filepath to reference image
reference = RESOURCES / f"{name}.png"

# Command line
cmd = ["compare", "-metric", "MSE", output_file, reference.resolve().as_posix(), "null:"]
proc = subprocess.run(cmd, capture_output=True)

# Check if our regex pattern can find a match in the output text
search = COMPARE_RESULT.search(proc.stderr.decode())
if not search:
assert False, proc.stderr.decode()

# There's a match so let's convert to a float
diff = float(search.group(1))

# Verify that difference is within acceptable tolerance
assert diff < TOLERANCE

# Start the manager and map convenience functions
manager_nospawn.start(DecorationConfig)
manager_nospawn.take_screenshot = screenshot
manager_nospawn.assert_similar = assert_similar

yield manager_nospawn
Loading

0 comments on commit 9d3072a

Please sign in to comment.