1use serde::{Deserialize, Serialize};
9
10use crate::result::{ExecResult, LatchRequest};
11
12#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[serde(rename_all = "lowercase")]
24pub enum EntryType {
25 #[default]
27 Text,
28 File,
30 Directory,
32 Executable,
34 Symlink,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50#[serde(default)]
51pub struct OutputNode {
52 pub name: String,
54 pub entry_type: EntryType,
56 pub text: Option<String>,
65 pub cells: Vec<String>,
67 pub children: Vec<OutputNode>,
69}
70
71impl OutputNode {
72 pub fn new(name: impl Into<String>) -> Self {
74 Self {
75 name: name.into(),
76 ..Default::default()
77 }
78 }
79
80 pub fn text(content: impl Into<String>) -> Self {
82 Self {
83 text: Some(content.into()),
84 ..Default::default()
85 }
86 }
87
88 pub fn with_entry_type(mut self, entry_type: EntryType) -> Self {
90 self.entry_type = entry_type;
91 self
92 }
93
94 pub fn with_cells(mut self, cells: Vec<String>) -> Self {
96 self.cells = cells;
97 self
98 }
99
100 pub fn with_children(mut self, children: Vec<OutputNode>) -> Self {
102 self.children = children;
103 self
104 }
105
106 pub fn with_text(mut self, text: impl Into<String>) -> Self {
108 self.text = Some(text.into());
109 self
110 }
111
112 pub fn is_text_only(&self) -> bool {
114 self.text.is_some() && self.name.is_empty() && self.cells.is_empty() && self.children.is_empty()
115 }
116
117 pub fn has_children(&self) -> bool {
119 !self.children.is_empty()
120 }
121
122 pub fn estimated_byte_size(&self) -> usize {
124 if self.children.is_empty() {
125 self.name.len() + self.text.as_ref().map_or(0, |t| t.len())
126 } else {
127 let mut size = self.name.len() + 2; for (i, child) in self.children.iter().enumerate() {
130 if i > 0 {
131 size += 1; }
133 size += child.estimated_byte_size();
134 }
135 size + 1 }
137 }
138
139 pub fn write_canonical(&self, w: &mut dyn std::io::Write, budget: usize) -> std::io::Result<usize> {
141 if self.children.is_empty() {
142 w.write_all(self.name.as_bytes())?;
143 return Ok(self.name.len());
144 }
145 let mut written = 0;
146 w.write_all(self.name.as_bytes())?;
147 written += self.name.len();
148 if written >= budget {
149 return Ok(written);
150 }
151 w.write_all(b"/{")?;
152 written += 2;
153 for (i, child) in self.children.iter().enumerate() {
154 if written >= budget {
155 break;
156 }
157 if i > 0 {
158 w.write_all(b",")?;
159 written += 1;
160 }
161 written += child.write_canonical(w, budget.saturating_sub(written))?;
162 }
163 w.write_all(b"}")?;
164 written += 1;
165 Ok(written)
166 }
167
168 pub fn display_name(&self) -> &str {
170 if self.name.is_empty() {
171 self.text.as_deref().unwrap_or("")
172 } else {
173 &self.name
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[serde(default)]
198#[non_exhaustive]
199pub struct OutputData {
200 pub headers: Option<Vec<String>>,
202 pub root: Vec<OutputNode>,
204 #[serde(skip)]
213 #[cfg_attr(feature = "schema", schemars(skip))]
214 pub rich_json: Option<serde_json::Value>,
215}
216
217impl OutputData {
218 pub fn new() -> Self {
220 Self::default()
221 }
222
223 pub fn text(content: impl Into<String>) -> Self {
227 Self {
228 headers: None,
229 root: vec![OutputNode::text(content)],
230 rich_json: None,
231 }
232 }
233
234 pub fn nodes(nodes: Vec<OutputNode>) -> Self {
236 Self {
237 headers: None,
238 root: nodes,
239 rich_json: None,
240 }
241 }
242
243 pub fn table(headers: Vec<String>, nodes: Vec<OutputNode>) -> Self {
245 Self {
246 headers: Some(headers),
247 root: nodes,
248 rich_json: None,
249 }
250 }
251
252 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
254 self.headers = Some(headers);
255 self
256 }
257
258 pub fn with_rich_json(mut self, value: serde_json::Value) -> Self {
260 self.rich_json = Some(value);
261 self
262 }
263
264 pub fn is_simple_text(&self) -> bool {
266 self.root.len() == 1 && self.root[0].is_text_only()
267 }
268
269 pub fn is_flat(&self) -> bool {
271 self.root.iter().all(|n| !n.has_children())
272 }
273
274 pub fn is_tabular(&self) -> bool {
276 self.root.iter().any(|n| !n.cells.is_empty())
277 }
278
279 pub fn as_text(&self) -> Option<&str> {
281 if self.is_simple_text() {
282 self.root[0].text.as_deref()
283 } else {
284 None
285 }
286 }
287
288 pub fn into_text(mut self) -> Result<String, Self> {
292 if self.root.len() == 1 && self.root[0].is_text_only() {
293 Ok(self.root.pop().and_then(|n| n.text).unwrap_or_default())
294 } else {
295 Err(self)
296 }
297 }
298
299 pub fn estimated_byte_size(&self) -> usize {
304 if self.root.len() == 1 && self.root[0].is_text_only() {
305 return self.root[0].text.as_ref().map_or(0, |t| t.len());
306 }
307
308 if self.is_flat() {
309 let mut size = 0;
310 for (i, n) in self.root.iter().enumerate() {
311 if i > 0 {
312 size += 1; }
314 size += n.display_name().len();
315 for cell in &n.cells {
316 size += 1 + cell.len(); }
318 }
319 return size;
320 }
321
322 let mut size = 0;
324 for (i, n) in self.root.iter().enumerate() {
325 if i > 0 {
326 size += 1; }
328 size += n.estimated_byte_size();
329 }
330 size
331 }
332
333 pub fn write_canonical(&self, w: &mut dyn std::io::Write, budget: Option<usize>) -> std::io::Result<usize> {
339 let mut written = 0usize;
340 let budget = budget.unwrap_or(usize::MAX);
341
342 if self.root.len() == 1 && self.root[0].is_text_only() {
343 if let Some(ref text) = self.root[0].text {
344 w.write_all(text.as_bytes())?;
345 return Ok(text.len());
346 }
347 return Ok(0);
348 }
349
350 if self.is_flat() {
351 for (i, n) in self.root.iter().enumerate() {
352 if i > 0 {
353 w.write_all(b"\n")?;
354 written += 1;
355 }
356 let name = n.display_name();
357 w.write_all(name.as_bytes())?;
358 written += name.len();
359 for cell in &n.cells {
360 w.write_all(b"\t")?;
361 w.write_all(cell.as_bytes())?;
362 written += 1 + cell.len();
363 }
364 if written > budget {
365 return Ok(written);
366 }
367 }
368 return Ok(written);
369 }
370
371 for (i, n) in self.root.iter().enumerate() {
373 if i > 0 {
374 w.write_all(b"\n")?;
375 written += 1;
376 }
377 written += n.write_canonical(w, budget.saturating_sub(written))?;
378 if written > budget {
379 return Ok(written);
380 }
381 }
382 Ok(written)
383 }
384
385 pub fn to_canonical_string(&self) -> String {
394 if let Some(text) = self.as_text() {
395 return text.to_string();
396 }
397
398 if self.is_flat() {
400 return self.root.iter()
401 .map(|n| {
402 if n.cells.is_empty() {
403 n.display_name().to_string()
404 } else {
405 let mut parts = vec![n.display_name().to_string()];
407 parts.extend(n.cells.iter().cloned());
408 parts.join("\t")
409 }
410 })
411 .collect::<Vec<_>>()
412 .join("\n");
413 }
414
415 fn format_node(node: &OutputNode) -> String {
417 if node.children.is_empty() {
418 node.name.clone()
419 } else {
420 let children: Vec<String> = node.children.iter()
421 .map(format_node)
422 .collect();
423 format!("{}/{{{}}}", node.name, children.join(","))
424 }
425 }
426
427 self.root.iter()
428 .map(format_node)
429 .collect::<Vec<_>>()
430 .join("\n")
431 }
432
433 pub fn to_json(&self) -> serde_json::Value {
444 if let Some(rich) = &self.rich_json {
447 return rich.clone();
448 }
449 if let Some(text) = self.as_text() {
451 return serde_json::Value::String(text.to_string());
452 }
453
454 if let Some(ref headers) = self.headers {
459 fn row_to_json(node: &OutputNode, headers: &[String]) -> serde_json::Value {
460 let mut map = serde_json::Map::new();
461 if let Some(first) = headers.first() {
463 map.insert(first.clone(), serde_json::Value::String(node.name.clone()));
464 }
465 for (header, cell) in headers.iter().skip(1).zip(node.cells.iter()) {
467 map.insert(header.clone(), serde_json::Value::String(cell.clone()));
468 }
469 if !node.children.is_empty() {
470 let children: Vec<serde_json::Value> = node
471 .children
472 .iter()
473 .map(|child| row_to_json(child, headers))
474 .collect();
475 map.insert("children".to_string(), serde_json::Value::Array(children));
476 }
477 serde_json::Value::Object(map)
478 }
479 let rows: Vec<serde_json::Value> = self
480 .root
481 .iter()
482 .map(|node| row_to_json(node, headers))
483 .collect();
484 return serde_json::Value::Array(rows);
485 }
486
487 if !self.is_flat() {
489 fn node_to_json(node: &OutputNode) -> serde_json::Value {
490 if node.children.is_empty() {
491 serde_json::Value::Null
492 } else {
493 let mut map = serde_json::Map::new();
494 for child in &node.children {
495 map.insert(child.name.clone(), node_to_json(child));
496 }
497 serde_json::Value::Object(map)
498 }
499 }
500
501 if self.root.len() == 1 {
503 return node_to_json(&self.root[0]);
504 }
505 let mut map = serde_json::Map::new();
507 for node in &self.root {
508 map.insert(node.name.clone(), node_to_json(node));
509 }
510 return serde_json::Value::Object(map);
511 }
512
513 let items: Vec<serde_json::Value> = self.root.iter()
515 .map(|n| serde_json::Value::String(n.display_name().to_string()))
516 .collect();
517 serde_json::Value::Array(items)
518 }
519}
520
521#[non_exhaustive]
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub enum OutputFormat {
529 Json,
531}
532
533fn latch_envelope(result: &ExecResult, latch: &LatchRequest) -> serde_json::Value {
545 let error = if !result.err.is_empty() {
546 result.err.clone()
547 } else {
548 result.text_out().into_owned()
549 };
550 let mut obj = serde_json::json!({
551 "error": error,
552 "code": result.code,
553 });
554 if let Some(data) = &result.data {
555 obj["data"] = crate::result::value_to_json(data);
556 }
557 if let Ok(v) = serde_json::to_value(latch) {
559 obj["latch"] = v;
560 }
561 obj
562}
563
564pub fn apply_output_format(mut result: ExecResult, format: OutputFormat) -> ExecResult {
570 if result.is_bytes() {
573 let envelope = crate::bytes::bytes_to_envelope(result.out_bytes().unwrap_or(&[]));
574 match format {
575 OutputFormat::Json => {
576 result.set_out(
577 serde_json::to_string(&envelope).unwrap_or_else(|_| "null".to_string()),
578 );
579 result.data = Some(crate::result::json_to_value(envelope));
580 result.set_output(None);
581 }
582 }
583 return result;
584 }
585 if let Some(latch) = &result.latch {
595 let obj = match format {
596 OutputFormat::Json => latch_envelope(&result, latch),
597 };
598 let out = serde_json::to_string(&obj).unwrap_or_else(|_| "null".to_string());
599 result.data = Some(crate::result::json_to_value(obj));
600 result.set_out(out);
601 result.set_output(None);
602 return result;
603 }
604 if !result.has_output() && result.text_out().is_empty() {
605 if !result.ok() && !result.err.is_empty() {
611 match format {
612 OutputFormat::Json => {
613 let mut obj = serde_json::json!({
614 "error": result.err,
615 "code": result.code,
616 });
617 if let Some(data) = &result.data {
622 obj["data"] = crate::result::value_to_json(data);
623 }
624 let out =
625 serde_json::to_string(&obj).unwrap_or_else(|_| "null".to_string());
626 result.set_out(out);
627 result.data = Some(crate::result::json_to_value(obj));
628 }
629 }
630 }
631 return result;
632 }
633 match format {
634 OutputFormat::Json => {
635 if let Some(output) = result.output() {
636 let json_value = output.to_json();
637 result.set_out(serde_json::to_string(&json_value)
639 .unwrap_or_else(|_| "null".to_string()));
640 result.data = Some(crate::result::json_to_value(json_value));
641 } else if let Some(data) = &result.data {
642 let json_out = serde_json::to_string(&crate::result::value_to_json(data))
647 .unwrap_or_else(|_| "null".to_string());
648 result.set_out(json_out);
649 } else {
650 let text = result.text_out().into_owned();
652 let json_out = serde_json::to_string(&text)
653 .unwrap_or_else(|_| "null".to_string());
654 result.data = Some(crate::value::Value::String(text));
655 result.set_out(json_out);
656 }
657 result.set_output(None);
659 result
660 }
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
669 fn entry_type_variants() {
670 assert_ne!(EntryType::File, EntryType::Directory);
671 assert_ne!(EntryType::Directory, EntryType::Executable);
672 assert_ne!(EntryType::Executable, EntryType::Symlink);
673 }
674
675 #[test]
676 fn to_json_simple_text() {
677 let output = OutputData::text("hello world");
678 assert_eq!(output.to_json(), serde_json::json!("hello world"));
679 }
680
681 #[test]
682 fn to_json_flat_list() {
683 let output = OutputData::nodes(vec![
684 OutputNode::new("file1"),
685 OutputNode::new("file2"),
686 OutputNode::new("file3"),
687 ]);
688 assert_eq!(output.to_json(), serde_json::json!(["file1", "file2", "file3"]));
689 }
690
691 #[test]
692 fn to_json_table() {
693 let output = OutputData::table(
694 vec!["NAME".into(), "SIZE".into(), "TYPE".into()],
695 vec![
696 OutputNode::new("foo.rs").with_cells(vec!["1024".into(), "file".into()]),
697 OutputNode::new("bar/").with_cells(vec!["4096".into(), "dir".into()]),
698 ],
699 );
700 assert_eq!(output.to_json(), serde_json::json!([
701 {"NAME": "foo.rs", "SIZE": "1024", "TYPE": "file"},
702 {"NAME": "bar/", "SIZE": "4096", "TYPE": "dir"},
703 ]));
704 }
705
706 #[test]
707 fn to_json_table_with_children() {
708 let output = OutputData::table(
713 vec!["NAME".into(), "TYPE".into(), "SIZE".into()],
714 vec![
715 OutputNode::new(".")
716 .with_entry_type(EntryType::Directory)
717 .with_children(vec![
718 OutputNode::new("top.txt").with_cells(vec!["-".into(), "6".into()]),
719 OutputNode::new("a").with_cells(vec!["d".into(), "60".into()]),
720 ]),
721 OutputNode::new("a")
722 .with_entry_type(EntryType::Directory)
723 .with_children(vec![
724 OutputNode::new("mid.txt").with_cells(vec!["-".into(), "3".into()]),
725 ]),
726 ],
727 );
728 assert_eq!(output.to_json(), serde_json::json!([
729 {"NAME": ".", "children": [
730 {"NAME": "top.txt", "TYPE": "-", "SIZE": "6"},
731 {"NAME": "a", "TYPE": "d", "SIZE": "60"},
732 ]},
733 {"NAME": "a", "children": [
734 {"NAME": "mid.txt", "TYPE": "-", "SIZE": "3"},
735 ]},
736 ]));
737 }
738
739 #[test]
740 fn to_json_tree() {
741 let child1 = OutputNode::new("main.rs").with_entry_type(EntryType::File);
742 let child2 = OutputNode::new("utils.rs").with_entry_type(EntryType::File);
743 let subdir = OutputNode::new("lib")
744 .with_entry_type(EntryType::Directory)
745 .with_children(vec![child2]);
746 let root = OutputNode::new("src")
747 .with_entry_type(EntryType::Directory)
748 .with_children(vec![child1, subdir]);
749
750 let output = OutputData::nodes(vec![root]);
751 assert_eq!(output.to_json(), serde_json::json!({
752 "main.rs": null,
753 "lib": {"utils.rs": null},
754 }));
755 }
756
757 #[test]
758 fn to_json_tree_multiple_roots() {
759 let root1 = OutputNode::new("src")
760 .with_entry_type(EntryType::Directory)
761 .with_children(vec![OutputNode::new("main.rs")]);
762 let root2 = OutputNode::new("docs")
763 .with_entry_type(EntryType::Directory)
764 .with_children(vec![OutputNode::new("README.md")]);
765
766 let output = OutputData::nodes(vec![root1, root2]);
767 assert_eq!(output.to_json(), serde_json::json!({
768 "src": {"main.rs": null},
769 "docs": {"README.md": null},
770 }));
771 }
772
773 #[test]
774 fn to_json_empty() {
775 let output = OutputData::new();
776 assert_eq!(output.to_json(), serde_json::json!([]));
777 }
778
779 #[test]
780 fn apply_output_format_clears_sentinel() {
781 let output = OutputData::table(
782 vec!["NAME".into()],
783 vec![OutputNode::new("test")],
784 );
785 let result = ExecResult::with_output(output);
786 assert!(result.has_output(), "before: sentinel present");
787
788 let formatted = apply_output_format(result, OutputFormat::Json);
789 assert!(!formatted.has_output(), "after Json: sentinel cleared");
790 }
791
792 #[test]
793 fn apply_output_format_no_double_encoding() {
794 let output = OutputData::nodes(vec![
795 OutputNode::new("file1"),
796 OutputNode::new("file2"),
797 ]);
798 let result = ExecResult::with_output(output);
799
800 let after_json = apply_output_format(result, OutputFormat::Json);
801 let json_out = after_json.text_out().into_owned();
802 assert!(!after_json.has_output(), "sentinel cleared by Json");
803
804 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
805 assert_eq!(parsed, serde_json::json!(["file1", "file2"]));
806 }
807
808 #[test]
809 fn apply_output_format_populates_data() {
810 let output = OutputData::nodes(vec![
811 OutputNode::new("file1"),
812 OutputNode::new("file2"),
813 ]);
814 let result = ExecResult::with_output(output);
815 assert!(result.data.is_none(), "before: no data on non-text output");
816
817 let formatted = apply_output_format(result, OutputFormat::Json);
818 assert!(formatted.data.is_some(), "after Json: data populated");
819
820 let data = formatted.data.unwrap();
822 assert!(matches!(data, crate::value::Value::Json(_)), "data should be Json variant");
823 if let crate::value::Value::Json(json) = data {
824 assert_eq!(json, serde_json::json!(["file1", "file2"]));
825 }
826 }
827
828 #[test]
829 fn apply_output_format_prefers_structured_data_over_text() {
830 use crate::value::Value;
834 let result = ExecResult::success_with_data("1", Value::Int(1));
835 assert!(!result.has_output(), "no OutputData sentinel on this path");
836
837 let formatted = apply_output_format(result, OutputFormat::Json);
838 let json_out = formatted.text_out().into_owned();
839 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
840 assert_eq!(parsed, serde_json::json!(1), "number, not the string \"1\"");
841 assert_eq!(formatted.data, Some(Value::Int(1)));
843 }
844
845 #[test]
846 fn apply_output_format_compact_json() {
847 let output = OutputData::nodes(vec![
848 OutputNode::new("file1"),
849 OutputNode::new("file2"),
850 ]);
851 let result = ExecResult::with_output(output);
852
853 let formatted = apply_output_format(result, OutputFormat::Json);
854 let out = formatted.text_out();
856 assert!(!out.contains('\n'), "should be compact JSON, got: {}", out);
857 assert_eq!(&*out, r#"["file1","file2"]"#);
858 }
859
860 #[test]
861 fn apply_output_format_emits_json_error_object_on_failure() {
862 let result = ExecResult::failure(2, "grep: unknown flag --bogus-flag");
866 assert!(!result.has_output());
867 assert!(result.text_out().is_empty());
868
869 let formatted = apply_output_format(result, OutputFormat::Json);
870 let out = formatted.text_out().into_owned();
871 let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
872 assert_eq!(
873 parsed,
874 serde_json::json!({"error": "grep: unknown flag --bogus-flag", "code": 2})
875 );
876 assert!(matches!(formatted.data, Some(crate::value::Value::Json(_))));
878 }
879
880 #[test]
881 fn apply_output_format_preserves_structured_data_on_error() {
882 let mut result = ExecResult::failure(2, "rm: confirmation required (latch enabled)");
887 result.data = Some(crate::value::Value::Json(serde_json::json!({
888 "nonce": "a3f7b2c1",
889 "command": "rm",
890 "paths": ["important.dat"],
891 "hint": "rm --confirm=\"a3f7b2c1\" important.dat",
892 "ttl": 60,
893 })));
894
895 let formatted = apply_output_format(result, OutputFormat::Json);
896 let out = formatted.text_out().into_owned();
897 let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
898 assert_eq!(parsed["error"], "rm: confirmation required (latch enabled)");
899 assert_eq!(parsed["code"], 2);
900 assert_eq!(parsed["data"]["nonce"], "a3f7b2c1");
901 assert_eq!(parsed["data"]["ttl"], 60);
902 match &formatted.data {
904 Some(crate::value::Value::Json(v)) => assert_eq!(v["data"]["nonce"], "a3f7b2c1"),
905 other => panic!("expected nested JSON data, got {other:?}"),
906 }
907 }
908
909 #[test]
910 fn apply_output_format_surfaces_latch_even_when_result_has_output() {
911 let mut result = ExecResult::from_output(2, "[1] Latched\n", "");
919 result.set_output(Some(OutputData::text("[1] Latched\n")));
920 assert!(result.has_output(), "precondition: this result DOES have output");
921 result.latch = Some(Box::new(LatchRequest {
922 nonce: "a3f7b2c1".to_string(),
923 command: "rm".to_string(),
924 paths: vec!["precious.txt".to_string()],
925 hint: "rm --confirm=\"a3f7b2c1\" precious.txt".to_string(),
926 tool: "rm".to_string(),
927 argv: vec!["precious.txt".to_string()],
928 ttl: 60,
929 job_id: None,
930 }));
931
932 let formatted = apply_output_format(result, OutputFormat::Json);
933 let out = formatted.text_out().into_owned();
934 let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
935 assert_eq!(
936 parsed["latch"]["nonce"], "a3f7b2c1",
937 "the nonce must be reachable from a latched result that also has \
938 output, not just the no-output error path: {parsed}"
939 );
940 assert_eq!(parsed["latch"]["command"], "rm");
941 assert_eq!(parsed["code"], 2);
942 assert_eq!(parsed["error"], "[1] Latched\n");
945 assert!(
946 formatted.latch_request().is_some(),
947 "the typed latch must survive --json formatting"
948 );
949 }
950
951 #[test]
952 fn apply_output_format_leaves_clean_no_match_empty() {
953 let result = ExecResult::failure(1, "");
956 let formatted = apply_output_format(result, OutputFormat::Json);
957 assert!(formatted.text_out().is_empty());
958 assert!(formatted.data.is_none());
959 }
960
961 #[test]
962 fn apply_output_format_empty_success_stays_empty() {
963 let result = ExecResult::success("");
964 let formatted = apply_output_format(result, OutputFormat::Json);
965 assert!(formatted.text_out().is_empty());
966 assert!(formatted.data.is_none());
967 }
968
969 #[test]
970 fn estimated_byte_size_text_only_node() {
971 let node = OutputNode::text("hello world");
972 assert_eq!(node.estimated_byte_size(), 11);
974 }
975
976 #[test]
977 fn estimated_byte_size_named_node() {
978 let node = OutputNode::new("file.txt");
979 assert_eq!(node.estimated_byte_size(), 8);
980 }
981
982 #[test]
983 fn write_canonical_respects_budget() {
984 let parent = OutputNode::new("root")
985 .with_children(vec![
986 OutputNode::new("aaaa"),
987 OutputNode::new("bbbb"),
988 OutputNode::new("cccc"),
989 ]);
990 let mut buf = Vec::new();
992 let written = parent.write_canonical(&mut buf, 8).unwrap();
993 let output = String::from_utf8(buf).unwrap();
994 assert!(written <= 16, "should respect budget, wrote {} bytes: {}", written, output);
996 assert!(output.starts_with("root"), "should start with root: {}", output);
998 }
999
1000 #[test]
1001 fn into_text_simple() {
1002 let data = OutputData::text("hello");
1003 assert_eq!(data.into_text(), Ok("hello".to_string()));
1004 }
1005
1006 #[test]
1007 fn into_text_non_simple() {
1008 let data = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
1009 assert!(data.into_text().is_err());
1010 }
1011
1012 #[test]
1013 fn into_text_empty() {
1014 let data = OutputData::text("");
1015 assert_eq!(data.into_text(), Ok("".to_string()));
1016 }
1017}