1#![forbid(unsafe_code)]
4#![forbid(missing_docs)]
5
6use async_trait::async_trait;
7use candid::{Decode, Encode};
8use ic_http_types::{HttpRequest, HttpResponse};
9#[cfg(feature = "pocket_ic")]
10pub use pocket_ic_query_call::{PocketIcAsyncHttpQuery, PocketIcHttpQuery};
11use regex::Regex;
12use std::fmt;
13use std::fmt::Debug;
14
15pub struct MetricsAssert<T> {
61 actual: T,
62 metrics: Vec<String>,
63}
64
65impl<T> MetricsAssert<T> {
66 pub fn from_http_query<E>(actual: T) -> Self
69 where
70 T: CanisterHttpQuery<E>,
71 E: Debug,
72 {
73 let metrics =
74 decode_metrics_response_or_unwrap(actual.http_query(encoded_metrics_request()));
75 Self { actual, metrics }
76 }
77
78 pub async fn from_async_http_query<E>(actual: T) -> Self
81 where
82 T: AsyncCanisterHttpQuery<E>,
83 E: Debug,
84 {
85 let metrics =
86 decode_metrics_response_or_unwrap(actual.http_query(encoded_metrics_request()).await);
87 Self { actual, metrics }
88 }
89
90 pub fn into(self) -> T {
92 self.actual
93 }
94
95 pub fn assert_contains_metric_matching<P: AsRef<str> + fmt::Display>(self, pattern: P) -> Self {
97 assert!(
98 !self.find_metrics_matching(pattern.as_ref()).is_empty(),
99 "Expected to find metric matching '{}', but none matched in:\n{:?}",
100 pattern,
101 self.metrics
102 );
103 self
104 }
105
106 pub fn assert_does_not_contain_metric_matching(self, pattern: &str) -> Self {
108 let matches = self.find_metrics_matching(pattern);
109 assert!(
110 matches.is_empty(),
111 "Expected not to find any metric matching '{pattern}', but found the following matches:\n{matches:?}"
112 );
113 self
114 }
115
116 fn find_metrics_matching(&self, pattern: &str) -> Vec<String> {
117 let regex = Regex::new(pattern).unwrap_or_else(|_| panic!("Invalid regex: {pattern}"));
118 self.metrics
119 .iter()
120 .filter(|line| regex.is_match(line))
121 .cloned()
122 .collect()
123 }
124}
125
126fn encoded_metrics_request() -> Vec<u8> {
127 let request = HttpRequest {
128 method: "GET".to_string(),
129 url: "/metrics".to_string(),
130 headers: Default::default(),
131 body: Default::default(),
132 };
133 Encode!(&request).expect("failed to encode HTTP request")
134}
135
136fn decode_metrics_response_or_unwrap<E: Debug>(response: Result<Vec<u8>, E>) -> Vec<String> {
137 let response = Decode!(&response.expect("failed to retrieve metrics"), HttpResponse)
138 .expect("failed to decode HTTP response");
139 assert_eq!(response.status_code, 200_u16);
140 String::from_utf8_lossy(response.body.as_slice())
141 .trim()
142 .split('\n')
143 .map(|line| line.to_string())
144 .collect()
145}
146
147pub trait CanisterHttpQuery<E: Debug> {
149 fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
151}
152
153#[async_trait]
155pub trait AsyncCanisterHttpQuery<E: Debug> {
156 async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
158}
159
160#[cfg(feature = "pocket_ic")]
161mod pocket_ic_query_call {
162 use super::*;
163 use candid::Principal;
164 use ic_management_canister_types::CanisterId;
165 use pocket_ic::{PocketIc, RejectResponse, nonblocking};
166
167 pub trait PocketIcHttpQuery {
170 fn get_pocket_ic(&self) -> &PocketIc;
172
173 fn get_canister_id(&self) -> CanisterId;
175 }
176
177 impl<T: PocketIcHttpQuery> CanisterHttpQuery<RejectResponse> for T {
178 fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
179 self.get_pocket_ic().query_call(
180 self.get_canister_id(),
181 Principal::anonymous(),
182 "http_request",
183 request,
184 )
185 }
186 }
187
188 pub trait PocketIcAsyncHttpQuery {
191 fn get_pocket_ic(&self) -> &nonblocking::PocketIc;
194
195 fn get_canister_id(&self) -> CanisterId;
197 }
198
199 #[async_trait]
200 impl<T: PocketIcAsyncHttpQuery + Send + Sync> AsyncCanisterHttpQuery<RejectResponse> for T {
201 async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
202 self.get_pocket_ic()
203 .query_call(
204 self.get_canister_id(),
205 Principal::anonymous(),
206 "http_request",
207 request,
208 )
209 .await
210 }
211 }
212}