Files
PacketStation/tests/server/test_search.py
T

229 lines
8.6 KiB
Python
Raw Normal View History

"""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