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
155 lines
4.6 KiB
Python
155 lines
4.6 KiB
Python
"""Tests for tagsync.platform_windows: sidecar writer + support matrix.
|
|
|
|
Runs on any OS — the Windows-specific online-only stat test is skipped
|
|
except on Windows.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tagsync.platform_windows import (
|
|
XMP_WRITABLE_EXTS,
|
|
WindowsWriter,
|
|
)
|
|
|
|
|
|
# --- supports() --------------------------------------------------------------
|
|
|
|
|
|
def test_supports_xmp_formats():
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
for ext in (".jpg", ".pdf", ".docx", ".mp4"):
|
|
assert w.supports(Path(f"foo{ext}")), f"expected support for {ext}"
|
|
|
|
|
|
def test_supports_rejects_unknown_without_sidecars():
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
assert not w.supports(Path("archive.zip"))
|
|
assert not w.supports(Path("notes.txt"))
|
|
|
|
|
|
def test_supports_accepts_unknown_with_sidecars():
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
|
|
assert w.supports(Path("archive.zip"))
|
|
assert w.supports(Path("notes.txt"))
|
|
|
|
|
|
def test_supports_is_case_insensitive():
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
assert w.supports(Path("IMAGE.JPG"))
|
|
assert w.supports(Path("report.PDF"))
|
|
|
|
|
|
# --- sidecar writer ---------------------------------------------------------
|
|
|
|
|
|
def test_sidecar_written_for_unsupported_type(tmp_path):
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
|
|
target = tmp_path / "archive.zip"
|
|
target.touch()
|
|
|
|
w.write_tags(target, ["project-alpha", "urgent"])
|
|
|
|
sidecar = tmp_path / "archive.zip.tags.json"
|
|
assert sidecar.exists()
|
|
data = json.loads(sidecar.read_text())
|
|
assert data == {"tags": ["project-alpha", "urgent"]}
|
|
|
|
|
|
def test_sidecar_cleared_when_tags_empty(tmp_path):
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
|
|
target = tmp_path / "archive.zip"
|
|
target.touch()
|
|
(tmp_path / "archive.zip.tags.json").write_text('{"tags": ["x"]}')
|
|
|
|
w.write_tags(target, [])
|
|
assert not (tmp_path / "archive.zip.tags.json").exists()
|
|
|
|
|
|
def test_sidecar_clear_nonexistent_is_ok(tmp_path):
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=True)
|
|
target = tmp_path / "archive.zip"
|
|
target.touch()
|
|
# Should not raise even though no sidecar exists.
|
|
w.write_tags(target, [])
|
|
|
|
|
|
def test_unsupported_without_sidecars_raises(tmp_path):
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
target = tmp_path / "archive.zip"
|
|
target.touch()
|
|
with pytest.raises(RuntimeError):
|
|
w.write_tags(target, ["tag"])
|
|
|
|
|
|
# --- exiftool invocation ----------------------------------------------------
|
|
#
|
|
# We don't actually run exiftool; monkeypatch subprocess.run to capture args.
|
|
|
|
|
|
def test_exiftool_args_are_well_formed(tmp_path, monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_run(args, **kwargs):
|
|
captured["args"] = args
|
|
class R:
|
|
returncode = 0
|
|
stderr = ""
|
|
return R()
|
|
|
|
import tagsync.platform_windows as pw
|
|
monkeypatch.setattr(pw.subprocess, "run", fake_run)
|
|
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
target = tmp_path / "pic.jpg"
|
|
target.touch()
|
|
w.write_tags(target, ["alpha", "beta"])
|
|
|
|
args = captured["args"]
|
|
assert args[0] == "exiftool"
|
|
assert "-overwrite_original" in args
|
|
# Subject should be cleared then appended for each tag.
|
|
assert "-XMP-dc:Subject=" in args
|
|
assert "-XMP-dc:Subject+=alpha" in args
|
|
assert "-XMP-dc:Subject+=beta" in args
|
|
assert str(target) in args
|
|
|
|
|
|
def test_exiftool_failure_raises(tmp_path, monkeypatch):
|
|
def fake_run(args, **kwargs):
|
|
class R:
|
|
returncode = 1
|
|
stderr = "bad things happened"
|
|
return R()
|
|
|
|
import tagsync.platform_windows as pw
|
|
monkeypatch.setattr(pw.subprocess, "run", fake_run)
|
|
|
|
w = WindowsWriter(exiftool="exiftool", write_sidecars=False)
|
|
target = tmp_path / "pic.jpg"
|
|
target.touch()
|
|
with pytest.raises(RuntimeError, match="bad things happened"):
|
|
w.write_tags(target, ["x"])
|
|
|
|
|
|
# --- online-only detection --------------------------------------------------
|
|
|
|
|
|
@pytest.mark.windows
|
|
def test_is_online_only_returns_false_for_normal_files(tmp_path):
|
|
w = WindowsWriter(exiftool="exiftool")
|
|
f = tmp_path / "regular.txt"
|
|
f.touch()
|
|
assert w.is_online_only(f) is False
|
|
|
|
|
|
def test_is_online_only_returns_false_when_attribute_unavailable(tmp_path):
|
|
"""On non-Windows, st_file_attributes doesn't exist -> should return False."""
|
|
w = WindowsWriter(exiftool="exiftool")
|
|
f = tmp_path / "regular.txt"
|
|
f.touch()
|
|
assert w.is_online_only(f) is False
|