use std::fmt; use std::iter::FromIterator; use snapbox::assert_eq; use toml_edit::{array, table, value, Document, Item, Key, Table, Value}; macro_rules! parse_key { ($s:expr) => {{ let key = $s.parse::(); assert!(key.is_ok()); key.unwrap() }}; } macro_rules! as_table { ($e:ident) => {{ assert!($e.is_table()); $e.as_table_mut().unwrap() }}; } // Copied from https://github.com/colin-kiegel/rust-pretty-assertions/issues/24 /// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`. /// Used in different `assert*!` macros in combination with `pretty_assertions` crate to make /// test failures to show nice diffs. #[derive(PartialEq, Eq)] struct PrettyString<'a>(pub &'a str); /// Make diff to display string as multi-line string impl<'a> fmt::Debug for PrettyString<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } } struct Test { doc: Document, } fn given(input: &str) -> Test { let doc = input.parse::(); assert!(doc.is_ok()); Test { doc: doc.unwrap() } } impl Test { fn running(&mut self, func: F) -> &mut Self where F: Fn(&mut Table), { { let root = self.doc.as_table_mut(); func(root); } self } #[track_caller] fn produces_display(&self, expected: &str) -> &Self { assert_eq(expected, self.doc.to_string()); self } } // insertion #[test] fn test_insert_leaf_table() { given( r#"[servers] [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" [other.table]"#, ) .running(|root| { root["servers"]["beta"] = table(); root["servers"]["beta"]["ip"] = value("10.0.0.2"); root["servers"]["beta"]["dc"] = value("eqdc10"); }) .produces_display( r#"[servers] [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" [servers.beta] ip = "10.0.0.2" dc = "eqdc10" [other.table] "#, ); } #[test] fn test_inserted_leaf_table_goes_after_last_sibling() { given( r#" [package] [dependencies] [[example]] [dependencies.opencl] [dev-dependencies]"#, ) .running(|root| { root["dependencies"]["newthing"] = table(); }) .produces_display( r#" [package] [dependencies] [[example]] [dependencies.opencl] [dependencies.newthing] [dev-dependencies] "#, ); } #[test] fn test_inserting_tables_from_different_parsed_docs() { given("[a]") .running(|root| { let other = "[b]".parse::().unwrap(); root["b"] = other["b"].clone(); }) .produces_display("[a]\n[b]\n"); } #[test] fn test_insert_nonleaf_table() { given( r#" [other.table]"#, ) .running(|root| { root["servers"] = table(); root["servers"]["alpha"] = table(); root["servers"]["alpha"]["ip"] = value("10.0.0.1"); root["servers"]["alpha"]["dc"] = value("eqdc10"); }) .produces_display( r#" [other.table] [servers] [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" "#, ); } #[test] fn test_insert_array() { given( r#" [package] title = "withoutarray""#, ) .running(|root| { root["bin"] = array(); assert!(root["bin"].is_array_of_tables()); let array = root["bin"].as_array_of_tables_mut().unwrap(); { let mut table = Table::new(); table["hello"] = value("world"); array.push(table); } array.push(Table::new()); }) .produces_display( r#" [package] title = "withoutarray" [[bin]] hello = "world" [[bin]] "#, ); } #[test] fn test_insert_values() { given( r#" [tbl.son]"#, ) .running(|root| { root["tbl"]["key1"] = value("value1"); root["tbl"]["key2"] = value(42); root["tbl"]["key3"] = value(8.1415926); }) .produces_display( r#"[tbl] key1 = "value1" key2 = 42 key3 = 8.1415926 [tbl.son] "#, ); } // removal #[test] fn test_remove_leaf_table() { given( r#" [servers] # Indentation (tabs and/or spaces) is allowed but not required [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" [servers.beta] ip = "10.0.0.2" dc = "eqdc10""#, ) .running(|root| { let servers = root.get_mut("servers").unwrap(); let servers = as_table!(servers); assert!(servers.remove("alpha").is_some()); }) .produces_display( r#" [servers] [servers.beta] ip = "10.0.0.2" dc = "eqdc10" "#, ); } #[test] fn test_remove_nonleaf_table() { given( r#" title = "not relevant" # comment 1 [a.b.c] # comment 1.1 key1 = 1 # comment 1.2 # comment 2 [b] # comment 2.1 key2 = 2 # comment 2.2 # comment 3 [a] # comment 3.1 key3 = 3 # comment 3.2 [[a.'array']] b = 1 [[a.b.c.trololololololo]] # ohohohohoho c = 2 key3 = 42 # comment on some other table [some.other.table] # comment 4 [a.b] # comment 4.1 key4 = 4 # comment 4.2 key41 = 41 # comment 4.3 "#, ) .running(|root| { assert!(root.remove("a").is_some()); }) .produces_display( r#" title = "not relevant" # comment 2 [b] # comment 2.1 key2 = 2 # comment 2.2 # comment on some other table [some.other.table] "#, ); } #[test] fn test_remove_array_entry() { given( r#" [package] name = "hello" version = "1.0.0" [[bin]] name = "world" path = "src/bin/world/main.rs" [dependencies] nom = "4.0" # future is here [[bin]] name = "delete me please" path = "src/bin/dmp/main.rs""#, ) .running(|root| { let dmp = root.get_mut("bin").unwrap(); assert!(dmp.is_array_of_tables()); let dmp = dmp.as_array_of_tables_mut().unwrap(); assert_eq!(dmp.len(), 2); dmp.remove(1); assert_eq!(dmp.len(), 1); }) .produces_display( r#" [package] name = "hello" version = "1.0.0" [[bin]] name = "world" path = "src/bin/world/main.rs" [dependencies] nom = "4.0" # future is here "#, ); } #[test] fn test_remove_array() { given( r#" [package] name = "hello" version = "1.0.0" [[bin]] name = "world" path = "src/bin/world/main.rs" [dependencies] nom = "4.0" # future is here [[bin]] name = "delete me please" path = "src/bin/dmp/main.rs""#, ) .running(|root| { assert!(root.remove("bin").is_some()); }) .produces_display( r#" [package] name = "hello" version = "1.0.0" [dependencies] nom = "4.0" # future is here "#, ); } #[test] fn test_remove_value() { given( r#" name = "hello" # delete this version = "1.0.0" # please documentation = "https://docs.rs/hello""#, ) .running(|root| { let value = root.remove("version"); assert!(value.is_some()); let value = value.unwrap(); assert!(value.is_value()); let value = value.as_value().unwrap(); assert!(value.is_str()); let value = value.as_str().unwrap(); assert_eq(value, "1.0.0"); }) .produces_display( r#" name = "hello" documentation = "https://docs.rs/hello" "#, ); } #[test] fn test_remove_last_value_from_implicit() { given( r#" [a] b = 1"#, ) .running(|root| { let a = root.get_mut("a").unwrap(); assert!(a.is_table()); let a = as_table!(a); a.set_implicit(true); let value = a.remove("b"); assert!(value.is_some()); let value = value.unwrap(); assert!(value.is_value()); let value = value.as_value().unwrap(); assert_eq!(value.as_integer(), Some(1)); }) .produces_display(r#""#); } // values #[test] fn test_sort_values() { given( r#" [a.z] [a] # this comment is attached to b b = 2 # as well as this a = 1 c = 3 [a.y]"#, ) .running(|root| { let a = root.get_mut("a").unwrap(); let a = as_table!(a); a.sort_values(); }) .produces_display( r#" [a.z] [a] a = 1 # this comment is attached to b b = 2 # as well as this c = 3 [a.y] "#, ); } #[test] fn test_sort_values_by() { given( r#" [a.z] [a] # this comment is attached to b b = 2 # as well as this a = 1 "c" = 3 [a.y]"#, ) .running(|root| { let a = root.get_mut("a").unwrap(); let a = as_table!(a); // Sort by the representation, not the value. So "\"c\"" sorts before "a" because '"' sorts // before 'a'. a.sort_values_by(|k1, _, k2, _| k1.display_repr().cmp(&k2.display_repr())); }) .produces_display( r#" [a.z] [a] "c" = 3 a = 1 # this comment is attached to b b = 2 # as well as this [a.y] "#, ); } #[test] fn test_set_position() { given( r#" [package] [dependencies] [dependencies.opencl] [dev-dependencies]"#, ) .running(|root| { for (header, table) in root.iter_mut() { if header == "dependencies" { let tab = as_table!(table); tab.set_position(0); let (_, segmented) = tab.iter_mut().next().unwrap(); as_table!(segmented).set_position(5) } } }) .produces_display( r#" [dependencies] [package] [dev-dependencies] [dependencies.opencl] "#, ); } #[test] fn test_multiple_zero_positions() { given( r#" [package] [dependencies] [dependencies.opencl] a="" [dev-dependencies]"#, ) .running(|root| { for (_, table) in root.iter_mut() { as_table!(table).set_position(0) } }) .produces_display( r#" [package] [dependencies] [dev-dependencies] [dependencies.opencl] a="" "#, ); } #[test] fn test_multiple_max_usize_positions() { given( r#" [package] [dependencies] [dependencies.opencl] a="" [dev-dependencies]"#, ) .running(|root| { for (_, table) in root.iter_mut() { as_table!(table).set_position(usize::MAX) } }) .produces_display( r#" [dependencies.opencl] a="" [package] [dependencies] [dev-dependencies] "#, ); } macro_rules! as_array { ($entry:ident) => {{ assert!($entry.is_value()); let a = $entry.as_value_mut().unwrap(); assert!(a.is_array()); a.as_array_mut().unwrap() }}; } #[test] fn test_insert_replace_into_array() { given( r#" a = [1,2,3] b = []"#, ) .running(|root| { { let a = root.get_mut("a").unwrap(); let a = as_array!(a); assert_eq!(a.len(), 3); assert!(a.get(2).is_some()); a.push(4); assert_eq!(a.len(), 4); a.fmt(); } let b = root.get_mut("b").unwrap(); let b = as_array!(b); assert!(b.is_empty()); b.push("hello"); assert_eq!(b.len(), 1); b.push_formatted(Value::from("world").decorated("\n", "\n")); b.push_formatted(Value::from("test").decorated("", "")); b.insert(1, "beep"); b.insert_formatted(2, Value::from("boop").decorated(" ", " ")); // This should preserve formatting. assert_eq!(b.replace(2, "zoink").as_str(), Some("boop")); // This should replace formatting. assert_eq!( b.replace_formatted(4, Value::from("yikes").decorated(" ", "")) .as_str(), Some("test") ); dbg!(root); }) .produces_display( r#" a = [1, 2, 3, 4] b = ["hello", "beep", "zoink" , "world" , "yikes"] "#, ); } #[test] fn test_remove_from_array() { given( r#" a = [1, 2, 3, 4] b = ["hello"]"#, ) .running(|root| { { let a = root.get_mut("a").unwrap(); let a = as_array!(a); assert_eq!(a.len(), 4); assert!(a.remove(3).is_integer()); assert_eq!(a.len(), 3); } let b = root.get_mut("b").unwrap(); let b = as_array!(b); assert_eq!(b.len(), 1); assert!(b.remove(0).is_str()); assert!(b.is_empty()); }) .produces_display( r#" a = [1, 2, 3] b = [] "#, ); } #[test] fn test_format_array() { given( r#" a = [ 1, "2", 3.0, ] "#, ) .running(|root| { for (_, v) in root.iter_mut() { if let Item::Value(Value::Array(array)) = v { array.fmt(); } } }) .produces_display( r#" a = [1, "2", 3.0] "#, ); } macro_rules! as_inline_table { ($entry:ident) => {{ assert!($entry.is_value()); let a = $entry.as_value_mut().unwrap(); assert!(a.is_inline_table()); a.as_inline_table_mut().unwrap() }}; } #[test] fn test_insert_into_inline_table() { given( r#" a = {a=2, c = 3} b = {}"#, ) .running(|root| { { let a = root.get_mut("a").unwrap(); let a = as_inline_table!(a); assert_eq!(a.len(), 2); assert!(a.contains_key("a") && a.get("c").is_some() && a.get_mut("c").is_some()); a.get_or_insert("b", 42); assert_eq!(a.len(), 3); a.fmt(); } let b = root.get_mut("b").unwrap(); let b = as_inline_table!(b); assert!(b.is_empty()); b.get_or_insert("hello", "world"); assert_eq!(b.len(), 1); b.fmt() }) .produces_display( r#" a = { a = 2, c = 3, b = 42 } b = { hello = "world" } "#, ); } #[test] fn test_remove_from_inline_table() { given( r#" a = {a=2, c = 3, b = 42} b = {'hello' = "world"}"#, ) .running(|root| { { let a = root.get_mut("a").unwrap(); let a = as_inline_table!(a); assert_eq!(a.len(), 3); assert!(a.remove("c").is_some()); assert_eq!(a.len(), 2); } let b = root.get_mut("b").unwrap(); let b = as_inline_table!(b); assert_eq!(b.len(), 1); assert!(b.remove("hello").is_some()); assert!(b.is_empty()); }) .produces_display( r#" a = {a=2, b = 42} b = {} "#, ); } #[test] fn test_as_table_like() { given( r#" a = {a=2, c = 3, b = 42} x = {} [[bin]] [b] x = "y" [empty]"#, ) .running(|root| { let a = root["a"].as_table_like(); assert!(a.is_some()); let a = a.unwrap(); assert_eq!(a.iter().count(), 3); assert_eq!(a.len(), 3); assert_eq!(a.get("a").and_then(Item::as_integer), Some(2)); let b = root["b"].as_table_like(); assert!(b.is_some()); let b = b.unwrap(); assert_eq!(b.iter().count(), 1); assert_eq!(b.len(), 1); assert_eq!(b.get("x").and_then(Item::as_str), Some("y")); assert_eq!(root["x"].as_table_like().map(|t| t.iter().count()), Some(0)); assert_eq!( root["empty"].as_table_like().map(|t| t.is_empty()), Some(true) ); assert!(root["bin"].as_table_like().is_none()); }); } #[test] fn test_inline_table_append() { let mut a = Value::from_iter(vec![ (parse_key!("a"), 1), (parse_key!("b"), 2), (parse_key!("c"), 3), ]); let a = a.as_inline_table_mut().unwrap(); let mut b = Value::from_iter(vec![ (parse_key!("c"), 4), (parse_key!("d"), 5), (parse_key!("e"), 6), ]); let b = b.as_inline_table_mut().unwrap(); a.extend(b.iter()); assert_eq!(a.len(), 5); assert!(a.contains_key("e")); assert_eq!(b.len(), 3); } #[test] fn test_insert_dotted_into_std_table() { given("") .running(|root| { root["nixpkgs"] = table(); root["nixpkgs"]["src"] = table(); root["nixpkgs"]["src"] .as_table_like_mut() .unwrap() .set_dotted(true); root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs"); }) .produces_display( r#"[nixpkgs] src.git = "https://github.com/nixos/nixpkgs" "#, ); } #[test] fn test_insert_dotted_into_implicit_table() { given("") .running(|root| { root["nixpkgs"] = table(); root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs"); root["nixpkgs"]["src"] .as_table_like_mut() .unwrap() .set_dotted(true); }) .produces_display( r#"[nixpkgs] src.git = "https://github.com/nixos/nixpkgs" "#, ); } #[test] fn sorting_with_references() { let values = vec!["foo", "qux", "bar"]; let mut array = toml_edit::Array::from_iter(values); array.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str())); }