cf961603fe
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.
blaze/ids.rs component/command/notification tables
blaze/config.rs injectable identity + endpoints, nothing hardcoded
blaze/session.rs per-connection state
blaze/client_config.rs the fetchClientConfig tables
blaze/responses.rs 16 Blaze::* response bodies
blaze/dispatch.rs (component, command) -> Vec<Frame>
Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.
98 tests green across both crates; clippy clean.
MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.
The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.
Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.
Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.
Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
461 lines
19 KiB
Python
461 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""Freeze the Python Blaze responder's DISPATCH contract as replayable fixtures.
|
|
|
|
The crate-level fixtures in `openfut-protocol-blaze` pin the *codec*: given a
|
|
field tree, what bytes come out. This file pins the layer above: given an
|
|
inbound Fire2 frame and a session, **which frames go back, in what order**.
|
|
|
|
That is the whole contract of a Blaze adapter, and it is the thing a rewrite can
|
|
silently get wrong in ways a codec test cannot see — a missing post-login
|
|
notification, a reply where the oracle stays silent, notifications in the wrong
|
|
order, session state not carried between RPCs.
|
|
|
|
Every transaction is produced by calling the real
|
|
`blaze_responder_v3b.dispatch()`. Session state is threaded across a scripted
|
|
connection exactly as it would be on a live socket, so ordering-dependent
|
|
behaviour (preAuth captures the locale; login sets the auth code that
|
|
getAuthToken later returns) is captured rather than assumed.
|
|
|
|
Determinism: the oracle's clock is pinned and its PRNG seeded, and the
|
|
deployment-dependent addresses are set before import (the responder reads them
|
|
at import time). See the sibling generator in openfut-protocol-blaze.
|
|
|
|
NO SECRETS. The identity here (persona 33068179 / "CAGE") is the project's fixed
|
|
synthetic offline identity. Session keys are minted from a seeded PRNG.
|
|
|
|
Usage: python3 fixtures/generate.py (write)
|
|
python3 fixtures/generate.py --check (verify committed files are current)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import sys
|
|
from collections import OrderedDict
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
TOOLS = os.path.normpath(os.path.join(HERE, "..", "..", "fifa17-recon", "tools"))
|
|
if not os.path.isdir(TOOLS):
|
|
sys.exit("cannot find the Python oracle at %s" % TOOLS)
|
|
sys.path.insert(0, TOOLS)
|
|
|
|
CHECK_ONLY = "--check" in sys.argv[1:]
|
|
# Internal mode: re-exec of this script with sentinel addresses, used to derive
|
|
# the templated client-config table (see emit_config_table).
|
|
CONFIG_TABLE_MODE = "--_config_table" in sys.argv[1:]
|
|
sys.argv = [sys.argv[0]]
|
|
|
|
# Sentinels substituted back into template tokens. Deliberately not IP-shaped so
|
|
# a stray literal cannot be mistaken for a real address.
|
|
SENTINELS = [
|
|
("ADVERTISE-SENTINEL", "{advertise}"),
|
|
("BIND-SENTINEL", "{bind}"),
|
|
("POWCONTENT-SENTINEL", "{pow_content_host}"),
|
|
("POWHOST-SENTINEL", "{pow_host}"),
|
|
]
|
|
|
|
if CONFIG_TABLE_MODE:
|
|
os.environ["OPENFUT_ADVERTISE"] = "ADVERTISE-SENTINEL"
|
|
os.environ["OPENFUT_BIND"] = "BIND-SENTINEL"
|
|
os.environ["POW_CONTENT_HOST"] = "POWCONTENT-SENTINEL"
|
|
os.environ["POW_HOST"] = "POWHOST-SENTINEL"
|
|
import blaze_responder_v3b as _B # noqa: E402
|
|
out = {cfid: _B.client_config_for(cfid) for cfid in sorted(_B.CLIENT_CONFIGS)}
|
|
out["__default__"] = _B.client_config_for("__no_such_section__")
|
|
print(json.dumps(out))
|
|
raise SystemExit(0)
|
|
|
|
# Pin deployment config BEFORE import — the responder snapshots these at import
|
|
# time into module globals used by the response builders.
|
|
#
|
|
# Distinct, obviously-fake values on purpose: if the Rust adapter hardcoded an
|
|
# address instead of reading its config, these make the failure loud rather than
|
|
# accidentally matching a loopback default.
|
|
ADVERTISE = "198.51.100.7"
|
|
BIND = "0.0.0.0"
|
|
POW_CONTENT_HOST = "198.51.100.7:8085"
|
|
POW_HOST = "198.51.100.7:8094"
|
|
|
|
os.environ["OPENFUT_ADVERTISE"] = ADVERTISE
|
|
os.environ["OPENFUT_BIND"] = BIND
|
|
os.environ["POW_CONTENT_HOST"] = POW_CONTENT_HOST
|
|
os.environ["POW_HOST"] = POW_HOST
|
|
|
|
import heat2 # noqa: E402
|
|
import blaze_responder_v3b as B # noqa: E402
|
|
from fut_account import ACCOUNT # noqa: E402
|
|
|
|
FIXED_NOW = 1754870400
|
|
INT, STRING, STRUCT, LIST, MAP, BLOB = (
|
|
heat2.INT, heat2.STRING, heat2.STRUCT, heat2.LIST, heat2.MAP, heat2.BLOB)
|
|
|
|
RECORDS = []
|
|
|
|
|
|
# ------------------------------------------------------------------ helpers
|
|
|
|
def req_frame(component, command, fields=None, msg_num=1, msg_type=None,
|
|
user_index=0):
|
|
"""Build an inbound request frame the way the client would."""
|
|
msg_type = B.MESSAGE if msg_type is None else msg_type
|
|
payload = heat2.encode_tdf(fields) if fields else b""
|
|
return B.fire2(component, command, msg_num, msg_type, payload,
|
|
user_index=user_index)
|
|
|
|
|
|
def tx(session, name, frame, note=""):
|
|
"""Run one frame through the real dispatcher and record what came back."""
|
|
hdr = B.parse_fire2_header(frame)
|
|
body = frame[16 + hdr["metadata_len"]:]
|
|
fields = heat2.decode_tdf(body) if body else OrderedDict()
|
|
out = B.dispatch(hdr, fields, body, session["sess"])
|
|
|
|
RECORDS.append(OrderedDict((
|
|
("kind", "tx"),
|
|
("session", session["id"]),
|
|
("name", name),
|
|
("note", note),
|
|
("request_hex", frame.hex()),
|
|
("responses", [f.hex() for f in out]),
|
|
)))
|
|
return out
|
|
|
|
|
|
def new_session(sid):
|
|
s = {"id": sid, "sess": B.Session()}
|
|
RECORDS.append(OrderedDict((
|
|
("kind", "session"),
|
|
("id", sid),
|
|
# Minted per connection by the oracle; the Rust side must be able to
|
|
# inject it, because it appears in LoginResponse.SESS.KEY, the
|
|
# UserAuthenticated push and PostAuthResponse.TELE.SESS and all three
|
|
# must be the same string.
|
|
("session_key", s["sess"].session_key),
|
|
("account_locale", s["sess"].account_locale),
|
|
("service_name", s["sess"].service_name),
|
|
)))
|
|
return s
|
|
|
|
|
|
# ------------------------------------------------------------------ script
|
|
|
|
def build():
|
|
RECORDS.append(OrderedDict((
|
|
("kind", "config"),
|
|
("advertise", ADVERTISE),
|
|
("bind", BIND),
|
|
("pow_content_host", POW_CONTENT_HOST),
|
|
("pow_host", POW_HOST),
|
|
("now", FIXED_NOW),
|
|
("identity", OrderedDict((
|
|
("persona_id", ACCOUNT.persona_id),
|
|
("persona_name", ACCOUNT.persona_name),
|
|
("user_id", ACCOUNT.user_id),
|
|
("ext_id", ACCOUNT.ext_id),
|
|
("email", ACCOUNT.email),
|
|
("namespace", ACCOUNT.NAMESPACE),
|
|
("client_platform", ACCOUNT.CLIENT_PLATFORM),
|
|
("persona_status", ACCOUNT.PERSONA_STATUS),
|
|
("user_session_type", ACCOUNT.USER_SESSION_TYPE),
|
|
("account_locale_int", ACCOUNT.account_locale_int),
|
|
("locale", ACCOUNT.locale),
|
|
("content_id", ACCOUNT.CONTENT_ID),
|
|
("entitlement_tag", ACCOUNT.ENTITLEMENT_TAG),
|
|
("entitlement_group", ACCOUNT.ENTITLEMENT_GROUP),
|
|
("title_id", ACCOUNT.TITLE_ID),
|
|
("client_id", ACCOUNT.CLIENT_ID),
|
|
("platform", ACCOUNT.PLATFORM),
|
|
("server_version", B.SERVER_VERSION),
|
|
))),
|
|
)))
|
|
|
|
# ================= main connection: the real boot order =================
|
|
#
|
|
# Mirrors what FIFA 17 actually does, because ordering is load-bearing:
|
|
# preAuth captures the locale that later ALOC fields echo, and login sets
|
|
# the auth code that getAuthToken returns afterwards.
|
|
m = new_session("main")
|
|
|
|
tx(m, "preauth", req_frame(B.COMP_UTIL, B.CMD_PREAUTH, OrderedDict([
|
|
("CDAT", (STRUCT, OrderedDict([
|
|
("IITO", (INT, 0)),
|
|
("LANG", (INT, 0x656E5553)), # 'enUS'
|
|
("SVCN", (STRING, "fifa-2017-pc")), # echoed back as INST
|
|
("TYPE", (INT, 0)),
|
|
]))),
|
|
("CINF", (STRUCT, OrderedDict([
|
|
("BSDK", (STRING, "15.1.1.3.0")),
|
|
("CLNT", (STRING, "FIFA17")),
|
|
("ENV", (STRING, "prod")),
|
|
("LOC", (INT, 0x656E5553)),
|
|
]))),
|
|
("FCCR", (STRUCT, OrderedDict([("CFID", (STRING, "BlazeSDK"))]))),
|
|
])), "first RPC; echoes SVCN as INST and captures LANG for ALOC")
|
|
|
|
tx(m, "ping", req_frame(B.COMP_UTIL, B.CMD_PING, msg_num=2),
|
|
"Util::ping -> STIM only")
|
|
|
|
# Every section the responder knows, plus unknown ones. The known sections
|
|
# each add their own rows on top of the shared FUT/RS4 base — OSDK_ROSTER in
|
|
# particular carries the roster URL, itself a documented loading gate — so
|
|
# covering only "BlazeSDK" would leave most of the table unverified.
|
|
for cfid in ("BlazeSDK", "netres", "IdentityParams", "OSDK_CORE",
|
|
"OSDK_CLIENT", "OSDK_NUCLEUS", "OSDK_ROSTER", "OSDK_TICKER",
|
|
"OSDK_WEBOFFER", "OSDK_POW", "OSDK_ABUSE_REPORTING",
|
|
"OSDK_XMS_ABUSE_REPORTING", "UTAS", "FUT", "",
|
|
"TOTALLY_UNKNOWN"):
|
|
tx(m, "fetch_config_%s" % (cfid or "empty"),
|
|
req_frame(B.COMP_UTIL, B.CMD_FETCHCLIENTCONFIG,
|
|
OrderedDict([("CFID", (STRING, cfid))]), msg_num=3),
|
|
"unknown CFIDs still get the shared FUT/POW rows")
|
|
|
|
tx(m, "get_auth_token_before_login",
|
|
req_frame(B.COMP_AUTH, B.CMD_GETAUTHTOKEN, msg_num=4),
|
|
"no auth code yet -> synthesised OPENFUT-<key[:16]> token")
|
|
|
|
tx(m, "logout_before_login", req_frame(B.COMP_AUTH, B.CMD_LOGOUT, msg_num=5),
|
|
"routine LoginStateLogout (state 500), NOT a failure; empty reply")
|
|
|
|
tx(m, "login", req_frame(B.COMP_AUTH, B.CMD_LOGIN, OrderedDict([
|
|
("AUTH", (STRING, "OPENFUT-TEST-AUTHCODE")),
|
|
("EXTB", (BLOB, b"")),
|
|
("PNAM", (STRING, "")),
|
|
]), msg_num=6),
|
|
"reply THEN three UserSessions pushes, in that order")
|
|
|
|
tx(m, "get_auth_token_after_login",
|
|
req_frame(B.COMP_AUTH, B.CMD_GETAUTHTOKEN, msg_num=7),
|
|
"now echoes the login's AUTH verbatim")
|
|
|
|
tx(m, "get_account", req_frame(B.COMP_AUTH, B.CMD_GETACCOUNT, msg_num=8),
|
|
"the RPC behind 'Unable to retrieve account information'")
|
|
tx(m, "get_persona", req_frame(B.COMP_AUTH, B.CMD_GETPERSONA, msg_num=9))
|
|
tx(m, "list_personas", req_frame(B.COMP_AUTH, B.CMD_LISTPERSONAS, msg_num=10))
|
|
|
|
for cmd, label in ((B.CMD_LISTUSERENTITLEMENTS2, "listUserEntitlements2"),
|
|
(0x20, "listEntitlements"),
|
|
(0x30, "listPersonaEntitlements2"),
|
|
(0x27, "grantEntitlement2")):
|
|
tx(m, "entitlements_%s" % label,
|
|
req_frame(B.COMP_AUTH, cmd, msg_num=11),
|
|
"all four aliases return the same two ONLINE_ACCESS records")
|
|
|
|
tx(m, "post_auth", req_frame(B.COMP_UTIL, B.CMD_POSTAUTH, msg_num=12),
|
|
"TELE/TICK/UROP; TELE.SESS must equal the login session key")
|
|
tx(m, "fetch_qos_config", req_frame(B.COMP_UTIL, 0x15, msg_num=13))
|
|
tx(m, "user_settings_load",
|
|
req_frame(B.COMP_UTIL, B.CMD_USERSETTINGSLOAD, msg_num=14))
|
|
tx(m, "user_settings_save",
|
|
req_frame(B.COMP_UTIL, B.CMD_USERSETTINGSSAVE, msg_num=15),
|
|
"accepted and discarded; empty reply")
|
|
tx(m, "set_client_state",
|
|
req_frame(B.COMP_UTIL, B.CMD_SETCLIENTSTATE, msg_num=16))
|
|
tx(m, "set_client_metrics",
|
|
req_frame(B.COMP_UTIL, B.CMD_SETCLIENTMETRICS, msg_num=17))
|
|
|
|
tx(m, "update_network_info",
|
|
req_frame(B.COMP_USERSESSIONS, B.CMD_UPDATENETWORKINFO, msg_num=18),
|
|
"empty reply PLUS an unsolicited ExtendedDataUpdate push")
|
|
|
|
tx(m, "get_lists", req_frame(B.COMP_ASSOCLISTS, B.CMD_GETLISTS, msg_num=19))
|
|
|
|
tx(m, "census_subscribe",
|
|
req_frame(B.COMP_CENSUSDATA, B.CMD_SUBSCRIBETOCENSUSDATAUPDATES,
|
|
OrderedDict([("RSUB", (INT, 1))]), msg_num=20),
|
|
"non-zero TimeValues or the client storms at ~30/s and hangs the FUT load")
|
|
|
|
tx(m, "logout_after_login",
|
|
req_frame(B.COMP_AUTH, B.CMD_LOGOUT, msg_num=21),
|
|
"session teardown after a login; still an empty reply")
|
|
|
|
# ============================ fallback behaviour ========================
|
|
f = new_session("fallbacks")
|
|
|
|
tx(f, "transport_ping",
|
|
req_frame(B.COMP_UTIL, B.CMD_PING, msg_num=30, msg_type=B.PING),
|
|
"msgType PING -> PING_REPLY with an empty body, whatever the command")
|
|
|
|
for mt, label in ((B.REPLY, "reply"), (B.NOTIFICATION, "notification"),
|
|
(B.ERROR_REPLY, "error_reply"),
|
|
(B.PING_REPLY, "ping_reply")):
|
|
tx(f, "ignores_%s" % label,
|
|
req_frame(B.COMP_UTIL, B.CMD_PING, msg_num=31, msg_type=mt),
|
|
"not a request -> NO frames at all")
|
|
|
|
tx(f, "unknown_command",
|
|
req_frame(B.COMP_UTIL, 0x0FFF, msg_num=32),
|
|
"unimplemented RPC still gets an EMPTY reply so the client cannot hang")
|
|
tx(f, "unknown_component",
|
|
req_frame(0x1234, 0x0001, msg_num=33),
|
|
"same fallback for an entirely unknown component")
|
|
|
|
tx(f, "user_index_is_echoed",
|
|
req_frame(B.COMP_UTIL, B.CMD_PING, msg_num=34, user_index=7),
|
|
"a reply echoes component/command/msgNum/userIndex verbatim")
|
|
|
|
# ================= locale echo on a non-default client ==================
|
|
loc = new_session("locale")
|
|
tx(loc, "preauth_de_locale",
|
|
req_frame(B.COMP_UTIL, B.CMD_PREAUTH, OrderedDict([
|
|
("CDAT", (STRUCT, OrderedDict([
|
|
("LANG", (INT, 0x64654445)), # 'deDE'
|
|
("SVCN", (STRING, "fifa-2017-pc-de")),
|
|
]))),
|
|
])),
|
|
"a non-enUS client: SVCN echo AND the captured locale must both change")
|
|
tx(loc, "login_with_de_locale",
|
|
req_frame(B.COMP_AUTH, B.CMD_LOGIN, msg_num=41),
|
|
"UserAuthenticated.ALOC must carry the captured deDE locale")
|
|
|
|
|
|
# ------------------------------------------------------------------- output
|
|
|
|
def emit_config_table():
|
|
"""Derive the fetchClientConfig tables as address-TEMPLATED data.
|
|
|
|
These are 227-243 key/value rows per CFID, almost all of them the same URL.
|
|
Hand-transcribing them into Rust would be 400 lines of string literals that
|
|
nobody can review and one typo can break; deriving them mechanically from
|
|
the oracle removes that whole class of error and keeps them regenerable.
|
|
They are reverse-engineered *data*, not logic — the same reason
|
|
`openfut-core` loads its content from `data/` rather than from source.
|
|
|
|
The values are templated on {advertise}/{bind}/{pow_content_host}/{pow_host}
|
|
so the adapter stays configurable; baking an address in here would recreate
|
|
exactly the hardcoding the client/server split removed.
|
|
|
|
Correctness is not assumed: the caller substitutes real addresses back in
|
|
and diffs against the oracle. See verify_config_table.
|
|
"""
|
|
import subprocess
|
|
|
|
raw = subprocess.run(
|
|
[sys.executable, os.path.abspath(__file__), "--_config_table"],
|
|
capture_output=True, text=True, check=True,
|
|
# Inherit nothing address-shaped; the child sets its own sentinels.
|
|
env={k: v for k, v in os.environ.items()
|
|
if not k.startswith(("OPENFUT_", "POW_", "FUT_"))},
|
|
).stdout
|
|
table = json.loads(raw)
|
|
|
|
def templatise(value):
|
|
for sentinel, token in SENTINELS:
|
|
value = value.replace(sentinel, token)
|
|
# Collapse whole URLs to URL-level tokens where one exists, so the Rust
|
|
# side builds them in exactly one place (AdapterConfig::utas_base and
|
|
# friends) instead of re-deriving the shape here. Without this the
|
|
# helpers become dead code and a hardcoded address in them goes
|
|
# undetected — verified by mutation testing. Longest first.
|
|
for whole, token in (
|
|
("http://{advertise}:8099/", "{utas_base}"),
|
|
("http://{bind}:42131", "{nucleus_base}"),
|
|
("http://{pow_content_host}", "{pow_content_url}"),
|
|
):
|
|
if value == whole:
|
|
return token
|
|
return value
|
|
|
|
return {cfid: [[k, templatise(v)] for k, v in rows]
|
|
for cfid, rows in table.items()}
|
|
|
|
|
|
def verify_config_table(table):
|
|
"""Substitute the real addresses back and require the oracle's exact rows.
|
|
|
|
This is what makes the templated table trustworthy rather than plausible.
|
|
"""
|
|
subst = {
|
|
"{utas_base}": "http://%s:8099/" % ADVERTISE,
|
|
"{nucleus_base}": "http://%s:42131" % BIND,
|
|
"{pow_content_url}": "http://%s" % POW_CONTENT_HOST,
|
|
"{advertise}": ADVERTISE,
|
|
"{bind}": BIND,
|
|
"{pow_content_host}": POW_CONTENT_HOST,
|
|
"{pow_host}": POW_HOST,
|
|
}
|
|
|
|
def render(v):
|
|
for token, real in subst.items():
|
|
v = v.replace(token, real)
|
|
return v
|
|
|
|
for cfid, rows in table.items():
|
|
expected = B.client_config_for(
|
|
"__no_such_section__" if cfid == "__default__" else cfid)
|
|
got = [(k, render(v)) for k, v in rows]
|
|
if got != [(k, v) for k, v in expected]:
|
|
for (gk, gv), (ek, ev) in zip(got, expected):
|
|
if (gk, gv) != (ek, ev):
|
|
sys.exit("config template mismatch in %s: %r -> %r, oracle "
|
|
"has %r -> %r" % (cfid, gk, gv, ek, ev))
|
|
sys.exit("config template row-count mismatch in %s: %d vs %d"
|
|
% (cfid, len(got), len(expected)))
|
|
print("config table verified against the oracle for %d sections"
|
|
% len(table))
|
|
|
|
|
|
def frozen_clock():
|
|
import time as _time
|
|
original = _time.time
|
|
_time.time = lambda: float(FIXED_NOW)
|
|
return original, _time
|
|
|
|
|
|
def write(path, records):
|
|
body = "".join(json.dumps(r, separators=(",", ":")) + "\n" for r in records)
|
|
if CHECK_ONLY:
|
|
if not os.path.exists(path):
|
|
sys.exit("MISSING: %s has never been generated" % path)
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
if fh.read() != body:
|
|
sys.exit("STALE: %s does not match the oracle; re-run without "
|
|
"--check" % path)
|
|
print("current: %s (%d records)" % (os.path.basename(path), len(records)))
|
|
return
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
fh.write(body)
|
|
print("wrote %s (%d records)" % (os.path.basename(path), len(records)))
|
|
|
|
|
|
def write_json(path, obj):
|
|
body = json.dumps(obj, indent=1, sort_keys=True) + "\n"
|
|
if CHECK_ONLY:
|
|
if not os.path.exists(path):
|
|
sys.exit("MISSING: %s has never been generated" % path)
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
if fh.read() != body:
|
|
sys.exit("STALE: %s does not match the oracle" % path)
|
|
print("current: %s" % os.path.basename(path))
|
|
return
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
fh.write(body)
|
|
print("wrote %s (%d sections)" % (os.path.basename(path), len(obj)))
|
|
|
|
|
|
def main():
|
|
# The oracle logs every dispatch to stdout; useful live, pure noise here.
|
|
B.log = lambda *_a, **_k: None
|
|
|
|
table = emit_config_table()
|
|
verify_config_table(table)
|
|
write_json(os.path.join(HERE, "client_config.json"), table)
|
|
|
|
random.seed(0xB1A2E)
|
|
original_time, time_mod = frozen_clock()
|
|
try:
|
|
build()
|
|
finally:
|
|
time_mod.time = original_time
|
|
|
|
write(os.path.join(HERE, "blaze_transactions.jsonl"), RECORDS)
|
|
txs = [r for r in RECORDS if r["kind"] == "tx"]
|
|
frames = sum(len(r["responses"]) for r in txs)
|
|
print("%d transactions, %d response frames, %d sessions"
|
|
% (len(txs), frames,
|
|
len([r for r in RECORDS if r["kind"] == "session"])))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|