use toml_edit::Item; use super::super::UserPreferences; use super::*; #[test] fn test_json_to_toml_round_trip_bool() { let item = TomlBackedUserPreferences::json_value_to_toml_item("true", None); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item); assert_eq!(json, Some("true".to_string())); } #[test] fn test_json_to_toml_round_trip_integer() { let item = TomlBackedUserPreferences::json_value_to_toml_item("42", None); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item); assert_eq!(json, Some("42".to_string())); } #[test] fn test_json_to_toml_round_trip_float() { let item = TomlBackedUserPreferences::json_value_to_toml_item("3.14", None); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item); assert_eq!(json, Some("3.14".to_string())); } #[test] fn test_json_to_toml_round_trip_string() { let item = TomlBackedUserPreferences::json_value_to_toml_item("\"hello\"", None); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item); assert_eq!(json, Some("\"hello\"".to_string())); } #[test] fn test_json_to_toml_round_trip_object() { let input = r#"{"dark":"Phenomenon","light":"Paper"}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); // Objects should become TOML tables, not strings. assert!(matches!(item, Item::Table(_))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_json_to_toml_round_trip_null() { let item = TomlBackedUserPreferences::json_value_to_toml_item("null", None); assert!(matches!(item, Item::None)); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item); assert_eq!(json, None); } #[test] fn test_json_to_toml_round_trip_array_of_strings() { let input = r#"["cat","echo","ls"]"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); assert!(matches!(item, Item::Value(toml_edit::Value::Array(_)))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_json_to_toml_round_trip_array_of_objects() { let input = r#"[{"AnchoredRegex":"^bash$"},{"AnchoredRegex":"^fish$"}]"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); assert!(matches!(item, Item::Value(toml_edit::Value::Array(_)))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_json_to_toml_round_trip_nested_struct() { let input = r#"{"advanced_mode":false,"global":{"mode":"PreviousDir","custom_dir":""},"split_pane":{"mode":"HomeDir","custom_dir":"/tmp"}}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); assert!(matches!(item, Item::Table(_))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_json_to_toml_round_trip_object_with_nulls() { // Null fields should be omitted from the table, so the round-trip // drops them. Verify that non-null fields survive. let input = r#"{"keybinding":null,"active_pin_position":"Top","pin_screen":null,"hide_window_when_unfocused":true}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); assert!(matches!(item, Item::Table(_))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); // Null fields are dropped. let expected = serde_json::json!({ "active_pin_position": "Top", "hide_window_when_unfocused": true, }); assert_eq!(actual, expected); } #[test] fn test_json_to_toml_round_trip_empty_array() { let input = "[]"; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); assert!(matches!(item, Item::Value(toml_edit::Value::Array(_)))); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); assert_eq!(json, "[]"); } #[test] fn test_max_table_depth_zero_renders_inline() { // depth 0 = entire value rendered inline let input = r#"{"uniform_padding":0.0}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, Some(0)); // Should be an inline Value, not an Item::Table assert!(matches!( item, Item::Value(toml_edit::Value::InlineTable(_)) )); let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_max_table_depth_one_renders_top_level_as_table_nested_as_inline() { // depth 1 = top-level object is a section table, but nested objects are inline let input = r#"{"active_pin_position":"Top","sizes":{"width":100,"height":50},"enabled":true}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, Some(1)); // Top level should be a table let table = match &item { Item::Table(t) => t, other => panic!("expected Table, got {other:?}"), }; // Primitive fields should be normal values assert!(matches!( table.get("active_pin_position"), Some(Item::Value(_)) )); assert!(matches!(table.get("enabled"), Some(Item::Value(_)))); // Nested object should be an inline table value, not a sub-table let sizes = table.get("sizes").expect("sizes should exist"); assert!( matches!(sizes, Item::Value(toml_edit::Value::InlineTable(_))), "nested object at depth 1 should be inline, got {sizes:?}" ); // Round-trip the JSON let json = TomlBackedUserPreferences::toml_item_to_json_string(&item).unwrap(); let expected: serde_json::Value = serde_json::from_str(input).unwrap(); let actual: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(actual, expected); } #[test] fn test_max_table_depth_none_renders_all_as_tables() { // depth None = unlimited, nested objects become section tables let input = r#"{"sizes":{"width":100,"height":50}}"#; let item = TomlBackedUserPreferences::json_value_to_toml_item(input, None); let table = match &item { Item::Table(t) => t, other => panic!("expected Table, got {other:?}"), }; // Nested object should be a sub-table, not inline assert!( matches!(table.get("sizes"), Some(Item::Table(_))), "nested object with None depth should be a section table" ); } #[test] fn test_write_and_read_with_hierarchy() { use super::super::UserPreferences; let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test_settings.toml"); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); prefs .write_value_with_hierarchy("font_name", "\"Hack\"".to_string(), Some("font"), None) .unwrap(); prefs .write_value_with_hierarchy("font_size", "13.0".to_string(), Some("font"), None) .unwrap(); prefs .write_value_with_hierarchy( "use_thin_strokes", "true".to_string(), Some("font.display"), None, ) .unwrap(); // Read back let font_name = prefs .read_value_with_hierarchy("font_name", Some("font")) .unwrap(); assert_eq!(font_name, Some("\"Hack\"".to_string())); let font_size = prefs .read_value_with_hierarchy("font_size", Some("font")) .unwrap(); assert_eq!(font_size, Some("13.0".to_string())); let thin_strokes = prefs .read_value_with_hierarchy("use_thin_strokes", Some("font.display")) .unwrap(); assert_eq!(thin_strokes, Some("true".to_string())); // Verify the TOML file structure let contents = std::fs::read_to_string(&file_path).unwrap(); assert!(contents.contains("[font]")); assert!(contents.contains("font_name")); assert!(contents.contains("font_size")); assert!(contents.contains("[font.display]")); assert!(contents.contains("use_thin_strokes")); } #[test] fn test_write_and_read_struct_value() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test_settings.toml"); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); // Write a struct value (JSON object) under a hierarchy. let struct_json = r#"{"left_alt":false,"right_alt":true}"#; prefs .write_value_with_hierarchy( "extra_meta_keys", struct_json.to_string(), Some("keys"), None, ) .unwrap(); // Read back — should produce equivalent JSON. let read_back = prefs .read_value_with_hierarchy("extra_meta_keys", Some("keys")) .unwrap() .unwrap(); let expected: serde_json::Value = serde_json::from_str(struct_json).unwrap(); let actual: serde_json::Value = serde_json::from_str(&read_back).unwrap(); assert_eq!(actual, expected); // Verify the TOML file uses a sub-table, not a JSON string. let contents = std::fs::read_to_string(&file_path).unwrap(); assert!(contents.contains("[keys.extra_meta_keys]")); assert!(contents.contains("left_alt = false")); assert!(contents.contains("right_alt = true")); // Should NOT contain a JSON blob. assert!(!contents.contains(r#"{"left_alt"#)); } #[test] fn test_new_with_invalid_toml_returns_error_and_recovers_on_reload() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("broken_settings.toml"); // Write invalid TOML to the file. std::fs::write(&file_path, "this is [not valid toml =").unwrap(); // new() should succeed with an empty document and return the parse error. let (prefs, parse_error) = TomlBackedUserPreferences::new(file_path.clone()); assert!( parse_error.is_some(), "expected a parse error for invalid TOML" ); // The preferences should behave as empty — no values present. assert_eq!( prefs .read_value_with_hierarchy("font_name", Some("font")) .unwrap(), None ); // is_settings_file should still return true. assert!(prefs.is_settings_file()); // Now fix the file with valid TOML. std::fs::write(&file_path, "[font]\nfont_name = \"Hack\"\n").unwrap(); // reload_from_disk should succeed and pick up the new value. assert!(prefs.reload_from_disk().is_ok()); assert_eq!( prefs .read_value_with_hierarchy("font_name", Some("font")) .unwrap(), Some("\"Hack\"".to_string()) ); } #[test] fn test_writes_inhibited_when_file_initially_broken() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("broken_settings.toml"); let original_content = "this is [not valid toml ="; std::fs::write(&file_path, original_content).unwrap(); let (prefs, parse_error) = TomlBackedUserPreferences::new(file_path.clone()); assert!(parse_error.is_some()); // Writing a setting should succeed in-memory but NOT overwrite the file. prefs .write_value_with_hierarchy("font_name", "\"Hack\"".to_string(), Some("font"), None) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); assert_eq!( on_disk, original_content, "broken file should not be overwritten by writes during inhibited state" ); // Fix the file and reload. std::fs::write(&file_path, "# now valid\n").unwrap(); assert!(prefs.reload_from_disk().is_ok()); // After reload, writes should flush to disk again. prefs .write_value_with_hierarchy("font_name", "\"Hack\"".to_string(), Some("font"), None) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); assert!( on_disk.contains("font_name"), "writes should flush to disk after successful reload" ); } #[test] fn test_string_value_for_numeric_setting_reads_as_json_string() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test_settings.toml"); // Write a TOML file where font_size is a string instead of a number. std::fs::write(&file_path, "[font]\nfont_size = \"not_a_number\"\n").unwrap(); let (prefs, parse_error) = TomlBackedUserPreferences::new(file_path); assert!( parse_error.is_none(), "valid TOML should parse without error" ); // Read the value — should return the JSON-encoded string. let value = prefs .read_value_with_hierarchy("font_size", Some("font")) .unwrap(); // The TOML string "not_a_number" should be JSON-encoded as "\"not_a_number\"" assert_eq!( value, Some("\"not_a_number\"".to_string()), "TOML string should be JSON-encoded" ); // Attempting to deserialize as f32 should fail. let result = serde_json::from_str::(value.as_deref().unwrap()); assert!(result.is_err(), "JSON string should not deserialize as f32"); } #[test] fn test_remove_with_hierarchy() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test_settings.toml"); let (prefs, _) = TomlBackedUserPreferences::new(file_path); prefs .write_value_with_hierarchy("font_name", "\"Hack\"".to_string(), Some("font"), None) .unwrap(); let val = prefs .read_value_with_hierarchy("font_name", Some("font")) .unwrap(); assert!(val.is_some()); prefs .remove_value_with_hierarchy("font_name", Some("font")) .unwrap(); let val = prefs .read_value_with_hierarchy("font_name", Some("font")) .unwrap(); assert!(val.is_none()); } #[test] fn test_per_key_write_inhibition_preserves_value_in_file() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("settings.toml"); // Write a file with one valid and one "invalid" setting (the TOML itself // is fine, but the caller decides the value is wrong for the type). std::fs::write( &file_path, "[font]\nfont_size = \"abc\"\nfont_name = \"Hack\"\n", ) .unwrap(); let (prefs, parse_error) = TomlBackedUserPreferences::new(file_path.clone()); assert!(parse_error.is_none(), "TOML itself is valid"); // Simulate the settings layer detecting the bad value. prefs.inhibit_writes_for_key("font_size", Some("font")); // Writing to the inhibited key should be a no-op. prefs .write_value_with_hierarchy("font_size", "13.0".to_string(), Some("font"), None) .unwrap(); // Writing to the NON-inhibited key should succeed. prefs .write_value_with_hierarchy("font_name", "\"Fira Code\"".to_string(), Some("font"), None) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); // The inhibited key's original value must still be present. assert!( on_disk.contains("font_size = \"abc\""), "inhibited key's original value should be preserved, got: {on_disk}" ); // The non-inhibited key's new value should be written. assert!( on_disk.contains("Fira Code"), "non-inhibited key should be updated, got: {on_disk}" ); } #[test] fn test_per_key_write_inhibition_blocks_remove() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("settings.toml"); std::fs::write(&file_path, "[font]\nfont_size = \"abc\"\n").unwrap(); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); prefs.inhibit_writes_for_key("font_size", Some("font")); // Removing the inhibited key should be a no-op. prefs .remove_value_with_hierarchy("font_size", Some("font")) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); assert!( on_disk.contains("font_size = \"abc\""), "inhibited key should not be removed, got: {on_disk}" ); } #[test] fn test_reload_clears_per_key_inhibitions() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("settings.toml"); std::fs::write(&file_path, "[font]\nfont_size = \"abc\"\n").unwrap(); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); prefs.inhibit_writes_for_key("font_size", Some("font")); // Fix the file. std::fs::write(&file_path, "[font]\nfont_size = 14.0\n").unwrap(); prefs.reload_from_disk().unwrap(); // After reload, the inhibition is cleared — writes should work. prefs .write_value_with_hierarchy("font_size", "16.0".to_string(), Some("font"), None) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); assert!( on_disk.contains("16.0") || on_disk.contains("16"), "write should succeed after reload cleared inhibition, got: {on_disk}" ); } #[test] fn test_clear_all_write_inhibitions() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("settings.toml"); std::fs::write(&file_path, "[font]\nfont_size = \"abc\"\nfont_name = 123\n").unwrap(); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); prefs.inhibit_writes_for_key("font_size", Some("font")); prefs.inhibit_writes_for_key("font_name", Some("font")); prefs.clear_all_write_inhibitions(); // Both keys should now be writable. prefs .write_value_with_hierarchy("font_size", "14.0".to_string(), Some("font"), None) .unwrap(); prefs .write_value_with_hierarchy("font_name", "\"Hack\"".to_string(), Some("font"), None) .unwrap(); let on_disk = std::fs::read_to_string(&file_path).unwrap(); assert!( on_disk.contains("14") && on_disk.contains("Hack"), "both keys should be writable after clearing all inhibitions, got: {on_disk}" ); } #[test] fn test_file_content_hash_returns_none_for_missing_file() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("does_not_exist.toml"); assert_eq!( None, TomlBackedUserPreferences::file_content_hash(&file_path) ); } #[test] fn test_file_content_hash_returns_none_for_empty_file() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("empty.toml"); std::fs::write(&file_path, "").unwrap(); assert_eq!( None, TomlBackedUserPreferences::file_content_hash(&file_path) ); } #[test] fn test_file_content_hash_returns_none_for_whitespace_only_file() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("whitespace.toml"); std::fs::write(&file_path, " \n\t \n").unwrap(); assert_eq!( None, TomlBackedUserPreferences::file_content_hash(&file_path) ); } #[test] fn test_file_content_hash_is_deterministic_for_identical_content() { let dir = tempfile::tempdir().unwrap(); let path_a = dir.path().join("a.toml"); let path_b = dir.path().join("b.toml"); let contents = "[font]\nfont_size = 14.0\n"; std::fs::write(&path_a, contents).unwrap(); std::fs::write(&path_b, contents).unwrap(); let hash_a = TomlBackedUserPreferences::file_content_hash(&path_a); let hash_b = TomlBackedUserPreferences::file_content_hash(&path_b); assert!(hash_a.is_some()); assert_eq!(hash_a, hash_b); } #[test] fn test_file_content_hash_differs_for_different_content() { let dir = tempfile::tempdir().unwrap(); let path_a = dir.path().join("a.toml"); let path_b = dir.path().join("b.toml"); std::fs::write(&path_a, "[font]\nfont_size = 14.0\n").unwrap(); std::fs::write(&path_b, "[font]\nfont_size = 18.0\n").unwrap(); let hash_a = TomlBackedUserPreferences::file_content_hash(&path_a); let hash_b = TomlBackedUserPreferences::file_content_hash(&path_b); assert!(hash_a.is_some()); assert!(hash_b.is_some()); assert_ne!(hash_a, hash_b); } // Pretty-printing tests: verify that wide inline containers get broken // across lines and that short ones stay on a single line. These drive the // `prettify_item` pass in `toml_backed.rs`. /// Writes `value_json` under the given hierarchy + key and returns the full /// file contents. Also asserts the round-trip (read back as JSON matches). fn write_and_read_file( hierarchy: Option<&str>, key: &str, value_json: &str, max_table_depth: Option, ) -> String { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("settings.toml"); let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone()); prefs .write_value_with_hierarchy(key, value_json.to_string(), hierarchy, max_table_depth) .unwrap(); // Round-trip: reading back should yield the same JSON value. let read_back = prefs .read_value_with_hierarchy(key, hierarchy) .unwrap() .expect("value was just written"); let expected: serde_json::Value = serde_json::from_str(value_json).unwrap(); let actual: serde_json::Value = serde_json::from_str(&read_back).unwrap(); assert_eq!(actual, expected, "round-trip should preserve the value"); std::fs::read_to_string(&file_path).unwrap() } #[test] fn test_pretty_print_wide_array_of_inline_tables() { // Shaped like `custom_secret_regex_list`: each inline table has a // short field count but the whole array is very wide. let json = r#"[ {"name":"IPv4 Address","pattern":"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"}, {"name":"OpenAI API Key","pattern":"\\bsk-[a-zA-Z0-9]{48}\\b"}, {"name":"GitHub Token","pattern":"\\bghp_[A-Za-z0-9_]{36}\\b"} ]"#; let contents = write_and_read_file(Some("privacy"), "custom_secret_regex_list", json, None); // Expect each inline table on its own indented line, with the closing // bracket on a fresh line. assert!( contents.contains("custom_secret_regex_list = [\n"), "array should open with a newline, got:\n{contents}" ); assert!( contents.contains("\n { name = \"IPv4 Address\""), "each inline table should start on a new indented line, got:\n{contents}" ); assert!( contents.contains("\n]\n") || contents.ends_with("\n]"), "closing bracket should be on its own line, got:\n{contents}" ); // Each inline table itself is short (2 fields, fits well under // MAX_INLINE_WIDTH), so it should still be on a single line. assert!( !contents.contains("{ name =\n") && !contents.contains("{\n name"), "individual inline tables should stay on one line, got:\n{contents}" ); } #[test] fn test_pretty_print_short_primitive_array_stays_inline() { let json = r#"["cat","echo","ls"]"#; let contents = write_and_read_file(Some("terminal"), "allowed_commands", json, None); // The whole thing fits on one line; it should stay inline. assert!( contents.contains("allowed_commands = [\"cat\", \"echo\", \"ls\"]"), "short primitive array should stay inline, got:\n{contents}" ); assert!( !contents.contains("allowed_commands = [\n"), "short primitive array should not be broken, got:\n{contents}" ); } #[test] fn test_pretty_print_long_primitive_array_goes_multiline() { // Construct a primitive array whose single-line rendering clearly // exceeds `MAX_INLINE_WIDTH`. let items: Vec = (0..20) .map(|i| format!("\"/some/fairly/long/path/entry/number/{i}\"")) .collect(); let json = format!("[{}]", items.join(",")); let contents = write_and_read_file(Some("agents"), "allowlist", &json, None); assert!( contents.contains("allowlist = [\n"), "long primitive array should open multi-line, got:\n{contents}" ); // Each element should sit on its own indented line. assert!( contents.contains("\n \"/some/fairly/long/path/entry/number/0\","), "first element should be on its own indented line with trailing comma, got:\n{contents}" ); } #[test] fn test_pretty_print_short_inline_table_stays_inline() { // Force inline via max_table_depth = 0 and verify it stays on one line. let json = r#"{"a":1,"b":2}"#; let contents = write_and_read_file(Some("section"), "small", json, Some(0)); assert!( contents.contains("small = { a = 1, b = 2 }"), "short inline table should stay on one line, got:\n{contents}" ); } #[test] fn test_pretty_print_wide_inline_table_goes_multiline() { // Force inline via max_table_depth = 0, with many long-valued fields so // the single-line rendering is wider than `MAX_INLINE_WIDTH`. let json = concat!( "{", "\"first\":\"a string value that is long enough\",", "\"second\":\"another string value that is long enough\",", "\"third\":\"one more string value that is long enough\"", "}" ); let contents = write_and_read_file(Some("section"), "big", json, Some(0)); assert!( contents.contains("big = {\n"), "wide inline table should open multi-line, got:\n{contents}" ); // Each field should sit on its own indented line. assert!( contents.contains("\n first = "), "first field should be on its own indented line, got:\n{contents}" ); assert!( contents.contains("\n second = "), "second field should be on its own indented line, got:\n{contents}" ); } #[test] fn test_pretty_print_propagates_to_parent_array() { // A short outer array (one element) containing a wide inline table. // The inline table must go multi-line (width rule), and that forces // the array multi-line too (propagation rule) — otherwise we'd render // something ugly like `[{\n ...\n}]`. let json = concat!( "[{", "\"first\":\"a string value that is long enough\",", "\"second\":\"another string value that is long enough\",", "\"third\":\"one more string value that is long enough\"", "}]" ); let contents = write_and_read_file(Some("section"), "items", json, None); assert!( contents.contains("items = [\n"), "array should be multi-line because its child is multi-line, got:\n{contents}" ); assert!( contents.contains("{\n"), "inline table should be multi-line, got:\n{contents}" ); }