ic_testkit/pic/
startup.rs1use std::{any::Any, panic::catch_unwind};
2
3use pocket_ic::{PocketIc, PocketIcBuilder};
4
5#[derive(Debug, Eq, PartialEq)]
10pub enum PocketIcStartError {
11 BinaryUnavailable { message: String },
12 BinaryInvalid { message: String },
13 DownloadFailed { message: String },
14 ServerStartFailed { message: String },
15 StartupTimedOut { message: String },
16 Panic { message: String },
17}
18
19#[derive(Debug, Eq, PartialEq)]
20pub(super) enum PocketIcPanicKind {
21 DeadInstanceTransport { message: String },
22 Other { message: String },
23}
24
25pub(super) fn try_build_pocket_ic(
26 builder: PocketIcBuilder,
27) -> Result<PocketIc, PocketIcStartError> {
28 let build = catch_unwind(|| builder.build());
29
30 match build {
31 Ok(pocket_ic) => Ok(pocket_ic),
32 Err(payload) => Err(classify_pocket_ic_start_panic(payload)),
33 }
34}
35
36impl std::fmt::Display for PocketIcStartError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::BinaryUnavailable { message }
40 | Self::BinaryInvalid { message }
41 | Self::DownloadFailed { message }
42 | Self::ServerStartFailed { message }
43 | Self::StartupTimedOut { message }
44 | Self::Panic { message } => f.write_str(message),
45 }
46 }
47}
48
49impl std::error::Error for PocketIcStartError {}
50
51pub(super) fn panic_payload_to_string(payload: &(dyn Any + Send)) -> String {
53 if let Some(message) = payload.downcast_ref::<String>() {
54 return message.clone();
55 }
56 if let Some(message) = payload.downcast_ref::<&'static str>() {
57 return (*message).to_string();
58 }
59
60 "non-string panic payload".to_string()
61}
62
63pub(super) fn classify_pocket_ic_panic(payload: Box<dyn Any + Send>) -> PocketIcPanicKind {
66 let message = panic_payload_to_string(payload.as_ref());
67
68 if is_dead_instance_transport_error(&message) {
69 return PocketIcPanicKind::DeadInstanceTransport { message };
70 }
71
72 PocketIcPanicKind::Other { message }
73}
74
75pub(super) fn panic_is_dead_instance_transport(payload: &(dyn Any + Send)) -> bool {
78 matches!(
79 classify_pocket_ic_panic(Box::new(panic_payload_to_string(payload))),
80 PocketIcPanicKind::DeadInstanceTransport { .. }
81 )
82}
83
84pub(super) fn is_dead_instance_transport_error(message: &str) -> bool {
87 message.contains("ConnectionRefused")
88 || message.contains("tcp connect error")
89 || message.contains("IncompleteMessage")
90 || message.contains("connection closed before message completed")
91 || message.contains("channel closed")
92}
93
94fn classify_pocket_ic_start_panic(payload: Box<dyn Any + Send>) -> PocketIcStartError {
96 let message = match classify_pocket_ic_panic(payload) {
97 PocketIcPanicKind::DeadInstanceTransport { message }
98 | PocketIcPanicKind::Other { message } => message,
99 };
100
101 if message.starts_with("Failed to validate PocketIC server binary") {
102 if message.contains("No such file or directory") || message.contains("os error 2") {
103 return PocketIcStartError::BinaryUnavailable { message };
104 }
105
106 return PocketIcStartError::BinaryInvalid { message };
107 }
108
109 if message.starts_with("Failed to download PocketIC server")
110 || message.starts_with("Failed to write PocketIC server binary")
111 {
112 return PocketIcStartError::DownloadFailed { message };
113 }
114
115 if message.starts_with("Failed to start PocketIC binary")
116 || message.starts_with("Failed to create PocketIC server directory")
117 {
118 return PocketIcStartError::ServerStartFailed { message };
119 }
120
121 if message.starts_with("Timed out waiting for PocketIC server being available") {
122 return PocketIcStartError::StartupTimedOut { message };
123 }
124
125 PocketIcStartError::Panic { message }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::{
131 PocketIcPanicKind, PocketIcStartError, classify_pocket_ic_panic,
132 classify_pocket_ic_start_panic, is_dead_instance_transport_error,
133 };
134
135 #[test]
136 fn pocket_ic_start_error_classifies_missing_binary() {
137 let error = classify_pocket_ic_start_panic(Box::new(
138 "Failed to validate PocketIC server binary `/tmp/pocket-ic`: `No such file or directory (os error 2)`.".to_string(),
139 ));
140
141 assert!(matches!(
142 error,
143 PocketIcStartError::BinaryUnavailable { .. }
144 ));
145 }
146
147 #[test]
148 fn pocket_ic_start_error_classifies_failed_spawn() {
149 let error = classify_pocket_ic_start_panic(Box::new(
150 "Failed to start PocketIC binary (/tmp/pocket-ic)".to_string(),
151 ));
152
153 assert!(matches!(
154 error,
155 PocketIcStartError::ServerStartFailed { .. }
156 ));
157 }
158
159 #[test]
160 fn dead_instance_transport_error_detects_connection_refused() {
161 assert!(is_dead_instance_transport_error(
162 "reqwest::Error { source: ConnectError(\"tcp connect error\", 127.0.0.1:1234, Os { code: 111, kind: ConnectionRefused, message: \"Connection refused\" }) }"
163 ));
164 }
165
166 #[test]
167 fn dead_instance_transport_error_detects_incomplete_message() {
168 assert!(is_dead_instance_transport_error(
169 "reqwest::Error { source: hyper::Error(IncompleteMessage) }"
170 ));
171 }
172
173 #[test]
174 fn classify_pocket_ic_panic_marks_dead_instance_transport() {
175 let classified = classify_pocket_ic_panic(Box::new(
176 "reqwest::Error { source: hyper::Error(IncompleteMessage) }".to_string(),
177 ));
178
179 assert!(matches!(
180 classified,
181 PocketIcPanicKind::DeadInstanceTransport { .. }
182 ));
183 }
184}