229 lines
8.6 KiB
Python
229 lines
8.6 KiB
Python
"""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
|