From 66fe1cc9184596188d6fe8c7a490777a0125f1c5 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Thu, 26 Mar 2026 23:09:42 +0200 Subject: [PATCH] [AZ-152] Add test infrastructure: pytest conftest, fixtures, constants patching Made-with: Cursor --- _docs/03_implementation/batch_01_report.md | 28 ++++ tests/__init__.py | 0 tests/conftest.py | 149 +++++++++++++++++++++ tests/performance/__init__.py | 0 tests/performance/conftest.py | 0 tests/performance/test_placeholder.py | 2 + tests/test_infrastructure.py | 59 ++++++++ 7 files changed, 238 insertions(+) create mode 100644 _docs/03_implementation/batch_01_report.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/performance/__init__.py create mode 100644 tests/performance/conftest.py create mode 100644 tests/performance/test_placeholder.py create mode 100644 tests/test_infrastructure.py diff --git a/_docs/03_implementation/batch_01_report.md b/_docs/03_implementation/batch_01_report.md new file mode 100644 index 0000000..7dbfdcc --- /dev/null +++ b/_docs/03_implementation/batch_01_report.md @@ -0,0 +1,28 @@ +# Batch Report + +**Batch**: 1 +**Tasks**: AZ-152 (test_infrastructure) +**Date**: 2026-03-26 + +## Task Results + +| Task | Status | Files Modified | Tests | Issues | +|------|--------|---------------|-------|--------| +| AZ-152_test_infrastructure | Done | 6 files | 12/12 passed | None | + +## Files Created + +| File | Lines | Purpose | +|------|-------|---------| +| tests/conftest.py | 149 | Session/function fixtures, constants patching, collect_ignore for legacy scripts | +| tests/__init__.py | 0 | Package init | +| tests/performance/conftest.py | 0 | Performance conftest (empty, inherits parent) | +| tests/performance/__init__.py | 0 | Package init | +| tests/test_infrastructure.py | 59 | Smoke tests for all fixtures and constants patching | +| tests/performance/test_placeholder.py | 2 | Placeholder so performance test collection succeeds | + +## Code Review Verdict: PASS +## Auto-Fix Attempts: 0 +## Stuck Agents: None + +## Next Batch: AZ-153, AZ-154, AZ-155, AZ-156, AZ-157, AZ-158, AZ-159, AZ-160, AZ-161, AZ-162, AZ-163 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f68ed67 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,149 @@ +import shutil +from pathlib import Path + +import pytest + +_PROJECT_ROOT = Path(__file__).resolve().parent.parent +_DATASET_IMAGES = _PROJECT_ROOT / "_docs/00_problem/input_data/dataset/images" +_DATASET_LABELS = _PROJECT_ROOT / "_docs/00_problem/input_data/dataset/labels" +_ONNX_MODEL = _PROJECT_ROOT / "_docs/00_problem/input_data/azaion.onnx" +_CLASSES_JSON = _PROJECT_ROOT / "classes.json" + +collect_ignore = ["security_test.py", "imagelabel_visualize_test.py"] + + +def apply_constants_patch(monkeypatch, base: Path): + import constants as c + from os import path + + root = str(base.resolve()) + azaion = path.join(root, "azaion") + monkeypatch.setattr(c, "azaion", azaion) + data_dir = path.join(azaion, "data") + monkeypatch.setattr(c, "data_dir", data_dir) + monkeypatch.setattr(c, "data_images_dir", path.join(data_dir, c.images)) + monkeypatch.setattr(c, "data_labels_dir", path.join(data_dir, c.labels)) + processed_dir = path.join(azaion, "data-processed") + monkeypatch.setattr(c, "processed_dir", processed_dir) + monkeypatch.setattr(c, "processed_images_dir", path.join(processed_dir, c.images)) + monkeypatch.setattr(c, "processed_labels_dir", path.join(processed_dir, c.labels)) + corrupted_dir = path.join(azaion, "data-corrupted") + monkeypatch.setattr(c, "corrupted_dir", corrupted_dir) + monkeypatch.setattr(c, "corrupted_images_dir", path.join(corrupted_dir, c.images)) + monkeypatch.setattr(c, "corrupted_labels_dir", path.join(corrupted_dir, c.labels)) + monkeypatch.setattr(c, "sample_dir", path.join(azaion, "data-sample")) + monkeypatch.setattr(c, "datasets_dir", path.join(azaion, "datasets")) + models_dir = path.join(azaion, "models") + monkeypatch.setattr(c, "models_dir", models_dir) + monkeypatch.setattr(c, "CURRENT_PT_MODEL", path.join(models_dir, f"{c.prefix[:-1]}.pt")) + monkeypatch.setattr(c, "CURRENT_ONNX_MODEL", path.join(models_dir, f"{c.prefix[:-1]}.onnx")) + + +@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_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 diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/performance/conftest.py b/tests/performance/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/performance/test_placeholder.py b/tests/performance/test_placeholder.py new file mode 100644 index 0000000..04218ef --- /dev/null +++ b/tests/performance/test_placeholder.py @@ -0,0 +1,2 @@ +def test_performance_package_collects(): + assert True diff --git a/tests/test_infrastructure.py b/tests/test_infrastructure.py new file mode 100644 index 0000000..b3c20cf --- /dev/null +++ b/tests/test_infrastructure.py @@ -0,0 +1,59 @@ +import constants as c + + +def test_fixture_images_dir_has_jpegs(fixture_images_dir): + jpgs = list(fixture_images_dir.glob("*.jpg")) + assert len(jpgs) == 100 + + +def test_fixture_labels_dir_has_yolo_labels(fixture_labels_dir): + txts = list(fixture_labels_dir.glob("*.txt")) + assert len(txts) == 100 + + +def test_fixture_onnx_model_bytes(fixture_onnx_model): + assert len(fixture_onnx_model) > 0 + + +def test_fixture_classes_json_exists(fixture_classes_json): + assert fixture_classes_json.is_file() + assert fixture_classes_json.name == "classes.json" + + +def test_work_dir_layout(work_dir): + assert (work_dir / "images").is_dir() + assert (work_dir / "labels").is_dir() + + +def test_sample_image_label_paths(sample_image_label): + img, lbl = sample_image_label + assert img.suffix.lower() == ".jpg" + assert lbl.suffix == ".txt" + assert img.exists() and lbl.exists() + assert img.stem == lbl.stem + + +def test_sample_images_labels_factory(sample_images_labels): + img_dir, lbl_dir = sample_images_labels(3) + assert len(list(img_dir.glob("*.jpg"))) == 3 + assert len(list(lbl_dir.glob("*.txt"))) == 3 + + +def test_corrupted_label_content(corrupted_label): + text = corrupted_label.read_text(encoding="utf-8") + assert "1.5" in text + + +def test_edge_bbox_label_exists(edge_bbox_label): + assert edge_bbox_label.read_text(encoding="utf-8").strip() + + +def test_empty_label_file(empty_label): + assert empty_label.read_text(encoding="utf-8") == "" + + +def test_constants_patch_uses_tmp(constants_patch, tmp_path): + constants_patch(tmp_path) + assert c.azaion.startswith(str(tmp_path)) + assert c.data_dir.startswith(str(tmp_path)) + assert c.CURRENT_ONNX_MODEL.startswith(str(tmp_path))