mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-12 12:32:11 -03:00
test: make the backend suite pass on Windows
The new informational Windows job surfaced 91 failures: four real bugs (fixed in
the previous two commits) and a long tail of POSIX assumptions in the tests
themselves. None of them assert behaviour that differs on Windows, so they are
fixed rather than skipped wherever the behaviour is the same:
* Path spelling: expectations built with `str(tmp_path)` are compared through an
identity helper (`normalize_root_identity`) or the platform's separator, since
the code hands out forward-slashed business paths while `str()` is native.
* Root labels: `_root_display_labels` prefixes the drive on Windows ("C: loras"),
so label assertions compare the final path component or derive the expected
label from the same helper.
* `~` expansion reads USERPROFILE (ntpath), not HOME; timer granularity rounds
0.1s down; a case-only rename counts as "already named" via `os.path.normcase`.
* Genuinely POSIX-only strategies are skipped with a reason instead of failing:
the headless clipboard fallback, dbus file-manager selection, xdg-open, and
the case-fold index test that simulates a case-sensitive filesystem.
This commit is contained in:
@@ -532,7 +532,7 @@ async def test_execute_download_extracts_zip_single_model(monkeypatch, tmp_path)
|
||||
extracted = save_dir / "model.safetensors"
|
||||
assert extracted.exists()
|
||||
saved_call = MetadataManager.save_metadata.await_args
|
||||
assert saved_call.args[0] == str(extracted)
|
||||
assert saved_call.args[0] == str(extracted).replace(os.sep, "/")
|
||||
# SHA256 comes from metadata (API value), not recalculated
|
||||
assert saved_call.args[1].sha256 == "sha256"
|
||||
# The zip-level AutoV3 was cleared; the extracted file is not safetensors,
|
||||
@@ -693,7 +693,7 @@ async def test_execute_download_extracts_zip_pt_embedding(monkeypatch, tmp_path)
|
||||
extracted = save_dir / "embedding.pt"
|
||||
assert extracted.exists()
|
||||
saved_call = MetadataManager.save_metadata.await_args
|
||||
assert saved_call.args[0] == str(extracted)
|
||||
assert saved_call.args[0] == str(extracted).replace(os.sep, "/")
|
||||
# SHA256 comes from metadata (API value), not recalculated
|
||||
assert saved_call.args[1].sha256 == "sha256"
|
||||
assert dummy_scanner.add_model_to_cache.await_count == 1
|
||||
|
||||
@@ -48,6 +48,20 @@ class RecordingWebSocketManager:
|
||||
self.broadcasts.append(payload)
|
||||
|
||||
|
||||
def _root_label_names(labels) -> List[str]:
|
||||
"""The root names behind display labels.
|
||||
|
||||
``_root_display_labels`` prefixes the drive on Windows ("C: loras") and may
|
||||
grow a label leftwards to keep it unique ("parent/loras"), so a test that only
|
||||
cares *which* root was walked compares the final path component.
|
||||
"""
|
||||
|
||||
return [
|
||||
label.replace("\\", "/").rstrip("/").rsplit("/", 1)[-1].rsplit(" ", 1)[-1]
|
||||
for label in labels
|
||||
]
|
||||
|
||||
|
||||
def _normalize_path(path: Path) -> str:
|
||||
return str(path).replace(os.sep, "/")
|
||||
|
||||
@@ -2306,6 +2320,10 @@ def test_count_cached_entries_per_root_uses_longest_prefix():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.name == "nt",
|
||||
reason="simulates a case-sensitive filesystem; Windows matches case itself",
|
||||
)
|
||||
async def test_reconcile_case_fold_fallback_is_indexed_not_linear(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
@@ -2465,7 +2483,10 @@ async def test_reconcile_walk_broadcasts_per_root_progress(tmp_path: Path, monke
|
||||
assert len(walk_messages) >= 2
|
||||
|
||||
# Intermediate ticks name the root being walked.
|
||||
assert any(message["active_roots"] == ["loras"] for message in walk_messages)
|
||||
assert any(
|
||||
_root_label_names(message["active_roots"]) == ["loras"]
|
||||
for message in walk_messages
|
||||
)
|
||||
|
||||
final = walk_messages[-1]
|
||||
assert final["files_seen"] == 4
|
||||
@@ -2546,7 +2567,7 @@ async def test_reconcile_scoped_scan_leaves_other_roots_untouched(tmp_path: Path
|
||||
assert summary is not None
|
||||
assert summary["added"] == 1
|
||||
assert summary["removed"] == 0
|
||||
assert summary["scanned_roots"] == ["a"]
|
||||
assert _root_label_names(summary["scanned_roots"]) == ["a"]
|
||||
|
||||
cached_paths = {item["file_path"] for item in scanner._cache.raw_data}
|
||||
assert cached_paths == {
|
||||
@@ -2717,11 +2738,13 @@ async def test_reconcile_reports_root_filtered_out_at_startup(tmp_path: Path, mo
|
||||
assert summary["added"] == 0
|
||||
assert summary["removed"] == 0
|
||||
# ...but the switched-off root is no longer silent: it is reported with the
|
||||
# entries that were kept for it.
|
||||
# entries that were kept for it. Its label is what the shared label helper
|
||||
# makes of the pair (drive-prefixed on Windows).
|
||||
labels = model_scanner._root_display_labels([str(_primary), str(missing)])
|
||||
assert summary["skipped_roots"] == [
|
||||
{
|
||||
"path": _normalize_path(missing),
|
||||
"label": missing.name,
|
||||
"label": labels[str(missing)],
|
||||
"reason": "root_unavailable",
|
||||
}
|
||||
]
|
||||
@@ -2746,7 +2769,7 @@ async def test_scoped_scan_stays_quiet_about_filtered_out_roots(tmp_path: Path,
|
||||
assert summary is not None
|
||||
# The caller asked for one root: a root it did not ask about is not its
|
||||
# business, but its entries are still kept.
|
||||
assert summary["scanned_roots"] == [primary.name]
|
||||
assert _root_label_names(summary["scanned_roots"]) == [primary.name]
|
||||
assert "root_unavailable" not in {
|
||||
entry["reason"] for entry in summary["skipped_roots"]
|
||||
}
|
||||
@@ -3091,7 +3114,7 @@ async def test_scoped_scan_accepts_the_host_path_spelling(tmp_path: Path, monkey
|
||||
summary = await scanner._reconcile_cache(scope=scope)
|
||||
|
||||
assert summary is not None
|
||||
assert summary["scanned_roots"] == [primary.name]
|
||||
assert _root_label_names(summary["scanned_roots"]) == [primary.name]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -901,7 +901,7 @@ async def test_p_checkpoint_pending_discovery_excludes_staging(tmp_path: Path, m
|
||||
|
||||
pending = await scanner._find_pending_models_from_filesystem()
|
||||
paths = [entry["file_path"] for entry in pending]
|
||||
assert str(root / "real.safetensors") in paths
|
||||
assert str(root / "real.safetensors").replace(os.sep, "/") in paths
|
||||
assert not any(PENDING_DELETE_DIR_NAME in p for p in paths)
|
||||
|
||||
|
||||
@@ -921,7 +921,7 @@ async def test_p_usage_stats_lookup_excludes_staging(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
# Staged file is not matched; only the real one is returned.
|
||||
assert result == str(root / "mycheckpoint.safetensors")
|
||||
assert result == str(root / "mycheckpoint.safetensors").replace(os.sep, "/")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -62,7 +62,7 @@ async def test_pacing_enforces_min_interval(monkeypatch):
|
||||
await coordinator.wait_for_slot("example.com")
|
||||
await coordinator.wait_for_slot("example.com")
|
||||
elapsed = time.monotonic() - start
|
||||
assert elapsed >= 0.1
|
||||
assert elapsed >= 0.09 # Windows timer granularity rounds 0.1s down
|
||||
|
||||
|
||||
async def test_pacing_is_per_destination(monkeypatch):
|
||||
@@ -89,7 +89,7 @@ async def test_register_rate_limit_arms_cooldown_and_waits(monkeypatch):
|
||||
start = time.monotonic()
|
||||
await coordinator.wait_for_slot("example.com")
|
||||
elapsed = time.monotonic() - start
|
||||
assert elapsed >= 0.14
|
||||
assert elapsed >= 0.09 # Windows timer granularity rounds 0.1s down4
|
||||
assert not coordinator.in_cooldown("example.com")
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ async def test_make_request_paces_consecutive_calls(monkeypatch):
|
||||
await downloader.make_request("GET", "https://api.example.com/a")
|
||||
await downloader.make_request("GET", "https://api.example.com/b")
|
||||
elapsed = time.monotonic() - start
|
||||
assert elapsed >= 0.14
|
||||
assert elapsed >= 0.09 # Windows timer granularity rounds 0.1s down4
|
||||
|
||||
|
||||
async def test_make_request_gate_disabled_returns_429_immediately(monkeypatch):
|
||||
|
||||
@@ -36,6 +36,7 @@ from py.utils.example_images_processor import (
|
||||
)
|
||||
from py.utils.metadata_manager import MetadataManager
|
||||
from tests.conftest import MockCache, MockModelService, MockScanner
|
||||
import os
|
||||
|
||||
|
||||
class StubLockProvider:
|
||||
@@ -655,7 +656,8 @@ async def test_filename_template_use_case_reverts_to_recorded_original_when_temp
|
||||
lifecycle = StubLifecycleService()
|
||||
|
||||
async def metadata_loader(metadata_path: str) -> Dict[str, Any]:
|
||||
if metadata_path == "/library/alpha-renamed.metadata.json":
|
||||
# Compare the basename: the sidecar path is platform-spelled.
|
||||
if os.path.basename(metadata_path) == "alpha-renamed.metadata.json":
|
||||
return {"original_file_name": "alpha-original"}
|
||||
return {}
|
||||
|
||||
@@ -713,8 +715,10 @@ async def test_filename_template_use_case_skips_revert_matching_current_name() -
|
||||
async def test_filename_template_use_case_counts_conflicts_as_failures() -> None:
|
||||
_set_filename_template("{model_name}")
|
||||
scanner = MockScanner(cache=MockCache([
|
||||
_filename_template_model("/library/alpha.safetensors", "Alpha"),
|
||||
_filename_template_model("/library/beta.safetensors", "Beta"),
|
||||
# The names differ beyond case: on Windows a case-only difference counts
|
||||
# as "already named that" (os.path.normcase) and would be skipped.
|
||||
_filename_template_model("/library/alpha.safetensors", "Alpha Two"),
|
||||
_filename_template_model("/library/beta.safetensors", "Beta Two"),
|
||||
]))
|
||||
lifecycle = StubLifecycleService()
|
||||
lifecycle.error = ValueError("A file with this name already exists")
|
||||
|
||||
Reference in New Issue
Block a user