import csv import shutil import sys from pathlib import Path import pytest _PROJECT_ROOT = Path(__file__).resolve().parent.parent _TESTS_DIR = Path(__file__).resolve().parent _TEST_ROOT = _TESTS_DIR / "root" _DATASET_IMAGES = _TEST_ROOT / "data" / "images" _DATASET_LABELS = _TEST_ROOT / "data" / "labels" _ONNX_MODEL = _PROJECT_ROOT / "_docs/00_problem/input_data/azaion.onnx" _PT_MODEL = _PROJECT_ROOT / "_docs/00_problem/input_data/azaion-2025-03-10.pt" _CLASSES_JSON = _PROJECT_ROOT / "src" / "classes.json" _CONFIG_TEST = _PROJECT_ROOT / "config.test.yaml" _MODELS_DIR = _TEST_ROOT / "models" collect_ignore = ["security_test.py", "imagelabel_visualize_test.py"] _E2E_MODULE = "test_training_e2e" _test_results = [] def pytest_collection_modifyitems(items): e2e = [i for i in items if _E2E_MODULE in i.nodeid] rest = [i for i in items if _E2E_MODULE not in i.nodeid] items[:] = e2e + rest @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" or (report.when == "setup" and report.skipped): _test_results.append({ "module": item.nodeid.rsplit("::", 1)[0], "name": item.name, "result": report.outcome.upper(), "duration": round(report.duration, 3), }) def pytest_sessionfinish(session, exitstatus): if _test_results: results_dir = Path(__file__).resolve().parent / "test-results" results_dir.mkdir(exist_ok=True) with open(results_dir / "test-results.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["module", "test", "result", "duration_s"]) for r in _test_results: writer.writerow([r["module"], r["name"], r["result"], f"{r['duration']:.3f}"]) import constants as c test_config = c.Config.from_yaml(str(_CONFIG_TEST), root=str(_TEST_ROOT)) for d in (_DATASET_IMAGES, _DATASET_LABELS, test_config.datasets_dir, test_config.corrupted_dir, str(_MODELS_DIR)): shutil.rmtree(str(d), ignore_errors=True) def apply_constants_patch(monkeypatch, base: Path): import constants as c monkeypatch.setattr(c, "config", c.Config.from_yaml(str(_CONFIG_TEST), root=str(base / "azaion"))) @pytest.fixture(scope="session") def fixture_images_dir(): p = _DATASET_IMAGES if not p.is_dir(): pytest.skip(f"missing dataset images: {p}") return p @pytest.fixture(scope="session") def fixture_labels_dir(): p = _DATASET_LABELS if not p.is_dir(): pytest.skip(f"missing dataset labels: {p}") return p @pytest.fixture(scope="session") def fixture_onnx_model(): p = _ONNX_MODEL if not p.is_file(): pytest.skip(f"missing onnx model: {p}") return p.read_bytes() @pytest.fixture(scope="session") def fixture_pt_model(): p = _PT_MODEL if not p.is_file(): pytest.skip(f"missing pt model: {p}") return str(p) @pytest.fixture(scope="session") def fixture_classes_json(): p = _CLASSES_JSON if not p.is_file(): pytest.skip(f"missing classes.json: {p}") return p @pytest.fixture def constants_patch(monkeypatch): def _apply(base: Path): apply_constants_patch(monkeypatch, base) return _apply @pytest.fixture def work_dir(tmp_path): w = tmp_path / "work" (w / "images").mkdir(parents=True) (w / "labels").mkdir(parents=True) return w @pytest.fixture def sample_image_label(fixture_images_dir, fixture_labels_dir, tmp_path): imgs = sorted(fixture_images_dir.glob("*.jpg")) if not imgs: raise RuntimeError("no images in fixture_images_dir") stem = imgs[0].stem src_img = fixture_images_dir / f"{stem}.jpg" src_lbl = fixture_labels_dir / f"{stem}.txt" dst_img = tmp_path / "images" / f"{stem}.jpg" dst_lbl = tmp_path / "labels" / f"{stem}.txt" dst_img.parent.mkdir(parents=True, exist_ok=True) dst_lbl.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_img, dst_img) shutil.copy2(src_lbl, dst_lbl) return dst_img, dst_lbl @pytest.fixture def sample_images_labels(fixture_images_dir, fixture_labels_dir, tmp_path): def _factory(count: int): if count < 1: raise ValueError("count must be >= 1") imgs = sorted(fixture_images_dir.glob("*.jpg")) if count > len(imgs): raise ValueError("count exceeds available images") out_img = tmp_path / "images" out_lbl = tmp_path / "labels" out_img.mkdir(parents=True, exist_ok=True) out_lbl.mkdir(parents=True, exist_ok=True) for p in imgs[:count]: stem = p.stem shutil.copy2(fixture_images_dir / f"{stem}.jpg", out_img / f"{stem}.jpg") shutil.copy2(fixture_labels_dir / f"{stem}.txt", out_lbl / f"{stem}.txt") return out_img, out_lbl return _factory @pytest.fixture def corrupted_label(tmp_path): p = tmp_path / "labels" / "corrupted.txt" p.parent.mkdir(parents=True, exist_ok=True) p.write_text("0 1.5 0.5 0.1 0.1\n", encoding="utf-8") return p @pytest.fixture def edge_bbox_label(tmp_path): p = tmp_path / "labels" / "edge.txt" p.parent.mkdir(parents=True, exist_ok=True) p.write_text("0 0.01 0.5 0.02 0.3\n", encoding="utf-8") return p @pytest.fixture def empty_label(tmp_path): p = tmp_path / "labels" / "empty.txt" p.parent.mkdir(parents=True, exist_ok=True) p.write_text("", encoding="utf-8") return p @pytest.fixture(scope="session") def exported_models(): from ultralytics import YOLO import constants as c import exports as exports_mod _MODELS_DIR.mkdir(parents=True, exist_ok=True) pt_path = str(_MODELS_DIR / "test.pt") YOLO("yolo11n.pt").save(pt_path) old_config = c.config c.config = c.Config.from_yaml(str(_CONFIG_TEST), root=str(_TEST_ROOT)) imgsz = c.config.export.onnx_imgsz exports_mod.export_onnx(pt_path) if sys.platform == "darwin": exports_mod.export_coreml(pt_path) c.config = old_config onnx_files = list(_MODELS_DIR.glob("test*.onnx")) return { "onnx": str(onnx_files[0]) if onnx_files else None, "model_dir": _MODELS_DIR, "pt_path": pt_path, "imgsz": imgsz, }