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.
Files changed (103) hide show
  1. {fixtureqa-0.6.0/fixtureqa.egg-info → fixtureqa-0.7.0}/PKG-INFO +1 -1
  2. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/README.md +1 -1
  3. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/scenario_runner.py +37 -11
  4. fixtureqa-0.6.0/fixture/static/assets/index-DKAYJxvL.css → fixtureqa-0.7.0/fixture/static/assets/index-B-hpSZWZ.css +1 -1
  5. fixtureqa-0.6.0/fixture/static/assets/index-C46Em7NG.js → fixtureqa-0.7.0/fixture/static/assets/index-BJKwfdd8.js +1 -1
  6. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/index.html +2 -2
  7. {fixtureqa-0.6.0 → fixtureqa-0.7.0/fixtureqa.egg-info}/PKG-INFO +1 -1
  8. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/SOURCES.txt +2 -2
  9. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/pyproject.toml +1 -1
  10. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_scenarios.py +54 -0
  11. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/LICENSE +0 -0
  12. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/__init__.py +0 -0
  13. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/__main__.py +0 -0
  14. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/__init__.py +0 -0
  15. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/app.py +0 -0
  16. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/connection_manager.py +0 -0
  17. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/deps.py +0 -0
  18. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/__init__.py +0 -0
  19. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/admin.py +0 -0
  20. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/auth.py +0 -0
  21. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/branding.py +0 -0
  22. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/custom_tags.py +0 -0
  23. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/fix_spec.py +0 -0
  24. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/messages.py +0 -0
  25. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/perf.py +0 -0
  26. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/scenarios.py +0 -0
  27. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/sessions.py +0 -0
  28. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/setup.py +0 -0
  29. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/spec_overlay.py +0 -0
  30. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/templates.py +0 -0
  31. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/routers/ws.py +0 -0
  32. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/api/schemas.py +0 -0
  33. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/config/__init__.py +0 -0
  34. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/__init__.py +0 -0
  35. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/atomic_io.py +0 -0
  36. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/auth.py +0 -0
  37. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/config_store.py +0 -0
  38. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/custom_tag_store.py +0 -0
  39. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/db_migrations.py +0 -0
  40. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/events.py +0 -0
  41. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_application.py +0 -0
  42. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_builder.py +0 -0
  43. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_parser.py +0 -0
  44. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_spec_parser.py +0 -0
  45. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_tags.py +0 -0
  46. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/fix_time.py +0 -0
  47. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/housekeeping.py +0 -0
  48. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/inbound.py +0 -0
  49. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/json_store.py +0 -0
  50. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/latency_csv_writer.py +0 -0
  51. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/message_log.py +0 -0
  52. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/message_store.py +0 -0
  53. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/models.py +0 -0
  54. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_charts.py +0 -0
  55. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_engine.py +0 -0
  56. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_models.py +0 -0
  57. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_payload.py +0 -0
  58. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_stats.py +0 -0
  59. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_store.py +0 -0
  60. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/perf_writer.py +0 -0
  61. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/scenario_store.py +0 -0
  62. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/session.py +0 -0
  63. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/session_manager.py +0 -0
  64. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/spec_overlay_store.py +0 -0
  65. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/template_store.py +0 -0
  66. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/user_store.py +0 -0
  67. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/value_gen.py +0 -0
  68. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/core/venue_responses.py +0 -0
  69. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/fix_specs/FIX42.xml +0 -0
  70. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/fix_specs/FIX44.xml +0 -0
  71. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/server.py +0 -0
  72. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  73. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  74. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  75. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/static/favicon.svg +0 -0
  76. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixture/ui/__init__.py +0 -0
  77. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/dependency_links.txt +0 -0
  78. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/entry_points.txt +0 -0
  79. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/requires.txt +0 -0
  80. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/fixtureqa.egg-info/top_level.txt +0 -0
  81. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/setup.cfg +0 -0
  82. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_atomic_io.py +0 -0
  83. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_auth.py +0 -0
  84. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_config_store.py +0 -0
  85. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_connection_manager.py +0 -0
  86. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_db_migrations.py +0 -0
  87. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_fix_builder.py +0 -0
  88. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_health.py +0 -0
  89. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_inbound.py +0 -0
  90. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_inbound_validation.py +0 -0
  91. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_message_store.py +0 -0
  92. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_api.py +0 -0
  93. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_engine.py +0 -0
  94. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_models.py +0 -0
  95. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_payload.py +0 -0
  96. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_perf_rehydrate.py +0 -0
  97. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_auto_response.py +0 -0
  98. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_lifecycle.py +0 -0
  99. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_session_manager_concurrency.py +0 -0
  100. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_sessions.py +0 -0
  101. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_templates.py +0 -0
  102. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_value_gen.py +0 -0
  103. {fixtureqa-0.6.0 → fixtureqa-0.7.0}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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 = value_gen.eval_field_value(raw_field, counters)
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
- value_gen.eval_field_value(str(value), counters, tag))
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 or None."""
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)