`pytest tests/` previously crashed during collection and 7 of 17 test files were dead: 61 tests were reachable, all via ad-hoc standalone scripts. Now a bare `pytest` collects everything and passes 238 tests with ComfyUI absent (verified by running the suite from outside the ComfyUI tree, where `import comfy` raises ModuleNotFoundError). Import structure: - Drop tests/__init__.py. With it, pytest walks up to the project root's __init__.py -- the ComfyUI node entry point -- and imports ComfyUI before any test runs. - Import project code as `src.<module>` instead of putting src/ on sys.path and importing bare `merge.algorithms` / `validation` / `types`. Modules in src/ use package-relative imports (`from ..types import ...`) that cannot resolve when loaded top-level, and `types` collided with the stdlib module. Same change for the mock.patch targets in test_algorithms. - Consolidate conftest.py in tests/, mocking comfy, folder_paths, comfy_extras and nodes. It stays in tests/ rather than the project root because pytest imports a root-level conftest as part of the root package, executing the ComfyUI entry point. - Guard the script-style runners behind `if __name__ == "__main__":` so they no longer sys.exit() during collection. Those files still run standalone. - Drop run_pytest.py: a mocking wrapper made redundant by conftest, unused and pointing at an unresolvable default path. Bugs the dead tests were hiding: - validators: the INCOMPATIBLE_DIMENSIONS check sat after the `continue` that skips the reference tensor, so a lone LoRA with mismatched up/down ranks passed validation unchecked. It is a per-LoRA check and now runs for every entry. - decomposition: __init__ exported a QRDecomposer that exists nowhere, so `import src.decomposition` raised ImportError. Export and tests removed. Stale expectations corrected: - return_statistics is a constructor argument, not a decompose() kwarg. - The zero-matrix rank guard only applies under dynamic rank selection; the test now exercises that path, plus a new case pinning fixed-rank behavior. - `reconstruction_error < 0.5` for a rank-10 truncation of a random 100x50 Gaussian is unreachable -- the optimum is 0.7557 and the decomposer hits 0.7568. Assert near-optimality instead, and add a genuinely low-rank case that reconstructs to 0.003. - sym/asym distributions differ only by float32 rounding (~5e-7), below the default atol of 1e-8. RUN_TESTS.md is rewritten against the real setup: correct interpreter path, the two test-file styles, the import rules for adding tests, and a per-file coverage table. It no longer documents test_gradient_analyzer_integration.py, which is not in the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
import os, sys
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import importlib.util
|
|
import torch
|
|
from gta_helpers import load_gta, gta, run
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, os.path.join(REPO, "src"))
|
|
|
|
|
|
def _load(modfile, name):
|
|
spec = importlib.util.spec_from_file_location(name, os.path.join(REPO, "src", modfile))
|
|
m = importlib.util.module_from_spec(spec)
|
|
m.__name__ = name
|
|
m.__package__ = name.rsplit(".", 1)[0]
|
|
sys.modules[name] = m
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
def _delta_from_lora(up, down, alpha):
|
|
rank = up.shape[1]
|
|
return (alpha / rank) * (up @ down)
|
|
|
|
|
|
def test_refactor_reconstructs_delta():
|
|
torch.manual_seed(0)
|
|
U = torch.randn(32, 8)
|
|
V = torch.randn(8, 48)
|
|
S_diag = torch.tensor([10.0, 8.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1])
|
|
delta = U @ torch.diag(S_diag) @ V
|
|
refactor = _load("merge/lora_refactor.py", "merge.lora_refactor")
|
|
up, down, alpha = refactor.merged_delta_to_lora(delta, target_rank=16,
|
|
energy=0.999)
|
|
rank = up.shape[1]
|
|
recon = (alpha / rank) * (up @ down)
|
|
err = (recon - delta).norm() / delta.norm()
|
|
assert err < 0.05, f"reconstruction error {err}"
|
|
|
|
|
|
def test_style_plus_character_nonoverlap_keeps_strength():
|
|
torch.manual_seed(0)
|
|
A = torch.zeros(4, 6); A[0, :] = 2.0
|
|
B = torch.zeros(4, 6); B[2, :] = 2.0
|
|
merged = gta.gta_merge([A, B], torch.tensor([1.0, 1.0]), mode="ties",
|
|
density=1.0, normalize=True)
|
|
assert torch.allclose(merged[0], A[0])
|
|
assert torch.allclose(merged[2], B[2])
|
|
|
|
|
|
def test_normalize_does_not_collapse_as_1_over_n_squared():
|
|
torch.manual_seed(0)
|
|
deltas = [torch.randn(6, 6) for _ in range(4)]
|
|
avg = torch.stack(deltas).mean(0) # snapshot: gta_merge consumes `deltas`
|
|
merged = gta.gta_merge(deltas, torch.ones(4), mode="ties", density=1.0,
|
|
normalize=True)
|
|
assert merged.norm() > 0.5 * avg.norm()
|
|
|
|
|
|
# Runnable as a plain script (`python tests/<file>.py`); under pytest the
|
|
# test_* functions are collected directly, so the script runner must not fire
|
|
# at import time -- it calls sys.exit() and would abort collection.
|
|
if __name__ == "__main__":
|
|
run([
|
|
("refactor_reconstructs", test_refactor_reconstructs_delta),
|
|
("nonoverlap_keeps_strength", test_style_plus_character_nonoverlap_keeps_strength),
|
|
("no_1_over_n_squared", test_normalize_does_not_collapse_as_1_over_n_squared),
|
|
])
|