diff --git a/.gitignore b/.gitignore index 8aa76ff15..6fc47753d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,10 @@ /data.ms /snapshots /dumps + + +# Snapshots +## ... large +*.full.snap +## ... unreviewed +*.snap.new diff --git a/Cargo.lock b/Cargo.lock index ffafdc0c0..5546dd4ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2242,6 +2242,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "meili-snap" +version = "0.1.0" +dependencies = [ + "insta", + "md5", + "once_cell", +] + [[package]] name = "meilisearch-auth" version = "0.29.1" diff --git a/Cargo.toml b/Cargo.toml index a17e7a170..2b756f87c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "meilisearch-http", "meilisearch-types", "meilisearch-auth", + "meili-snap", "index-scheduler", "dump", "file-store", diff --git a/meili-snap/Cargo.toml b/meili-snap/Cargo.toml new file mode 100644 index 000000000..27efc7e97 --- /dev/null +++ b/meili-snap/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "meili-snap" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +insta = { version = "1.19.1", features = ["json", "redactions"] } +md5 = "0.7.0" +once_cell = "1.15" \ No newline at end of file diff --git a/meili-snap/src/lib.rs b/meili-snap/src/lib.rs new file mode 100644 index 000000000..8477abb24 --- /dev/null +++ b/meili-snap/src/lib.rs @@ -0,0 +1,229 @@ +use once_cell::sync::Lazy; +use std::borrow::Cow; +use std::path::PathBuf; +use std::sync::Mutex; +use std::{collections::HashMap, path::Path}; + +static SNAPSHOT_NAMES: Lazy>> = Lazy::new(|| Mutex::default()); + +/// Return the md5 hash of the given string +pub fn hash_snapshot(snap: &str) -> String { + let hash = md5::compute(snap.as_bytes()); + let hash_str = format!("{hash:x}"); + hash_str +} + +#[track_caller] +pub fn default_snapshot_settings_for_test(name: Option<&str>) -> (insta::Settings, Cow<'_, str>) { + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + let path = Path::new(std::panic::Location::caller().file()); + let filename = path.file_name().unwrap().to_str().unwrap(); + settings.set_omit_expression(true); + + let test_name = std::thread::current() + .name() + .unwrap() + .rsplit("::") + .next() + .unwrap() + .to_owned(); + + let path = Path::new("snapshots") + .join(filename) + .join(&test_name) + .to_owned(); + settings.set_snapshot_path(path.clone()); + let snap_name = if let Some(name) = name { + Cow::Borrowed(name) + } else { + let mut snapshot_names = SNAPSHOT_NAMES.lock().unwrap(); + let counter = snapshot_names.entry(path).or_default(); + *counter += 1; + Cow::Owned(format!("{counter}")) + }; + + (settings, snap_name) +} + +/** +Create a hashed snapshot test. + +## Arguments: + +1. The content of the snapshot. It is an expression whose result implements the `fmt::Display` trait. +2. `name: `: the identifier for the snapshot test (optional) +3. `@""` to write the hash of the snapshot inline + +## Behaviour +The content of the snapshot will be saved both in full and as a hash. The full snapshot will +be saved with the name `.full.snap` but will not be saved to the git repository. The hashed +snapshot will be saved inline. If `` is not specified, then a global counter is used to give an +identifier to the snapshot. + +Running `cargo test` will check whether the old snapshot is identical to the +current one. If they are equal, the test passes. Otherwise, the test fails. + +Use the command line `cargo insta` to approve or reject new snapshots. + +## Example +```ignore +// The full snapshot is saved under 1.full.snap and contains `10` +snapshot_hash!(10, @"d3d9446802a44259755d38e6d163e820"); +// The full snapshot is saved under snap_name.full.snap and contains `hello world` +snapshot_hash!("hello world", name: "snap_name", @"5f93f983524def3dca464469d2cf9f3e"); +``` +*/ +#[macro_export] +macro_rules! snapshot_hash { + ($value:expr, @$inline:literal) => { + let (settings, snap_name) = $crate::default_snapshot_settings_for_test(None); + settings.bind(|| { + let snap = format!("{}", $value); + let hash_snap = $crate::hash_snapshot(&snap); + insta::assert_snapshot!(hash_snap, @$inline); + insta::assert_snapshot!(format!("{}.full", snap_name), snap); + }); + }; + ($value:expr, name: $name:expr, @$inline:literal) => { + let snap_name = format!("{}", $name); + let (settings, snap_name) = $crate::default_snapshot_settings_for_test(Some(&snap_name)); + settings.bind(|| { + let snap = format!("{}", $value); + let hash_snap = $crate::hash_snapshot(&snap); + insta::assert_snapshot!(hash_snap, @$inline); + insta::assert_snapshot!(format!("{}.full", snap_name), snap); + }); + }; +} + +/** +Create a hashed snapshot test. + +## Arguments: +1. The content of the snapshot. It is an expression whose result implements the `fmt::Display` trait. +2. Optionally one of: + 1. `name: `: the identifier for the snapshot test + 2. `@""` to write the hash of the snapshot inline + +## Behaviour +The content of the snapshot will be saved in full with the given name +or using a global counter to give it an identifier. + +Running `cargo test` will check whether the old snapshot is identical to the +current one. If they are equal, the test passes. Otherwise, the test fails. + +Use the command line `cargo insta` to approve or reject new snapshots. + +## Example +```ignore +// The full snapshot is saved under 1.snap and contains `10` +snapshot!(10); +// The full snapshot is saved under snap_name.snap and contains `10` +snapshot!("hello world", name: "snap_name"); +// The full snapshot is saved inline +snapshot!(format!("{:?}", vec![1, 2]), @"[1, 2]"); +``` +*/ +#[macro_export] +macro_rules! snapshot { + ($value:expr, name: $name:expr) => { + let snap_name = format!("{}", $name); + let (settings, snap_name) = $crate::default_snapshot_settings_for_test(Some(&snap_name)); + settings.bind(|| { + let snap = format!("{}", $value); + insta::assert_snapshot!(format!("{}", snap_name), snap); + }); + }; + ($value:expr, @$inline:literal) => { + // Note that the name given as argument does not matter since it is only an inline snapshot + // We don't pass None because otherwise `meili-snap` will try to assign it a unique identifier + let (settings, _) = $crate::default_snapshot_settings_for_test(Some("_dummy_argument")); + settings.bind(|| { + let snap = format!("{}", $value); + insta::assert_snapshot!(snap, @$inline); + }); + }; + ($value:expr) => { + let (settings, snap_name) = $crate::default_snapshot_settings_for_test(None); + settings.bind(|| { + let snap = format!("{}", $value); + insta::assert_snapshot!(format!("{}", snap_name), snap); + }); + }; +} + +#[cfg(test)] +mod tests { + + #[test] + fn snap() { + snapshot_hash!(10, @"d3d9446802a44259755d38e6d163e820"); + snapshot_hash!(20, @"98f13708210194c475687be6106a3b84"); + snapshot_hash!(30, @"34173cb38f07f89ddbebc2ac9128303f"); + + snapshot!(40, @"40"); + snapshot!(50, @"50"); + snapshot!(60, @"60"); + + snapshot!(70); + snapshot!(80); + snapshot!(90); + + snapshot!(100, name: "snap_name_1"); + snapshot_hash!(110, name: "snap_name_2", @"5f93f983524def3dca464469d2cf9f3e"); + + snapshot!(120); + snapshot!(format!("{:?}", vec![1, 2]), @"[1, 2]"); + } + + // Currently the name of this module is not part of the snapshot path + // It does not bother me, but maybe it is worth changing later on. + mod snap { + #[test] + fn some_test() { + snapshot_hash!(10, @"d3d9446802a44259755d38e6d163e820"); + snapshot_hash!(20, @"98f13708210194c475687be6106a3b84"); + snapshot_hash!(30, @"34173cb38f07f89ddbebc2ac9128303f"); + + snapshot!(40, @"40"); + snapshot!(50, @"50"); + snapshot!(60, @"60"); + + snapshot!(70); + snapshot!(80); + snapshot!(90); + + snapshot!(100, name: "snap_name_1"); + snapshot_hash!(110, name: "snap_name_2", @"5f93f983524def3dca464469d2cf9f3e"); + + snapshot!(120); + + snapshot_hash!("", name: "", @"d41d8cd98f00b204e9800998ecf8427e"); + } + } +} + +/// Create a string from the value by serializing it as Json, optionally +/// redacting some parts of it. +/// +/// The second argument to the macro can be an object expression for redaction. +/// It's in the form { selector => replacement }. For more information about redactions +/// refer to the redactions feature in the `insta` guide. +#[macro_export] +macro_rules! json_string { + ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => { + { + let (_, snap) = insta::_prepare_snapshot_for_redaction!($value, {$($k => $v),*}, Json, File); + snap + } + }; + ($value:expr) => {{ + let value = insta::_macro_support::serialize_value( + &$value, + insta::_macro_support::SerializationFormat::Json, + insta::_macro_support::SnapshotLocation::File + ); + value + }}; +} diff --git a/meili-snap/src/snapshots/lib.rs/snap/4.snap b/meili-snap/src/snapshots/lib.rs/snap/4.snap new file mode 100644 index 000000000..5d0878f16 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/snap/4.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +70 diff --git a/meili-snap/src/snapshots/lib.rs/snap/5.snap b/meili-snap/src/snapshots/lib.rs/snap/5.snap new file mode 100644 index 000000000..ea547b823 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/snap/5.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +80 diff --git a/meili-snap/src/snapshots/lib.rs/snap/6.snap b/meili-snap/src/snapshots/lib.rs/snap/6.snap new file mode 100644 index 000000000..e91bbe6f7 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/snap/6.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +90 diff --git a/meili-snap/src/snapshots/lib.rs/snap/7.snap b/meili-snap/src/snapshots/lib.rs/snap/7.snap new file mode 100644 index 000000000..5ae6bb922 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/snap/7.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +120 diff --git a/meili-snap/src/snapshots/lib.rs/snap/snap_name_1.snap b/meili-snap/src/snapshots/lib.rs/snap/snap_name_1.snap new file mode 100644 index 000000000..3964679e6 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/snap/snap_name_1.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +100 diff --git a/meili-snap/src/snapshots/lib.rs/some_test/4.snap b/meili-snap/src/snapshots/lib.rs/some_test/4.snap new file mode 100644 index 000000000..5d0878f16 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/some_test/4.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +70 diff --git a/meili-snap/src/snapshots/lib.rs/some_test/5.snap b/meili-snap/src/snapshots/lib.rs/some_test/5.snap new file mode 100644 index 000000000..ea547b823 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/some_test/5.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +80 diff --git a/meili-snap/src/snapshots/lib.rs/some_test/6.snap b/meili-snap/src/snapshots/lib.rs/some_test/6.snap new file mode 100644 index 000000000..e91bbe6f7 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/some_test/6.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +90 diff --git a/meili-snap/src/snapshots/lib.rs/some_test/7.snap b/meili-snap/src/snapshots/lib.rs/some_test/7.snap new file mode 100644 index 000000000..5ae6bb922 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/some_test/7.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +120 diff --git a/meili-snap/src/snapshots/lib.rs/some_test/snap_name_1.snap b/meili-snap/src/snapshots/lib.rs/some_test/snap_name_1.snap new file mode 100644 index 000000000..3964679e6 --- /dev/null +++ b/meili-snap/src/snapshots/lib.rs/some_test/snap_name_1.snap @@ -0,0 +1,4 @@ +--- +source: meili-snap/src/lib.rs +--- +100 diff --git a/meilisearch-http/src/option.rs b/meilisearch-http/src/option.rs index 175706d8c..82269f371 100644 --- a/meilisearch-http/src/option.rs +++ b/meilisearch-http/src/option.rs @@ -1,13 +1,11 @@ use std::convert::TryFrom; -use std::env; -use std::fs; use std::io::{BufReader, Read}; use std::num::ParseIntError; use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use std::{fmt, fs}; +use std::{env, fmt, fs}; use byte_unit::{Byte, ByteError}; use clap::Parser; @@ -646,6 +644,17 @@ fn load_ocsp(filename: &Option) -> anyhow::Result> { Ok(ret) } +/// Checks if the key is defined in the environment variables. +/// If not, inserts it with the given value. +pub fn export_to_env_if_not_present(key: &str, value: T) +where + T: AsRef, +{ + if let Err(VarError::NotPresent) = std::env::var(key) { + std::env::set_var(key, value); + } +} + /// Functions used to get default value for `Opt` fields, needs to be function because of serde's default attribute. fn default_db_path() -> PathBuf {