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