1use std::time::Duration;
2
3use crate::Acceptance;
4
5#[derive(Clone, Copy, Debug, Default)]
7pub struct Counts {
8 pub sent: u64,
9 pub attempted: u64,
10 pub successful: u64,
11 pub timeout: u64,
12 pub transport: u64,
13 pub status: u64,
14 pub body: u64,
15}
16
17impl Counts {
18 pub fn failed(self) -> u64 {
19 self.attempted - self.successful
20 }
21
22 pub(crate) fn add(
23 &mut self,
24 other: Self,
25 ) {
26 self.sent += other.sent;
27 self.attempted += other.attempted;
28 self.successful += other.successful;
29 self.timeout += other.timeout;
30 self.transport += other.transport;
31 self.status += other.status;
32 self.body += other.body;
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct Phase {
39 pub index: usize,
40 pub connections: usize,
41 pub requests_per_connection: usize,
42 pub total_concurrency: usize,
43 pub duration: Duration,
44}
45
46#[derive(Clone, Copy, Debug)]
47pub struct PhaseStats {
48 pub phase: Phase,
49 pub counts: Counts,
50 pub elapsed: Duration,
52 pub connections_at_target: usize,
54 pub completed: bool,
56 pub cancelled: bool,
57}
58
59impl PhaseStats {
60 pub fn accepted(
61 &self,
62 acceptance: Acceptance,
63 ) -> bool {
64 self.completed
65 && !self.cancelled
66 && self.connections_at_target == self.phase.connections
67 && acceptance.accepts(self.counts.successful, self.counts.attempted)
68 }
69
70 pub fn requests_per_sec(&self) -> f64 {
71 self.counts.successful as f64 / self.elapsed.as_secs_f64()
72 }
73}
74
75#[derive(Clone, Debug, Default)]
77pub struct Stats {
78 pub cpu_workers: usize,
79 pub phases: usize,
80 pub total: Counts,
81 pub last_accepted: Option<PhaseStats>,
82 pub last_phase: Option<PhaseStats>,
83}
84
85#[derive(Debug)]
86pub struct Done {
87 pub stats: Stats,
88}
89
90#[derive(Debug, thiserror::Error)]
91pub enum BreakReason {
92 #[error("invalid plan: {0}")]
93 Invalid(&'static str),
94 #[error("client I/O: {0}")]
95 Io(#[from] std::io::Error),
96 #[error("client resources: {0}")]
97 Resources(&'static str),
98 #[error("HTTP protocol: {0}")]
99 Protocol(&'static str),
100 #[error("client worker: {0}")]
101 Worker(&'static str),
102 #[error("the client did not establish the declared concurrency")]
103 Unfulfilled,
104 #[error("the phase exceeded the HTTP acceptance budget")]
105 Acceptance,
106 #[error("concurrency representation exhausted")]
107 Overflow,
108 #[error("cancelled")]
109 Cancelled,
110}
111
112#[derive(Debug, thiserror::Error)]
113#[error("{reason}")]
114pub struct Broken {
115 pub reason: BreakReason,
116 pub stats: Box<Stats>,
117}
118
119pub trait Observer {
121 fn phase_started(
123 &mut self,
124 _phase: &Phase,
125 ) {
126 }
127
128 fn phase_finished(
130 &mut self,
131 _stats: &PhaseStats,
132 ) {
133 }
134}
135
136impl Observer for () {}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 #[test]
142 fn perfect_status_counts_do_not_pass_when_concurrency_was_not_established()
143 {
144 let mut measured = PhaseStats {
145 phase: Phase {
146 index: 0,
147 connections: 4,
148 requests_per_connection: 100,
149 total_concurrency: 400,
150 duration: Duration::from_secs(1),
151 },
152 counts: Counts {
153 attempted: 10_000,
154 successful: 10_000,
155 ..Counts::default()
156 },
157 elapsed: Duration::from_secs(1),
158 connections_at_target: 3,
159 completed: true,
160 cancelled: false,
161 };
162 let accept = "99.99".parse().unwrap();
163 assert!(!measured.accepted(accept));
164 measured.connections_at_target = 4;
165 assert!(measured.accepted(accept));
166 measured.counts.successful = 9_998;
167 assert!(!measured.accepted(accept));
168 }
169}