From 9bad001c041d8fbfc47db9a76924661a165d2ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Wed, 22 May 2024 15:29:56 +0200 Subject: [PATCH] chore: remove fixture imports in the tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They ideally should find their way by themselves. > You don’t need to import the fixture you want to use in a test, > it automatically gets discovered by pytest. The discovery of fixture > functions starts at test classes, then test modules, then conftest.py > files and finally builtin and third party plugins.> > > — [pytest docs](https://docs.pytest.org/en/4.6.x/fixture.html#conftest-py-sharing-fixture-functions) --- tests/__init__.py | 150 --------------------- tests/conftest.py | 150 +++++++++++++++++++++ tests/gui/__init__.py | 54 -------- tests/gui/conftest.py | 54 ++++++++ tests/gui/test_main_window.py | 2 - tests/gui/test_updater.py | 2 +- tests/isolation_provider/base.py | 9 -- tests/isolation_provider/test_container.py | 9 -- tests/isolation_provider/test_qubes.py | 9 -- tests/test_cli.py | 10 +- tests/test_document.py | 2 - tests/test_large_set.py | 1 - tests/test_util.py | 2 - 13 files changed, 206 insertions(+), 248 deletions(-) create mode 100644 tests/gui/conftest.py diff --git a/tests/__init__.py b/tests/__init__.py index 4985667..44edd95 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,153 +1,3 @@ -import platform import sys -import zipfile -from pathlib import Path -from typing import Callable, List - -import pytest - -from dangerzone.document import SAFE_EXTENSION sys.dangerzone_dev = True # type: ignore[attr-defined] - - -SAMPLE_DIRECTORY = "test_docs" -BASIC_SAMPLE_PDF = "sample-pdf.pdf" -BASIC_SAMPLE_DOC = "sample-doc.doc" -SAMPLE_EXTERNAL_DIRECTORY = "test_docs_external" -SAMPLE_COMPRESSED_DIRECTORY = "test_docs_compressed" - -test_docs_dir = Path(__file__).parent.joinpath(SAMPLE_DIRECTORY) -test_docs_compressed_dir = Path(__file__).parent.joinpath(SAMPLE_COMPRESSED_DIRECTORY) - -test_docs = [ - p - for p in test_docs_dir.rglob("*") - if p.is_file() - and not (p.name.endswith(SAFE_EXTENSION) or p.name.startswith("sample_bad")) -] - -# Pytest parameter decorators -for_each_doc = pytest.mark.parametrize( - "doc", test_docs, ids=[str(doc.name) for doc in test_docs] -) - - -@pytest.fixture -def sample_pdf() -> str: - return str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) - - -# External Docs - base64 docs encoded for externally sourced documents -# XXX to reduce the chance of accidentally opening them -test_docs_external_dir = Path(__file__).parent.joinpath(SAMPLE_EXTERNAL_DIRECTORY) - - -@pytest.fixture -def sample_doc() -> str: - return str(test_docs_dir.joinpath(BASIC_SAMPLE_DOC)) - - -@pytest.fixture -def sample_bad_height() -> str: - return str(test_docs_dir.joinpath("sample_bad_max_height.pdf")) - - -@pytest.fixture -def sample_bad_width() -> str: - return str(test_docs_dir.joinpath("sample_bad_max_width.pdf")) - - -def get_docs_external(pattern: str = "*") -> List[Path]: - if not pattern.endswith("*"): - pattern = f"{pattern}.b64" - return [ - p - for p in test_docs_external_dir.rglob(pattern) - if p.is_file() and not (p.name.endswith(SAFE_EXTENSION)) - ] - - -# Pytest parameter decorators -def for_each_external_doc(glob_pattern: str = "*") -> Callable: - test_docs_external = get_docs_external(glob_pattern) - return pytest.mark.parametrize( - "doc", - test_docs_external, - ids=[str(doc.name).rstrip(".b64") for doc in test_docs_external], - ) - - -class TestBase: - sample_doc = str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) - - -@pytest.fixture -def unreadable_pdf(tmp_path: Path) -> str: - file_path = tmp_path / "document.pdf" - file_path.touch(mode=0o000) - return str(file_path) - - -@pytest.fixture -def pdf_11k_pages(tmp_path: Path) -> str: - """11K page document with pages of 1x1 px. Generated with the command: - - gs -sDEVICE=pdfwrite -o sample-11k-pages.pdf -dDEVICEWIDTHPOINTS=1 -dDEVICEHEIGHTPOINTS=1 -c 11000 {showpage} repeat - """ - - filename = "sample-11k-pages.pdf" - zip_path = test_docs_compressed_dir / f"{filename}.zip" - with zipfile.ZipFile(zip_path, "r") as zip_file: - zip_file.extractall(tmp_path) - return str(tmp_path / filename) - - -@pytest.fixture -def uncommon_text() -> str: - """Craft a string with Unicode characters that are considered not common. - - Create a string that contains the following uncommon characters: - - * ANSI escape sequences: \033[31;1;4m and \033[0m - * A Unicode character that resembles an English character: greek "X" (U+03A7) - * A Unicode control character that is not part of ASCII: zero-width joiner - (U+200D) - * An emoji: Cross Mark (U+274C) - * A surrogate escape used to decode an invalid UTF-8 sequence 0xF0 (U+DCF0) - """ - return "\033[31;1;4m BaD TeΧt \u200d ❌ \udcf0 \033[0m" - - -@pytest.fixture -def uncommon_filename(uncommon_text: str) -> str: - """Craft a filename with Unicode characters that are considered not common. - - We reuse the same uncommon string as above, with a small exception for macOS and - Windows. - - Because the NTFS filesystem in Windows and APFS filesystem in macOS accept only - UTF-8 encoded strings [1], we cannot create a filename with invalid Unicode - characters. So, in order to test the rest of the corner cases, we replace U+DCF0 - with an empty string. - - Windows has the extra restriction that it cannot have escape characters in - filenames, so we replace the ASCII Escape character (\033 / U+001B) as well. - - [1]: https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations - """ - if platform.system() == "Darwin": - uncommon_text = uncommon_text.replace("\udcf0", "") - elif platform.system() == "Windows": - uncommon_text = uncommon_text.replace("\udcf0", "").replace("\033", "") - return uncommon_text + ".pdf" - - -@pytest.fixture -def sanitized_text() -> str: - """Return a sanitized version of the uncommon_text. - - Take the uncommon text string and replace all the control/invalid characters with - "�". The rest of the characters (emojis and non-English leters) are retained as is. - """ - return "�[31;1;4m BaD TeΧt � ❌ � �[0m" diff --git a/tests/conftest.py b/tests/conftest.py index 84fcc1e..3bef1af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,162 @@ +import platform +import sys import typing +import zipfile +from pathlib import Path +from typing import Callable, List import pytest +from dangerzone.document import SAFE_EXTENSION from dangerzone.gui import Application +sys.dangerzone_dev = True # type: ignore[attr-defined] + # Use this fixture to make `pytest-qt` invoke our custom QApplication. # See https://pytest-qt.readthedocs.io/en/latest/qapplication.html#testing-custom-qapplications @pytest.fixture(scope="session") def qapp_cls() -> typing.Type[Application]: return Application + + +@pytest.fixture +def unreadable_pdf(tmp_path: Path) -> str: + file_path = tmp_path / "document.pdf" + file_path.touch(mode=0o000) + return str(file_path) + + +@pytest.fixture +def pdf_11k_pages(tmp_path: Path) -> str: + """11K page document with pages of 1x1 px. Generated with the command: + + gs -sDEVICE=pdfwrite -o sample-11k-pages.pdf -dDEVICEWIDTHPOINTS=1 -dDEVICEHEIGHTPOINTS=1 -c 11000 {showpage} repeat + """ + + filename = "sample-11k-pages.pdf" + zip_path = test_docs_compressed_dir / f"{filename}.zip" + with zipfile.ZipFile(zip_path, "r") as zip_file: + zip_file.extractall(tmp_path) + return str(tmp_path / filename) + + +@pytest.fixture +def uncommon_text() -> str: + """Craft a string with Unicode characters that are considered not common. + + Create a string that contains the following uncommon characters: + + * ANSI escape sequences: \033[31;1;4m and \033[0m + * A Unicode character that resembles an English character: greek "X" (U+03A7) + * A Unicode control character that is not part of ASCII: zero-width joiner + (U+200D) + * An emoji: Cross Mark (U+274C) + * A surrogate escape used to decode an invalid UTF-8 sequence 0xF0 (U+DCF0) + """ + return "\033[31;1;4m BaD TeΧt \u200d ❌ \udcf0 \033[0m" + + +@pytest.fixture +def uncommon_filename(uncommon_text: str) -> str: + """Craft a filename with Unicode characters that are considered not common. + + We reuse the same uncommon string as above, with a small exception for macOS and + Windows. + + Because the NTFS filesystem in Windows and APFS filesystem in macOS accept only + UTF-8 encoded strings [1], we cannot create a filename with invalid Unicode + characters. So, in order to test the rest of the corner cases, we replace U+DCF0 + with an empty string. + + Windows has the extra restriction that it cannot have escape characters in + filenames, so we replace the ASCII Escape character (\033 / U+001B) as well. + + [1]: https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations + """ + if platform.system() == "Darwin": + uncommon_text = uncommon_text.replace("\udcf0", "") + elif platform.system() == "Windows": + uncommon_text = uncommon_text.replace("\udcf0", "").replace("\033", "") + return uncommon_text + ".pdf" + + +@pytest.fixture +def sanitized_text() -> str: + """Return a sanitized version of the uncommon_text. + + Take the uncommon text string and replace all the control/invalid characters with + "�". The rest of the characters (emojis and non-English leters) are retained as is. + """ + return "�[31;1;4m BaD TeΧt � ❌ � �[0m" + + +@pytest.fixture +def sample_doc() -> str: + return str(test_docs_dir.joinpath(BASIC_SAMPLE_DOC)) + + +@pytest.fixture +def sample_bad_height() -> str: + return str(test_docs_dir.joinpath("sample_bad_max_height.pdf")) + + +@pytest.fixture +def sample_bad_width() -> str: + return str(test_docs_dir.joinpath("sample_bad_max_width.pdf")) + + +@pytest.fixture +def sample_pdf() -> str: + return str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) + + +SAMPLE_DIRECTORY = "test_docs" +BASIC_SAMPLE_PDF = "sample-pdf.pdf" +BASIC_SAMPLE_DOC = "sample-doc.doc" +SAMPLE_EXTERNAL_DIRECTORY = "test_docs_external" +SAMPLE_COMPRESSED_DIRECTORY = "test_docs_compressed" + +test_docs_dir = Path(__file__).parent.joinpath(SAMPLE_DIRECTORY) +test_docs_compressed_dir = Path(__file__).parent.joinpath(SAMPLE_COMPRESSED_DIRECTORY) + +test_docs = [ + p + for p in test_docs_dir.rglob("*") + if p.is_file() + and not (p.name.endswith(SAFE_EXTENSION) or p.name.startswith("sample_bad")) +] + +# Pytest parameter decorators +for_each_doc = pytest.mark.parametrize( + "doc", test_docs, ids=[str(doc.name) for doc in test_docs] +) + + +# External Docs - base64 docs encoded for externally sourced documents +# XXX to reduce the chance of accidentally opening them +test_docs_external_dir = Path(__file__).parent.joinpath(SAMPLE_EXTERNAL_DIRECTORY) + + +def get_docs_external(pattern: str = "*") -> List[Path]: + if not pattern.endswith("*"): + pattern = f"{pattern}.b64" + return [ + p + for p in test_docs_external_dir.rglob(pattern) + if p.is_file() and not (p.name.endswith(SAFE_EXTENSION)) + ] + + +# Pytest parameter decorators +def for_each_external_doc(glob_pattern: str = "*") -> Callable: + test_docs_external = get_docs_external(glob_pattern) + return pytest.mark.parametrize( + "doc", + test_docs_external, + ids=[str(doc.name).rstrip(".b64") for doc in test_docs_external], + ) + + +class TestBase: + sample_doc = str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) diff --git a/tests/gui/__init__.py b/tests/gui/__init__.py index cbf49de..e69de29 100644 --- a/tests/gui/__init__.py +++ b/tests/gui/__init__.py @@ -1,54 +0,0 @@ -from pathlib import Path -from typing import Optional - -import pytest -from pytest import MonkeyPatch -from pytest_mock import MockerFixture - -from dangerzone import util -from dangerzone.gui import Application -from dangerzone.gui.logic import DangerzoneGui -from dangerzone.gui.updater import UpdaterThread -from dangerzone.isolation_provider.dummy import Dummy - - -def get_qt_app() -> Application: - if Application.instance() is None: # type: ignore [call-arg] - return Application() - else: - return Application.instance() # type: ignore [call-arg] - - -def generate_isolated_updater( - tmp_path: Path, - monkeypatch: MonkeyPatch, - app_mocker: Optional[MockerFixture] = None, -) -> UpdaterThread: - """Generate an Updater class with its own settings.""" - if app_mocker: - app = app_mocker.MagicMock() - else: - app = get_qt_app() - - dummy = Dummy() - # XXX: We can monkey-patch global state without wrapping it in a context manager, or - # worrying that it will leak between tests, for two reasons: - # - # 1. Parallel tests in PyTest take place in different processes. - # 2. The monkeypatch fixture tears down the monkey-patch after each test ends. - monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path) - dangerzone = DangerzoneGui(app, isolation_provider=dummy) - updater = UpdaterThread(dangerzone) - return updater - - -@pytest.fixture -def updater( - tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture -) -> UpdaterThread: - return generate_isolated_updater(tmp_path, monkeypatch, mocker) - - -@pytest.fixture -def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread: - return generate_isolated_updater(tmp_path, monkeypatch) diff --git a/tests/gui/conftest.py b/tests/gui/conftest.py new file mode 100644 index 0000000..cbf49de --- /dev/null +++ b/tests/gui/conftest.py @@ -0,0 +1,54 @@ +from pathlib import Path +from typing import Optional + +import pytest +from pytest import MonkeyPatch +from pytest_mock import MockerFixture + +from dangerzone import util +from dangerzone.gui import Application +from dangerzone.gui.logic import DangerzoneGui +from dangerzone.gui.updater import UpdaterThread +from dangerzone.isolation_provider.dummy import Dummy + + +def get_qt_app() -> Application: + if Application.instance() is None: # type: ignore [call-arg] + return Application() + else: + return Application.instance() # type: ignore [call-arg] + + +def generate_isolated_updater( + tmp_path: Path, + monkeypatch: MonkeyPatch, + app_mocker: Optional[MockerFixture] = None, +) -> UpdaterThread: + """Generate an Updater class with its own settings.""" + if app_mocker: + app = app_mocker.MagicMock() + else: + app = get_qt_app() + + dummy = Dummy() + # XXX: We can monkey-patch global state without wrapping it in a context manager, or + # worrying that it will leak between tests, for two reasons: + # + # 1. Parallel tests in PyTest take place in different processes. + # 2. The monkeypatch fixture tears down the monkey-patch after each test ends. + monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path) + dangerzone = DangerzoneGui(app, isolation_provider=dummy) + updater = UpdaterThread(dangerzone) + return updater + + +@pytest.fixture +def updater( + tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture +) -> UpdaterThread: + return generate_isolated_updater(tmp_path, monkeypatch, mocker) + + +@pytest.fixture +def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread: + return generate_isolated_updater(tmp_path, monkeypatch) diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index 23e0da9..1d0b018 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -15,8 +15,6 @@ from dangerzone.gui.logic import DangerzoneGui from dangerzone.gui.main_window import ContentWidget from dangerzone.gui.updater import UpdateReport, UpdaterThread -from .. import sample_doc, sample_pdf -from . import qt_updater as updater from .test_updater import assert_report_equal, default_updater_settings ## diff --git a/tests/gui/test_updater.py b/tests/gui/test_updater.py index d18eebe..430a621 100644 --- a/tests/gui/test_updater.py +++ b/tests/gui/test_updater.py @@ -16,7 +16,7 @@ from dangerzone.gui.updater import UpdateReport, UpdaterThread from dangerzone.util import get_version from ..test_settings import default_settings_0_4_1, save_settings -from . import generate_isolated_updater, qt_updater, updater +from .conftest import generate_isolated_updater def default_updater_settings() -> dict: diff --git a/tests/isolation_provider/base.py b/tests/isolation_provider/base.py index 5775d74..017a144 100644 --- a/tests/isolation_provider/base.py +++ b/tests/isolation_provider/base.py @@ -9,15 +9,6 @@ from dangerzone.document import Document from dangerzone.isolation_provider import base from dangerzone.isolation_provider.qubes import running_on_qubes -from .. import ( - pdf_11k_pages, - sample_bad_height, - sample_bad_width, - sample_doc, - sanitized_text, - uncommon_text, -) - TIMEOUT_STARTUP = 60 # Timeout in seconds until the conversion sandbox starts. diff --git a/tests/isolation_provider/test_container.py b/tests/isolation_provider/test_container.py index 6a05f83..18fa8bd 100644 --- a/tests/isolation_provider/test_container.py +++ b/tests/isolation_provider/test_container.py @@ -7,15 +7,6 @@ import pytest from dangerzone.isolation_provider.container import Container from dangerzone.isolation_provider.qubes import is_qubes_native_conversion -# XXX Fixtures used in abstract Test class need to be imported regardless -from .. import ( - pdf_11k_pages, - sample_bad_height, - sample_bad_width, - sample_doc, - sanitized_text, - uncommon_text, -) from .base import IsolationProviderTermination, IsolationProviderTest diff --git a/tests/isolation_provider/test_qubes.py b/tests/isolation_provider/test_qubes.py index 45c5107..297fdd7 100644 --- a/tests/isolation_provider/test_qubes.py +++ b/tests/isolation_provider/test_qubes.py @@ -15,15 +15,6 @@ from dangerzone.isolation_provider.qubes import ( running_on_qubes, ) -# XXX Fixtures used in abstract Test class need to be imported regardless -from .. import ( - pdf_11k_pages, - sample_bad_height, - sample_bad_width, - sample_doc, - sanitized_text, - uncommon_text, -) from .base import IsolationProviderTermination, IsolationProviderTest diff --git a/tests/test_cli.py b/tests/test_cli.py index 1e37887..e9e9eda 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -20,15 +20,7 @@ from dangerzone.cli import cli_main, display_banner from dangerzone.document import ARCHIVE_SUBDIR, SAFE_EXTENSION from dangerzone.isolation_provider.qubes import is_qubes_native_conversion -from . import ( - TestBase, - for_each_doc, - for_each_external_doc, - sample_bad_height, - sample_pdf, - uncommon_filename, - uncommon_text, -) +from .conftest import for_each_doc, for_each_external_doc # TODO explore any symlink edge cases # TODO simulate ctrl-c, ctrl-d, SIGINT/SIGKILL/SIGTERM... (man 7 signal), etc? diff --git a/tests/test_document.py b/tests/test_document.py index 80a3964..375ffaa 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -8,8 +8,6 @@ import pytest from dangerzone import errors from dangerzone.document import ARCHIVE_SUBDIR, SAFE_EXTENSION, Document -from . import sample_pdf, unreadable_pdf - def test_input_sample_init(sample_pdf: str) -> None: Document(sample_pdf) diff --git a/tests/test_large_set.py b/tests/test_large_set.py index d596c74..6365dec 100644 --- a/tests/test_large_set.py +++ b/tests/test_large_set.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import List import pytest -from _pytest.fixtures import FixtureRequest from dangerzone.document import SAFE_EXTENSION diff --git a/tests/test_util.py b/tests/test_util.py index a4126f9..2aa14cd 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,8 +6,6 @@ import pytest from dangerzone import util -from . import sanitized_text, uncommon_text - VERSION_FILE_NAME = "version.txt"