Newer
Older
TIASDataHub / tests / test_stats.py
"""目録の集計(`tiasdata stats`.SPEC_03「CLI」).

- 種類・所見の値ごとの件数を返す
- 対応表を開かない(SPEC_02「対応表」: 対応表を開くのは取り込みのときだけ)
"""

import sqlite3
from contextlib import closing

from conftest import ImportEnv

from tiasdata.cli import main
from tiasdata.config import load_config
from tiasdata.importers import run_import
from tiasdata.stats import collect_stats


def _open(env: ImportEnv) -> sqlite3.Connection:
    return sqlite3.connect(env.catalog_root / "catalog.sqlite")


def test_empty_catalog(import_env: ImportEnv) -> None:
    assert main(["init"]) == 0
    with closing(_open(import_env)) as conn:
        stats = collect_stats(conn)
    assert stats.subjects == 0
    assert stats.captures == 0
    assert stats.subjects_with_multiple_captures == 0
    assert set(stats.assets_by_kind.values()) == {0}
    assert "tongue_crop_masked" in stats.assets_by_kind
    assert stats.annotations == {}
    assert stats.superseded_annotations == 0


def test_counts_after_import(import_env: ImportEnv) -> None:
    assert main(["init"]) == 0
    run_import(load_config(import_env.config_path), import_env.source_name)
    with closing(_open(import_env)) as conn:
        stats = collect_stats(conn)
    assert stats.subjects == 7
    assert stats.captures == 18
    # ダミーの 7 人はみな 2 枚以上
    assert stats.subjects_with_multiple_captures == 7
    assert stats.assets_by_kind == {
        "color_chart": 0,
        "tongue_crop_masked": 18,
        "tongue_mask": 0,
        "tongue_raw": 0,
    }
    tongue = stats.annotations["tongue_color"]
    assert tongue.total == 18
    assert tongue.by_field["label"] == {
        "dark_red": 4,
        "light_red": 2,
        "pale": 4,
        "purple": 4,
        "red": 4,
    }
    assert tongue.by_field["severity"] == {"mild": 8, "normal": 2, "strong": 8}
    # 9 フォルダ(ラベル×程度の組み合わせ)ごとに 2 枚
    assert len(tongue.by_value) == 9
    assert set(tongue.by_value.values()) == {2}
    assert stats.superseded_annotations == 0


def test_stats_does_not_need_identity_db(import_env: ImportEnv) -> None:
    assert main(["init"]) == 0
    run_import(load_config(import_env.config_path), import_env.source_name)
    import_env.identity_path.unlink()
    assert main(["stats"]) == 0
    assert not import_env.identity_path.exists()


def test_reviewed_images_are_counted_with_marks(import_env: ImportEnv) -> None:
    # 2026-10-08 の仕様変更: 要確認の番号も登録し,撮影に要確認の印を付ける
    from conftest import write_reviews

    write_reviews(import_env.reviews_dir, "dummy_tongue", [1, 2])  # light_red の 2 枚
    assert main(["init"]) == 0
    run_import(load_config(import_env.config_path), import_env.source_name)
    with closing(_open(import_env)) as conn:
        stats = collect_stats(conn)
    assert stats.captures == 18
    assert stats.review_marks_on_captures == 2
    assert stats.review_marks_on_files == 0
    assert stats.assets_without_capture == 0


def _share_first_image(env: ImportEnv, number: int) -> None:
    # 番号 1 と同じ中身を同じフォルダ(同じラベル)に置き,対応表に行を足す
    from conftest import write_patient_map

    first = env.source.images[0]
    (first.path.parent / f"{number}.png").write_bytes(first.path.read_bytes())
    rows = [(n, 1, key) for n, key in sorted(env.patient_map.patient_keys.items())]
    write_patient_map(env.patient_map.path, [*rows, (number, 1, "990088")])


def test_unlinked_files_and_review_marks_are_counted(import_env: ImportEnv) -> None:
    # 2026-10-08: 撮影にひも付かないファイルの件数,review の印の件数(撮影・ファイル).
    # review は所見の種類ごとの内訳に入れない
    from conftest import write_reviews

    _share_first_image(import_env, 19)
    write_reviews(import_env.reviews_dir, "dummy_tongue", [1, 5, 19])
    assert main(["init"]) == 0
    run_import(load_config(import_env.config_path), import_env.source_name)
    with closing(_open(import_env)) as conn:
        stats = collect_stats(conn)
    assert stats.captures == 17
    assert stats.assets_by_kind["tongue_crop_masked"] == 18
    assert stats.assets_without_capture == 1
    assert stats.review_marks_on_captures == 1
    assert stats.review_marks_on_files == 1
    assert set(stats.annotations) == {"tongue_color"}
    # 撮影 17 件と撮影にひも付かないファイル 1 件の舌色
    assert stats.annotations["tongue_color"].total == 18


def test_superseded_review_mark_is_not_counted(import_env: ImportEnv) -> None:
    from conftest import write_reviews

    write_reviews(import_env.reviews_dir, "dummy_tongue", [1, 2])
    assert main(["init"]) == 0
    run_import(load_config(import_env.config_path), import_env.source_name)
    with closing(_open(import_env)) as conn:
        ids = [
            i
            for (i,) in conn.execute(
                "SELECT annotation_id FROM annotations WHERE kind = 'review' ORDER BY annotation_id"
            )
        ]
        conn.execute(
            "UPDATE annotations SET superseded_by = ? WHERE annotation_id = ?", (ids[1], ids[0])
        )
        conn.commit()
        stats = collect_stats(conn)
    assert stats.review_marks_on_captures == 1
    assert stats.review_marks_on_files == 0
    assert stats.superseded_annotations == 1


def test_empty_catalog_has_no_unlinked_or_marks(import_env: ImportEnv) -> None:
    assert main(["init"]) == 0
    with closing(_open(import_env)) as conn:
        stats = collect_stats(conn)
    assert stats.assets_without_capture == 0
    assert stats.review_marks_on_captures == 0
    assert stats.review_marks_on_files == 0