ruso_runtime/runtime/
error.rs1use thiserror::Error;
2
3use crate::contract::QualifiedMatch;
4use crate::runtime::binary::BytecodeError;
5
6#[derive(Debug, Error)]
11pub enum RuntimeError {
12 #[error("bytecode: {0}")]
13 Bytecode(#[from] BytecodeError),
14 #[error("unknown request or probe: {0}")]
15 UnknownTarget(String),
16
17 #[error("request {name} is not HTTP (dns/tcp probe)")]
18 WrongProbeKind { name: String },
19
20 #[error("match failed: {0}")]
21 MatchFailed(String),
22
23 #[error("assertion failed: {0}")]
24 AssertFailed(String),
25
26 #[error("extract failed for variable {name}: {reason}")]
27 ExtractFailed { name: String, reason: String },
28
29 #[error("flow control: {0}")]
30 Flow(String),
31
32 #[error("invalid duration: {0}")]
33 InvalidDuration(String),
34
35 #[error("http error: {0}")]
36 Http(#[from] reqwest::Error),
37
38 #[error("io error: {0}")]
39 Io(#[from] std::io::Error),
40
41 #[error("regex error: {0}")]
42 Regex(#[from] regex::Error),
43
44 #[error("{0}")]
45 Other(String),
46}
47
48impl RuntimeError {
49 pub fn match_failed(matcher: &QualifiedMatch, detail: impl Into<String>) -> Self {
50 Self::MatchFailed(format!("{matcher:?}: {}", detail.into()))
51 }
52
53 pub fn assert_failed(matcher: &QualifiedMatch, detail: impl Into<String>) -> Self {
54 Self::AssertFailed(format!("{matcher:?}: {}", detail.into()))
55 }
56
57 pub fn full_message(&self) -> String {
67 join_source_chain(self)
68 }
69}
70
71fn join_source_chain(error: &dyn std::error::Error) -> String {
78 let mut message = error.to_string();
79 let mut next = error.source();
80 while let Some(cause) = next {
81 let cause_text = cause.to_string();
82 if !message.contains(&cause_text) {
83 message.push_str(": ");
84 message.push_str(&cause_text);
85 }
86 next = cause.source();
87 }
88 message
89}
90
91#[cfg(test)]
92mod tests {
93 use super::join_source_chain;
94 use std::error::Error;
95 use std::fmt;
96
97 #[derive(Debug)]
100 struct Layer {
101 message: &'static str,
102 source: Option<Box<Layer>>,
103 }
104
105 impl Layer {
106 fn leaf(message: &'static str) -> Box<Self> {
107 Box::new(Self {
108 message,
109 source: None,
110 })
111 }
112 fn wrap(message: &'static str, source: Box<Layer>) -> Self {
113 Self {
114 message,
115 source: Some(source),
116 }
117 }
118 }
119
120 impl fmt::Display for Layer {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.write_str(self.message)
123 }
124 }
125
126 impl Error for Layer {
127 fn source(&self) -> Option<&(dyn Error + 'static)> {
128 self.source.as_deref().map(|s| s as &(dyn Error + 'static))
129 }
130 }
131
132 #[test]
133 fn single_error_has_no_suffix() {
134 let err = Layer {
135 message: "io error",
136 source: None,
137 };
138 assert_eq!(join_source_chain(&err), "io error");
139 }
140
141 #[test]
142 fn joins_each_distinct_cause() {
143 let err = Layer::wrap(
144 "error sending request",
145 Box::new(Layer::wrap(
146 "client error (Connect)",
147 Layer::leaf("invalid peer certificate"),
148 )),
149 );
150 assert_eq!(
151 join_source_chain(&err),
152 "error sending request: client error (Connect): invalid peer certificate"
153 );
154 }
155
156 #[test]
157 fn skips_a_cause_already_present() {
158 let err = Layer::wrap(
160 "error sending request",
161 Box::new(Layer::wrap(
162 "error sending request",
163 Layer::leaf("UnknownIssuer"),
164 )),
165 );
166 assert_eq!(
167 join_source_chain(&err),
168 "error sending request: UnknownIssuer"
169 );
170 }
171}