Skip to content

Commit

Permalink
Simplified mock
Browse files Browse the repository at this point in the history
  • Loading branch information
couling committed Jan 3, 2024
1 parent fb88a61 commit 550db8f
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 117 deletions.
29 changes: 20 additions & 9 deletions source/mkdocs_deploy/abstract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import urllib.parse
from abc import abstractmethod
from typing import Callable, IO, Iterable, Optional, Protocol, Union
from enum import Enum
from typing import Callable, IO, Iterable, Optional, Protocol

from .versions import DeploymentAlias, DeploymentSpec

Expand All @@ -13,6 +14,16 @@ class RedirectMechanismNotFound(Exception):
pass


class _RootDirType(Enum):
ROOT_DIR = 0


ROOT_DIR = _RootDirType.ROOT_DIR


Version = str | _RootDirType


class Source(Protocol):
"""
Source is where a site is loaded from.
Expand Down Expand Up @@ -79,7 +90,7 @@ def delete_version(self, version_id: str) -> None:
"""

@abstractmethod
def upload_file(self, version_id: str | None, filename: str, file_obj: IO[bytes]) -> None:
def upload_file(self, version_id: Version, filename: str, file_obj: IO[bytes]) -> None:
"""
Upload a file to the target
Expand All @@ -90,7 +101,7 @@ def upload_file(self, version_id: str | None, filename: str, file_obj: IO[bytes]
"""

@abstractmethod
def download_file(self, version_id: str | None, filename: str) -> IO[bytes]:
def download_file(self, version_id: Version, filename: str) -> IO[bytes]:
"""
Open a file handle to read content of a file
Expand All @@ -105,7 +116,7 @@ def download_file(self, version_id: str | None, filename: str) -> IO[bytes]:


@abstractmethod
def delete_file(self, version_id: str | None, filename: str) -> None:
def delete_file(self, version_id: Version, filename: str) -> None:
"""
Delete a file, or mark it for deletion on close.
:param version_id: The version to delete from
Expand Down Expand Up @@ -133,7 +144,7 @@ def close(self, success: bool = False) -> None:
"""

@abstractmethod
def set_alias(self, alias_id: str | None, alias: Optional[DeploymentAlias]) -> None:
def set_alias(self, alias_id: Version, alias: Optional[DeploymentAlias]) -> None:
"""
Create or delete an alias.
Expand Down Expand Up @@ -185,26 +196,26 @@ class RedirectMechanism(Protocol):
"""

@abstractmethod
def create_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None:
def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None:
"""
Create a redirect
:param session: The TargetSession to apply changes to
:param alias: The new alias to create. If ``...`` is passed, then a redirect from the root is created. IE: ""
:param alias: The new alias to create. If ``None`` is passed, then a redirect from the root is created. IE: ""
defines what the default version is.
:param version_id: The version to redirect to.
"""

@abstractmethod
def delete_redirect(self, session: TargetSession, alias: Union[str, type(...)]) -> None:
def delete_redirect(self, session: TargetSession, alias:Version) -> None:
"""
Delete the named redirect.
:param session: The TargetSession to apply changes to
:param alias: The alias to delete. ``...`` is the default redirect.
"""

def refresh_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None:
def refresh_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None:
"""
Called to ensure all redirects still work after a version has been altered.
Expand Down
26 changes: 0 additions & 26 deletions source/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,10 @@
from pathlib import Path

import pytest

from mkdocs_deploy import abstract
from mkdocs_deploy.plugins import local_filesystem


@pytest.fixture(autouse=True)
def _clean_plugins(monkeypatch: pytest.MonkeyPatch):
"""Ensure that all tests run with uninitialized plugins"""
monkeypatch.setattr(abstract, "_SOURCES", {})
monkeypatch.setattr(abstract, "_TARGETS", {})

@pytest.fixture()
def mock_source_path(tmp_path: Path) -> Path:
base_path = tmp_path / "mock_source"
base_path.mkdir(exist_ok=False, parents=True)
return base_path


@pytest.fixture()
def mock_source(mock_source_path: Path) -> abstract.Source:
return local_filesystem.LocalFileTreeSource(mock_source_path)


@pytest.fixture()
def mock_target_path(tmp_path: Path) -> Path:
base_path = tmp_path / "mock_target"
base_path.mkdir(exist_ok=False, parents=True)
return base_path


@pytest.fixture()
def mock_target(mock_target_path: Path) -> abstract.Target:
return local_filesystem.LocalFileTreeTarget(str(mock_target_path))
111 changes: 111 additions & 0 deletions source/tests/mock_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import io
from copy import deepcopy
from io import BytesIO
from typing import IO, Iterable

from mkdocs_deploy import abstract, versions
from mkdocs_deploy.abstract import TargetSession, Version
from mkdocs_deploy.versions import DeploymentAlias, DeploymentSpec


class BaseMockPlugin:

files: dict[str, bytes]

def __init__(self):
self.files = {}


class MockSource(abstract.Source):

def __init__(self, files: dict[str, bytes] | None = None):
self.files = files.copy() if files is not None else {}

def iter_files(self) -> Iterable[str]:
yield from self.files.keys()

def open_file_for_read(self, filename: str) -> IO[bytes]:
return io.BytesIO(initial_bytes=self.files[filename])


class MockRedirectMechanism(abstract.RedirectMechanism):

def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None:
pass

def delete_redirect(self, session: TargetSession, alias: Version) -> None:
pass


class MockTargetSession(abstract.TargetSession):
files: dict[tuple[Version, str], bytes]
internal_deployment_spec: abstract.DeploymentSpec
closed: bool = False
close_success: bool = False
aliases: dict[Version, versions.DeploymentAlias]

def __init__(self):
self.files = {}
self.deleted_files = set()
self.internal_deployment_spec = abstract.DeploymentSpec()
self.aliases = {}

def start_version(self, version_id: str, title: str) -> None:
self.internal_deployment_spec.versions[version_id] = versions.DeploymentVersion(title=title)

def delete_version(self, version_id: str) -> None:
existing_files = [f for v, f in self.files.keys() if v == version_id]
del self.internal_deployment_spec.versions[version_id]
for file in existing_files:
del self.files[(version_id, file)]

def upload_file(self, version_id: Version, filename: str, file_obj: IO[bytes]) -> None:
if version_id not in self.internal_deployment_spec.versions:
raise abstract.VersionNotFound(version_id)
self.files[(version_id, filename)] = file_obj.read()

def download_file(self, version_id: Version, filename: str) -> IO[bytes]:
if version_id not in self.internal_deployment_spec.versions:
raise abstract.VersionNotFound(version_id)
return BytesIO(self.files[(version_id, filename)])

def delete_file(self, version_id: Version, filename: str) -> None:
if version_id not in self.internal_deployment_spec.versions:
raise abstract.VersionNotFound(version_id)
del self.files[(version_id, filename)]

def iter_files(self, version_id: str) -> Iterable[str]:
for version, file in self.files:
if version_id == version:
yield file

def close(self, success: bool = False) -> None:
self.closed = True
self.close_success = success

def set_alias(self, alias_id: Version, alias: DeploymentAlias | None) -> None:
if alias is None:
del self.aliases[alias_id]
return
self.aliases[alias_id] = deepcopy(alias)

@property
def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]:
return {"mock": MockRedirectMechanism()}

@property
def deployment_spec(self) -> DeploymentSpec:
return deepcopy(self.internal_deployment_spec)


class MockTarget(abstract.Target):

files: dict[tuple[str, str], bytes]
internal_deployment_spec: versions.DeploymentSpec

def __init__(self):
self.files = {}
self.internal_deployment_spec = versions.DeploymentSpec()

def start_session(self) -> MockTargetSession:
return MockTargetSession()
1 change: 0 additions & 1 deletion source/tests/mock_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Any, NamedTuple, TypeVar
from copy import deepcopy

_T = TypeVar("_T")

Expand Down
109 changes: 28 additions & 81 deletions source/tests/test_modules/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,29 @@
import uuid
from pathlib import Path

import pytest

from mkdocs_deploy import abstract, actions, versions
from mkdocs_deploy import actions, versions
from ..mock_plugin import MockSource, MockTargetSession
from ..mock_wrapper import mock_wrapper


@pytest.fixture()
def mock_source_files(mock_source: abstract.Source, mock_source_path: Path) -> dict[str, bytes]:
(mock_source_path / "index.html").write_text(str(uuid.uuid4()))
(mock_source_path / "subdir").mkdir(exist_ok=False)
(mock_source_path / "subdir" / "foo.txt").write_text(str(uuid.uuid4()))
results = {}
for file in mock_source.iter_files():
with mock_source.open_file_for_read(file) as file_handle:
results[file] = file_handle.read()
return results
MOCK_SOURCE_FILES = {
"index.html": str(uuid.uuid4()).encode(),
"subdir/foo.txt": str(uuid.uuid4()).encode(),
}


@pytest.mark.parametrize("title", ["Version 1.1", None], ids=["Explicit Title", "Implicit Title"])
def test_upload(
mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes], title: str | None
):
def test_upload(title: str | None):
VERSION = "1.1"
session, session_method_calls = mock_wrapper(mock_target.start_session())
source = MockSource(MOCK_SOURCE_FILES)
session, session_method_calls = mock_wrapper(MockTargetSession())

try:
actions.upload(
source=mock_source,
target=session,
version_id=VERSION,
title=title,
)
actions.upload(source=source, target=session, version_id=VERSION, title=title)

assert session_method_calls[0].name == "start_version"

uploaded_files = set(session.iter_files(VERSION))
assert uploaded_files == set(mock_source_files.keys())
for file in mock_source_files.keys():
with session.download_file(VERSION, file) as file_handle:
assert file_handle.read() == mock_source_files[file]

assert session.files == {(VERSION, file): content for file, content in source.files.items()}

assert session.deployment_spec.versions == {
VERSION: versions.DeploymentVersion(title=title or VERSION),
Expand All @@ -52,64 +35,28 @@ def test_upload(
session.close(success=True)


def test_upload_implicit_title_does_not_override_existing_one(
mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes]
):
def test_upload_implicit_title_does_not_override_existing_one():
VERSION = "1.1"
session = mock_target.start_session()
try:
session._deployment_spec.versions[VERSION] = versions.DeploymentVersion(title="foo bar") # type: ignore
actions.upload(
source=mock_source,
target=session,
version_id=VERSION,
title=None,
)

uploaded_files = set(session.iter_files(VERSION))
assert uploaded_files == set(mock_source_files.keys())
for file in mock_source_files.keys():
with session.download_file(VERSION, file) as file_handle:
assert file_handle.read() == mock_source_files[file]
source = MockSource(MOCK_SOURCE_FILES)
session, session_method_calls = mock_wrapper(MockTargetSession())
session.start_version(VERSION, "foo bar")

actions.upload(source=source, target=session, version_id=VERSION, title=None)

assert session.deployment_spec.versions == {
VERSION: versions.DeploymentVersion(title="foo bar" or VERSION),
}
except:
session.close(success=False)
raise
else:
session.close(success=True)
assert session.deployment_spec.versions == {
VERSION: versions.DeploymentVersion(title="foo bar"),
}


def test_upload_explicit_title_overrides_existing_one(
mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes]
):
def test_upload_explicit_title_overrides_existing_one():
VERSION = "1.1"
VERSION_TITLE = "Version 1.1"
session = mock_target.start_session()
try:
session._deployment_spec.versions[VERSION] = versions.DeploymentVersion(title="foo bar") # type: ignore
actions.upload(
source=mock_source,
target=session,
version_id=VERSION,
title=VERSION_TITLE,
)
source = MockSource(MOCK_SOURCE_FILES)
session, session_method_calls = mock_wrapper(MockTargetSession())
session.start_version(VERSION, "foo bar")

uploaded_files = set(session.iter_files(VERSION))
assert uploaded_files == set(mock_source_files.keys())
for file in mock_source_files.keys():
with session.download_file(VERSION, file) as file_handle:
assert file_handle.read() == mock_source_files[file]
actions.upload(source=source, target=session, version_id=VERSION, title=VERSION_TITLE)


assert session.deployment_spec.versions == {
VERSION: versions.DeploymentVersion(title=VERSION_TITLE or VERSION),
}
except:
session.close(success=False)
raise
else:
session.close(success=True)
assert session.deployment_spec.versions == {
VERSION: versions.DeploymentVersion(title=VERSION_TITLE),
}

0 comments on commit 550db8f

Please sign in to comment.