testinghq 1.0.0__py3-none-any.whl

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 (62) hide show
  1. testinghq/__init__.py +4 -0
  2. testinghq/barrage/__init__.py +18 -0
  3. testinghq/barrage/executor.py +134 -0
  4. testinghq/barrage/fire.py +549 -0
  5. testinghq/barrage/payloads.py +49 -0
  6. testinghq/barrage/report.py +650 -0
  7. testinghq/barrage/runner.py +588 -0
  8. testinghq/blast/__init__.py +1 -0
  9. testinghq/blast/attachments.py +235 -0
  10. testinghq/blast/catalog.py +418 -0
  11. testinghq/blast/corpus.py +204 -0
  12. testinghq/blast/corrupt.py +678 -0
  13. testinghq/blast/generate.py +313 -0
  14. testinghq/blast/payload.py +222 -0
  15. testinghq/blast/serialize.py +124 -0
  16. testinghq/cli/__init__.py +32 -0
  17. testinghq/cli/__main__.py +15 -0
  18. testinghq/cli/barrage.py +293 -0
  19. testinghq/cli/blast.py +516 -0
  20. testinghq/cli/common.py +275 -0
  21. testinghq/cli/main.py +98 -0
  22. testinghq/cli/pipeline.py +715 -0
  23. testinghq/cli/sigcheck.py +115 -0
  24. testinghq/cli/utility.py +145 -0
  25. testinghq/compare/__init__.py +32 -0
  26. testinghq/compare/load.py +356 -0
  27. testinghq/compare/runs.py +503 -0
  28. testinghq/config_doc.py +392 -0
  29. testinghq/core/__init__.py +1 -0
  30. testinghq/core/config.py +354 -0
  31. testinghq/core/config_schema.py +682 -0
  32. testinghq/core/exit_codes.py +90 -0
  33. testinghq/core/formats.py +500 -0
  34. testinghq/core/guardrails.py +336 -0
  35. testinghq/core/ratelimit.py +136 -0
  36. testinghq/core/report.py +533 -0
  37. testinghq/core/targets.py +109 -0
  38. testinghq/core/transport.py +388 -0
  39. testinghq/pipeline/__init__.py +166 -0
  40. testinghq/pipeline/adapters.py +1607 -0
  41. testinghq/pipeline/common.py +903 -0
  42. testinghq/pipeline/expectations.py +949 -0
  43. testinghq/pipeline/fixtures/steady_intents.json +264 -0
  44. testinghq/pipeline/ledger.py +844 -0
  45. testinghq/pipeline/loop.py +1106 -0
  46. testinghq/pipeline/messages.py +298 -0
  47. testinghq/pipeline/readback.py +523 -0
  48. testinghq/pipeline/redeliver.py +1228 -0
  49. testinghq/pipeline/sigcheck.py +999 -0
  50. testinghq/pipeline/steady.py +1257 -0
  51. testinghq/pipeline/toolconfig.py +86 -0
  52. testinghq/pipeline/verify.py +1063 -0
  53. testinghq/report_html/__init__.py +521 -0
  54. testinghq/report_html/render.js +178 -0
  55. testinghq/report_html/template.html +129 -0
  56. testinghq/reporting.py +1088 -0
  57. testinghq-1.0.0.dist-info/METADATA +266 -0
  58. testinghq-1.0.0.dist-info/RECORD +62 -0
  59. testinghq-1.0.0.dist-info/WHEEL +5 -0
  60. testinghq-1.0.0.dist-info/entry_points.txt +2 -0
  61. testinghq-1.0.0.dist-info/licenses/LICENSE +21 -0
  62. testinghq-1.0.0.dist-info/top_level.txt +1 -0
testinghq/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """TestingHQ: fuzz, load-test and verify your inbound email webhook."""
2
+
3
+ #: Kept equal to `version` in pyproject.toml; tests/unit/test_release.py checks.
4
+ __version__ = "1.0.0"
@@ -0,0 +1,18 @@
1
+ """Barrage: a load generator for TestingHQ.
2
+
3
+ Barrage fires provider-shaped payloads at an endpoint the operator controls,
4
+ at a high but controlled rate, and reports throughput, latency distribution,
5
+ and error behaviour under sustained load. It is a load tester against your
6
+ own infrastructure.
7
+
8
+ Barrage is explicitly NOT an email sender, NOT a flooding tool, and NOT for
9
+ endpoints you do not own. The rate ceiling, the configured-target-only rule,
10
+ and the dry-run default are what keep it a load tester rather than a
11
+ weapon. They are non-negotiable: nothing in this package weakens them.
12
+
13
+ Where Blast (testinghq/blast) proves the parser is correct under messy
14
+ input, Barrage proves the pipeline holds under load. Same suite, same
15
+ installable, same core: Barrage reuses testinghq.core.ratelimit,
16
+ testinghq.core.guardrails, and testinghq.core.transport rather than
17
+ reimplementing any of them.
18
+ """
@@ -0,0 +1,134 @@
1
+ """Dispatch executors for Barrage.
2
+
3
+ Two strategies behind one small interface, so `runner.py` stays the only thing
4
+ that knows about pacing:
5
+
6
+ - `SerialExecutor` runs each task inline on the calling thread. It is the
7
+ correct implementation for `concurrency == 1` and what hermetic tests with a
8
+ fake clock get, because one worker has nothing to overlap.
9
+ - `PoolExecutor` is a fixed set of daemon worker threads fed from a queue.
10
+
11
+ Threads rather than asyncio because the transport is a blocking `http.client`
12
+ call and the project takes no runtime dependencies. One thread per in-flight
13
+ request is why `concurrency` is capped (see `runner.check_rate_ceiling`).
14
+
15
+ Interface: `submit(fn) -> Future`, `shutdown(cancel_pending=...)`, and
16
+ `max_in_flight`. The runner waits on the futures itself, with a deadline, so
17
+ an executor never decides how long a run lasts.
18
+
19
+ The pool's threads are daemon threads on purpose. A request the run has
20
+ abandoned at its deadline may still be blocked in a socket read, bounded only
21
+ by the transport timeout; a non-daemon thread would hold the interpreter open
22
+ until it returned.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import queue
27
+ import threading
28
+ from concurrent.futures import Future
29
+ from typing import Callable, Set
30
+
31
+
32
+ class _Completed(Future):
33
+ """A Future that is already resolved, for inline execution."""
34
+
35
+ def __init__(self, value=None, exception=None):
36
+ super().__init__()
37
+ if exception is not None:
38
+ self.set_exception(exception)
39
+ else:
40
+ self.set_result(value)
41
+
42
+
43
+ class SerialExecutor:
44
+ """Runs each task inline, immediately, on the calling thread."""
45
+
46
+ max_in_flight = 1
47
+
48
+ def submit(self, fn: Callable[[], object]) -> Future:
49
+ # Exception, not BaseException: a KeyboardInterrupt must stop the run
50
+ # where it happened instead of being parked on a Future.
51
+ try:
52
+ value = fn()
53
+ except Exception as exc: # noqa: BLE001 - surfaced by the runner
54
+ return _Completed(exception=exc)
55
+ return _Completed(value=value)
56
+
57
+ def shutdown(self, cancel_pending: bool = True) -> None:
58
+ return None
59
+
60
+
61
+ class PoolExecutor:
62
+ """A fixed pool of daemon worker threads.
63
+
64
+ `max_workers` is the number of tasks running at once. Work submitted while
65
+ every worker is busy waits in the queue; the task itself decides what to do
66
+ about having waited (Barrage drops a request that waited too long).
67
+ """
68
+
69
+ def __init__(self, max_workers: int, thread_name_prefix: str = "barrage") -> None:
70
+ if max_workers < 1:
71
+ raise ValueError(f"max_workers must be >= 1, got {max_workers!r}")
72
+ self.max_in_flight = max_workers
73
+ self._queue: "queue.SimpleQueue" = queue.SimpleQueue()
74
+ self._pending: Set[Future] = set()
75
+ self._lock = threading.Lock()
76
+ self._closed = False
77
+ self._threads = [
78
+ threading.Thread(
79
+ target=self._work, name=f"{thread_name_prefix}-{i}", daemon=True
80
+ )
81
+ for i in range(max_workers)
82
+ ]
83
+ for thread in self._threads:
84
+ thread.start()
85
+
86
+ def _work(self) -> None:
87
+ while True:
88
+ item = self._queue.get()
89
+ if item is None:
90
+ return
91
+ future, fn = item
92
+ with self._lock:
93
+ self._pending.discard(future)
94
+ if not future.set_running_or_notify_cancel():
95
+ continue
96
+ try:
97
+ result = fn()
98
+ except BaseException as exc: # noqa: BLE001 - delivered via the Future
99
+ future.set_exception(exc)
100
+ else:
101
+ future.set_result(result)
102
+
103
+ def submit(self, fn: Callable[[], object]) -> Future:
104
+ future: Future = Future()
105
+ with self._lock:
106
+ if self._closed:
107
+ raise RuntimeError("cannot submit to an executor that was shut down")
108
+ self._pending.add(future)
109
+ self._queue.put((future, fn))
110
+ return future
111
+
112
+ def shutdown(self, cancel_pending: bool = True) -> None:
113
+ """Stop accepting work and let the workers exit once idle.
114
+
115
+ With `cancel_pending`, work still in the queue is cancelled and never
116
+ runs. A task already running finishes on its own thread; it is not
117
+ waited for, because a request on the wire cannot be recalled and the
118
+ run has already decided what to call it.
119
+ """
120
+ with self._lock:
121
+ self._closed = True
122
+ pending, self._pending = self._pending, set()
123
+ if cancel_pending:
124
+ for future in pending:
125
+ future.cancel()
126
+ for _ in self._threads:
127
+ self._queue.put(None)
128
+
129
+
130
+ def make_executor(concurrency: int) -> "SerialExecutor | PoolExecutor":
131
+ """Serial for one worker, a pool otherwise."""
132
+ if concurrency == 1:
133
+ return SerialExecutor()
134
+ return PoolExecutor(concurrency)