Merge pull request #8243 from feat/e2e_screenshots

This commit is contained in:
toofar 2024-10-05 11:30:57 +13:00
commit 3bc30e748d
14 changed files with 154 additions and 15 deletions

View File

@ -27,6 +27,7 @@ jobs:
PY_COLORS: "1"
DOCKER: "${{ matrix.image }}"
CI: true
TMPDIR: "${{ runner.temp }}"
volumes:
# Hardcoded because we can't use ${{ runner.temp }} here apparently.
- /home/runner/work/_temp/:/home/runner/work/_temp/
@ -42,6 +43,21 @@ jobs:
if: "endsWith(matrix.image, '-qt6')"
- name: Run tox
run: dbus-run-session tox -e ${{ matrix.testenv }}
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |
${{ runner.temp }}/pytest-of-user/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
irc:
timeout-minutes: 2
continue-on-error: true

View File

@ -107,6 +107,7 @@ jobs:
DOCKER: "${{ matrix.image }}"
CI: true
PYTEST_ADDOPTS: "--color=yes"
TMPDIR: "${{ runner.temp }}"
volumes:
# Hardcoded because we can't use ${{ runner.temp }} here apparently.
- /home/runner/work/_temp/:/home/runner/work/_temp/
@ -119,6 +120,21 @@ jobs:
run: "python scripts/dev/ci/problemmatchers.py tests ${{ runner.temp }}"
- name: Run tox
run: "dbus-run-session -- tox -e ${{ matrix.testenv }}"
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.image }}"
path: |
${{ runner.temp }}/pytest-of-user/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@ -222,6 +238,8 @@ jobs:
- name: Upgrade 3rd party assets
run: "tox exec -e ${{ matrix.testenv }} -- python scripts/dev/update_3rdparty.py --gh-token ${{ secrets.GITHUB_TOKEN }}"
if: "startsWith(matrix.os, 'windows-')"
- name: "Set TMPDIR for pytest"
run: 'echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"'
- name: "Run ${{ matrix.testenv }}"
run: "dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}"
if: "startsWith(matrix.os, 'ubuntu-')"
@ -236,6 +254,21 @@ jobs:
uses: codecov/codecov-action@v3
with:
name: "${{ matrix.testenv }}"
- name: Gather info
id: info
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash
if: failure()
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: "end2end-screenshots-${{ steps.info.outputs.date }}-${{ steps.info.outputs.sha_short }}-${{ matrix.testenv }}-${{ matrix.os }}"
path: |
${{ runner.temp }}/pytest-of-runner/pytest-current/pytest-screenshots/*.png
if-no-files-found: ignore
if: failure()
codeql:
if: "!contains(github.event.head_commit.message, '[ci skip]')"

View File

@ -30,6 +30,10 @@ Removed
- Support for macOS 11 Big Sur is dropped. Binaries are now built on macOS 12
Monterey and are unlikely to still run on older macOS versions.
- Failed end2end tests will now save screenshots of the browser window when
run under xvfb (the default on linux). Screenshots will be under
`$TEMP/pytest-current/pytest-screenshots/` or attached to the GitHub actions
run as an artifact. (#7625)
Changed
~~~~~~~

View File

@ -20,6 +20,7 @@ git+https://github.com/pygments/pygments.git
git+https://github.com/pytest-dev/pytest-repeat.git
git+https://github.com/pytest-dev/pytest-cov.git
git+https://github.com/The-Compiler/pytest-xvfb.git
git+https://github.com/python-pillow/Pillow.git
git+https://github.com/pytest-dev/pytest-xdist.git
git+https://github.com/john-kurkowski/tldextract

View File

@ -35,6 +35,7 @@ packaging==24.1
parse==1.20.2
parse_type==0.6.3
platformdirs==4.3.6
pillow==10.4.0
pluggy==1.5.0
py-cpuinfo==9.0.0
Pygments==2.18.0

View File

@ -25,6 +25,7 @@ pytest-cov
# To avoid windows from popping up
pytest-xvfb
PyVirtualDisplay
pillow
# To run on multiple cores with -n
pytest-xdist

View File

@ -86,3 +86,4 @@ filterwarnings =
# Python 3.12: https://github.com/ionelmc/pytest-benchmark/issues/240 (fixed but not released)
ignore:(datetime\.)?datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version\. Use timezone-aware objects to represent datetimes in UTC. (datetime\.)?datetime\.now\(datetime\.UTC\)\.:DeprecationWarning:pytest_benchmark\.utils
faulthandler_timeout = 90
xvfb_colordepth = 24

View File

@ -10,6 +10,7 @@ import sys
import contextlib
import enum
import argparse
import tempfile
from typing import Iterator, Optional, Dict
from qutebrowser.qt.core import QStandardPaths
@ -311,14 +312,15 @@ def _create(path: str) -> None:
0700. If the destination directory exists already the permissions
should not be changed.
"""
if APPNAME == 'qute_test' and path.startswith('/home'): # pragma: no cover
for k, v in os.environ.items():
if k == 'HOME' or k.startswith('XDG_'):
log.init.debug(f"{k} = {v}")
raise AssertionError(
"Trying to create directory inside /home during "
"tests, this should not happen."
)
if APPNAME == 'qute_test':
if path.startswith('/home') and not path.startswith(tempfile.gettempdir()): # pragma: no cover
for k, v in os.environ.items():
if k == 'HOME' or k.startswith('XDG_'):
log.init.debug(f"{k} = {v}")
raise AssertionError(
"Trying to create directory inside /home during "
"tests, this should not happen."
)
os.makedirs(path, 0o700, exist_ok=True)

View File

@ -168,5 +168,6 @@
"mdurl": "https://github.com/executablebooks/mdurl/commits/master",
"blinker": "https://blinker.readthedocs.io/en/stable/#changes",
"exceptiongroup": "https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst",
"nh3": "https://github.com/messense/nh3/commits/main"
"nh3": "https://github.com/messense/nh3/commits/main",
"pillow": "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
}

View File

@ -372,7 +372,8 @@ def pytest_runtest_makereport(item, call):
@pytest.hookimpl(hookwrapper=True)
def pytest_terminal_summary(terminalreporter):
"""Group benchmark results on CI."""
"""Add custom pytest summary sections."""
# Group benchmark results on CI.
if testutils.ON_CI:
terminalreporter.write_line(
testutils.gha_group_begin('Benchmark results'))
@ -380,3 +381,21 @@ def pytest_terminal_summary(terminalreporter):
terminalreporter.write_line(testutils.gha_group_end())
else:
yield
# List any screenshots of failed end2end tests that were generated during
# the run. Screenshots are captured from QuteProc.after_test()
properties = lambda report: dict(report.user_properties)
reports = [
report
for report in terminalreporter.getreports("")
if "screenshot" in properties(report)
]
screenshots = [
pathlib.Path(properties(report)["screenshot"])
for report in reports
]
if screenshots:
terminalreporter.ensure_newline()
screenshot_dir = screenshots[0].parent
terminalreporter.section(f"End2end screenshots available in: {screenshot_dir}", sep="-", blue=True, bold=True)

View File

@ -18,10 +18,15 @@ from qutebrowser.qt.core import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
# pylint: disable=unused-import
# Import fixtures that the bdd tests rely on.
from end2end.fixtures.notificationserver import notification_server
from end2end.fixtures.webserver import server, server_per_test, server2, ssl_server
from end2end.fixtures.quteprocess import (quteproc_process, quteproc,
quteproc_new)
from end2end.fixtures.quteprocess import (
quteproc_process, quteproc,
quteproc_new,
screenshot_dir,
take_x11_screenshot,
)
from end2end.fixtures.testprocess import pytest_runtest_makereport
# pylint: enable=unused-import
from qutebrowser.utils import qtutils, utils, version

View File

@ -5,6 +5,7 @@
"""Fixtures to run qutebrowser in a QProcess and communicate."""
import pathlib
import os
import re
import sys
import time
@ -18,6 +19,7 @@ import json
import yaml
import pytest
from PIL.ImageGrab import grab
from qutebrowser.qt.core import pyqtSignal, QUrl, QPoint
from qutebrowser.qt.gui import QImage, QColor
@ -416,7 +418,8 @@ class QuteProc(testprocess.Process):
'--debug-flag', 'werror',
'--debug-flag', 'test-notification-service',
'--debug-flag', 'caret',
'--qt-flag', 'disable-features=PaintHoldingCrossOrigin']
'--qt-flag', 'disable-features=PaintHoldingCrossOrigin',
'--qt-arg', 'geometry', '800x600+0+0']
if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox():
args += testutils.DISABLE_SECCOMP_BPF_ARGS
@ -545,6 +548,8 @@ class QuteProc(testprocess.Process):
except AttributeError:
pass
else:
if call.failed:
self._take_x11_screenshot_of_failed_test()
if call.failed or hasattr(call, 'wasxfail') or call.skipped:
super().after_test()
return
@ -872,6 +877,10 @@ class QuteProc(testprocess.Process):
self.send_cmd(cmd.format('no-scroll-filtering'))
self.send_cmd(cmd.format('log-scroll-pos'))
def _take_x11_screenshot_of_failed_test(self):
fixture = self.request.getfixturevalue('take_x11_screenshot')
fixture()
class YamlLoader(yaml.SafeLoader):
@ -912,6 +921,39 @@ def _xpath_escape(text):
return 'concat({})'.format(', '.join(parts))
@pytest.fixture
def screenshot_dir(request, tmp_path_factory):
"""Return the path of a directory to save e2e screenshots in."""
path = tmp_path_factory.getbasetemp()
if "PYTEST_XDIST_WORKER" in os.environ:
# If we are running under xdist remove the per-worker directory
# (like "popen-gw0") so the user doesn't have to search through
# multiple folders for the screenshot they are looking for.
path = path.parent
path /= "pytest-screenshots"
path.mkdir(exist_ok=True)
return path
@pytest.fixture
def take_x11_screenshot(request, screenshot_dir, record_property, xvfb):
"""Take a screenshot of the current pytest-xvfb display.
Screenshots are saved to the location of the `screenshot_dir` fixture.
"""
def doit():
if not xvfb:
# Likely we are being run with --no-xvfb
return
img = grab(xdisplay=f":{xvfb.display}")
fpath = screenshot_dir / f"{request.node.name}.png"
img.save(fpath)
record_property("screenshot", str(fpath))
return doit
@pytest.fixture(scope='module')
def quteproc_process(qapp, server, request):
"""Fixture for qutebrowser process which is started once per file."""
@ -923,7 +965,7 @@ def quteproc_process(qapp, server, request):
@pytest.fixture
def quteproc(quteproc_process, server, request):
def quteproc(quteproc_process, server, request, take_x11_screenshot):
"""Per-test qutebrowser fixture which uses the per-file process."""
request.node._quteproc_log = quteproc_process.captured_log
quteproc_process.before_test()

View File

@ -98,8 +98,9 @@ def test_quteproc_error_message(qtbot, quteproc, cmd, request_mock):
quteproc.after_test()
def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock):
def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock, monkeypatch):
"""Make sure the test does not fail on teardown if the main test failed."""
monkeypatch.setattr(quteproc, "_take_x11_screenshot_of_failed_test", lambda: None)
request_mock.node.rep_call.failed = True
with qtbot.wait_signal(quteproc.got_error):
quteproc.send_cmd(':message-error test')
@ -108,6 +109,17 @@ def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock):
quteproc.after_test()
def test_quteproc_screenshot_on_fail(qtbot, quteproc, request_mock, monkeypatch, mocker):
"""Make sure we call the method to take a screenshot to test failure."""
take_screenshot_spy = mocker.Mock()
monkeypatch.setattr(
quteproc, "_take_x11_screenshot_of_failed_test", take_screenshot_spy
)
request_mock.node.rep_call.failed = True
quteproc.after_test()
take_screenshot_spy.assert_called_once()
def test_quteproc_skip_via_js(qtbot, quteproc):
with pytest.raises(pytest.skip.Exception, match='test'):
quteproc.send_cmd(':jseval console.log("[SKIP] test");')

View File

@ -31,6 +31,7 @@ passenv =
QT_QUICK_BACKEND
FORCE_COLOR
DBUS_SESSION_BUS_ADDRESS
RUNNER_TEMP
HYPOTHESIS_EXAMPLES_DIR
basepython =
py: {env:PYTHON:python3}