fixtureqa 0.6.0__tar.gz → 0.7.0__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.6.0/fixtureqa.egg-info → fixtureqa-0.7.0}/PKG-INFO +1 -1
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/README.md +1 -1
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/scenario_runner.py +37 -11
- fixtureqa-0.6.0/fixture/static/assets/index-DKAYJxvL.css → fixtureqa-0.7.0/fixture/static/assets/index-B-hpSZWZ.css +1 -1
- fixtureqa-0.6.0/fixture/static/assets/index-C46Em7NG.js → fixtureqa-0.7.0/fixture/static/assets/index-BJKwfdd8.js +1 -1
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/index.html +2 -2
- {fixtureqa-0.6.0 → fixtureqa-0.7.0/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/SOURCES.txt +2 -2
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/pyproject.toml +1 -1
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_scenarios.py +54 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/LICENSE +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/__main__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/app.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/deps.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/auth.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/events.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/latency_csv_writer.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/models.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_charts.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_engine.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_models.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_stats.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/session.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/value_gen.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/server.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/setup.cfg +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_auth.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_config_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_health.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_inbound.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_message_store.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_engine.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_auto_response.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_sessions.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_templates.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_value_gen.py +0 -0
- {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_ws.py +0 -0
|
@@ -15,7 +15,7 @@ A Python FIX protocol testing tool. Supports FIX 4.2 and 4.4, dynamic session cr
|
|
|
15
15
|
- **Message log** — real-time IN/OUT stream with field breakdown, filtering, cursor pagination
|
|
16
16
|
- **Compose & send** — paste raw FIX or use message templates; auto-inject tag 60 and primary ID
|
|
17
17
|
- **Templates** — create/edit reusable message templates with FIX spec field definitions
|
|
18
|
-
- **Scenarios** — automated send/expect/delay test scripts with live pass/fail results
|
|
18
|
+
- **Scenarios** — automated send/expect/delay test scripts with live pass/fail results; repeatable steps/scenarios, value generators (`uuid()`, `ORD-{seq(8)}`, …) in send values, `{recv:<tag>}` to echo an order's tags into the ack/fill that answers it, and cross-session **suites** that coordinate through real FIX traffic (e.g. a client scenario and a venue scenario run together)
|
|
19
19
|
- **Venue simulation** — auto-ack and auto-fill responses for inbound NewOrderSingles
|
|
20
20
|
- **Performance testing** — rate-controlled order injection with latency correlation (p50/p95/p99), live dashboard, run history and CSV export; order/exec shapes templatable per counterparty; amend (35=G) and cancel (35=F) flavours with their own reply latencies (see [PERF.md](PERF.md))
|
|
21
21
|
- **Log analysis** — Overview stats, Throughput chart, Reject detail, Latency measurement (p50/p95/p99)
|
|
@@ -9,6 +9,7 @@ for timeout and abort signalling.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import re
|
|
12
13
|
import threading
|
|
13
14
|
import time
|
|
14
15
|
import uuid
|
|
@@ -25,6 +26,11 @@ from . import value_gen
|
|
|
25
26
|
|
|
26
27
|
SKIP_TAGS = {8, 9, 10, 34, 35, 49, 52, 56}
|
|
27
28
|
|
|
29
|
+
# {recv:<tag>} in a send value echoes that tag from the message the run's most
|
|
30
|
+
# recent expect step matched (e.g. a venue ack carries the order's ClOrdID via
|
|
31
|
+
# 11={recv:11}). No parens, so it never collides with value_gen's {gen(...)}.
|
|
32
|
+
_RECV_RE = re.compile(r"\{recv:(\d+)\}")
|
|
33
|
+
|
|
28
34
|
|
|
29
35
|
def _now_ms() -> float:
|
|
30
36
|
return time.monotonic() * 1000
|
|
@@ -362,6 +368,9 @@ class ScenarioRunner:
|
|
|
362
368
|
# repeated step and across scenario-level repeats (uuid()/random are
|
|
363
369
|
# always fresh regardless).
|
|
364
370
|
gen_counters: dict = {}
|
|
371
|
+
# Fields of the message the most recent expect matched, so a following
|
|
372
|
+
# send can echo them via {recv:<tag>} (FIFO: expect k matched order k).
|
|
373
|
+
last_recv: dict = {}
|
|
365
374
|
idx = 0
|
|
366
375
|
for run_iter in range(scenario_repeat):
|
|
367
376
|
for step in steps:
|
|
@@ -392,9 +401,11 @@ class ScenarioRunner:
|
|
|
392
401
|
for iteration in range(step_repeat):
|
|
393
402
|
try:
|
|
394
403
|
if step_type == "send":
|
|
395
|
-
error = self._run_send(step, session_id, uid, gen_counters)
|
|
404
|
+
error = self._run_send(step, session_id, uid, gen_counters, last_recv)
|
|
396
405
|
elif step_type == "expect":
|
|
397
|
-
error = self._run_expect(step, inbox, abort_flag)
|
|
406
|
+
error, matched = self._run_expect(step, inbox, abort_flag)
|
|
407
|
+
if matched is not None:
|
|
408
|
+
last_recv = matched
|
|
398
409
|
elif step_type == "delay":
|
|
399
410
|
self._run_delay(step, abort_flag)
|
|
400
411
|
if abort_flag.is_set():
|
|
@@ -432,8 +443,18 @@ class ScenarioRunner:
|
|
|
432
443
|
|
|
433
444
|
return overall_status
|
|
434
445
|
|
|
446
|
+
def _resolve_value(self, value: str, counters: dict, recv_fields: dict,
|
|
447
|
+
key=0) -> str:
|
|
448
|
+
"""Resolve {recv:<tag>} (echo a tag from the last matched message) then
|
|
449
|
+
the value_gen generators ({gen(...)} / whole-value). recv runs first so
|
|
450
|
+
it composes with generators (e.g. 17=EX-{recv:11}); an absent tag → ""."""
|
|
451
|
+
if isinstance(value, str) and recv_fields and "{recv:" in value:
|
|
452
|
+
value = _RECV_RE.sub(
|
|
453
|
+
lambda m: (recv_fields.get(m.group(1)) or [""])[0], value)
|
|
454
|
+
return value_gen.eval_field_value(value, counters, key)
|
|
455
|
+
|
|
435
456
|
def _run_send(self, step: dict, session_id: str, uid: str,
|
|
436
|
-
counters: dict) -> Optional[str]:
|
|
457
|
+
counters: dict, recv_fields: dict) -> Optional[str]:
|
|
437
458
|
"""Build and send a FIX message. Returns error string or None.
|
|
438
459
|
|
|
439
460
|
A 'raw' field is built group-aware via the session's DataDictionary, so
|
|
@@ -445,11 +466,12 @@ class ScenarioRunner:
|
|
|
445
466
|
{gen(...)} — evaluated fresh per send. `counters` is the run-scoped seq()
|
|
446
467
|
state (passed in so seq() keeps incrementing across a repeated step / a
|
|
447
468
|
repeated scenario, e.g. 11=ORD-{seq(4)} → ORD-0001, ORD-0002, …).
|
|
469
|
+
`recv_fields` is the last expect's matched message, echoed via {recv:tag}.
|
|
448
470
|
"""
|
|
449
471
|
raw_field = step.get("raw", "").strip()
|
|
450
472
|
if raw_field:
|
|
451
473
|
from .fix_builder import build_message_from_raw
|
|
452
|
-
raw_field =
|
|
474
|
+
raw_field = self._resolve_value(raw_field, counters, recv_fields)
|
|
453
475
|
try:
|
|
454
476
|
msg = build_message_from_raw(
|
|
455
477
|
raw_field, self._sm.get_data_dictionary(session_id), transact_time=_utc_ts())
|
|
@@ -490,7 +512,7 @@ class ScenarioRunner:
|
|
|
490
512
|
if tag not in SKIP_TAGS:
|
|
491
513
|
try:
|
|
492
514
|
msg.set_field(int(tag),
|
|
493
|
-
|
|
515
|
+
self._resolve_value(str(value), counters, recv_fields, tag))
|
|
494
516
|
except Exception:
|
|
495
517
|
pass
|
|
496
518
|
|
|
@@ -506,9 +528,13 @@ class ScenarioRunner:
|
|
|
506
528
|
|
|
507
529
|
def _run_expect(
|
|
508
530
|
self, step: dict, inbox: _RunInbox, abort_flag: threading.Event
|
|
509
|
-
) -> Optional[str]:
|
|
531
|
+
) -> tuple[Optional[str], Optional[dict]]:
|
|
510
532
|
"""Consume the next inbound message matching msg_type from the run inbox
|
|
511
|
-
and evaluate the step's assertions against it. Returns error
|
|
533
|
+
and evaluate the step's assertions against it. Returns (error, fields):
|
|
534
|
+
error is None on success; fields is the matched message's multi-value
|
|
535
|
+
field dict (None on timeout/abort) so a following send can echo it via
|
|
536
|
+
{recv:<tag>}. The matched fields are returned even when an assertion
|
|
537
|
+
fails (the consumed message is still the one we looked at)."""
|
|
512
538
|
expected_msg_type = step.get("msg_type", "")
|
|
513
539
|
timeout_ms = step.get("timeout_ms", 5000)
|
|
514
540
|
assertions: list[dict] = step.get("assertions", [])
|
|
@@ -516,10 +542,10 @@ class ScenarioRunner:
|
|
|
516
542
|
item = inbox.next_matching(expected_msg_type, timeout_ms / 1000.0, abort_flag)
|
|
517
543
|
|
|
518
544
|
if abort_flag.is_set():
|
|
519
|
-
return "Aborted"
|
|
545
|
+
return "Aborted", None
|
|
520
546
|
if item is None:
|
|
521
547
|
desc = expected_msg_type or "any"
|
|
522
|
-
return f"Timeout waiting for message type {desc!r} after {timeout_ms}ms"
|
|
548
|
+
return f"Timeout waiting for message type {desc!r} after {timeout_ms}ms", None
|
|
523
549
|
|
|
524
550
|
fields = item["fields"]
|
|
525
551
|
for assertion in assertions:
|
|
@@ -529,8 +555,8 @@ class ScenarioRunner:
|
|
|
529
555
|
value = assertion.get("value", "")
|
|
530
556
|
actual_list = fields.get(str(tag))
|
|
531
557
|
actual_repr = repr(actual_list[0]) if actual_list and len(actual_list) == 1 else repr(actual_list) if actual_list else "<absent>"
|
|
532
|
-
return f"Assertion failed: tag {tag} {op} {value!r} (actual: {actual_repr})"
|
|
533
|
-
return None
|
|
558
|
+
return f"Assertion failed: tag {tag} {op} {value!r} (actual: {actual_repr})", fields
|
|
559
|
+
return None, fields
|
|
534
560
|
|
|
535
561
|
def _run_delay(self, step: dict, abort_flag: threading.Event) -> None:
|
|
536
562
|
delay_ms = step.get("delay_ms", 0)
|