"""Phase 8: the find bar search (`q=`), one test per field of the vocabulary, on `GET /api/packets`. Every word must match some field; a word matches by substring, case insensitive. Each distinctive value below is on one packet only, so a search returns exactly the packets that carry it. The world is built once per module. """ from __future__ import annotations from collections.abc import Iterator from dataclasses import dataclass, field from typing import Any import pytest from pmhelpers import add_alias, make_project, make_unit, module_shop, quick_draft from pshelpers import needs_pg from shophelpers import Shop, ok, run, step pytestmark = needs_pg LONG = "Q" * 60 # a title of exactly 60 characters @dataclass class SearchWorld: shop: Shop k: dict[str, int] = field(default_factory=dict) every: set[int] = field(default_factory=set) def ids(self, *keys: str) -> set[int]: return {self.k[x] for x in keys} def make( shop: Shop, w: SearchWorld, key: str, unit_id: int, title: str, *, ready_dept: str | None = None, **over: Any, ) -> dict[str, Any]: pkt = quick_draft(shop, unit_id, title, n=len(w.k) + 1, send=ready_dept, **over) w.k[key] = int(pkt["id"]) w.every.add(int(pkt["id"])) return pkt @pytest.fixture(scope="module") def world(tmp_path_factory: pytest.TempPathFactory) -> Iterator[SearchWorld]: with module_shop(tmp_path_factory.mktemp("search")) as shop: s = shop.settings w = SearchWorld(shop) p1 = make_project(s, "L489", "BPX separator", customer="BPX Energy", po="PO-4471") add_alias(s, p1, "OLD489X") u52, u53, u54 = (make_unit(s, p1, lot) for lot in ("052", "053", "054")) p2 = make_project(s, "L512", "Pump skid", customer="Vortex Oil", po="PO-9") u2 = make_unit(s, p2, "001") make(shop, w, "title", u52, "Inlet spool", drawing="D-48810", rev="B") make(shop, w, "op", u53, "Bravo spool", op="HYDRO-TEST") make(shop, w, "wps", u53, "Charlie spool", wps="WPS-77X") make(shop, w, "cust", u54, "Delta spool", customer="Acme Gas") make( shop, w, "wo", u54, "Echo spool", work_orders=[{"wo_base": "5ZZQ", "lot": "007", "split": "0", "sub": "0", "qty": 1, "target_hrs": 1}], ) make( shop, w, "heat", u52, "Foxtrot spool", bom=[ {"item": 1, "qty": 1, "part_id": "FLG-9876", "description": "Weld neck flange"}, {"item": 2, "qty": 1, "part_id": "FLG-9877", "description": "Blind flange"}, ], ) make( shop, w, "mat", u53, "Golf spool", bom=[ {"item": 1, "qty": 2, "part_id": "GSK-1", "description": "Spiral wound gasket", "material": "A182-F316"} ], ) make(shop, w, "p2", u2, "Hotel spool") make(shop, w, "asg", u2, "India spool", ready_dept="03") make(shop, w, "co", u52, "Juliet spool") make(shop, w, "dept", u2, "Kilo spool", ready_dept="06") make(shop, w, "flag", u54, "Lima spool") make(shop, w, "long", u54, LONG) # the assigned welder and the stamp asg = ok(shop("sup.wes").get(f"/api/packets/{w.k['asg']}")) ok( step( shop("sup.wes"), asg, "assign", station_id=shop.stall("03", "Stall 2"), welder_id=shop.uid("weld.andy") ) ) run(s, "UPDATE packets SET welder_stamp = 'A7Q' WHERE id = %s", (w.k["asg"],)) run(s, "UPDATE users SET welder_stamp = 'A7Q' WHERE sam_account = 'weld.andy'") # two BOM lines with one heat and two welds with one stamp: one packet, never two rows run(s, "UPDATE bom_lines SET heat = 'HX44921' WHERE packet_id = %s", (w.k["heat"],)) for no in (1, 2): run( s, "INSERT INTO welds (packet_id, weld_no, items, type, stamp, welder_id) VALUES (%s, %s, '{1}'," " 'Fillet', 'ZQ9', (SELECT id FROM users WHERE sam_account = 'lead.lou'))", (w.k["heat"], no), ) # a change order with an ECN run( s, "INSERT INTO change_orders (packet_id, co_no, origin, review, minor, ecn, reason)" " VALUES (%s, 47, 'manual', 'approved', true, 'ECN-7781', 'Nozzle moved')", (w.k["co"],), ) # a VISUAL draft with a flag kind run( s, "UPDATE packets SET source_kind = 'visual', flags = '{rev_mismatch}', drawing_state = 'rev_mismatch'" " WHERE id = %s", (w.k["flag"],), ) yield w def found(w: SearchWorld, q: str, sam: str = "sup.wes") -> list[int]: r = w.shop(sam).get("/api/packets", params={"q": q}) assert r.status_code == 200, r.text return [int(c["id"]) for c in r.json()["packets"]] def hits(w: SearchWorld, q: str) -> set[int]: got = found(w, q) assert len(got) == len(set(got)), ("duplicate rows", q, got) return set(got) P1 = ("title", "op", "wps", "cust", "wo", "heat", "mat", "co", "flag", "long") P2 = ("p2", "asg", "dept") DRAFTS = tuple(k for k in (*P1, *P2) if k not in ("asg", "dept")) @pytest.mark.parametrize( ("q", "keys"), [ ("L512", P2), # project number ("pump", P2), # project name ("L512001", P2), # unit number ("L5121", P2), # unit number with the lot's zeros stripped (L512 || 1) ("L48952", ("title", "heat", "co")), # the same for lot 052 ("lot 52", ("title", "heat", "co")), # lot 052 by its number ("HYDRO-TEST", ("op",)), # operation ("D-48810", ("title",)), # drawing ("D-48810 rev B", ("title",)), # drawing and rev ("WPS-77X", ("wps",)), # WPS ("acme", ("cust",)), # packet customer ("vortex", P2), # project customer ("4471", P1), # project PO ("OLD489X", P1), # project alias ("5zzq", ("wo",)), # work order base ("5ZZQ/007", ("wo",)), # work order and lot ("hx449", ("heat",)), # BOM heat ("FLG-9876", ("heat",)), # BOM part id ("spiral", ("mat",)), # BOM description ("A182-F316", ("mat",)), # BOM material ("andy", ("asg",)), # assigned welder's name ("A7Q", ("asg",)), # assigned welder's stamp ("ZQ9", ("heat",)), # weld stamp ("lou", ("heat",)), # the welder named on a weld ("CO-47", ("co",)), # change order number ("ECN-7781", ("co",)), # ECN ("stall", ("asg",)), # station name ("bldg", ("dept",)), # department name ("sent", ("asg",)), # status label: assigned is "Sent to station" ("rev_mismatch", ("flag",)), # flag kind ("flagged", DRAFTS), # every flagged draft (hand drafts and the flagged VISUAL draft) ("inlet", ("title",)), # title ("INLET", ("title",)), # case insensitive ], ) def test_search_field(world: SearchWorld, q: str, keys: tuple[str, ...]) -> None: assert hits(world, q) == world.ids(*keys) def test_lot_and_rev_alone_match_everything(world: SearchWorld) -> None: assert hits(world, "lot") == world.every assert hits(world, "rev") == world.every assert hits(world, "lot 052") == world.ids("title", "heat", "co") def test_several_words_narrow(world: SearchWorld) -> None: assert hits(world, "L489") == world.ids(*P1) assert hits(world, "L489 spool") == world.ids(*(k for k in P1 if k != "long")) assert hits(world, "L489 52") == world.ids("title", "heat", "co") assert hits(world, "L489 52 inlet") == world.ids("title") assert hits(world, "L512 HYDRO-TEST") == set() assert hits(world, " inlet L489 ") == world.ids("title") def test_no_duplicates_from_child_rows(world: SearchWorld) -> None: for q in ("hx449", "ZQ9", "FLG", "lou", "flange"): got = found(world, q) assert got == [world.k["heat"]], (q, got) def test_long_word_is_cut_at_60(world: SearchWorld) -> None: assert hits(world, LONG) == world.ids("long") assert hits(world, LONG + "ZZZZ") == world.ids("long") # cut to the first 60 characters assert hits(world, "Q" * 61) == world.ids("long") def test_only_eight_words_count(world: SearchWorld) -> None: assert hits(world, " ".join(["inlet"] * 8)) == world.ids("title") assert hits(world, " ".join(["lot"] * 8 + ["inlet"])) == world.every def test_like_wildcards_are_literal(world: SearchWorld) -> None: assert hits(world, "%") == set() assert hits(world, "_") == world.ids("flag") # only the flag kind has an underscore