summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authortoofar <toofar@spalge.com>2023-12-02 13:34:55 +1300
committertoofar <toofar@spalge.com>2023-12-02 13:36:14 +1300
commit23a168926262f2304652baee57b7793c87ffe359 (patch)
tree01806daeb5c9a4e1b9ee51400d46c4d6866c687f /tests
parent7d5acb697086c903d485679f3dc754777c8e6ef5 (diff)
parent75c78cadc4c7391df4654798b4b6de0c96007597 (diff)
downloadqutebrowser-23a168926262f2304652baee57b7793c87ffe359.tar.gz
qutebrowser-23a168926262f2304652baee57b7793c87ffe359.zip
Merge branch 'main' into pr/7992
This is just to get the CI to re-trigger. Apparently the checkout action doesn't actually checkout the branch being proposed for merge but checks out an actual merge commit. So if you push a PR while main is broken it'll say changes from main are on your branch. That's a little unexpected. main is fixed now and I tried re-running the CI jobs from the web UI but they are still failing with the same errors. Hence this merge of main just to get a change on the branch. I could have gone and found a typo to fix instead. I know I've left enough of them around. ref: https://github.com/actions/checkout/issues/881
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py7
-rw-r--r--tests/end2end/fixtures/quteprocess.py3
-rw-r--r--tests/end2end/test_invocations.py89
-rw-r--r--tests/unit/config/test_qtargs.py14
-rw-r--r--tests/unit/misc/test_elf.py6
-rw-r--r--tests/unit/misc/test_pakjoy.py441
6 files changed, 530 insertions, 30 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
index 2fea48c43..9d7c5c29c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -197,7 +197,12 @@ def qapp_args():
"""Make QtWebEngine unit tests run on older Qt versions + newer kernels."""
if testutils.disable_seccomp_bpf_sandbox():
return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG]
- return [sys.argv[0]]
+
+ # Disabling PaintHoldingCrossOrigin makes tests needing UI interaction with
+ # QtWebEngine more reliable.
+ # Only needed with QtWebEngine and Qt 6.5, but Qt just ignores arguments it
+ # doesn't know about anyways.
+ return [sys.argv[0], "--webEngineArgs", "--disable-features=PaintHoldingCrossOrigin"]
@pytest.fixture(scope='session')
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index a2f870e32..de9b490ca 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -390,7 +390,8 @@ class QuteProc(testprocess.Process):
'--json-logging', '--loglevel', 'vdebug',
'--backend', backend, '--debug-flag', 'no-sql-history',
'--debug-flag', 'werror', '--debug-flag',
- 'test-notification-service']
+ 'test-notification-service',
+ '--qt-flag', 'disable-features=PaintHoldingCrossOrigin']
if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox():
args += testutils.DISABLE_SECCOMP_BPF_ARGS
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index af81781f6..a55efb129 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -15,6 +15,7 @@ import re
import json
import platform
from contextlib import nullcontext as does_not_raise
+from unittest.mock import ANY
import pytest
from qutebrowser.qt.core import QProcess, QPoint
@@ -882,30 +883,86 @@ def test_sandboxing(
line.expected = True
pytest.skip("chrome://sandbox/ not supported")
+ if len(text.split("\n")) == 1:
+ # Try again, maybe the JS hasn't run yet?
+ text = quteproc_new.get_content()
+ print(text)
+
bpf_text = "Seccomp-BPF sandbox"
yama_text = "Ptrace Protection with Yama LSM"
- header, *lines, empty, result = text.split("\n")
- assert not empty
-
- expected_status = {
- "Layer 1 Sandbox": "Namespace" if has_namespaces else "None",
+ if not utils.is_windows:
+ header, *lines, empty, result = text.split("\n")
+ assert not empty
- "PID namespaces": "Yes" if has_namespaces else "No",
- "Network namespaces": "Yes" if has_namespaces else "No",
+ expected_status = {
+ "Layer 1 Sandbox": "Namespace" if has_namespaces else "None",
- bpf_text: "Yes" if has_seccomp else "No",
- f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No",
+ "PID namespaces": "Yes" if has_namespaces else "No",
+ "Network namespaces": "Yes" if has_namespaces else "No",
- f"{yama_text} (Broker)": "Yes" if has_yama else "No",
- f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No",
- }
+ bpf_text: "Yes" if has_seccomp else "No",
+ f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No",
- assert header == "Sandbox Status"
- assert result == expected_result
+ f"{yama_text} (Broker)": "Yes" if has_yama else "No",
+ f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No",
+ }
- status = dict(line.split("\t") for line in lines)
- assert status == expected_status
+ assert header == "Sandbox Status"
+ assert result == expected_result
+
+ status = dict(line.split("\t") for line in lines)
+ assert status == expected_status
+
+ else: # utils.is_windows
+ # The sandbox page on Windows if different that Linux and macOS. It's
+ # a lot more complex. There is a table up top with lots of columns and
+ # a row per tab and helper process then a json object per row down
+ # below with even more detail (which we ignore).
+ # https://www.chromium.org/Home/chromium-security/articles/chrome-sandbox-diagnostics-for-windows/
+
+ # We're not getting full coverage of the table and there doesn't seem
+ # to be a simple summary like for linux. The "Sandbox" and "Lockdown"
+ # column are probably the key ones.
+ # We are looking at all the rows in the table for the sake of
+ # completeness, but I expect there will always be just one row with a
+ # renderer process in it for this test. If other helper processes pop
+ # up we might want to exclude them.
+ lines = text.split("\n")
+ assert lines.pop(0) == "Sandbox Status"
+ header = lines.pop(0).split("\t")
+ rows = []
+ current_line = lines.pop(0)
+ while current_line.strip():
+ if lines[0].startswith("\t"):
+ # Continuation line. Not sure how to 100% identify them
+ # but new rows should start with a process ID.
+ current_line += lines.pop(0)
+ continue
+
+ columns = current_line.split("\t")
+ assert len(header) == len(columns)
+ rows.append(dict(zip(header, columns)))
+ current_line = lines.pop(0)
+
+ assert rows
+
+ # I'm using has_namespaces as a proxy for "should be sandboxed" here,
+ # which is a bit lazy but its either that or match on the text
+ # "sandboxing" arg. The seccomp-bpf arg does nothing on windows, so
+ # we only have the off and on states.
+ for row in rows:
+ assert row == {
+ "Process": ANY,
+ "Type": "Renderer",
+ "Name": "",
+ "Sandbox": "Renderer" if has_namespaces else "Not Sandboxed",
+ "Lockdown": "Lockdown" if has_namespaces else "",
+ "Integrity": ANY if has_namespaces else "",
+ "Mitigations": ANY if has_namespaces else "",
+ "Component Filter": ANY if has_namespaces else "",
+ "Lowbox/AppContainer": "",
+ }
@pytest.mark.not_frozen
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index 419faad12..2414d4ba9 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -156,14 +156,12 @@ class TestWebEngineArgs:
assert '--enable-in-process-stack-traces' not in args
@pytest.mark.parametrize(
- 'qt_version, qt6, value, has_arg',
+ 'qt6, value, has_arg',
[
- ('5.15.2', False, 'auto', False),
- ('6.5.3', True, 'auto', True),
- ('6.6.0', True, 'auto', False),
- ('6.5.3', True, 'always', True),
- ('6.5.3', True, 'never', False),
- ('6.6.0', True, 'always', True),
+ (False, 'auto', False),
+ (True, 'auto', True),
+ (True, 'always', True),
+ (True, 'never', False),
],
)
def test_accelerated_2d_canvas(
@@ -172,12 +170,10 @@ class TestWebEngineArgs:
version_patcher,
config_stub,
monkeypatch,
- qt_version,
qt6,
value,
has_arg,
):
- version_patcher(qt_version)
config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = value
monkeypatch.setattr(machinery, 'IS_QT6', qt6)
diff --git a/tests/unit/misc/test_elf.py b/tests/unit/misc/test_elf.py
index 88b984dcd..6ae23357c 100644
--- a/tests/unit/misc/test_elf.py
+++ b/tests/unit/misc/test_elf.py
@@ -9,7 +9,7 @@ import pytest
import hypothesis
from hypothesis import strategies as hst
-from qutebrowser.misc import elf
+from qutebrowser.misc import elf, binparsing
from qutebrowser.utils import utils
@@ -117,7 +117,7 @@ def test_find_versions(data, expected):
),
])
def test_find_versions_invalid(data, message):
- with pytest.raises(elf.ParseError) as excinfo:
+ with pytest.raises(binparsing.ParseError) as excinfo:
elf._find_versions(data)
assert str(excinfo.value) == message
@@ -132,5 +132,5 @@ def test_hypothesis(data):
fobj = io.BytesIO(data)
try:
elf._parse_from_file(fobj)
- except elf.ParseError as e:
+ except binparsing.ParseError as e:
print(e)
diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py
new file mode 100644
index 000000000..65d02ec7e
--- /dev/null
+++ b/tests/unit/misc/test_pakjoy.py
@@ -0,0 +1,441 @@
+# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) <mail@qutebrowser.org>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import os
+import io
+import json
+import struct
+import pathlib
+import logging
+import shutil
+
+import pytest
+
+from qutebrowser.misc import pakjoy, binparsing
+from qutebrowser.utils import utils, version, standarddir
+
+
+pytest.importorskip("qutebrowser.qt.webenginecore")
+
+
+pytestmark = pytest.mark.usefixtures("cache_tmpdir")
+
+
+versions = version.qtwebengine_versions(avoid_init=True)
+
+
+# Used to skip happy path tests with the real resources file.
+#
+# Since we don't know how reliably the Google Meet hangouts extensions is
+# reliably in the resource files, and this quirk is only targeting 6.6
+# anyway.
+skip_if_unsupported = pytest.mark.skipif(
+ versions.webengine != utils.VersionNumber(6, 6),
+ reason="Code under test only runs on 6.6",
+)
+
+
+@pytest.fixture(autouse=True)
+def prepare_env(qapp, monkeypatch):
+ monkeypatch.setattr(pakjoy.objects, "qapp", qapp)
+ monkeypatch.delenv(pakjoy.RESOURCES_ENV_VAR, raising=False)
+ monkeypatch.delenv(pakjoy.DISABLE_ENV_VAR, raising=False)
+
+
+def patch_version(monkeypatch, *args):
+ monkeypatch.setattr(
+ pakjoy.version,
+ "qtwebengine_versions",
+ lambda **kwargs: version.WebEngineVersions(
+ webengine=utils.VersionNumber(*args),
+ chromium=None,
+ source="unittest",
+ ),
+ )
+
+
+@pytest.fixture
+def unaffected_version(monkeypatch):
+ patch_version(monkeypatch, 6, 6, 1)
+
+
+@pytest.fixture
+def affected_version(monkeypatch):
+ patch_version(monkeypatch, 6, 6)
+
+
+@pytest.mark.parametrize("workdir_exists", [True, False])
+def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists):
+ workdir = cache_tmpdir / pakjoy.CACHE_DIR_NAME
+ if workdir_exists:
+ workdir.mkdir()
+ (workdir / "some_patched_file.pak").ensure()
+ fake_open = mocker.patch("qutebrowser.misc.pakjoy.open")
+
+ with pakjoy.patch_webengine():
+ pass
+
+ assert not fake_open.called
+ assert not workdir.exists()
+
+
+def test_escape_hatch(affected_version, mocker, monkeypatch):
+ fake_open = mocker.patch("qutebrowser.misc.pakjoy.open")
+ monkeypatch.setenv(pakjoy.DISABLE_ENV_VAR, "1")
+
+ with pakjoy.patch_webengine():
+ pass
+
+ assert not fake_open.called
+
+
+class TestFindWebengineResources:
+ @pytest.fixture
+ def qt_data_path(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path):
+ """Patch qtutils.library_path() to return a temp dir."""
+ qt_data_path = tmp_path / "qt_data"
+ qt_data_path.mkdir()
+ monkeypatch.setattr(pakjoy.qtutils, "library_path", lambda _which: qt_data_path)
+ return qt_data_path
+
+ @pytest.fixture
+ def application_dir_path(
+ self,
+ monkeypatch: pytest.MonkeyPatch,
+ tmp_path: pathlib.Path,
+ qt_data_path: pathlib.Path, # needs patching
+ ):
+ """Patch QApplication.applicationDirPath() to return a temp dir."""
+ app_dir_path = tmp_path / "app_dir"
+ app_dir_path.mkdir()
+ monkeypatch.setattr(
+ pakjoy.objects.qapp, "applicationDirPath", lambda: app_dir_path
+ )
+ return app_dir_path
+
+ @pytest.fixture
+ def fallback_path(
+ self,
+ monkeypatch: pytest.MonkeyPatch,
+ tmp_path: pathlib.Path,
+ qt_data_path: pathlib.Path, # needs patching
+ application_dir_path: pathlib.Path, # needs patching
+ ):
+ """Patch the fallback path to return a temp dir."""
+ home_path = tmp_path / "home"
+ monkeypatch.setattr(pakjoy.pathlib.Path, "home", lambda: home_path)
+
+ app_path = home_path / f".{pakjoy.objects.qapp.applicationName()}"
+ app_path.mkdir(parents=True)
+ return app_path
+
+ @pytest.mark.parametrize("create_file", [True, False])
+ def test_overridden(
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, create_file: bool
+ ):
+ """Test the overridden path is used."""
+ override_path = tmp_path / "override"
+ override_path.mkdir()
+ monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(override_path))
+ if create_file: # should get this no matter if file exists or not
+ (override_path / pakjoy.PAK_FILENAME).touch()
+ assert pakjoy._find_webengine_resources() == override_path
+
+ @pytest.mark.parametrize("with_subfolder", [True, False])
+ def test_qt_data_path(self, qt_data_path: pathlib.Path, with_subfolder: bool):
+ """Test qtutils.library_path() is used."""
+ resources_path = qt_data_path
+ if with_subfolder:
+ resources_path /= "resources"
+ resources_path.mkdir()
+ (resources_path / pakjoy.PAK_FILENAME).touch()
+ assert pakjoy._find_webengine_resources() == resources_path
+
+ def test_application_dir_path(self, application_dir_path: pathlib.Path):
+ """Test QApplication.applicationDirPath() is used."""
+ (application_dir_path / pakjoy.PAK_FILENAME).touch()
+ assert pakjoy._find_webengine_resources() == application_dir_path
+
+ def test_fallback_path(self, fallback_path: pathlib.Path):
+ """Test fallback path is used."""
+ (fallback_path / pakjoy.PAK_FILENAME).touch()
+ assert pakjoy._find_webengine_resources() == fallback_path
+
+ def test_nowhere(self, fallback_path: pathlib.Path):
+ """Test we raise if we can't find the resources."""
+ with pytest.raises(
+ binparsing.ParseError, match="Couldn't find webengine resources dir"
+ ):
+ pakjoy._find_webengine_resources()
+
+
+def json_without_comments(bytestring):
+ str_without_comments = "\n".join(
+ [
+ line
+ for line in bytestring.decode("utf-8").split("\n")
+ if not line.strip().startswith("//")
+ ]
+ )
+ return json.loads(str_without_comments)
+
+
+def read_patched_manifest():
+ patched_resources = pathlib.Path(os.environ[pakjoy.RESOURCES_ENV_VAR])
+
+ with open(patched_resources / pakjoy.PAK_FILENAME, "rb") as fd:
+ reparsed = pakjoy.PakParser(fd)
+
+ return json_without_comments(reparsed.manifest)
+
+
+@pytest.mark.usefixtures("affected_version")
+class TestWithRealResourcesFile:
+ """Tests that use the real pak file form the Qt installation."""
+
+ @skip_if_unsupported
+ def test_happy_path(self):
+ # Go through the full patching processes with the real resources file from
+ # the current installation. Make sure our replacement string is in it
+ # afterwards.
+ with pakjoy.patch_webengine():
+ json_manifest = read_patched_manifest()
+
+ assert (
+ pakjoy.REPLACEMENT_URL.decode("utf-8")
+ in json_manifest["externally_connectable"]["matches"]
+ )
+
+ def test_copying_resources(self):
+ # Test we managed to copy some files over
+ work_dir = pakjoy.copy_webengine_resources()
+
+ assert work_dir is not None
+ assert work_dir.exists()
+ assert work_dir == pathlib.Path(standarddir.cache()) / pakjoy.CACHE_DIR_NAME
+ assert (work_dir / pakjoy.PAK_FILENAME).exists()
+ assert len(list(work_dir.glob("*"))) > 1
+
+ def test_copying_resources_overwrites(self):
+ work_dir = pakjoy.copy_webengine_resources()
+ assert work_dir is not None
+ tmpfile = work_dir / "tmp.txt"
+ tmpfile.touch()
+
+ pakjoy.copy_webengine_resources()
+ assert not tmpfile.exists()
+
+ @pytest.mark.parametrize("osfunc", ["copytree", "rmtree"])
+ def test_copying_resources_oserror(self, monkeypatch, caplog, osfunc):
+ # Test errors from the calls to shutil are handled
+ pakjoy.copy_webengine_resources() # run twice so we hit rmtree too
+ caplog.clear()
+
+ def raiseme(err):
+ raise err
+
+ monkeypatch.setattr(
+ pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc))
+ )
+ with caplog.at_level(logging.ERROR, "misc"):
+ with pakjoy.patch_webengine():
+ pass
+
+ assert caplog.messages == [
+ "Failed to copy webengine resources, not applying quirk"
+ ]
+
+ def test_expected_file_not_found(self, cache_tmpdir, monkeypatch, caplog):
+ with caplog.at_level(logging.ERROR, "misc"):
+ pakjoy._patch(pathlib.Path(cache_tmpdir) / "doesntexist")
+ assert caplog.messages[-1].startswith(
+ "Resource pak doesn't exist at expected location! "
+ "Not applying quirks. Expected location: "
+ )
+
+
+def json_manifest_factory(extension_id=pakjoy.HANGOUTS_MARKER, url=pakjoy.TARGET_URL):
+ assert isinstance(extension_id, bytes)
+ assert isinstance(url, bytes)
+
+ return f"""
+ {{
+ {extension_id.decode("utf-8")}
+ "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAQt2ZDdPfoSe/JI6ID5bgLHRCnCu9T36aYczmhw/tnv6QZB2I6WnOCMZXJZlRdqWc7w9jo4BWhYS50Vb4weMfh/I0On7VcRwJUgfAxW2cHB+EkmtI1v4v/OU24OqIa1Nmv9uRVeX0GjhQukdLNhAE6ACWooaf5kqKlCeK+1GOkQIDAQAB",
+ "name": "Google Hangouts",
+ // Note: Always update the version number when this file is updated. Chrome
+ // triggers extension preferences update on the version increase.
+ "version": "1.3.21",
+ "manifest_version": 2,
+ "externally_connectable": {{
+ "matches": [
+ "{url.decode("utf-8")}",
+ "http://localhost:*/*"
+ ]
+ }}
+ }}
+ """.strip().encode(
+ "utf-8"
+ )
+
+
+def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1):
+ if entries is None:
+ entries = [json_manifest_factory()]
+
+ buffer = io.BytesIO()
+ buffer.write(struct.pack("<I", version))
+ buffer.write(struct.pack(pakjoy.PakHeader._FORMAT, encoding, len(entries), 0))
+
+ entry_headers_size = (len(entries) + 1) * 6
+ start_of_data = buffer.tell() + entry_headers_size
+
+ # Normally the sentinel sits between the headers and the data. But to get
+ # full coverage we want to insert it in other positions.
+ with_indices = list(enumerate(entries, 1))
+ if sentinel_position == -1:
+ with_indices.append((0, b""))
+ elif sentinel_position is not None:
+ with_indices.insert(sentinel_position, (0, b""))
+
+ accumulated_data_offset = start_of_data
+ for idx, entry in with_indices:
+ buffer.write(struct.pack(pakjoy.PakEntry._FORMAT, idx, accumulated_data_offset))
+ accumulated_data_offset += len(entry)
+
+ for entry in entries:
+ assert isinstance(entry, bytes)
+ buffer.write(entry)
+
+ buffer.seek(0)
+ return buffer
+
+
+@pytest.mark.usefixtures("affected_version")
+class TestWithConstructedResourcesFile:
+ """Tests that use a constructed pak file to give us more control over it."""
+
+ @pytest.mark.parametrize(
+ "offset",
+ [0, 42, pakjoy.HANGOUTS_ID], # test both slow search and fast path
+ )
+ def test_happy_path(self, offset):
+ entries = [b""] * offset + [json_manifest_factory()]
+ assert entries[offset] != b""
+ buffer = pak_factory(entries=entries)
+
+ parser = pakjoy.PakParser(buffer)
+
+ json_manifest = json_without_comments(parser.manifest)
+
+ assert (
+ pakjoy.TARGET_URL.decode("utf-8")
+ in json_manifest["externally_connectable"]["matches"]
+ )
+
+ def test_bad_version(self):
+ buffer = pak_factory(version=99)
+
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Unsupported .pak version 99",
+ ):
+ pakjoy.PakParser(buffer)
+
+ @pytest.mark.parametrize(
+ "position, error",
+ [
+ (0, "Unexpected sentinel entry"),
+ (None, "Missing sentinel entry"),
+ ],
+ )
+ def test_bad_sentinal_position(self, position, error):
+ buffer = pak_factory(sentinel_position=position)
+
+ with pytest.raises(binparsing.ParseError):
+ pakjoy.PakParser(buffer)
+
+ @pytest.mark.parametrize(
+ "entry",
+ [
+ b"{foo}",
+ b"V2VsbCBoZWxsbyB0aGVyZQo=",
+ ],
+ )
+ def test_marker_not_found(self, entry):
+ buffer = pak_factory(entries=[entry])
+
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Couldn't find hangouts manifest",
+ ):
+ pakjoy.PakParser(buffer)
+
+ def test_url_not_found(self):
+ buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")])
+
+ parser = pakjoy.PakParser(buffer)
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Couldn't find URL in manifest",
+ ):
+ parser.find_patch_offset()
+
+ def test_url_not_found_high_level(self, cache_tmpdir, caplog, affected_version):
+ buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")])
+
+ # Write bytes to file so we can test pakjoy._patch()
+ tmpfile = pathlib.Path(cache_tmpdir) / "bad.pak"
+ with open(tmpfile, "wb") as fd:
+ fd.write(buffer.read())
+
+ with caplog.at_level(logging.ERROR, "misc"):
+ pakjoy._patch(tmpfile)
+
+ assert caplog.messages == ["Failed to apply quirk to resources pak."]
+
+ @pytest.fixture
+ def resources_path(
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
+ ) -> pathlib.Path:
+ resources_path = tmp_path / "resources"
+ resources_path.mkdir()
+
+ buffer = pak_factory()
+ with open(resources_path / pakjoy.PAK_FILENAME, "wb") as fd:
+ fd.write(buffer.read())
+
+ monkeypatch.setattr(pakjoy.qtutils, "library_path", lambda _which: tmp_path)
+ return resources_path
+
+ @pytest.fixture
+ def quirk_dir_path(self, tmp_path: pathlib.Path) -> pathlib.Path:
+ return tmp_path / "cache" / pakjoy.CACHE_DIR_NAME
+
+ def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path):
+ """Go through the full patching processes with a fake resources file."""
+ with pakjoy.patch_webengine():
+ assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path)
+ json_manifest = read_patched_manifest()
+
+ assert (
+ pakjoy.REPLACEMENT_URL.decode("utf-8")
+ in json_manifest["externally_connectable"]["matches"]
+ )
+ assert pakjoy.RESOURCES_ENV_VAR not in os.environ
+
+ def test_preset_env_var(
+ self,
+ resources_path: pathlib.Path,
+ monkeypatch: pytest.MonkeyPatch,
+ quirk_dir_path: pathlib.Path,
+ ):
+ new_resources_path = resources_path.with_name(resources_path.name + "_moved")
+ shutil.move(resources_path, new_resources_path)
+ monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(new_resources_path))
+
+ with pakjoy.patch_webengine():
+ assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path)
+
+ assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(new_resources_path)