1use std::borrow::Cow;
6use std::collections::BTreeMap;
7
8use crate::output::OutputData;
9use crate::value::Value;
10
11#[derive(Debug, Clone, PartialEq)]
19pub enum OutputPayload {
20 Text(String),
22 Bytes(Vec<u8>),
25}
26
27impl Default for OutputPayload {
28 fn default() -> Self {
29 OutputPayload::Text(String::new())
30 }
31}
32
33impl serde::Serialize for OutputPayload {
34 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
35 match self {
36 OutputPayload::Text(t) => serializer.serialize_str(t),
38 OutputPayload::Bytes(b) => crate::bytes::bytes_to_envelope(b).serialize(serializer),
39 }
40 }
41}
42
43impl<'de> serde::Deserialize<'de> for OutputPayload {
44 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45 let v = serde_json::Value::deserialize(deserializer)?;
46 match v {
47 serde_json::Value::String(s) => Ok(OutputPayload::Text(s)),
48 other => match crate::bytes::envelope_to_bytes(&other) {
49 Some(b) => Ok(OutputPayload::Bytes(b)),
50 None => Err(serde::de::Error::custom(
51 "ExecResult.out: expected a string or a base64 bytes envelope",
52 )),
53 },
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct BinaryNotText {
61 pub len: usize,
63}
64
65impl std::fmt::Display for BinaryNotText {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "output is binary ({} bytes), not text — pipe through base64/xxd or redirect to a file",
70 self.len
71 )
72 }
73}
74
75impl std::error::Error for BinaryNotText {}
76
77#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
92#[non_exhaustive]
93pub struct ExecResult {
94 pub code: i64,
96 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102 pub data_is_value: bool,
103 out: OutputPayload,
105 pub err: String,
114 pub data: Option<Value>,
117 output: Option<Box<OutputData>>,
127 pub did_spill: bool,
135 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
148 pub fault: bool,
149 #[serde(skip_serializing_if = "Option::is_none")]
152 pub original_code: Option<i64>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub content_type: Option<String>,
157 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
162 pub baggage: BTreeMap<String, String>,
163}
164
165impl ExecResult {
166 pub fn terminate_diagnostic(message: impl Into<String>) -> String {
174 let mut message = message.into();
175 if message.is_empty() {
176 return message;
177 }
178 let terminated = message.trim_end_matches('\n').len();
179 message.truncate(terminated);
180 message.push('\n');
181 message
182 }
183
184 pub fn success(out: impl Into<String>) -> Self {
186 Self {
187 code: 0,
188 data_is_value: false,
189 out: OutputPayload::Text(out.into()),
190 err: String::new(),
191 data: None,
192 output: None,
193 did_spill: false,
194 fault: false,
195 original_code: None,
196 content_type: None,
197 baggage: BTreeMap::new(),
198 }
199 }
200
201 pub fn with_output(output: OutputData) -> Self {
206 match output.into_text() {
209 Ok(text) => Self::success(text),
210 Err(output) => Self {
211 code: 0,
212 data_is_value: false,
213 out: OutputPayload::Text(String::new()),
214 err: String::new(),
215 data: None,
216 output: Some(Box::new(output)),
217 did_spill: false,
218 fault: false,
219 original_code: None,
220 content_type: None,
221 baggage: BTreeMap::new(),
222 },
223 }
224 }
225
226 pub fn success_bytes(bytes: Vec<u8>) -> Self {
228 let mut r = Self::success("");
229 r.out = OutputPayload::Bytes(bytes);
230 r
231 }
232
233 pub fn success_text_or_bytes(bytes: Vec<u8>) -> Self {
239 match String::from_utf8(bytes) {
240 Ok(text) => Self::success(text),
241 Err(e) => Self::success_bytes(e.into_bytes()),
242 }
243 }
244
245 pub fn success_data(data: Value) -> Self {
247 let out = value_to_json(&data).to_string();
248 Self {
249 code: 0,
250 data_is_value: false,
251 out: OutputPayload::Text(out),
252 err: String::new(),
253 data: Some(data),
254 output: None,
255 did_spill: false,
256 fault: false,
257 original_code: None,
258 content_type: None,
259 baggage: BTreeMap::new(),
260 }
261 }
262
263 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
272 Self {
273 code: 0,
274 data_is_value: false,
275 out: OutputPayload::Text(out.into()),
276 err: String::new(),
277 data: Some(data),
278 output: None,
279 did_spill: false,
280 fault: false,
281 original_code: None,
282 content_type: None,
283 baggage: BTreeMap::new(),
284 }
285 }
286
287 #[must_use]
294 pub fn into_fault(mut self) -> Self {
295 self.fault = true;
296 self
297 }
298
299 pub fn failure(code: i64, err: impl Into<String>) -> Self {
300 Self {
301 code,
302 data_is_value: false,
303 out: OutputPayload::Text(String::new()),
304 err: Self::terminate_diagnostic(err),
305 data: None,
306 output: None,
307 did_spill: false,
308 fault: false,
309 original_code: None,
310 content_type: None,
311 baggage: BTreeMap::new(),
312 }
313 }
314
315 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
321 Self {
322 data_is_value: false,
323 code,
324 out: OutputPayload::Text(stdout.into()),
325 err: stderr.into(),
326 data: None,
327 output: None,
328 did_spill: false,
329 fault: false,
330 original_code: None,
331 content_type: None,
332 baggage: BTreeMap::new(),
333 }
334 }
335
336 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
341 Self {
342 code: 0,
343 data_is_value: false,
344 out: OutputPayload::Text(text.into()),
345 err: String::new(),
346 data: None,
347 output: Some(Box::new(output)),
348 did_spill: false,
349 fault: false,
350 original_code: None,
351 content_type: None,
352 baggage: BTreeMap::new(),
353 }
354 }
355
356 pub fn from_parts(
358 code: i64,
359 out: String,
360 err: String,
361 data: Option<Value>,
362 ) -> Self {
363 Self {
364 data_is_value: false,
365 code,
366 out: OutputPayload::Text(out),
367 err: Self::terminate_diagnostic(err),
368 data,
369 output: None,
370 did_spill: false,
371 fault: false,
372 original_code: None,
373 content_type: None,
374 baggage: BTreeMap::new(),
375 }
376 }
377
378 pub fn with_code(mut self, code: i64) -> Self {
380 self.code = code;
381 self
382 }
383
384 pub fn text_out(&self) -> Cow<'_, str> {
399 match &self.out {
400 OutputPayload::Text(s) if !s.is_empty() => Cow::Borrowed(s),
401 OutputPayload::Bytes(b) => match std::str::from_utf8(b) {
402 Ok(s) => Cow::Borrowed(s),
403 Err(_) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
404 },
405 _ => match self.output {
407 Some(ref output) => Cow::Owned(output.to_canonical_string()),
408 None => Cow::Borrowed(""),
409 },
410 }
411 }
412
413 pub fn try_text_out(&self) -> Result<Cow<'_, str>, BinaryNotText> {
418 match &self.out {
419 OutputPayload::Bytes(b) => std::str::from_utf8(b)
420 .map(Cow::Borrowed)
421 .map_err(|_| BinaryNotText { len: b.len() }),
422 _ => Ok(self.text_out()),
423 }
424 }
425
426 pub fn out_bytes(&self) -> Option<&[u8]> {
428 match &self.out {
429 OutputPayload::Bytes(b) => Some(b),
430 OutputPayload::Text(_) => None,
431 }
432 }
433
434 pub fn is_bytes(&self) -> bool {
436 matches!(self.out, OutputPayload::Bytes(_))
437 }
438
439 pub fn output(&self) -> Option<&OutputData> {
441 self.output.as_deref()
442 }
443
444 pub fn has_output(&self) -> bool {
446 self.output.is_some()
447 }
448
449 pub fn set_out(&mut self, s: String) {
453 self.out = OutputPayload::Text(s);
454 }
455
456 pub fn set_out_bytes(&mut self, b: Vec<u8>) {
458 self.out = OutputPayload::Bytes(b);
459 }
460
461 pub fn push_out(&mut self, s: &str) {
463 match &mut self.out {
464 OutputPayload::Text(t) => t.push_str(s),
465 OutputPayload::Bytes(b) => b.extend_from_slice(s.as_bytes()),
466 }
467 }
468
469 pub fn clear_out(&mut self) {
471 self.out = OutputPayload::Text(String::new());
472 }
473
474 pub fn clear_stdout(&mut self) {
487 self.out = OutputPayload::Text(String::new());
488 self.output = None;
489 self.data = None;
490 }
491
492 pub fn set_output(&mut self, o: Option<OutputData>) {
494 self.output = o.map(Box::new);
495 }
496
497 pub fn take_output(&mut self) -> Option<OutputData> {
499 self.output.take().map(|o| *o)
500 }
501
502 pub fn materialize(&mut self) {
505 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
506 if let Some(ref output) = self.output {
507 self.out = OutputPayload::Text(output.to_canonical_string());
508 }
509 }
510 self.output = None;
511 }
512
513 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
516 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
517 self.output.take().map(|o| *o)
518 } else {
519 None
520 }
521 }
522
523 pub fn ok(&self) -> bool {
525 self.code == 0
526 }
527
528 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
530 self.content_type = Some(ct.into());
531 self
532 }
533
534}
535
536pub fn json_to_value(json: serde_json::Value) -> Value {
541 match json {
542 serde_json::Value::Null => Value::Null,
543 serde_json::Value::Bool(b) => Value::Bool(b),
544 serde_json::Value::Number(n) => {
545 if let Some(i) = n.as_i64() {
546 Value::Int(i)
547 } else if let Some(f) = n.as_f64() {
548 Value::Float(f)
549 } else {
550 Value::String(n.to_string())
551 }
552 }
553 serde_json::Value::String(s) => Value::String(s),
554 serde_json::Value::Object(_) => match crate::bytes::envelope_to_bytes(&json) {
557 Some(bytes) => Value::Bytes(bytes),
558 None => Value::Json(json),
559 },
560 serde_json::Value::Array(_) => Value::Json(json),
561 }
562}
563
564pub fn json_to_value_no_envelope(json: serde_json::Value) -> Value {
574 match json {
575 serde_json::Value::Null => Value::Null,
576 serde_json::Value::Bool(b) => Value::Bool(b),
577 serde_json::Value::Number(n) => {
578 if let Some(i) = n.as_i64() {
579 Value::Int(i)
580 } else if let Some(f) = n.as_f64() {
581 Value::Float(f)
582 } else {
583 Value::String(n.to_string())
584 }
585 }
586 serde_json::Value::String(s) => Value::String(s),
587 serde_json::Value::Object(_) | serde_json::Value::Array(_) => Value::Json(json),
590 }
591}
592
593pub fn value_to_json(value: &Value) -> serde_json::Value {
595 match value {
596 Value::Null => serde_json::Value::Null,
597 Value::Bool(b) => serde_json::Value::Bool(*b),
598 Value::Int(i) => serde_json::Value::Number((*i).into()),
599 Value::Float(f) => {
600 serde_json::Number::from_f64(*f)
604 .map(serde_json::Value::Number)
605 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
606 }
607 Value::String(s) => serde_json::Value::String(s.clone()),
608 Value::Json(json) => json.clone(),
609 Value::Bytes(data) => crate::bytes::bytes_to_envelope(data),
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn success_creates_ok_result() {
619 let result = ExecResult::success("hello world");
620 assert!(result.ok());
621 assert_eq!(result.code, 0);
622 assert_eq!(&*result.text_out(),"hello world");
623 assert!(result.err.is_empty());
624 }
625
626 #[test]
627 fn value_to_json_finite_float_is_number() {
628 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
629 }
630
631 #[test]
632 fn value_to_json_non_finite_float_serializes_to_string() {
633 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
635 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
636 assert_eq!(
637 value_to_json(&Value::Float(f64::NEG_INFINITY)),
638 serde_json::json!("-inf")
639 );
640 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
642 }
643
644 #[test]
645 fn failure_creates_non_ok_result() {
646 let result = ExecResult::failure(1, "command not found");
647 assert!(!result.ok());
648 assert_eq!(result.code, 1);
649 assert_eq!(result.err, "command not found\n");
650 }
651
652 #[test]
653 fn failure_ends_exactly_one_newline() {
654 assert_eq!(ExecResult::failure(1, "msg").err, "msg\n");
655 assert_eq!(ExecResult::failure(1, "msg\n").err, "msg\n");
656 assert_eq!(ExecResult::failure(1, "msg\n\n\n").err, "msg\n");
657 }
658
659 #[test]
660 fn failure_empty_message_stays_empty() {
661 assert_eq!(ExecResult::failure(1, "").err, "");
664 }
665
666 #[test]
667 fn terminate_diagnostic_keeps_multiline_interior_newlines() {
668 let msg = "wc: a: not found\nwc: b: not found";
669 assert_eq!(
670 ExecResult::terminate_diagnostic(msg),
671 "wc: a: not found\nwc: b: not found\n"
672 );
673 }
674
675 #[test]
676 fn from_parts_ends_the_diagnostic_line() {
677 assert_eq!(ExecResult::from_parts(1, String::new(), "boom".into(), None).err, "boom\n");
678 assert_eq!(ExecResult::from_parts(1, String::new(), String::new(), None).err, "");
679 }
680
681 #[test]
682 fn from_output_keeps_external_stderr_byte_faithful() {
683 let result = ExecResult::from_output(1, "", "died mid-line");
686 assert_eq!(result.err, "died mid-line");
687 }
688
689 #[test]
690 fn success_does_not_sniff_json_stdout() {
691 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
694 assert!(result.data.is_none());
695 assert_eq!(&*result.text_out(),r#"{"count": 42, "items": ["a", "b"]}"#);
696 }
697
698 #[test]
699 fn from_output_does_not_sniff_json_stdout() {
700 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
701 assert!(result.data.is_none());
702 assert_eq!(&*result.text_out(),"[1, 2, 3]");
703 }
704
705 #[test]
706 fn non_json_stdout_has_no_data() {
707 let result = ExecResult::success("just plain text");
708 assert!(result.data.is_none());
709 }
710
711 #[test]
712 fn success_data_creates_result_with_value() {
713 let value = Value::String("test data".into());
714 let result = ExecResult::success_data(value.clone());
715 assert!(result.ok());
716 assert_eq!(result.data, Some(value));
717 }
718
719 #[test]
720 fn did_spill_defaults_to_false() {
721 assert!(!ExecResult::success("hi").did_spill);
722 assert!(!ExecResult::failure(1, "err").did_spill);
723 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
724 }
725
726 #[test]
727 fn did_spill_is_serialized() {
728 let mut result = ExecResult::success("hi");
729 result.did_spill = true;
730 let json = serde_json::to_string(&result).unwrap();
731 assert!(json.contains("\"did_spill\":true"));
732 }
733
734 #[test]
735 fn original_code_omitted_when_none() {
736 let result = ExecResult::success("hi");
737 let json = serde_json::to_string(&result).unwrap();
738 assert!(!json.contains("original_code"));
739 }
740
741 #[test]
742 fn original_code_present_when_set() {
743 let mut result = ExecResult::success("hi");
744 result.original_code = Some(0);
745 let json = serde_json::to_string(&result).unwrap();
746 assert!(json.contains("\"original_code\":0"));
747 }
748
749 #[test]
750 fn default_is_empty_success() {
751 let result = ExecResult::default();
752 assert!(result.ok());
753 assert!(result.text_out().is_empty());
754 assert!(result.data.is_none());
755 assert!(result.content_type.is_none());
756 assert!(result.baggage.is_empty());
757 }
758
759 #[test]
760 fn from_parts_creates_result() {
761 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
762 assert_eq!(result.code, 42);
763 assert_eq!(&*result.text_out(),"out");
764 assert_eq!(result.err, "err\n");
765 assert!(result.data.is_none());
766 assert!(result.output.is_none());
767 }
768
769 #[test]
770 fn with_code_sets_code() {
771 let result = ExecResult::success("hi").with_code(42);
772 assert_eq!(result.code, 42);
773 assert_eq!(&*result.text_out(),"hi");
774 }
775
776 #[test]
777 fn output_getter() {
778 use crate::output::{OutputData, OutputNode};
779 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
781 let result = ExecResult::with_output(nodes);
782 assert!(result.output().is_some());
783 assert!(result.has_output());
784
785 let text_result = ExecResult::with_output(OutputData::text("test"));
787 assert!(!text_result.has_output());
788 assert_eq!(&*text_result.text_out(), "test");
789
790 let plain = ExecResult::success("text");
791 assert!(plain.output().is_none());
792 assert!(!plain.has_output());
793 }
794
795 #[test]
796 fn set_out_and_push_out_and_clear_out() {
797 let mut result = ExecResult::success("");
798 result.set_out("hello".into());
799 assert_eq!(&*result.text_out(),"hello");
800 result.push_out(" world");
801 assert_eq!(&*result.text_out(),"hello world");
802 result.clear_out();
803 assert!(result.text_out().is_empty());
804 }
805
806 #[test]
807 fn set_output_and_take_output() {
808 use crate::output::OutputData;
809 let mut result = ExecResult::success("");
810 assert!(result.take_output().is_none());
811
812 result.set_output(Some(OutputData::text("data")));
813 assert!(result.has_output());
814
815 let taken = result.take_output();
816 assert!(taken.is_some());
817 assert!(!result.has_output());
818 }
819
820 #[test]
821 fn materialize_populates_out_from_output() {
822 use crate::output::{OutputData, OutputNode};
823 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
825 let mut result = ExecResult::with_output(nodes);
826 assert!(matches!(&result.out, OutputPayload::Text(s) if s.is_empty()));
829 assert!(result.has_output());
830 result.materialize();
831 assert_eq!(&*result.text_out(),"a\nb");
832 assert!(result.output.is_none());
833 }
834
835 #[test]
836 fn value_bytes_round_trips_through_envelope() {
837 let v = Value::Bytes(vec![0u8, 1, 2, 255, 128]);
838 let json = value_to_json(&v);
839 assert_eq!(json["_type"], "bytes");
840 assert_eq!(json["len"], 5);
841 assert_eq!(json_to_value(json), v);
843 let obj = serde_json::json!({"name": "amy"});
845 assert!(matches!(json_to_value(obj), Value::Json(_)));
846 }
847
848 #[test]
849 fn no_envelope_never_decodes_bytes() {
850 let envelope = crate::bytes::bytes_to_envelope(&[1u8, 2, 3]);
854 assert!(matches!(json_to_value(envelope.clone()), Value::Bytes(_)));
856 assert!(matches!(
858 json_to_value_no_envelope(envelope),
859 Value::Json(serde_json::Value::Object(_))
860 ));
861 }
862
863 #[test]
864 fn no_envelope_shares_unwrap_law_for_scalars() {
865 assert_eq!(json_to_value_no_envelope(serde_json::json!(42)), Value::Int(42));
867 assert_eq!(json_to_value_no_envelope(serde_json::json!(1.5)), Value::Float(1.5));
868 assert_eq!(json_to_value_no_envelope(serde_json::json!(true)), Value::Bool(true));
869 assert_eq!(json_to_value_no_envelope(serde_json::json!("hi")), Value::String("hi".into()));
870 assert_eq!(json_to_value_no_envelope(serde_json::json!(null)), Value::Null);
871 assert!(matches!(
872 json_to_value_no_envelope(serde_json::json!([1, 2])),
873 Value::Json(serde_json::Value::Array(_))
874 ));
875 }
876
877 #[test]
878 fn output_payload_text_serializes_as_bare_string() {
879 let r = ExecResult::success("hello");
882 let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap();
883 assert_eq!(json["out"], "hello");
884 let back: ExecResult = serde_json::from_value(json).unwrap();
886 assert_eq!(&*back.text_out(), "hello");
887 assert!(!back.is_bytes());
888 }
889
890 #[test]
891 fn success_bytes_carries_binary_and_round_trips() {
892 let r = ExecResult::success_bytes(vec![0u8, 159, 146, 150]); assert!(r.is_bytes());
894 assert_eq!(r.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
895 assert!(r.try_text_out().is_err());
897 assert!(r.text_out().contains('\u{fffd}'));
899 let json: serde_json::Value = serde_json::to_value(&r).unwrap();
901 assert_eq!(json["out"]["_type"], "bytes");
902 let back: ExecResult = serde_json::from_value(json).unwrap();
903 assert_eq!(back.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
904 }
905
906 #[test]
907 fn valid_utf8_bytes_coerce_to_text() {
908 let r = ExecResult::success_bytes(b"plain text".to_vec());
909 assert!(r.is_bytes());
910 assert_eq!(r.try_text_out().unwrap(), "plain text");
911 assert_eq!(&*r.text_out(), "plain text");
912 }
913
914 #[test]
915 fn materialize_preserves_existing_out() {
916 use crate::output::OutputData;
917 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
918 result.materialize();
919 assert_eq!(&*result.text_out(),"custom");
920 }
921
922 #[test]
923 fn take_output_for_stream_when_out_empty() {
924 use crate::output::{OutputData, OutputNode};
925 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
927 let mut result = ExecResult::with_output(nodes);
928 let taken = result.take_output_for_stream();
929 assert!(taken.is_some());
930 assert!(!result.has_output());
931 }
932
933 #[test]
934 fn with_output_simple_text_populates_out_directly() {
935 use crate::output::OutputData;
936 let result = ExecResult::with_output(OutputData::text("hello"));
937 assert!(!result.has_output());
939 assert_eq!(&*result.text_out(), "hello");
940 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
942 assert!(json_result.data.is_none());
943 }
944
945
946 #[test]
947 fn clear_stdout_drops_data() {
948 let mut result = ExecResult::success_data(Value::Json(serde_json::json!([1, 2, 3])));
950 result.clear_stdout();
951 assert!(result.data.is_none(), "data-plane .data must clear");
952 }
953
954 #[test]
955 fn take_output_for_stream_when_out_populated() {
956 use crate::output::OutputData;
957 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
958 let taken = result.take_output_for_stream();
959 assert!(taken.is_none());
960 assert!(result.has_output()); }
962}