fixtureqa 0.4.8__tar.gz → 0.4.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fixtureqa-0.4.8/fixtureqa.egg-info → fixtureqa-0.4.9}/PKG-INFO +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_engine.py +4 -2
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_store.py +19 -2
- {fixtureqa-0.4.8 → fixtureqa-0.4.9/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/pyproject.toml +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_api.py +29 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_engine.py +21 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/LICENSE +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/README.md +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/exec_csv_writer.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/server.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-BwQf-cei.css +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/index-DrmyYeG0.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/static/index.html +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/SOURCES.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/setup.cfg +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_health.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.9}/tests/test_ws.py +0 -0
|
@@ -206,7 +206,7 @@ class PerfRun:
|
|
|
206
206
|
# client mode, count-bounded: external venue may never fill — bounded settle.
|
|
207
207
|
deadline = time.perf_counter() + max(_GRACE_DRAIN_S,
|
|
208
208
|
self.config.test.scenario_timeout_ms / 1000.0)
|
|
209
|
-
while self._pending and time.perf_counter() < deadline:
|
|
209
|
+
while self._pending and time.perf_counter() < deadline and not self._stopped_by_user:
|
|
210
210
|
await asyncio.sleep(0.02)
|
|
211
211
|
else:
|
|
212
212
|
await asyncio.sleep(_GRACE_DRAIN_S)
|
|
@@ -252,7 +252,9 @@ class PerfRun:
|
|
|
252
252
|
last_activity = time.perf_counter()
|
|
253
253
|
while True:
|
|
254
254
|
now = time.perf_counter()
|
|
255
|
-
|
|
255
|
+
# Stop pressed mid-drain must win immediately — the backlog gets
|
|
256
|
+
# finalized as lost instead of ground through at the fill rate.
|
|
257
|
+
if self._stopped_by_user or not self._pending or now >= cap:
|
|
256
258
|
break
|
|
257
259
|
cur_sent = self.stats.fills_sent + self.stats.acks_sent
|
|
258
260
|
if cur_sent != last_sent:
|
|
@@ -6,6 +6,7 @@ parameter is unused.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
+
import uuid
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
from .json_store import JsonListStore
|
|
@@ -28,8 +29,24 @@ class PerfStore(JsonListStore):
|
|
|
28
29
|
return item["config"] if item else None
|
|
29
30
|
|
|
30
31
|
def save(self, config: dict) -> str:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
"""Upsert by name: saving a config whose name already exists overwrites
|
|
33
|
+
it in place (same config_id), so the dropdown never accumulates
|
|
34
|
+
duplicates. Pre-existing duplicates of that name are collapsed into the
|
|
35
|
+
first one. One locked load-modify-save so the find+replace is atomic."""
|
|
36
|
+
name = config.get("name", "")
|
|
37
|
+
with self._lock:
|
|
38
|
+
items = self._load("")
|
|
39
|
+
existing = [it for it in items if it.get("name") == name]
|
|
40
|
+
if existing:
|
|
41
|
+
keep = existing[0]
|
|
42
|
+
keep["config"] = config
|
|
43
|
+
items = [it for it in items if it.get("name") != name or it is keep]
|
|
44
|
+
self._save("", items)
|
|
45
|
+
return keep["config_id"]
|
|
46
|
+
item = {"name": name, "config": config, "config_id": str(uuid.uuid4())}
|
|
47
|
+
items.append(item)
|
|
48
|
+
self._save("", items)
|
|
49
|
+
return item["config_id"]
|
|
33
50
|
|
|
34
51
|
def delete(self, config_id: str) -> bool:
|
|
35
52
|
return self.delete_item("", config_id)
|
|
@@ -113,6 +113,35 @@ def test_perf_configs_crud(authed):
|
|
|
113
113
|
assert client.get(f"/api/perf/configs/{cid}").status_code == 404
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def test_perf_config_save_same_name_overwrites(authed):
|
|
117
|
+
client, _ = authed
|
|
118
|
+
body = _run_body("c", "v")
|
|
119
|
+
body["name"] = "upsert-me"
|
|
120
|
+
cid1 = client.post("/api/perf/configs", json=body).json()["config_id"]
|
|
121
|
+
body["rate"]["orders_per_window"] = 99 # same name, new content
|
|
122
|
+
cid2 = client.post("/api/perf/configs", json=body).json()["config_id"]
|
|
123
|
+
assert cid2 == cid1 # overwrote in place
|
|
124
|
+
names = [c["name"] for c in client.get("/api/perf/configs").json()]
|
|
125
|
+
assert names.count("upsert-me") == 1 # no duplicate dropdown rows
|
|
126
|
+
got = client.get(f"/api/perf/configs/{cid1}").json()
|
|
127
|
+
assert got["rate"]["orders_per_window"] == 99 # latest content won
|
|
128
|
+
client.delete(f"/api/perf/configs/{cid1}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_perf_store_collapses_legacy_duplicates(tmp_path):
|
|
132
|
+
# Duplicates saved before the upsert fix collapse into the first entry the
|
|
133
|
+
# next time that name is saved.
|
|
134
|
+
from fixture.core.perf_store import PerfStore
|
|
135
|
+
store = PerfStore(str(tmp_path))
|
|
136
|
+
a = store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 1}})
|
|
137
|
+
store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 2}})
|
|
138
|
+
assert sum(c["name"] == "dup" for c in store.list()) == 2
|
|
139
|
+
cid = store.save({"name": "dup", "v": 3})
|
|
140
|
+
assert cid == a["config_id"] # first entry keeps its id
|
|
141
|
+
assert sum(c["name"] == "dup" for c in store.list()) == 1
|
|
142
|
+
assert store.get(cid)["v"] == 3
|
|
143
|
+
|
|
144
|
+
|
|
116
145
|
def test_perf_run_rejects_unstarted_session(authed):
|
|
117
146
|
client, _ = authed
|
|
118
147
|
port = _free_port()
|
|
@@ -699,6 +699,27 @@ async def test_venue_answers_inbound_amend():
|
|
|
699
699
|
assert run.stats.amends_received == 1
|
|
700
700
|
|
|
701
701
|
|
|
702
|
+
async def test_stop_during_drain_wins_immediately():
|
|
703
|
+
# Starved fill leg → the post-duration drain would grind for minutes
|
|
704
|
+
# (cap ≈ 2× backlog/fill_rate). Stop pressed mid-drain must end the run
|
|
705
|
+
# promptly, finalizing the backlog as lost, not wait out the drain.
|
|
706
|
+
cfg = _cfg(
|
|
707
|
+
rate=RateConfig(orders_per_window=50, order_window_ms=100, # ~500/s in
|
|
708
|
+
fills_per_window=1, fill_window_ms=1000), # 1 fill/s out
|
|
709
|
+
test=RunTestConfig(duration=1),
|
|
710
|
+
)
|
|
711
|
+
run = _run(cfg)
|
|
712
|
+
run.start()
|
|
713
|
+
await asyncio.sleep(1.3) # duration fired → inside drain
|
|
714
|
+
assert run.status == "running" # still draining the backlog
|
|
715
|
+
t0 = time.perf_counter()
|
|
716
|
+
await asyncio.wait_for(run.stop(), timeout=10)
|
|
717
|
+
assert time.perf_counter() - t0 < 5 # did not ride out the drain
|
|
718
|
+
assert run.status == "stopped"
|
|
719
|
+
assert not run._pending # backlog finalized
|
|
720
|
+
assert run.stats.orders_sent > run.stats.completions # it really was starved
|
|
721
|
+
|
|
722
|
+
|
|
702
723
|
async def test_venue_amend_without_pending_replace():
|
|
703
724
|
run = _run(_cfg(amend=AmendConfig(enabled=True)))
|
|
704
725
|
await run._handle_amend({35: "G", 11: "NEW-2", 41: "OLD-2", 376: "VC2",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|