Skip to content

Commit

Permalink
Add basic testing to qtile-extras
Browse files Browse the repository at this point in the history
For now, this just runs pytest but I'll add more (e.g. PEP8
etc.) later.
  • Loading branch information
elParaguayo committed Sep 3, 2021
1 parent 6681438 commit 65fb059
Show file tree
Hide file tree
Showing 14 changed files with 1,198 additions and 1 deletion.
89 changes: 89 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: ci

on:
push:
pull_request:

jobs:
build:
runs-on: ubuntu-20.04
name: "python ${{ matrix.python-version }}"
env:
WAYLAND: 1.19.0
WAYLAND_PROTOCOLS: 1.21
WLROOTS: 0.14.0
SEATD: 0.5.0
LIBDRM: 2.4.105
strategy:
matrix:
# if you change one of these, be sure to update
# /tox.ini:[gh-actions] as well
# python-version: [pypy-3.7, 3.7, 3.8, 3.9]
python-version: [3.9]
steps:
- uses: actions/checkout@v2
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install --no-install-recommends \
libdbus-1-dev libgirepository1.0-dev gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-gudev-1.0 graphviz \
imagemagick libpulse-dev lm-sensors git xserver-xephyr xterm xvfb ninja-build libegl1-mesa-dev \
libgles2-mesa-dev libgbm-dev libinput-dev libxkbcommon-dev libpixman-1-dev libpciaccess-dev \
dbus-x11 libnotify-bin
sudo pip -q install tox tox-gh-actions meson PyGObject
- name: Build wayland
run: |
wget -q --no-check-certificate https://wayland.freedesktop.org/releases/wayland-$WAYLAND.tar.xz
tar -xJf wayland-$WAYLAND.tar.xz
cd wayland-$WAYLAND
meson build -Ddocumentation=false --prefix=/usr
ninja -C build
sudo ninja -C build install
- name: Build wayland-protocols
run: |
wget -q --no-check-certificate https://wayland.freedesktop.org/releases/wayland-protocols-$WAYLAND_PROTOCOLS.tar.xz
tar -xJf wayland-protocols-$WAYLAND_PROTOCOLS.tar.xz
cd wayland-protocols-$WAYLAND_PROTOCOLS
meson build -Dtests=false --prefix=/usr
ninja -C build
sudo ninja -C build install
- name: Build seatd
run: |
wget -q --no-check-certificate https://git.sr.ht/~kennylevinsen/seatd/archive/$SEATD.tar.gz
tar -xzf $SEATD.tar.gz
cd seatd-$SEATD
meson build --prefix=/usr
ninja -C build
sudo ninja -C build install
- name: Build libdrm
run: |
wget -q --no-check-certificate https://gitlab.freedesktop.org/mesa/drm/-/archive/libdrm-$LIBDRM/drm-libdrm-$LIBDRM.tar.gz
tar -xzf drm-libdrm-$LIBDRM.tar.gz
cd drm-libdrm-$LIBDRM
meson build --prefix=/usr
ninja -C build
sudo ninja -C build install
- name: Build wlroots
run: |
wget -q --no-check-certificate https://github.com/swaywm/wlroots/archive/$WLROOTS.tar.gz
tar -xzf $WLROOTS.tar.gz
cd wlroots-$WLROOTS
meson build -Dexamples=false --prefix=/usr
ninja -C build
sudo ninja -C build install
# - name: Build qtile
# run: |
# pip install wheel
# pip install 'xcffib>=0.10.1'
# pip install --no-cache-dir cairocffi
# git clone git://github.com/qtile/qtile.git
# cd qtile
# pip install .
- name: run tests
run: |
# [ "$(grep -c -P '\t' CHANGELOG)" = "0" ]
tox -e py39
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ venv/
ENV/
env.bak/
venv.bak/
.vscode/

# Spyder project settings
.spyderproject
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
3 September 2021:
- First version
8 changes: 7 additions & 1 deletion qtile_extras/widget/alsavolumecontrol.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
import shutil
import subprocess

from libqtile.log_utils import logger
from libqtile.widget import base
from libqtile import bar, images

Expand All @@ -14,7 +16,7 @@ class ALSAWidget(base._Widget, base.PaddingMixin, base.MarginMixin):
defaults = [
("font", "sans", "Default font"),
("fontsize", None, "Font size"),
("mode", "both", "Display mode: 'icon', 'bar', 'both'."),
("mode", "bar", "Display mode: 'icon', 'bar', 'both'."),
("hide_interval", 5, "Timeout before bar is hidden after update"),
("text_format", "{volume}%", "String format"),
("bar_width", 75, "Width of display bar"),
Expand Down Expand Up @@ -253,6 +255,10 @@ def hide(self):

def _run(self, cmd):

if not shutil.which("amixer"):
logger.warning("'amixer' is not installed. Unable to set volume.")
return

# Run the amixer command and use regex to capture volume line
proc = subprocess.run(cmd.split(), capture_output=True)
matched = RE_VOL.search(proc.stdout.decode())
Expand Down
Empty file added test/__init__.py
Empty file.
59 changes: 59 additions & 0 deletions test/backend/wayland/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import contextlib
import os
import textwrap

from libqtile.backend.wayland.core import Core
from test.helpers import Backend

wlr_env = {
"WLR_BACKENDS": "headless",
"WLR_LIBINPUT_NO_DEVICES": "1",
"WLR_RENDERER_ALLOW_SOFTWARE": "1",
"WLR_RENDERER": "pixman",
"XDG_RUNTIME_DIR": "/tmp",
}


@contextlib.contextmanager
def wayland_environment(outputs):
"""This backend just needs some environmental variables set"""
env = wlr_env.copy()
env["WLR_HEADLESS_OUTPUTS"] = str(outputs)
yield env


class WaylandBackend(Backend):
name = "wayland"

def __init__(self, env, args=()):
self.env = env
self.args = args
self.core = Core
self.manager = None

def create(self):
"""This is used to instantiate the Core"""
os.environ.update(self.env)
return self.core(*self.args)

def configure(self, manager):
"""This backend needs to get WAYLAND_DISPLAY variable."""
success, display = manager.c.eval("self.core.display_name")
assert success
self.env["WAYLAND_DISPLAY"] = display

def fake_click(self, x, y):
"""Click at the specified coordinates"""
# Currently only restacks windows, and does not trigger bindings
self.manager.c.eval(textwrap.dedent(f"""
self.core.warp_pointer({x}, {y})
self.core._focus_by_click()
"""))

def get_all_windows(self):
"""Get a list of all windows in ascending order of Z position"""
success, result = self.manager.c.eval(textwrap.dedent("""
[win.wid for win in self.core.mapped_windows]
"""))
assert success
return eval(result)
Empty file added test/backend/x11/__init__.py
Empty file.
197 changes: 197 additions & 0 deletions test/backend/x11/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import contextlib
import os
import subprocess

import pytest
import xcffib
import xcffib.testing
import xcffib.xproto
import xcffib.xtest

from libqtile.backend.x11.core import Core
from libqtile.backend.x11.xcbq import Connection
from test.helpers import (
HEIGHT,
SECOND_HEIGHT,
SECOND_WIDTH,
WIDTH,
Backend,
BareConfig,
Retry,
TestManager,
)


@Retry(ignore_exceptions=(xcffib.ConnectionException,), return_on_fail=True)
def can_connect_x11(disp=':0', *, ok=None):
if ok is not None and not ok():
raise AssertionError()

conn = xcffib.connect(display=disp)
conn.disconnect()
return True


@contextlib.contextmanager
def xvfb():
with xcffib.testing.XvfbTest():
display = os.environ["DISPLAY"]
if not can_connect_x11(display):
raise OSError("Xvfb did not come up")

yield


@pytest.fixture(scope="session")
def display(): # noqa: F841
with xvfb():
yield os.environ["DISPLAY"]


class Xephyr:
"""Spawn Xephyr instance
Set-up a Xephyr instance with the given parameters. The Xephyr instance
must be started, and then stopped.
"""
def __init__(self,
outputs,
xoffset=None):

self.outputs = outputs
if xoffset is None:
self.xoffset = WIDTH
else:
self.xoffset = xoffset

self.proc = None # Handle to Xephyr instance, subprocess.Popen object
self.display = None
self.display_file = None

def __enter__(self):
try:
self.start_xephyr()
except: # noqa: E722
self.stop_xephyr()
raise

return self

def __exit__(self, _exc_type, _exc_val, _exc_tb):
self.stop_xephyr()

def start_xephyr(self):
"""Start Xephyr instance
Starts the Xephyr instance and sets the `self.display` to the display
which is used to setup the instance.
"""
# get a new display
display, self.display_file = xcffib.testing.find_display()
self.display = ":{}".format(display)

# build up arguments
args = [
"Xephyr",
"-name",
"qtile_test",
self.display,
"-ac",
"-screen",
"{}x{}".format(WIDTH, HEIGHT),
]
if self.outputs == 2:
args.extend(["-origin", "%s,0" % self.xoffset, "-screen",
"%sx%s" % (SECOND_WIDTH, SECOND_HEIGHT)])
args.extend(["+xinerama"])

self.proc = subprocess.Popen(args)

if can_connect_x11(self.display, ok=lambda: self.proc.poll() is None):
return

# we weren't able to get a display up
if self.proc.poll() is None:
raise AssertionError("Unable to connect to running Xephyr")
else:
raise AssertionError(
"Unable to start Xephyr, quit with return code "
f"{self.proc.returncode}"
)

def stop_xephyr(self):
"""Stop the Xephyr instance"""
# Xephyr must be started first
if self.proc is None:
return

# Kill xephyr only if it is running
if self.proc.poll() is None:
# We should always be able to kill xephyr nicely
self.proc.terminate()
self.proc.wait()

self.proc = None

# clean up the lock file for the display we allocated
try:
self.display_file.close()
os.remove(xcffib.testing.lock_path(int(self.display[1:])))
except OSError:
pass


@contextlib.contextmanager
def x11_environment(outputs, **kwargs):
"""This backend needs a Xephyr instance running"""
with xvfb():
with Xephyr(outputs, **kwargs) as x:
yield x


@pytest.fixture(scope="function")
def xmanager(request, xephyr):
"""
This replicates the `manager` fixture except that the x11 backend is hard-coded. We
cannot simply parametrize the `backend_name` fixture module-wide because it gets
parametrized by `pytest_generate_tests` in test/conftest.py and only one of these
parametrize calls can be used.
"""
config = getattr(request, "param", BareConfig)
backend = XBackend({"DISPLAY": xephyr.display}, args=[xephyr.display])

with TestManager(backend, request.config.getoption("--debuglog")) as manager:
manager.display = xephyr.display
manager.start(config)
yield manager


class XBackend(Backend):
name = "x11"

def __init__(self, env, args=()):
self.env = env
self.args = args
self.core = Core
self.manager = None

def fake_click(self, x, y):
"""Click at the specified coordinates"""
conn = Connection(self.env["DISPLAY"])
root = conn.default_screen.root.wid
xtest = conn.conn(xcffib.xtest.key)
xtest.FakeInput(6, 0, xcffib.xproto.Time.CurrentTime, root, x, y, 0)
xtest.FakeInput(4, 1, xcffib.xproto.Time.CurrentTime, root, 0, 0, 0)
xtest.FakeInput(5, 1, xcffib.xproto.Time.CurrentTime, root, 0, 0, 0)
conn.conn.flush()
self.manager.c.sync()
conn.finalize()

def get_all_windows(self):
"""Get a list of all windows in ascending order of Z position"""
conn = Connection(self.env["DISPLAY"])
root = conn.default_screen.root.wid
q = conn.conn.core.QueryTree(root).reply()
wins = list(q.children)
conn.finalize()
return wins
Loading

0 comments on commit 65fb059

Please sign in to comment.