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 pub fn find_metrics_matching(&self, pattern: &str) -> Vec<String> {
118 let regex = Regex::new(pattern).unwrap_or_else(|_| panic!("Invalid regex: {pattern}"));
119 self.metrics
120 .iter()
121 .filter(|line| regex.is_match(line))
122 .cloned()
123 .collect()
124 }
125}
126
127fn encoded_metrics_request() -> Vec<u8> {
128 let request = HttpRequest {
129 method: "GET".to_string(),
130 url: "/metrics".to_string(),
131 headers: Default::default(),
132 body: Default::default(),
133 };
134 Encode!(&request).expect("failed to encode HTTP request")
135}
136
137fn decode_metrics_response_or_unwrap<E: Debug>(response: Result<Vec<u8>, E>) -> Vec<String> {
138 let response = Decode!(&response.expect("failed to retrieve metrics"), HttpResponse)
139 .expect("failed to decode HTTP response");
140 assert_eq!(response.status_code, 200_u16);
141 String::from_utf8_lossy(response.body.as_slice())
142 .trim()
143 .split('\n')
144 .map(|line| line.to_string())
145 .collect()
146}
147
148pub trait CanisterHttpQuery<E: Debug> {
150 fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
152}
153
154#[async_trait]
156pub trait AsyncCanisterHttpQuery<E: Debug> {
157 async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
159}
160
161#[cfg(feature = "pocket_ic")]
162mod pocket_ic_query_call {
163 use super::*;
164 use candid::Principal;
165 use ic_management_canister_types::CanisterId;
166 use pocket_ic::{PocketIc, RejectResponse, nonblocking};
167
168 pub trait PocketIcHttpQuery {
171 fn get_pocket_ic(&self) -> &PocketIc;
173
174 fn get_canister_id(&self) -> CanisterId;
176 }
177
178 impl<T: PocketIcHttpQuery> CanisterHttpQuery<RejectResponse> for T {
179 fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
180 self.get_pocket_ic().query_call(
181 self.get_canister_id(),
182 Principal::anonymous(),
183 "http_request",
184 request,
185 )
186 }
187 }
188
189 pub trait PocketIcAsyncHttpQuery {
192 fn get_pocket_ic(&self) -> &nonblocking::PocketIc;
195
196 fn get_canister_id(&self) -> CanisterId;
198 }
199
200 #[async_trait]
201 impl<T: PocketIcAsyncHttpQuery + Send + Sync> AsyncCanisterHttpQuery<RejectResponse> for T {
202 async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
203 self.get_pocket_ic()
204 .query_call(
205 self.get_canister_id(),
206 Principal::anonymous(),
207 "http_request",
208 request,
209 )
210 .await
211 }
212 }
213}