#![forbid(unsafe_code)]
#![forbid(missing_docs)]
use async_trait::async_trait;
use candid::{Decode, Encode};
use ic_http_types::{HttpRequest, HttpResponse};
#[cfg(feature = "pocket_ic")]
pub use pocket_ic_query_call::{PocketIcAsyncHttpQuery, PocketIcHttpQuery};
use regex::Regex;
use std::fmt;
use std::fmt::Debug;
#[cfg_attr(feature = "pocket_ic", doc = "```rust")]
#[cfg_attr(not(feature = "pocket_ic"), doc = "```ignore")]
pub struct MetricsAssert<T> {
actual: T,
metrics: Vec<String>,
}
impl<T> MetricsAssert<T> {
pub fn from_http_query<E>(actual: T) -> Self
where
T: CanisterHttpQuery<E>,
E: Debug,
{
let metrics =
decode_metrics_response_or_unwrap(actual.http_query(encoded_metrics_request()));
Self { actual, metrics }
}
pub async fn from_async_http_query<E>(actual: T) -> Self
where
T: AsyncCanisterHttpQuery<E>,
E: Debug,
{
let metrics =
decode_metrics_response_or_unwrap(actual.http_query(encoded_metrics_request()).await);
Self { actual, metrics }
}
pub fn into(self) -> T {
self.actual
}
pub fn assert_contains_metric_matching<P: AsRef<str> + fmt::Display>(self, pattern: P) -> Self {
assert!(
!self.find_metrics_matching(pattern.as_ref()).is_empty(),
"Expected to find metric matching '{}', but none matched in:\n{:?}",
pattern,
self.metrics
);
self
}
pub fn assert_does_not_contain_metric_matching(self, pattern: &str) -> Self {
let matches = self.find_metrics_matching(pattern);
assert!(
matches.is_empty(),
"Expected not to find any metric matching '{pattern}', but found the following matches:\n{matches:?}"
);
self
}
pub fn find_metrics_matching(&self, pattern: &str) -> Vec<String> {
let regex = Regex::new(pattern).unwrap_or_else(|_| panic!("Invalid regex: {pattern}"));
self.metrics
.iter()
.filter(|line| regex.is_match(line))
.cloned()
.collect()
}
}
fn encoded_metrics_request() -> Vec<u8> {
let request = HttpRequest {
method: "GET".to_string(),
url: "/metrics".to_string(),
headers: Default::default(),
body: Default::default(),
};
Encode!(&request).expect("failed to encode HTTP request")
}
fn decode_metrics_response_or_unwrap<E: Debug>(response: Result<Vec<u8>, E>) -> Vec<String> {
let response = Decode!(&response.expect("failed to retrieve metrics"), HttpResponse)
.expect("failed to decode HTTP response");
assert_eq!(response.status_code, 200_u16);
String::from_utf8_lossy(response.body.as_slice())
.trim()
.split('\n')
.map(|line| line.to_string())
.collect()
}
pub trait CanisterHttpQuery<E: Debug> {
fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
}
#[async_trait]
pub trait AsyncCanisterHttpQuery<E: Debug> {
async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, E>;
}
#[cfg(feature = "pocket_ic")]
mod pocket_ic_query_call {
use super::*;
use candid::Principal;
use ic_management_canister_types::CanisterId;
use pocket_ic::{PocketIc, RejectResponse, nonblocking};
pub trait PocketIcHttpQuery {
fn get_pocket_ic(&self) -> &PocketIc;
fn get_canister_id(&self) -> CanisterId;
}
impl<T: PocketIcHttpQuery> CanisterHttpQuery<RejectResponse> for T {
fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
self.get_pocket_ic().query_call(
self.get_canister_id(),
Principal::anonymous(),
"http_request",
request,
)
}
}
pub trait PocketIcAsyncHttpQuery {
fn get_pocket_ic(&self) -> &nonblocking::PocketIc;
fn get_canister_id(&self) -> CanisterId;
}
#[async_trait]
impl<T: PocketIcAsyncHttpQuery + Send + Sync> AsyncCanisterHttpQuery<RejectResponse> for T {
async fn http_query(&self, request: Vec<u8>) -> Result<Vec<u8>, RejectResponse> {
self.get_pocket_ic()
.query_call(
self.get_canister_id(),
Principal::anonymous(),
"http_request",
request,
)
.await
}
}
}