1use crate::batch::{FrameStream, FramedLine, batch_frames};
37use crate::{
38 BatchOptions, BatchStream, Executor, ExecutorError, Input, LineStream, Output, StreamExt,
39};
40use alloc::boxed::Box;
41use tokio::io::AsyncRead;
42
43pub type JsonlStream = BatchStream<ExecutorError>;
64
65pub fn jsonl_lines(reader: impl AsyncRead + Send + Unpin + 'static) -> LineStream {
76 Box::pin(jsonl_frames(reader).map(|line| line.map(FramedLine::into_line)))
77}
78
79pub(crate) fn jsonl_frames(reader: impl AsyncRead + Send + Unpin + 'static) -> FrameStream {
82 Box::pin(asimov_flow::jsonl::jsonl_frames(reader).map(|line| line.map_err(Into::into)))
83}
84
85pub fn jsonl_batches(
90 reader: impl AsyncRead + Send + Unpin + 'static,
91 options: BatchOptions,
92) -> JsonlStream {
93 batch_frames(jsonl_frames(reader), options)
94}
95
96#[cfg(test)]
97mod framing_tests {
98 use super::*;
99 use crate::{Bytes, JsonlBatch, JsonlLine};
100 use alloc::{vec, vec::Vec};
101 use std::io::Cursor;
102 use tokio::io::AsyncWriteExt;
103
104 #[tokio::test]
105 async fn frames_a_read_buffer_into_shared_lines_without_payload_copies() {
106 let mut stream = jsonl_lines(Cursor::new(b"a\nb\r\nc\nlast"));
107 let mut lines = Vec::new();
108 for expected in [b"a\n".as_slice(), b"b\r\n", b"c\n"] {
109 let line = stream.next().await.unwrap().unwrap();
110 assert!(matches!(line, JsonlLine::Shared(_)));
111 assert_eq!(line.as_bytes(), expected);
112 lines.push(line);
113 }
114 assert_eq!(
115 lines[1].as_bytes().as_ptr(),
116 lines[0].as_bytes().as_ptr().wrapping_add(2)
117 );
118 let batch = JsonlBatch::new(lines);
119 assert!(batch.as_contiguous_bytes().is_none());
121 let last = stream.next().await.unwrap().unwrap();
122 assert_eq!(last.as_bytes(), b"last");
123 assert!(!last.is_terminated());
124 assert!(stream.next().await.is_none());
125 drop(stream);
127 assert_eq!(
128 batch.lines().collect::<Vec<_>>(),
129 [b"a\n".as_slice(), b"b\r\n", b"c\n"]
130 );
131 }
132
133 #[tokio::test]
134 async fn reader_batches_preserve_contiguous_metadata_across_batch_boundaries() {
135 let mut batches = jsonl_batches(
136 Cursor::new(b"a\nb\nc\nd\ntail"),
137 BatchOptions::new(2, 1024, core::time::Duration::from_secs(1)).unwrap(),
138 );
139 let first = batches.next().await.unwrap().unwrap();
140 let second = batches.next().await.unwrap().unwrap();
141 let tail = batches.next().await.unwrap().unwrap();
142 assert!(batches.next().await.is_none());
143 drop(batches);
144 assert_eq!(first.as_contiguous_bytes(), Some(b"a\nb\n".as_slice()));
145 assert_eq!(second.as_contiguous_bytes(), Some(b"c\nd\n".as_slice()));
146 assert_eq!(
147 second.as_contiguous_bytes().unwrap().as_ptr(),
148 first
149 .as_contiguous_bytes()
150 .unwrap()
151 .as_ptr()
152 .wrapping_add(4)
153 );
154 let pointer = first.as_contiguous_bytes().unwrap().as_ptr();
155 let lines = first.into_lines();
156 assert_eq!(lines[0].as_bytes().as_ptr(), pointer);
157 assert_eq!(lines[1].as_bytes().as_ptr(), pointer.wrapping_add(2));
158 assert_eq!(tail.lines().collect::<Vec<_>>(), [b"tail".as_slice()]);
159 assert!(tail.as_contiguous_bytes().is_none());
160 }
161
162 #[tokio::test]
163 async fn byte_threshold_lookahead_preserves_batch_views() {
164 let mut batches = jsonl_batches(
165 Cursor::new(b"a\nb\nc\n"),
166 BatchOptions::new(10, 3, core::time::Duration::from_secs(1)).unwrap(),
167 );
168 let first = batches.next().await.unwrap().unwrap();
169 let second = batches.next().await.unwrap().unwrap();
170 let third = batches.next().await.unwrap().unwrap();
171 assert!(batches.next().await.is_none());
172 assert_eq!(first.as_contiguous_bytes(), Some(b"a\n".as_slice()));
173 assert_eq!(second.as_contiguous_bytes(), Some(b"b\n".as_slice()));
174 assert_eq!(third.as_contiguous_bytes(), Some(b"c\n".as_slice()));
175 let first_pointer = first.as_contiguous_bytes().unwrap().as_ptr();
176 assert_eq!(
177 second.as_contiguous_bytes().unwrap().as_ptr(),
178 first_pointer.wrapping_add(2)
179 );
180 assert_eq!(
181 third.as_contiguous_bytes().unwrap().as_ptr(),
182 first_pointer.wrapping_add(4)
183 );
184 }
185
186 #[tokio::test]
187 async fn frames_split_crlf_long_lines_and_unterminated_tails() {
188 let (reader, mut writer) = tokio::io::duplex(7);
189 let mut expected = vec![b'x'; 128 * 1024];
190 expected.extend_from_slice(b"\r\n");
191 let data = expected.clone();
192 let writing = tokio::spawn(async move {
193 writer.write_all(&data[..data.len() - 1]).await.unwrap();
194 tokio::task::yield_now().await;
195 writer.write_all(b"\ntail").await.unwrap();
196 });
197 let mut lines = jsonl_lines(reader);
198 assert_eq!(lines.next().await.unwrap().unwrap().as_bytes(), expected);
199 assert_eq!(lines.next().await.unwrap().unwrap().as_bytes(), b"tail");
200 assert!(lines.next().await.is_none());
201 writing.await.unwrap();
202 }
203
204 #[tokio::test]
205 async fn every_chunk_boundary_preserves_framing() {
206 let data = Bytes::from_static(b"\n{}\r\n\xff\nend");
207 for width in 1..=data.len() {
208 let (reader, mut writer) = tokio::io::duplex(width);
209 let source = data.clone();
210 let writing = tokio::spawn(async move {
211 for chunk in source.chunks(width) {
212 writer.write_all(chunk).await.unwrap();
213 tokio::task::yield_now().await;
214 }
215 });
216 let mut lines = jsonl_lines(reader);
217 for expected in [b"\n".as_slice(), b"{}\r\n", b"\xff\n", b"end"] {
218 let line = lines.next().await.unwrap().unwrap();
219 assert_eq!(line.as_bytes(), expected);
220 assert!(JsonlLine::shared(line.into_bytes()).is_ok());
221 }
222 assert!(lines.next().await.is_none());
223 writing.await.unwrap();
224 }
225 }
226}
227
228impl Executor {
229 pub async fn execute_jsonl(&mut self) -> Result<JsonlStream, ExecutorError> {
240 self.execute_jsonl_with_input(&mut Input::Ignored).await
241 }
242
243 pub async fn execute_jsonl_with_input(
264 &mut self,
265 input: &mut Input,
266 ) -> Result<JsonlStream, ExecutorError> {
267 self.execute_jsonl_with_io(input, &mut Output::Captured)
268 .await
269 }
270
271 pub async fn execute_jsonl_with_output(
276 &mut self,
277 output: &mut Output,
278 ) -> Result<JsonlStream, ExecutorError> {
279 self.execute_jsonl_with_io(&mut Input::Ignored, output)
280 .await
281 }
282
283 pub async fn execute_jsonl_with_io(
300 &mut self,
301 input: &mut Input,
302 output: &mut Output,
303 ) -> Result<JsonlStream, ExecutorError> {
304 let options = self.batch_options();
305 let frames = self.execute_jsonl_frames_with_io(input, output).await?;
306 Ok(batch_frames(frames, options))
307 }
308
309 pub(crate) async fn execute_jsonl_frames_with_io(
312 &mut self,
313 input: &mut Input,
314 output: &mut Output,
315 ) -> Result<FrameStream, ExecutorError> {
316 let mut process = self.spawn().await?;
317 let stdout = if matches!(output, Output::Captured) {
318 process.stdout.take()
319 } else {
320 None
321 };
322 let mut input = core::mem::replace(input, Input::Ignored);
323 let mut destination = output.take_for_stream();
324 Ok(Box::pin(async_stream::try_stream! {
325 let completion = async move {
326 crate::executor::communicate(process, &mut input, &mut destination).await
327 };
328 tokio::pin!(completion);
329 let mut output = None;
330 if let Some(stdout) = stdout {
331 let mut lines = jsonl_frames(stdout);
332 loop {
333 let line = tokio::select! {
334 result = &mut completion, if output.is_none() => {
335 output = Some(result);
336 continue;
337 },
338 line = lines.next() => line,
339 };
340 match line {
341 Some(line) => yield line?,
342 None => break,
343 }
344 }
345 }
346 let output = match output {
347 Some(output) => output?,
348 None => completion.await?,
349 };
350 output.into_result()?;
351 }))
352 }
353}
354
355#[cfg(all(test, unix))]
356mod tests {
357 use super::*;
358 use crate::*;
359 use alloc::{string::ToString, vec, vec::Vec};
360 use std::{io::Cursor, process::Stdio, time::Duration};
361 use tokio::time::timeout;
362
363 #[tokio::test]
364 async fn graph_producers_stream_before_exit() {
365 let script = "printf '{}\\n'; exec sleep 30";
366 let args = vec!["-c".to_string(), script.to_string()];
367 macro_rules! check {
368 ($runner:expr) => {{
369 let mut stream = timeout(Duration::from_secs(5), $runner.execute())
370 .await
371 .expect("spawn must not wait for exit")
372 .unwrap();
373 let line = timeout(Duration::from_secs(5), stream.next())
374 .await
375 .expect("output must not wait for exit");
376 assert_eq!(
377 line.unwrap().unwrap().lines().collect::<Vec<_>>(),
378 vec![b"{}\n".to_vec()]
379 );
380 }};
381 }
382 check!(Adapter::new(
383 "/bin/sh",
384 Input::Ignored,
385 GraphOutput::Captured,
386 AdapterOptions {
387 other: args.clone(),
388 ..Default::default()
389 },
390 ));
391 check!(Emitter::new(
392 "/bin/sh",
393 GraphOutput::Captured,
394 EmitterOptions {
395 other: args.clone(),
396 ..Default::default()
397 },
398 ));
399 check!(Fetcher::new(
400 "/bin/sh",
401 script,
402 GraphOutput::Captured,
403 FetcherOptions {
404 other: vec!["-c".into()],
405 ..Default::default()
406 },
407 ));
408 check!(Reader::new(
409 "/bin/sh",
410 Input::Ignored,
411 GraphOutput::Captured,
412 ReaderOptions {
413 other: args.clone(),
414 ..Default::default()
415 },
416 ));
417 check!(Matcher::new(
418 "/bin/sh",
419 Input::Ignored,
420 GraphOutput::Captured,
421 MatcherOptions {
422 other: args.clone(),
423 ..Default::default()
424 },
425 ));
426 check!(Reasoner::new(
427 "/bin/sh",
428 Input::Ignored,
429 GraphOutput::Captured,
430 ReasonerOptions {
431 other: args,
432 ..Default::default()
433 },
434 ));
435 }
436
437 #[tokio::test]
438 async fn output_is_available_while_input_is_still_open() {
439 timeout(Duration::from_secs(5), async {
440 let source = Box::pin(async_stream::try_stream! {
441 yield JsonlBatch::try_from(vec![b"{}".to_vec()]).unwrap();
442 core::future::pending::<()>().await;
443 });
444 let mut stream = Matcher::new(
445 "/bin/cat",
446 GraphInput::Jsonl(source),
447 GraphOutput::Captured,
448 MatcherOptions::default(),
449 )
450 .execute()
451 .await
452 .unwrap();
453 assert_eq!(
454 stream
455 .next()
456 .await
457 .unwrap()
458 .unwrap()
459 .lines()
460 .collect::<Vec<_>>(),
461 vec![b"{}\n".to_vec()]
462 );
463 })
464 .await
465 .expect("output must not wait for input EOF");
466 }
467
468 #[tokio::test]
469 async fn dropping_stream_terminates_child() {
470 let mut stream = Emitter::new(
471 "/bin/sh",
472 GraphOutput::Captured,
473 EmitterOptions::builder()
474 .other("-c")
475 .other("printf '%s\\n' $$; exec sleep 30")
476 .build(),
477 )
478 .execute()
479 .await
480 .unwrap();
481 let pid = timeout(Duration::from_secs(5), stream.next())
482 .await
483 .unwrap()
484 .unwrap()
485 .unwrap();
486 let pid = std::str::from_utf8(pid.lines().next().unwrap())
487 .unwrap()
488 .trim();
489 drop(stream);
490 timeout(Duration::from_secs(5), async {
491 while tokio::process::Command::new("/bin/kill")
492 .args(["-0", pid])
493 .stdout(Stdio::null())
494 .stderr(Stdio::null())
495 .status()
496 .await
497 .unwrap()
498 .success()
499 {
500 tokio::time::sleep(Duration::from_millis(10)).await;
501 }
502 })
503 .await
504 .expect("dropping the stream must terminate the child");
505 }
506
507 #[tokio::test]
508 async fn feeds_large_graph_while_draining_both_output_pipes() {
509 timeout(Duration::from_secs(10), async {
510 let line = [b"\"".as_slice(), &vec![b'x'; 1024], b"\"\n"].concat();
511 let source_line = line.clone();
512 let source = Box::pin(async_stream::try_stream! {
513 for _ in 0..64 {
514 yield JsonlBatch::try_from(vec![source_line.clone(); 64]).unwrap();
515 }
516 });
517 let mut stream = Reasoner::new(
518 "/bin/sh",
519 GraphInput::Jsonl(source),
520 GraphOutput::Captured,
521 ReasonerOptions::builder().other("-c").other(
522 "i=0; while [ $i -lt 10000 ]; do printf 'diagnostic\\n' >&2; i=$((i + 1)); done; cat",
523 ).build(),
524 ).execute().await.unwrap();
525 let mut count = 0;
526 while let Some(actual) = stream.next().await {
527 let batch = actual.unwrap();
528 for actual in batch.lines() {
529 assert_eq!(actual, line);
530 }
531 count += batch.len();
532 }
533 assert_eq!(count, 4096);
534 }).await.expect("full-duplex graph transport must not deadlock");
535 }
536
537 #[tokio::test]
538 async fn composes_graph_producers_and_consumers() {
539 let source = Emitter::new(
540 "/bin/sh",
541 GraphOutput::Captured,
542 EmitterOptions::builder()
543 .other("-c")
544 .other("printf '{}\\r\\n{\"last\":true}'")
545 .build(),
546 )
547 .execute()
548 .await
549 .unwrap();
550 let mut stream = Matcher::new(
551 "/bin/cat",
552 GraphInput::Jsonl(source),
553 GraphOutput::Captured,
554 MatcherOptions::default(),
555 )
556 .execute()
557 .await
558 .map(flatten_batches)
559 .unwrap();
560 assert_eq!(stream.next().await.unwrap().unwrap().as_bytes(), b"{}\r\n");
561 assert_eq!(
562 stream.next().await.unwrap().unwrap().as_bytes(),
563 b"{\"last\":true}\n"
564 );
565 assert!(stream.next().await.is_none());
566 }
567
568 #[tokio::test]
569 async fn propagates_upstream_failure() {
570 timeout(Duration::from_secs(5), async {
571 let source = Emitter::new(
572 "/bin/sh",
573 GraphOutput::Captured,
574 EmitterOptions::builder()
575 .other("-c")
576 .other("printf 'upstream failed' >&2; exit 65")
577 .build(),
578 )
579 .execute()
580 .await
581 .unwrap();
582 let mut stream = Matcher::new(
583 "/bin/cat",
584 GraphInput::Jsonl(source),
585 GraphOutput::Captured,
586 MatcherOptions::default(),
587 )
588 .execute()
589 .await
590 .unwrap();
591 match stream.next().await.unwrap().unwrap_err() {
592 ExecutorError::Failure(code, Some(stderr)) => {
593 assert_eq!(code.code(), Some(65));
594 assert_eq!(stderr, "upstream failed");
595 },
596 error => panic!("unexpected error: {error}"),
597 }
598 assert!(stream.next().await.is_none());
599 })
600 .await
601 .expect("source failure must terminate the downstream child");
602 }
603
604 #[tokio::test]
605 async fn early_exit_cancels_idle_input() {
606 timeout(Duration::from_secs(5), async {
607 let input = Input::Jsonl(Box::pin(crate::stream::pending()));
608 let mut stream = Matcher::new(
609 "/bin/sh",
610 input,
611 GraphOutput::Captured,
612 MatcherOptions::builder()
613 .other("-c")
614 .other("exit 65")
615 .build(),
616 )
617 .execute()
618 .await
619 .unwrap();
620 assert!(matches!(
621 stream.next().await,
622 Some(Err(ExecutorError::Failure(_, _)))
623 ));
624 assert!(stream.next().await.is_none());
625 })
626 .await
627 .expect("child exit must not wait for an idle input stream");
628 }
629
630 #[tokio::test]
631 async fn writer_and_indexer_consume_jsonl() {
632 let input = || {
633 GraphInput::Jsonl(Box::pin(crate::stream::iter([
634 Ok(JsonlBatch::default()),
635 Ok(JsonlBatch::try_from(vec![b"{}".to_vec(), b"[]\r\n".to_vec()]).unwrap()),
636 ])))
637 };
638 let script = "test \"$(cat)\" = \"$(printf '{}\\n[]\\r')\" || exit 65";
639 Indexer::new(
640 "/bin/sh",
641 input(),
642 IndexerOptions::builder().other("-c").other(script).build(),
643 )
644 .execute()
645 .await
646 .unwrap();
647 let output = Writer::new(
648 "/bin/sh",
649 input(),
650 AnyOutput::Captured,
651 WriterOptions::builder()
652 .other("-c")
653 .other(alloc::format!("{script}; printf '\\000\\377'"))
654 .build(),
655 )
656 .execute()
657 .await
658 .unwrap();
659 assert_eq!(output.into_inner(), b"\x00\xff");
660 }
661
662 #[tokio::test]
663 async fn byte_graph_input_is_framed_and_spawn_failure_preserves_input() {
664 let mut input = GraphInput::AsyncRead(Box::new(Cursor::new(b"{}".to_vec()))).into_jsonl();
665 let mut missing = Executor::new("/this-jsonl-program-does-not-exist");
666 missing.command().stdin(input.as_stdio());
667 assert!(matches!(
668 missing.execute_jsonl_with_input(&mut input).await,
669 Err(ExecutorError::MissingProgram(_))
670 ));
671 let mut executor = Executor::new("/bin/cat");
672 executor
673 .command()
674 .stdin(input.as_stdio())
675 .stdout(Stdio::piped());
676 let mut stream = executor.execute_jsonl_with_input(&mut input).await.unwrap();
677 assert!(matches!(input, Input::Ignored));
678 assert_eq!(
679 stream
680 .next()
681 .await
682 .unwrap()
683 .unwrap()
684 .lines()
685 .collect::<Vec<_>>(),
686 vec![b"{}\n".to_vec()]
687 );
688 assert!(stream.next().await.is_none());
689 }
690}