#![allow(
clippy::unwrap_in_result,
reason = "unwraps are allowed anywhere in tests"
)]
#![allow(
clippy::arithmetic_side_effects,
reason = "tests are allowed have arithmetic_side_effects"
)]
use std::{
env, fmt,
io::{self, IsTerminal as _},
path::Path,
sync::Once,
};
use chrono::{DateTime, Utc};
use serde::{
de::{value::StrDeserializer, IntoDeserializer as _},
Deserialize,
};
use tracing::debug;
use tracing_subscriber::util::SubscriberInitExt as _;
use crate::{datetime, json, ObjectType, ReasonableStr, Version};
#[track_caller]
pub fn setup() {
static INITIALIZED: Once = Once::new();
INITIALIZED.call_once_force(|state| {
if state.is_poisoned() {
return;
}
let is_tty = io::stderr().is_terminal();
let level = match env::var("RUST_LOG") {
Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
Err(err) => match err {
env::VarError::NotPresent => tracing::Level::INFO,
env::VarError::NotUnicode(_) => {
panic!("`RUST_LOG` is not unicode");
}
},
};
let subscriber = tracing_subscriber::fmt()
.with_ansi(is_tty)
.with_file(true)
.with_level(false)
.with_line_number(true)
.with_max_level(level)
.with_target(false)
.with_test_writer()
.without_time()
.finish();
subscriber
.try_init()
.expect("Init tracing_subscriber::Subscriber");
});
}
pub fn read_file_content(file_path: &Path) -> io::Result<String> {
let mut content = std::fs::read_to_string(file_path)?;
json_strip_comments::strip(&mut content)?;
Ok(content)
}
impl<'buf> From<&'buf str> for ReasonableStr<'buf> {
fn from(value: &'buf str) -> Self {
ReasonableStr::new(value).unwrap()
}
}
pub trait ApproxEq<Rhs = Self> {
type Tolerance;
fn default_tolerance() -> Self::Tolerance;
#[must_use]
fn approx_eq(&self, other: &Rhs) -> bool {
self.approx_eq_tolerance(other, Self::default_tolerance())
}
#[must_use]
fn approx_eq_tolerance(&self, other: &Rhs, tolerance: Self::Tolerance) -> bool;
}
impl<T> ApproxEq for Option<T>
where
T: ApproxEq,
{
type Tolerance = T::Tolerance;
fn default_tolerance() -> Self::Tolerance {
T::default_tolerance()
}
fn approx_eq(&self, other: &Self) -> bool {
match (self, other) {
(Some(a), Some(b)) => a.approx_eq(b),
(None, None) => true,
_ => false,
}
}
fn approx_eq_tolerance(&self, other: &Self, tolerance: Self::Tolerance) -> bool {
match (self, other) {
(Some(a), Some(b)) => a.approx_eq_tolerance(b, tolerance),
(None, None) => true,
_ => false,
}
}
}
pub trait VersionedType: fmt::Debug {
const VERSION: Version;
}
#[track_caller]
pub fn assert_no_unexpected_fields(
object_type: ObjectType,
unexpected_fields: &json::UnexpectedFields<'_>,
) {
if !unexpected_fields.is_empty() {
const MAX_FIELD_DISPLAY: usize = 20;
if unexpected_fields.len() > MAX_FIELD_DISPLAY {
let truncated_fields = unexpected_fields
.iter()
.take(MAX_FIELD_DISPLAY)
.map(|path| path.to_string())
.collect::<Vec<_>>();
panic!(
"The {object_type} has `{}` unexpected fields;\n\
displaying the first ({}):\n{}\n... and {} more",
unexpected_fields.len(),
truncated_fields.len(),
truncated_fields.join(",\n"),
unexpected_fields.len() - truncated_fields.len(),
)
} else {
panic!(
"The {object_type} has `{}` unexpected fields unexpected fields:\n{}",
unexpected_fields.len(),
unexpected_fields.to_strings().join(",\n")
)
};
}
}
#[derive(Debug, Default)]
pub(crate) enum Expectation<T> {
Present(ExpectValue<T>),
#[default]
Absent,
}
#[derive(Debug)]
pub(crate) enum ExpectValue<T> {
Some(T),
Null,
}
impl<T> ExpectValue<T>
where
T: fmt::Debug,
{
pub fn into_option(self) -> Option<T> {
match self {
Self::Some(v) => Some(v),
Self::Null => None,
}
}
#[track_caller]
pub fn expect_value(self) -> T {
match self {
ExpectValue::Some(v) => v,
ExpectValue::Null => panic!("the field expects a value"),
}
}
}
impl<'de, T> Deserialize<'de> for Expectation<T>
where
T: Deserialize<'de>,
{
#[expect(clippy::unwrap_in_result, reason = "This is test util code")]
#[track_caller]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
if value.is_null() {
return Ok(Expectation::Present(ExpectValue::Null));
}
let v = T::deserialize(value).unwrap();
Ok(Expectation::Present(ExpectValue::Some(v)))
}
}
pub(crate) struct ExpectFile<T> {
pub value: Option<T>,
pub expect_file_name: String,
}
impl ExpectFile<String> {
pub fn as_deref(&self) -> ExpectFile<&str> {
ExpectFile {
value: self.value.as_deref(),
expect_file_name: self.expect_file_name.clone(),
}
}
}
impl<T> ExpectFile<T> {
pub fn with_value(value: Option<T>, file_name: &str) -> Self {
Self {
value,
expect_file_name: file_name.to_owned(),
}
}
pub fn only_file_name(file_name: &str) -> Self {
Self {
value: None,
expect_file_name: file_name.to_owned(),
}
}
}
pub(crate) trait IntoFields<E> {
fn into_fields(self) -> E;
}
#[track_caller]
pub fn datetime_from_str(s: &str) -> DateTime<Utc> {
let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
datetime::test::deser_to_utc(de).unwrap()
}
#[track_caller]
pub fn read_expect_json(json_file_path: &Path, feature: &str) -> ExpectFile<String> {
let json_dir = json_file_path
.parent()
.expect("The given file should live in a dir");
let json_file_name = json_file_path
.file_stem()
.expect("The `json_file_path` should be a file")
.to_str()
.expect("The `json_file_path` should have a valid name");
let expect_file_name = format!("output_{feature}__{json_file_name}.json");
debug!("Try to read expectation file: `{expect_file_name}`");
let json = read_file_content(&json_dir.join(&expect_file_name)).ok();
debug!("Successfully Read expectation file: `{expect_file_name}`");
ExpectFile {
value: json,
expect_file_name,
}
}
#[track_caller]
pub fn parse_expect_json<'de, T>(json: ExpectFile<&'de str>) -> ExpectFile<T>
where
T: Deserialize<'de>,
{
let ExpectFile {
value,
expect_file_name,
} = json;
let value = value.map(|json| {
serde_json::from_str(json)
.unwrap_or_else(|_| panic!("Unable to parse expect JSON `{expect_file_name}`"))
});
ExpectFile {
value,
expect_file_name: expect_file_name.clone(),
}
}
#[macro_export]
macro_rules! assert_approx_eq {
($left:expr, $right:expr $(,)?) => ({
use $crate::test::ApproxEq;
match (&$left, &$right) {
(left_val, right_val) => {
if !((*left_val).approx_eq(&*right_val)) {
let left = stringify!($left);
let right = stringify!($right);
panic!(
"assertion `{left} == {right}` failed\n\
\tleft: {left_val:?}\n\
\tright: {right_val:?}"
);
}
}
}
});
($left:expr, $right:expr, $($arg:tt)+) => ({
use $crate::test::ApproxEq;
match (&$left, &$right) {
(left_val, right_val) => {
if !((*left_val).approx_eq(&*right_val)) {
let left = stringify!($left);
let right = stringify!($right);
panic!(
"assertion `{left} == {right}` failed: {}\n\
\tleft: {left_val:?}\n\
\tright: {right_val:?}",
std::format_args!($($arg)+)
);
}
}
}
});
}
#[macro_export]
macro_rules! assert_approx_eq_tolerance {
($left:expr, $right:expr, $tolerance:expr $(,)?) => ({
use $crate::test::ApproxEq;
match (&$left, &$right) {
(left_val, right_val) => {
if !((*left_val).approx_eq_tolerance(&*right_val, $tolerance)) {
let left = stringify!($left);
let right = stringify!($right);
panic!(
"assertion `{left} ~= {right}` failed with tolerance `{}`\n\
\t{left}: {left_val:?}\n\
\t{right}: {right_val:?}",
$tolerance
);
}
}
}
});
($left:expr, $right:expr, $tolerance:expr, $($arg:tt)+) => ({
use $crate::test::ApproxEq;
match (&$left, &$right) {
(left_val, right_val) => {
if !((*left_val).approx_eq_tolerance(&*right_val, $tolerance)) {
let left = stringify!($left);
let right = stringify!($right);
panic!(
"assertion `{left} ~= {right}` failed with tolerance `{}`: {}\n\
\t{left}: {left_val:?}\n\
\t{right}: {right_val:?}",
$tolerance,
std::format_args!($($arg)+)
);
}
}
}
});
}