#![doc = include_str!("../readme.md")]
pub fn get_output_dir() -> &'static str {
if cfg!(feature = "dot_snapshots") {
".snapshots"
} else {
"snapshots"
}
}
#[derive(Clone, Debug)]
pub struct Ferrotype<S: ?Sized = ()> {
expect_errors: bool,
filter_memory_addresses: bool,
filter_uuids: bool,
filter_type_ids: bool,
filter_hashes: bool,
sections: Vec<(
// Heading
String,
// Body
String,
)>,
state: Box<S>,
}
impl Ferrotype<()> {
pub fn new() -> Self {
Self::new_with_state(Box::new(()))
}
}
impl<S: ?Sized> Ferrotype<S> {
pub fn new_with_state(state: Box<S>) -> Self {
Self {
expect_errors: false,
filter_memory_addresses: true,
filter_uuids: true,
filter_type_ids: true,
filter_hashes: true,
sections: Vec::new(),
state,
}
}
pub fn expects_errors(&mut self) -> bool {
self.expect_errors
}
pub fn set_expect_errors(&mut self, expect: bool) -> &mut Self {
self.expect_errors = expect;
self
}
pub fn filter_memory_addresses(&self) -> bool {
self.filter_memory_addresses
}
pub fn set_filter_memory_addresses(&mut self, to: bool) -> &mut Self {
self.filter_memory_addresses = to;
self
}
pub fn filter_uuids(&self) -> bool {
self.filter_uuids
}
pub fn set_filter_uuids(&mut self, to: bool) -> &mut Self {
self.filter_uuids = to;
self
}
pub fn filter_type_ids(&self) -> bool {
self.filter_type_ids
}
pub fn set_filter_type_ids(&mut self, to: bool) -> &mut Self {
self.filter_type_ids = to;
self
}
pub fn filter_hashes(&self) -> bool {
self.filter_hashes
}
pub fn set_filter_hashes(&mut self, to: bool) -> &mut Self {
self.filter_hashes = to;
self
}
pub fn set_filter_random_ids(&mut self, to: bool) -> &mut Self {
self.filter_memory_addresses = to;
self.filter_uuids = to;
self.filter_type_ids = to;
self.filter_hashes = to;
self
}
}
impl Default for Ferrotype<()> {
fn default() -> Self {
Self::new()
}
}
impl<S: ?Sized> Ferrotype<S> {
pub fn add(&mut self, title: &str, body: String) {
self.sections.push((title.to_owned(), body));
}
pub fn add_debug<T: std::fmt::Debug>(&mut self, title: &str, body: T) {
self.sections.push((title.to_owned(), format!("{body:#?}")));
}
fn format_title(title: &str) -> String {
let mut acc = String::new();
let mut prev = '_';
for ch in title.chars() {
if ch != '_' {
if prev == '_' {
for chu in ch.to_uppercase() {
acc.push(chu);
}
} else if prev.is_uppercase() {
for chl in ch.to_lowercase() {
acc.push(chl);
}
} else {
acc.push(ch);
}
}
prev = ch;
}
acc
}
pub fn as_string(&self) -> String {
self
.sections
.iter()
.map(|(title, body)| {
format!(
"{}: >\n{}",
Self::format_title(title),
body
.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<String>>()
.join("\n")
)
})
.collect::<Vec<String>>()
.join("\n\n")
}
pub fn print(&self) {
println!("{}", self.as_string());
}
pub fn print_stdout(&self) {
println!("====== Ferrotype Snapshot ======");
println!("{}\n", self.as_string());
println!("--------------------------------");
}
pub fn print_stderr(&self) {
eprintln!("====== Ferrotype Snapshot ======");
eprintln!("{}\n", self.as_string());
eprintln!("--------------------------------");
}
}
impl<S: ?Sized> std::fmt::Display for Ferrotype<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if cfg!(feature = "anstream") {
write!(
f,
"{}",
anstream::adapter::strip_str(&self.as_string()).to_string(),
)
} else {
write!(f, "{}", self.as_string())
}
}
}
#[macro_export]
macro_rules! assert {
(@ferrotype_name) => {{
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
type_name_of(f).rsplit("::")
.find(|&part| part != "f" && part != "{{closure}}")
.expect("Short function name")
}};
(@use_folder use_folder( $( $folder:ident ),+ )) => {{
let mut p = std::path::PathBuf::from($crate::get_output_dir());
$(
p.push(stringify!($folder));
)+
p
}};
(@use_folder) => {
std::path::PathBuf::from($crate::get_output_dir())
};
( $(#[ use_folder( $( $folder:ident ),+ ) ])? $snapshot:expr ) => {
let snapshot = { $snapshot };
let mut settings = insta::Settings::clone_current();
settings.set_sort_maps(true);
settings.set_omit_expression(false);
settings.set_prepend_module_to_snapshot(false);
settings.remove_description();
let mut filters = vec![];
if snapshot.filter_memory_addresses() {
filters.push((r"\s*(0x[\da-fA-F]+)", " [Redacted::Pointer]"));
}
if snapshot.filter_uuids() {
filters.push((r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "[Redacted::UUID]"));
}
if snapshot.filter_type_ids() {
filters.push((r"\s*TypeId\s*\{\s*t:\s*(\d+)\s*,\s*}", " [Redacted::TypeId]"));
}
if snapshot.filter_hashes() {
filters.push((r"hash:\s(\d+),", "hash: [Redacted::Hash], "));
}
settings.set_filters(filters);
#[allow(unused_labels)]
'set_snapshot_path: {
let folder = $crate::assert!(@use_folder $( use_folder( $( $folder ),+ ) )? );
{
let file_path = std::path::PathBuf::from(file!());
let in_tests_dir = {
let mut p = file_path.as_path();
let mut found = false;
while let Some(parent) = p.parent() {
if let Some(name) = parent.file_name() {
if name == "tests" || name == "test" {
found = true;
break;
}
}
p = parent;
}
found
};
settings.set_snapshot_path(if in_tests_dir {
folder
} else if file_path.parent().is_none() {
folder
} else {
std::path::Path::new("tests").join(folder)
});
}
}
settings.bind(|| {
insta::assert_snapshot!(
format!(
"{}_{}",
module_path!()
.split("::")
.last()
.unwrap(),
$crate::assert!(@ferrotype_name),
),
snapshot.to_string(),
format!("use {}::{}",
module_path!(),
$crate::assert!(@ferrotype_name),
).to_string().as_str()
);
});
};
}
#[cfg(feature = "bluegum")]
impl<S: ?Sized> Ferrotype<S> {
pub fn add_bluegum<T>(&mut self, title: &str, tree: &T)
where
T: bluegum::BluegumWithState<S>,
T: bluegum::Bluegum,
{
let builder = bluegum::Builder::render_with_state(tree, &*self.state);
let mut p = bluegum::Printer::default();
p.render_builder(builder);
self
.sections
.push((title.to_owned(), p.with_color().to_owned()));
}
pub fn add_bluegum_with_external_state<St, T>(
&mut self,
title: &str,
tree: &T,
state: &St,
) where
St: ?Sized,
T: bluegum::BluegumWithState<St>,
{
let builder = bluegum::Builder::render_with_state(tree, state);
let mut p = bluegum::Printer::default();
p.render_builder(builder);
self
.sections
.push((title.to_owned(), p.with_color().to_owned()));
}
pub fn add_bluegum_builder(
&mut self,
title: &str,
builder: bluegum::Builder,
) {
let mut p = bluegum::Printer::default();
p.render_builder(builder);
self
.sections
.push((title.to_owned(), p.with_color().to_owned()));
}
pub fn add_bluegum_builder_with<F>(&mut self, title: &str, builder: F)
where
F: FnOnce(&mut bluegum::Builder),
{
let mut p = bluegum::Printer::default();
p.render_builder_with(builder);
self
.sections
.push((title.to_owned(), p.with_color().to_owned()));
}
pub fn add_bluegum_with<T>(
&mut self,
title: &str,
tree: &T,
styles: bluegum::Styles,
) where
T: bluegum::Bluegum,
T: std::fmt::Debug,
{
let mut p = bluegum::Printer::new(styles);
p.render(tree);
self
.sections
.push((title.to_owned(), p.with_color().to_owned()));
}
}
#[cfg(feature = "tokenstream")]
impl<S: ?Sized> Ferrotype<S> {
pub fn add_token_stream(
&mut self,
title: &str,
ts: &proc_macro2::TokenStream,
) {
let file = syn::parse_file(&ts.to_string()).unwrap();
self.add(title, prettyplease::unparse(&file));
}
}
#[cfg(feature = "anstream")]
impl<S: ?Sized> Ferrotype<S> {
pub fn add_strip_str(&mut self, title: &str, body: String) {
self.sections.push((
title.to_owned(),
anstream::adapter::strip_str(&body).to_string(),
));
}
}
#[cfg(feature = "hex")]
impl<S: ?Sized> Ferrotype<S> {
pub fn add_hex(&mut self, title: &str, body: &[u8]) {
use pretty_hex::*;
self
.sections
.push((title.to_owned(), format!("{:?}", body.hex_dump())));
}
}
#[cfg(test)]
mod tests;