strobengine 0.6.0__tar.gz → 0.6.1__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 (98) hide show
  1. {strobengine-0.6.0 → strobengine-0.6.1}/CHANGELOG.md +18 -0
  2. {strobengine-0.6.0 → strobengine-0.6.1}/Cargo.lock +1 -1
  3. {strobengine-0.6.0 → strobengine-0.6.1}/Cargo.toml +1 -1
  4. {strobengine-0.6.0 → strobengine-0.6.1}/PKG-INFO +1 -1
  5. {strobengine-0.6.0 → strobengine-0.6.1}/pyproject.toml +1 -1
  6. {strobengine-0.6.0 → strobengine-0.6.1}/src/lib.rs +0 -1
  7. {strobengine-0.6.0 → strobengine-0.6.1}/src/metrics.rs +12 -0
  8. {strobengine-0.6.0 → strobengine-0.6.1}/src/report/schema.rs +25 -9
  9. {strobengine-0.6.0 → strobengine-0.6.1}/src/report/writer.rs +2 -0
  10. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/_strobengine.pyi +1 -0
  11. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/engine.py +6 -5
  12. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporter.py +10 -1
  13. {strobengine-0.6.0 → strobengine-0.6.1}/tests/test_engine.py +8 -6
  14. {strobengine-0.6.0 → strobengine-0.6.1}/uv.lock +1 -1
  15. {strobengine-0.6.0 → strobengine-0.6.1}/.dockerignore +0 -0
  16. {strobengine-0.6.0 → strobengine-0.6.1}/.github/workflows/ci.yml +0 -0
  17. {strobengine-0.6.0 → strobengine-0.6.1}/.github/workflows/docker-release.yml +0 -0
  18. {strobengine-0.6.0 → strobengine-0.6.1}/.github/workflows/publish-test.yml +0 -0
  19. {strobengine-0.6.0 → strobengine-0.6.1}/.github/workflows/publish.yml +0 -0
  20. {strobengine-0.6.0 → strobengine-0.6.1}/.gitignore +0 -0
  21. {strobengine-0.6.0 → strobengine-0.6.1}/.pre-commit-config.yaml +0 -0
  22. {strobengine-0.6.0 → strobengine-0.6.1}/.python-version +0 -0
  23. {strobengine-0.6.0 → strobengine-0.6.1}/Dockerfile +0 -0
  24. {strobengine-0.6.0 → strobengine-0.6.1}/LICENSE +0 -0
  25. {strobengine-0.6.0 → strobengine-0.6.1}/Makefile +0 -0
  26. {strobengine-0.6.0 → strobengine-0.6.1}/README.md +0 -0
  27. {strobengine-0.6.0 → strobengine-0.6.1}/cliff.toml +0 -0
  28. {strobengine-0.6.0 → strobengine-0.6.1}/docs/benchmarks.md +0 -0
  29. {strobengine-0.6.0 → strobengine-0.6.1}/docs/cli.md +0 -0
  30. {strobengine-0.6.0 → strobengine-0.6.1}/docs/dependencies.md +0 -0
  31. {strobengine-0.6.0 → strobengine-0.6.1}/docs/docker.md +0 -0
  32. {strobengine-0.6.0 → strobengine-0.6.1}/docs/grpc.md +0 -0
  33. {strobengine-0.6.0 → strobengine-0.6.1}/docs/http3.md +0 -0
  34. {strobengine-0.6.0 → strobengine-0.6.1}/docs/http_methods.md +0 -0
  35. {strobengine-0.6.0 → strobengine-0.6.1}/docs/infrastructure.md +0 -0
  36. {strobengine-0.6.0 → strobengine-0.6.1}/docs/install.md +0 -0
  37. {strobengine-0.6.0 → strobengine-0.6.1}/docs/quickstart.md +0 -0
  38. {strobengine-0.6.0 → strobengine-0.6.1}/docs/release.md +0 -0
  39. {strobengine-0.6.0 → strobengine-0.6.1}/docs/reports.md +0 -0
  40. {strobengine-0.6.0 → strobengine-0.6.1}/docs/roadmap.md +0 -0
  41. {strobengine-0.6.0 → strobengine-0.6.1}/docs/sse.md +0 -0
  42. {strobengine-0.6.0 → strobengine-0.6.1}/docs/table_of_contents.md +0 -0
  43. {strobengine-0.6.0 → strobengine-0.6.1}/docs/testing.md +0 -0
  44. {strobengine-0.6.0 → strobengine-0.6.1}/docs/websockets.md +0 -0
  45. {strobengine-0.6.0 → strobengine-0.6.1}/examples/grpc.py +0 -0
  46. {strobengine-0.6.0 → strobengine-0.6.1}/examples/http3.py +0 -0
  47. {strobengine-0.6.0 → strobengine-0.6.1}/examples/http_methods.py +0 -0
  48. {strobengine-0.6.0 → strobengine-0.6.1}/examples/load_test.py +0 -0
  49. {strobengine-0.6.0 → strobengine-0.6.1}/examples/spike_test.py +0 -0
  50. {strobengine-0.6.0 → strobengine-0.6.1}/examples/sse.py +0 -0
  51. {strobengine-0.6.0 → strobengine-0.6.1}/examples/stress_test.py +0 -0
  52. {strobengine-0.6.0 → strobengine-0.6.1}/examples/websocket.py +0 -0
  53. {strobengine-0.6.0 → strobengine-0.6.1}/rust-toolchain.toml +0 -0
  54. {strobengine-0.6.0 → strobengine-0.6.1}/scripts/release.py +0 -0
  55. {strobengine-0.6.0 → strobengine-0.6.1}/src/chaos.rs +0 -0
  56. {strobengine-0.6.0 → strobengine-0.6.1}/src/config.rs +0 -0
  57. {strobengine-0.6.0 → strobengine-0.6.1}/src/logging.rs +0 -0
  58. {strobengine-0.6.0 → strobengine-0.6.1}/src/progress.rs +0 -0
  59. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/grpc.rs +0 -0
  60. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/grpc_parser.rs +0 -0
  61. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/grpc_reflection.rs +0 -0
  62. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/http.rs +0 -0
  63. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/http3.rs +0 -0
  64. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/mod.rs +0 -0
  65. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/sse.rs +0 -0
  66. {strobengine-0.6.0 → strobengine-0.6.1}/src/protocols/websocket.rs +0 -0
  67. {strobengine-0.6.0 → strobengine-0.6.1}/src/report/mod.rs +0 -0
  68. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/__init__.py +0 -0
  69. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/cli.py +0 -0
  70. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/constants.py +0 -0
  71. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/py.typed +0 -0
  72. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/__init__.py +0 -0
  73. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/assets/chart.min.js +0 -0
  74. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/assets/report_template.html +0 -0
  75. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/baseline.py +0 -0
  76. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/csv_report.py +0 -0
  77. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/html_report.py +0 -0
  78. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/junit_report.py +0 -0
  79. {strobengine-0.6.0 → strobengine-0.6.1}/src/strobengine/reporting/markdown_report.py +0 -0
  80. {strobengine-0.6.0 → strobengine-0.6.1}/tests/__init__.py +0 -0
  81. {strobengine-0.6.0 → strobengine-0.6.1}/tests/conftest.py +0 -0
  82. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/__init__.py +0 -0
  83. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/conftest.py +0 -0
  84. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/mock_server.py +0 -0
  85. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_cli_interface.py +0 -0
  86. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_grpc_e2e.py +0 -0
  87. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_http3_e2e.py +0 -0
  88. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_http_scenarios.py +0 -0
  89. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_latency_stats_e2e.py +0 -0
  90. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_performance_sanity.py +0 -0
  91. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_persistence_e2e.py +0 -0
  92. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_sse_e2e.py +0 -0
  93. {strobengine-0.6.0 → strobengine-0.6.1}/tests/e2e/test_websocket_e2e.py +0 -0
  94. {strobengine-0.6.0 → strobengine-0.6.1}/tests/fixtures/service.proto +0 -0
  95. {strobengine-0.6.0 → strobengine-0.6.1}/tests/test_cli.py +0 -0
  96. {strobengine-0.6.0 → strobengine-0.6.1}/tests/test_logging.py +0 -0
  97. {strobengine-0.6.0 → strobengine-0.6.1}/tests/test_reporter.py +0 -0
  98. {strobengine-0.6.0 → strobengine-0.6.1}/tests/test_reporting.py +0 -0
@@ -4,6 +4,20 @@ All notable changes to `strobengine` will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.1] - 2026-09-13
8
+
9
+ ### Bug Fixes
10
+
11
+ - *(lib)* Remove unnecessary clippy::too_many_arguments suppression
12
+
13
+ ### Features
14
+
15
+ - *(reporter)* Use dynamic terminal width for histogram bar rendering
16
+
17
+ ### Refactoring
18
+
19
+ - *(schema)* Derive LatencyPercentiles from TestSummary via From impl
20
+ - *(engine)* Clone TestSummary before enrichment to prevent in-place mutation
7
21
  ## [0.6.0] - 2026-09-09
8
22
 
9
23
  ### Bug Fixes
@@ -32,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
46
  - *(metrics)* Add latency jitter, p99.99, and histogram bucketing
33
47
  - *(reporter)* Expose extended latency stats in terminal and artifact output
34
48
 
49
+ ### Miscellaneous Tasks
50
+
51
+ - *(release)* Bump version to 0.6.0
52
+
35
53
  ### Refactoring
36
54
 
37
55
  - *(lib)* Extract spawn_worker helper from execute_test
@@ -1785,7 +1785,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1785
1785
 
1786
1786
  [[package]]
1787
1787
  name = "strobengine"
1788
- version = "0.6.0"
1788
+ version = "0.6.1"
1789
1789
  dependencies = [
1790
1790
  "async-trait",
1791
1791
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "strobengine"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strobengine
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Requires-Dist: rich>=13.0.0
5
5
  Requires-Dist: typer>=0.27.0
6
6
  Requires-Dist: jinja2>=3.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "strobengine"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  description = "A load testing framework with a Python UI and a bare-metal Rust engine core."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -202,7 +202,6 @@ fn spawn_worker(
202
202
  })
203
203
  }
204
204
 
205
- #[allow(clippy::too_many_arguments)]
206
205
  async fn execute_test(
207
206
  engine: Arc<dyn ProtocolEngine>,
208
207
  url: String,
@@ -177,6 +177,18 @@ pub struct TestSummary {
177
177
 
178
178
  #[pymethods]
179
179
  impl TestSummary {
180
+ pub fn clone(&self) -> Self {
181
+ Clone::clone(self)
182
+ }
183
+
184
+ pub fn __copy__(&self) -> Self {
185
+ self.clone()
186
+ }
187
+
188
+ pub fn __deepcopy__(&self, _py: Python<'_>) -> Self {
189
+ self.clone()
190
+ }
191
+
180
192
  #[pyo3(signature = (indent=None))]
181
193
  pub fn to_json(&self, _py: Python<'_>, indent: Option<usize>) -> PyResult<String> {
182
194
  let json_str = if indent.is_some() {
@@ -69,9 +69,27 @@ pub struct LatencyPercentiles {
69
69
  pub p90_us: f64,
70
70
  pub p95_us: f64,
71
71
  pub p99_us: f64,
72
+ pub p99_99_us: f64,
72
73
  pub min_us: f64,
73
74
  pub max_us: f64,
74
75
  pub mean_us: f64,
76
+ pub std_dev_us: f64,
77
+ }
78
+
79
+ impl From<&crate::metrics::TestSummary> for LatencyPercentiles {
80
+ fn from(s: &crate::metrics::TestSummary) -> Self {
81
+ Self {
82
+ p50_us: s.p50_latency_ms * 1000.0,
83
+ p90_us: s.p90_latency_ms * 1000.0,
84
+ p95_us: s.p95_latency_ms * 1000.0,
85
+ p99_us: s.p99_latency_ms * 1000.0,
86
+ p99_99_us: s.p99_99_latency_ms * 1000.0,
87
+ min_us: s.min_latency_ms * 1000.0,
88
+ max_us: s.max_latency_ms * 1000.0,
89
+ mean_us: s.average_latency_ms * 1000.0,
90
+ std_dev_us: s.std_dev_latency_ms * 1000.0,
91
+ }
92
+ }
75
93
  }
76
94
 
77
95
  fn get_system_info() -> SystemInfo {
@@ -129,15 +147,7 @@ impl ReportArtifact {
129
147
  rps,
130
148
  bytes_transferred: summary.total_bytes_received,
131
149
  },
132
- latency_percentiles: LatencyPercentiles {
133
- p50_us: summary.p50_latency_ms * 1000.0,
134
- p90_us: summary.p90_latency_ms * 1000.0,
135
- p95_us: summary.p95_latency_ms * 1000.0,
136
- p99_us: summary.p99_latency_ms * 1000.0,
137
- min_us: summary.min_latency_ms * 1000.0,
138
- max_us: summary.max_latency_ms * 1000.0,
139
- mean_us: summary.average_latency_ms * 1000.0,
140
- },
150
+ latency_percentiles: LatencyPercentiles::from(summary),
141
151
  error_breakdown,
142
152
  avg_connection_latency_us: if summary.avg_connection_latency_us > 0.0 {
143
153
  Some(summary.avg_connection_latency_us)
@@ -202,9 +212,11 @@ mod tests {
202
212
  p90_us: 3000.0,
203
213
  p95_us: 4500.0,
204
214
  p99_us: 9000.0,
215
+ p99_99_us: 14000.0,
205
216
  min_us: 100.0,
206
217
  max_us: 15000.0,
207
218
  mean_us: 2200.0,
219
+ std_dev_us: 3500.0,
208
220
  },
209
221
  error_breakdown,
210
222
  avg_connection_latency_us: None,
@@ -274,18 +286,22 @@ mod tests {
274
286
  p90_us: 3000.0,
275
287
  p95_us: 4500.0,
276
288
  p99_us: 9000.0,
289
+ p99_99_us: 14000.0,
277
290
  min_us: 100.0,
278
291
  max_us: 15000.0,
279
292
  mean_us: 2200.0,
293
+ std_dev_us: 3500.0,
280
294
  };
281
295
  let json = serde_json::to_value(&lp).unwrap();
282
296
  assert_eq!(json["p50_us"], 1500.0);
283
297
  assert_eq!(json["p90_us"], 3000.0);
284
298
  assert_eq!(json["p95_us"], 4500.0);
285
299
  assert_eq!(json["p99_us"], 9000.0);
300
+ assert_eq!(json["p99_99_us"], 14000.0);
286
301
  assert_eq!(json["min_us"], 100.0);
287
302
  assert_eq!(json["max_us"], 15000.0);
288
303
  assert_eq!(json["mean_us"], 2200.0);
304
+ assert_eq!(json["std_dev_us"], 3500.0);
289
305
  }
290
306
 
291
307
  #[test]
@@ -129,9 +129,11 @@ mod tests {
129
129
  p90_us: 3000.0,
130
130
  p95_us: 4500.0,
131
131
  p99_us: 9000.0,
132
+ p99_99_us: 14000.0,
132
133
  min_us: 100.0,
133
134
  max_us: 15000.0,
134
135
  mean_us: 2200.0,
136
+ std_dev_us: 3500.0,
135
137
  },
136
138
  error_breakdown,
137
139
  avg_connection_latency_us: None,
@@ -114,6 +114,7 @@ class SseMetrics:
114
114
  def avg_ttfb_ms(self) -> float | None: ...
115
115
 
116
116
  class TestSummary:
117
+ def clone(self) -> TestSummary: ...
117
118
  @property
118
119
  def url(self) -> str: ...
119
120
  @property
@@ -212,13 +212,14 @@ class StrobEngine:
212
212
  )
213
213
 
214
214
  def _enrich_summary(self, summary: TestSummary) -> TestSummary:
215
- summary.timestamp = datetime.now(UTC).isoformat()
215
+ enriched = summary.clone()
216
+ enriched.timestamp = datetime.now(UTC).isoformat()
216
217
  if self.config is not None:
217
- summary.workers = self.config.concurrency
218
- summary.raw_command = (
219
- f"strobengine.run(url='{summary.url}', workers={summary.workers})"
218
+ enriched.workers = self.config.concurrency
219
+ enriched.raw_command = (
220
+ f"strobengine.run(url='{summary.url}', workers={enriched.workers})"
220
221
  )
221
- return summary
222
+ return enriched
222
223
 
223
224
  def run(self) -> TestSummary:
224
225
  opts = self._options
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import shutil
4
5
  import sys
5
6
 
6
7
  from strobengine._strobengine import TestSummary
@@ -46,11 +47,19 @@ def _format_bytes(n: int) -> str:
46
47
  return f"{val:.1f} PB"
47
48
 
48
49
 
49
- def _render_histogram(histogram: dict[str, int], max_width: int = 20) -> list[str]:
50
+ def _render_histogram(
51
+ histogram: dict[str, int], max_width: int | None = None
52
+ ) -> list[str]:
50
53
  """Render ASCII bar chart from latency histogram buckets."""
51
54
  if not histogram or not any(v > 0 for v in histogram.values()):
52
55
  return []
53
56
 
57
+ # Dynamic width based on terminal size
58
+ if max_width is None:
59
+ terminal_width = shutil.get_terminal_size((80, 24)).columns
60
+ # Reserve space for label (12) + separator (2) + count (~8) + padding (2)
61
+ max_width = max(10, terminal_width - 24)
62
+
54
63
  max_count = max(histogram.values())
55
64
  if max_count == 0:
56
65
  max_count = 1
@@ -70,14 +70,15 @@ class TestStrobEngineInit:
70
70
  class TestStrobEngineRun:
71
71
  @patch("strobengine.engine.run_load_test")
72
72
  def test_run_returns_summary(self, mock_run):
73
- expected = _make_summary()
74
- mock_run.return_value = expected
73
+ mock_run.return_value = _make_summary()
75
74
 
76
75
  engine = StrobEngine(url="http://example.com")
77
76
  result = engine.run()
78
77
 
79
78
  mock_run.assert_called_once_with(engine.config)
80
- assert result is expected
79
+ # Verify enrichment was applied (clone creates independent copy)
80
+ assert result.raw_command is not None
81
+ assert "http://example.com" in result.raw_command
81
82
 
82
83
  @patch("strobengine.engine.run_load_test")
83
84
  def test_run_with_custom_params(self, mock_run):
@@ -94,13 +95,14 @@ class TestStrobEngineRun:
94
95
  class TestStrobEngineRunAsync:
95
96
  @patch("strobengine.engine.run_load_test")
96
97
  async def test_run_async_returns_summary(self, mock_run):
97
- expected = _make_summary()
98
- mock_run.return_value = expected
98
+ mock_run.return_value = _make_summary()
99
99
 
100
100
  engine = StrobEngine(url="http://example.com")
101
101
  result = await engine.run_async()
102
102
 
103
- assert result is expected
103
+ # Verify enrichment was applied (clone creates independent copy)
104
+ assert result.raw_command is not None
105
+ assert "http://example.com" in result.raw_command
104
106
 
105
107
 
106
108
  class TestLoadTestFactory:
@@ -732,7 +732,7 @@ wheels = [
732
732
 
733
733
  [[package]]
734
734
  name = "strobengine"
735
- version = "0.6.0"
735
+ version = "0.6.1"
736
736
  source = { editable = "." }
737
737
  dependencies = [
738
738
  { name = "jinja2" },
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