CI / Tests (Linux, Python 3.12) (push) Failing after 4m51s
CI / Tests (Linux, Python 3.11) (push) Failing after 5m19s
CI / Tests (Linux, Python 3.9) (push) Failing after 5m3s
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
Cross-platform CLI that mirrors Dropbox web tags to OS-native tags: - macOS: Finder xattrs (content-preserving, no re-upload) - Windows: XMP:Subject via ExifTool (re-upload expected once per file) Includes an interactive wizard that auto-detects the OS and Dropbox folder, walks the user through read-only token creation, previews tagged files, applies tags, and installs a background scheduler (launchd on macOS, Task Scheduler on Windows). Ships with: - 47-test suite (pytest) covering config, state DB, run_sync, writers - Gitea Actions CI for Linux / macOS / Windows with coverage reporting
123 lines
3.2 KiB
Python
123 lines
3.2 KiB
Python
"""Shared pytest fixtures and helpers."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
# --- Isolate persistent state paths from the real user's home ---------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_user_state(tmp_path, monkeypatch):
|
|
"""Redirect all per-user state files into a tmp dir.
|
|
|
|
Ensures tests never clobber real settings, tokens, or state DB on the
|
|
developer's machine.
|
|
"""
|
|
fake_home = tmp_path / "home"
|
|
fake_home.mkdir()
|
|
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
|
|
|
# Re-import config so its module-level paths re-evaluate against the
|
|
# patched home. We clear and re-import to reset cached module constants.
|
|
import importlib
|
|
from tagsync import config
|
|
|
|
importlib.reload(config)
|
|
yield
|
|
# Leave things as-is after the test; tmp_path cleanup handles disk.
|
|
|
|
|
|
# --- Fake Dropbox API objects ------------------------------------------------
|
|
#
|
|
# The Dropbox SDK may or may not be installed in the test env. We don't want
|
|
# to require it, and the parts of core.py that need it are only reached via
|
|
# helpers we can monkeypatch out.
|
|
|
|
|
|
@dataclass
|
|
class FakeFileMetadata:
|
|
"""Stand-in for dropbox.files.FileMetadata."""
|
|
name: str
|
|
path_lower: str
|
|
path_display: str
|
|
|
|
|
|
@dataclass
|
|
class FakeUserTag:
|
|
tag_text: str
|
|
|
|
|
|
class FakeTag:
|
|
"""Stand-in for dropbox.files.Tag (union type)."""
|
|
def __init__(self, tag_text: str) -> None:
|
|
self._user = FakeUserTag(tag_text)
|
|
|
|
def get_user_generated_tag(self) -> FakeUserTag:
|
|
return self._user
|
|
|
|
|
|
@dataclass
|
|
class FakePathToTags:
|
|
path: str
|
|
tags: list
|
|
|
|
|
|
@dataclass
|
|
class FakeTagsResult:
|
|
paths_to_tags: list
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_core(monkeypatch):
|
|
"""Monkeypatch core's Dropbox-dependent helpers to controllable fakes.
|
|
|
|
Tests receive a `state` dict where they can configure:
|
|
- `files`: list of FakeFileMetadata returned by _list_all_files
|
|
- `tags_by_path`: dict[path_lower -> list[str]] returned by _fetch_tags
|
|
"""
|
|
from tagsync import core
|
|
|
|
state = {
|
|
"files": [],
|
|
"tags_by_path": {},
|
|
"fetch_calls": 0,
|
|
}
|
|
|
|
def fake_client(_token):
|
|
return object() # placeholder; never used because list/fetch are faked
|
|
|
|
def fake_list(_dbx):
|
|
return list(state["files"])
|
|
|
|
def fake_fetch(_dbx, files):
|
|
state["fetch_calls"] += 1
|
|
for f in files:
|
|
tag_names = sorted(state["tags_by_path"].get(f.path_lower, []))
|
|
yield f, tag_names
|
|
|
|
monkeypatch.setattr(core, "_dropbox_client", fake_client)
|
|
monkeypatch.setattr(core, "_list_all_files", fake_list)
|
|
monkeypatch.setattr(core, "_fetch_tags", fake_fetch)
|
|
return state
|
|
|
|
|
|
# --- Marker-based skips ------------------------------------------------------
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
platform_skip = {
|
|
"macos": sys.platform != "darwin",
|
|
"windows": sys.platform != "win32",
|
|
}
|
|
for item in items:
|
|
for mark_name, should_skip in platform_skip.items():
|
|
if mark_name in item.keywords and should_skip:
|
|
item.add_marker(
|
|
pytest.mark.skip(reason=f"requires {mark_name}")
|
|
)
|