Files

189 lines
7.4 KiB
Python
Raw Permalink Normal View History

"""Phase 5 station clock: every value is the server's. START starts it, the clock endpoint pauses and resumes it,
PULL_BACK and SUBMIT fold the running time in, the live elapsed grows without a write, and no request field sets
it. Time is moved by updating clock_running_since by SQL, so the tests do not wait."""
from __future__ import annotations
import time
from collections.abc import Iterator
from datetime import datetime
from typing import Any
import pytest
from fakedir import FakeDirectory
from pkthelpers import step
from pshelpers import needs_pg
from shophelpers import (
SIGN,
Shop,
finish_stage,
fresh,
im,
in_progress,
in_review,
last_log,
ok,
one,
open_shop,
ready_packet,
refused,
set_running_since,
start,
to_station,
)
from packetstation.settings import Settings
pytestmark = needs_pg
@pytest.fixture
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
yield from open_shop(pg_settings, fake_dir)
def stored(shop: Shop, pid: int) -> tuple[int, bool]:
row = one(
shop.settings,
"SELECT clock_elapsed_ms, clock_running_since IS NOT NULL FROM packet_stages"
" WHERE packet_id = %s AND closed_at IS NULL",
(pid,),
)
return int(row[0]), bool(row[1])
def clock_op(shop: Shop, pkt: dict[str, Any], op: str, by: str = "weld.andy", **extra: Any) -> Any:
return shop(by).post(f"/api/packets/{pkt['id']}/stage/clock", json={"op": op, **extra})
def ts(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def test_start_pause_resume_pull_back(shop: Shop) -> None:
pkt = to_station(shop, ready_packet(shop))
c = pkt["stage"]["clock"]
assert (c["running"], c["stored_ms"], c["elapsed_ms"], c["running_since"], c["started_at"]) == (
False,
0,
0,
None,
None,
)
pkt = start(shop, pkt)
c = pkt["stage"]["clock"]
assert c["running"] is True and c["stored_ms"] == 0 and c["running_since"] and c["started_at"]
started_at = c["started_at"]
# pause after 90 s of running
set_running_since(shop.settings, pkt["id"], 90)
out = ok(clock_op(shop, pkt, "pause"))
c = out["stage"]["clock"]
assert c["running"] is False and c["running_since"] is None
assert 90_000 <= c["stored_ms"] < 95_000 and c["elapsed_ms"] == c["stored_ms"]
assert stored(shop, pkt["id"]) == (c["stored_ms"], False)
assert last_log(shop("weld.andy"), pkt["id"])["action"] == "Paused"
paused_at = c["stored_ms"]
# resume, run 30 s, then the supervisor pulls it back: the running time is folded in
out = ok(clock_op(shop, out, "resume"))
assert out["stage"]["clock"]["running"] is True and out["stage"]["clock"]["started_at"] == started_at
assert last_log(shop("weld.andy"), pkt["id"])["action"] == "Resumed"
set_running_since(shop.settings, pkt["id"], 30)
out = ok(step(shop("sup.wes"), out, "pull_back"))
c = out["stage"]["clock"]
assert c["running"] is False and paused_at + 30_000 <= c["stored_ms"] < paused_at + 35_000
assert stored(shop, pkt["id"])[1] is False
# assigned and started again: it carries on from the stored value
out = start(shop, to_station(shop, out))
assert out["stage"]["clock"]["stored_ms"] == c["stored_ms"] and out["stage"]["clock"]["running"] is True
assert out["stage"]["clock"]["started_at"] == started_at
def test_live_elapsed_grows_without_a_write(shop: Shop) -> None:
pkt = in_progress(shop)
set_running_since(shop.settings, pkt["id"], 10)
a = fresh(shop("weld.andy"), pkt["id"])
time.sleep(1.1)
b = fresh(shop("weld.andy"), pkt["id"])
assert a["version"] == b["version"]
assert a["stage"]["clock"]["stored_ms"] == b["stage"]["clock"]["stored_ms"] == 0
assert 10_000 <= a["stage"]["clock"]["elapsed_ms"] < b["stage"]["clock"]["elapsed_ms"]
assert b["stage"]["clock"]["elapsed_ms"] - a["stage"]["clock"]["elapsed_ms"] >= 1_000
# elapsed = stored + (server_now - running_since), from the server's own clock
since = ts(b["stage"]["clock"]["running_since"])
now = ts(b["server_now"])
want = (now - since).total_seconds() * 1000
assert abs(b["stage"]["clock"]["elapsed_ms"] - want) < 1_000
def test_submit_stops_the_clock(shop: Shop) -> None:
pkt = finish_stage(shop, in_progress(shop))
set_running_since(shop.settings, pkt["id"], 3 * 3600)
out = ok(step(shop("weld.andy"), pkt, "submit", sign=SIGN))
c = out["stage"]["clock"]
assert c["running"] is False and 3 * 3_600_000 <= c["stored_ms"] < 3 * 3_600_000 + 10_000
assert last_log(shop("weld.andy"), pkt["id"])["note"] in ("1 weld · 3.0 h", "1 welds · 3.0 h")
def test_clock_state_conflicts(shop: Shop) -> None:
pkt = to_station(shop, ready_packet(shop))
# never started: pause and resume are refused
r = refused(clock_op(shop, pkt, "pause"), 409, "clock_state")
assert r["current"] == {"running": False, "started": False}
refused(clock_op(shop, pkt, "resume"), 409, "clock_state")
pkt = start(shop, pkt)
r = refused(clock_op(shop, pkt, "start"), 409, "clock_state")
assert r["current"]["running"] is True
refused(clock_op(shop, pkt, "resume"), 409, "clock_state")
ok(clock_op(shop, pkt, "pause"))
refused(clock_op(shop, pkt, "pause"), 409, "clock_state")
assert clock_op(shop, pkt, "stop").status_code == 422
# who: Q.C. and the warehouse may not touch it
for sam in ("qc.quinn", "wh.wade", "clerk.clara", "multi.mia"):
refused(clock_op(shop, pkt, "resume", by=sam), 403, "forbidden")
def test_no_field_sets_elapsed(shop: Shop) -> None:
pkt = in_progress(shop)
set_running_since(shop.settings, pkt["id"], 60)
big = 999_999_999
# the clock endpoint takes only op
r = clock_op(shop, pkt, "pause", elapsed_ms=big, clock_elapsed_ms=big, stored_ms=big)
assert r.status_code in (200, 422), r.text
if r.status_code == 422:
ok(clock_op(shop, pkt, "pause"))
ms, running = stored(shop, pkt["id"])
assert 60_000 <= ms < 70_000 and running is False
# the stage fields refuse or ignore clock columns, in both body shapes
url = f"/api/packets/{pkt['id']}/stage"
for body in (
{"set": {"clock_elapsed_ms": big, "clock_running_since": None}, "base": {}},
{"set": {"filler": "ER70S-6", "clock_elapsed_ms": big}, "base": {}},
):
r = shop("weld.andy").patch(url, json=body)
assert r.status_code in (200, 422), r.text
rv = fresh(shop("weld.andy"), pkt["id"])["stage"]["row_version"]
r = shop("weld.andy").patch(url, json={"clock_elapsed_ms": big, "notes": "x"}, headers=im(rv))
assert r.status_code in (200, 422), r.text
assert stored(shop, pkt["id"])[0] == ms
# the transitions carry no clock either
r = step(shop("sup.wes"), fresh(shop("sup.wes"), pkt["id"]), "pull_back", elapsed_ms=big, clock_elapsed_ms=big)
assert r.status_code in (200, 422), r.text
assert stored(shop, pkt["id"])[0] == ms
# and the read model's value is the stored one
assert fresh(shop("sup.wes"), pkt["id"])["stage"]["clock"]["elapsed_ms"] == ms
def test_closed_stage_clock(shop: Shop) -> None:
pkt = in_review(shop)
out = ok(step(shop("sup.wes"), pkt, "route_from_review", dept="09", sign=SIGN))
old = out["stages"][-1]
assert old["clock"]["running"] is False and old["clock"]["elapsed_ms"] == old["clock"]["stored_ms"]
new = out["stage"]["clock"]
assert (new["running"], new["stored_ms"], new["elapsed_ms"]) == (False, 0, 0)