File size: 5,766 Bytes
546c207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""End-to-end checks of the Streamlit app, run headlessly via AppTest.

These drive the real `app.py` script: type into the box, submit, inspect what
was rendered. They are deliberately few -- enough to prove the search path,
the multi-occurrence path and the error paths work.
"""

import pytest

from spareparts import config

pytest.importorskip("streamlit")
from streamlit.testing.v1 import AppTest  # noqa: E402

pytestmark = pytest.mark.skipif(
    not config.DB_PATH.exists(), reason="run `python -m spareparts.build_index` first"
)

APP = str(config.PROJECT_ROOT / "app.py")


def _search(term: str, timeout: int = 60) -> AppTest:
    app = AppTest.from_file(APP, default_timeout=timeout).run()
    app.text_input[0].set_value(term)
    app.button[0].click().run()
    return app


def _text(app: AppTest) -> str:
    parts = [el.value for el in app.markdown] + [el.value for el in app.caption]
    parts += [el.value for el in app.subheader] + [el.value for el in app.warning]
    parts += [el.value for el in app.error] + [el.value for el in app.info]
    return "\n".join(str(p) for p in parts)


def test_app_starts_without_a_search():
    app = AppTest.from_file(APP, default_timeout=60).run()
    assert not app.exception
    assert app.title[0].value == "Machine Part Finder"
    assert app.text_input[0].label == "Part number"


def test_valid_part_shows_metadata_and_an_image():
    app = _search("30642")
    assert not app.exception

    body = _text(app)
    assert "30642" in body
    assert "worm,cpl." in body          # canonical English name
    assert "Motor-drive" in body        # catalogue section title
    assert "Located" in body            # localization method, stated plainly

    # PDF page and section number are shown as distinct facts.
    metrics = {m.label: m.value for m in app.metric}
    assert metrics["PDF page"] == "34"
    assert metrics["Catalogue section"] == "07"

    assert len(app.image) == 1, "the annotated diagram should be displayed"


def test_unknown_part_is_reported_not_crashed():
    app = _search("99999")
    assert not app.exception
    assert app.error, "an unknown part must produce an error message"
    assert "not found in the catalogue" in app.error[0].value
    assert not app.image


@pytest.mark.parametrize("term", ["", "   ", "abc", "!!!"])
def test_empty_and_non_numeric_input_is_handled(term):
    app = _search(term)
    assert not app.exception
    assert not app.image
    if term.strip():
        assert app.warning or app.error


def test_messy_input_is_normalized_then_found():
    app = _search("  part no. 30642  ")
    assert not app.exception
    assert not app.error
    assert "worm,cpl." in _text(app)


def test_multiple_occurrences_offer_a_selector():
    app = _search("73290")          # drawn on ten catalogue pages
    assert not app.exception
    assert app.radio, "a part on several pages must offer a choice"

    options = app.radio[0].options
    assert len(options) > 1
    assert all(o.startswith("Occurrence ") for o in options)
    assert "PDF page" in options[0]
    # One page is shown at a time, and it is one the user chose.
    assert len(app.image) == 1


def test_selecting_another_occurrence_switches_page():
    app = _search("73290")
    first = {m.label: m.value for m in app.metric}["PDF page"]
    app.radio[0].set_value(app.radio[0].options[2]).run()
    assert not app.exception
    second = {m.label: m.value for m in app.metric}["PDF page"]
    assert first != second


def test_low_confidence_result_is_shown_as_approximate():
    """A conservative fallback must be visible as such, not dressed up."""
    from spareparts import index_db

    conn = index_db.connect()
    try:
        row = conn.execute(
            "SELECT l.part_no FROM labels l JOIN components c ON c.label_id = l.id"
            " WHERE c.method = 'capped' AND l.part_no IN"
            " (SELECT part_no FROM labels GROUP BY part_no HAVING COUNT(*) = 1)"
            " LIMIT 1"
        ).fetchone()
    finally:
        conn.close()

    app = _search(row[0])
    assert not app.exception
    body = _text(app)
    assert "Approximate" in body
    assert "Confidence" in body


def test_listed_but_not_drawn_part_shows_the_page_and_says_so():
    app = _search("70248")          # listed on page 39, not drawn there
    assert not app.exception
    assert not app.error
    assert app.warning, "the user must be told why there is no highlight"
    assert "no callout" in app.warning[0].value
    assert len(app.image) == 1, "the page itself should still be shown"


def test_no_ocr_engine_is_loaded_during_a_search():
    """Definition of done: searching must not run OCR or localization.

    Checked in a subprocess so that nothing another test imported can mask it.
    """
    import subprocess
    import sys
    import textwrap

    script = textwrap.dedent(
        """
        import sys
        from streamlit.testing.v1 import AppTest
        app = AppTest.from_file(sys.argv[1], default_timeout=60).run()
        app.text_input[0].set_value("30642")
        app.button[0].click().run()
        assert not app.exception, app.exception
        banned = [m for m in sys.modules
                  if "rapidocr" in m or m.startswith("spareparts.ocr")
                  or m.startswith("spareparts.localizer")]
        print("LOADED:" + ",".join(sorted(banned)))
        """
    )
    result = subprocess.run(
        [sys.executable, "-c", script, APP],
        capture_output=True, text=True, cwd=str(config.PROJECT_ROOT), timeout=300,
    )
    assert result.returncode == 0, result.stderr[-2000:]
    line = next(l for l in result.stdout.splitlines() if l.startswith("LOADED:"))
    assert line == "LOADED:", f"search pulled in offline-only modules: {line}"