chore: remove fixture imports in the tests

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)
This commit is contained in:
Alexis Métaireau 2024-05-22 15:29:56 +02:00 committed by Alexis Métaireau
parent d9d9ab91a3
commit 9bad001c04
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
13 changed files with 206 additions and 248 deletions

View file

@ -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
"<EFBFBD>". The rest of the characters (emojis and non-English leters) are retained as is.
"""
return "<EFBFBD>[31;1;4m BaD TeΧt <20><20> <20>[0m"

View file

@ -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
"<EFBFBD>". The rest of the characters (emojis and non-English leters) are retained as is.
"""
return "<EFBFBD>[31;1;4m BaD TeΧt <20><20> <20>[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))

View file

@ -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)

54
tests/gui/conftest.py Normal file
View file

@ -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)

View file

@ -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
##

View file

@ -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:

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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)

View file

@ -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

View file

@ -6,8 +6,6 @@ import pytest
from dangerzone import util
from . import sanitized_text, uncommon_text
VERSION_FILE_NAME = "version.txt"