diff --git a/cli/src/console.rs b/cli/src/console.rs index 4e24911..dc07113 100644 --- a/cli/src/console.rs +++ b/cli/src/console.rs @@ -329,7 +329,7 @@ pub async fn run_input(host: Host) -> Result<()> { } }); let mut printer = started.await??; - run_commands(host, lines, move |text| { + run_commands(host, lines, true, move |text| { printer.print(format!("{text}\n"))?; Ok(()) }) @@ -348,7 +348,7 @@ async fn run_plain(host: Host) -> Result<()> { } } }); - run_commands(host, lines, |text| { + run_commands(host, lines, false, |text| { use std::io::Write; writeln!(std::io::stdout(), "{text}")?; Ok(()) @@ -356,10 +356,11 @@ async fn run_plain(host: Host) -> Result<()> { .await } -// Both input modes keep the prompt available and label asynchronous results. +// Both input modes label results. Only terminal input starts concurrent commands. async fn run_commands( host: Host, mut lines: tokio::sync::mpsc::UnboundedReceiver>, + concurrent: bool, mut print: impl FnMut(String) -> Result<()>, ) -> Result<()> { let mut notifications = host.subscribe(); @@ -381,7 +382,7 @@ async fn run_commands( let text = result.unwrap_or_else(|error|format!("error: {error}")); if !text.is_empty() { print(format!("> {line}\n{}",indent(&text)))?; } }, - line = lines.recv(), if input_open => match line { + line = lines.recv(), if input_open && (concurrent || commands.is_empty()) => match line { Some(line) => { let line = line?; if line.trim()=="quit" { print("> quit\n Goodbye.".into())?; return Ok(()); } @@ -446,7 +447,7 @@ mod tests { let mut events = host.subscribe(); let (sender, lines) = tokio::sync::mpsc::unbounded_channel(); let (printed, mut output) = tokio::sync::mpsc::unbounded_channel(); - let runner = tokio::spawn(run_commands(host.clone(), lines, move |text| { + let runner = tokio::spawn(run_commands(host.clone(), lines, true, move |text| { printed.send(text).unwrap(); Ok(()) })); diff --git a/cli/tests/cli.rs b/cli/tests/cli.rs index c4b13a1..2edea04 100644 --- a/cli/tests/cli.rs +++ b/cli/tests/cli.rs @@ -8,7 +8,7 @@ use std::{ time::Duration, }; use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + io::AsyncWriteExt, process::{Child, Command}, }; @@ -307,51 +307,54 @@ async fn console_error_stops_host() { assert!(stopped > 0); } -/// Ps and kill remain available before a slow command returns. +/// Piped calls wait for activation, and EOF or quit preserves the last result. #[tokio::test] -async fn piped_console_accepts_commands_during_a_call() { - let (_folder, world) = world(); - install_probe(&world); - let mut child = binary() - .arg("run") - .arg(world) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap(); - let mut input = child.stdin.take().unwrap(); - let mut output = BufReader::new(child.stdout.take().unwrap()).lines(); - input.write_all(b"probe.busy\n").await.unwrap(); - let mut pid = None; - tokio::time::timeout(Duration::from_secs(15), async { - while pid.is_none() { - input.write_all(b"ps\n").await.unwrap(); - while let Some(line) = output.next_line().await.unwrap() { - if line.contains("probe busy console") { - pid = line.split_whitespace().next().map(str::to_owned); - break; - } - if line.contains("PID PLUGIN METHOD") { - break; - } - } +async fn piped_console_runs_dependent_commands_in_order() { + for ending in ["", "quit\n"] { + let (_folder, world) = world(); + std::fs::copy(probe(), world.join("candidate.wasm")).unwrap(); + std::fs::create_dir_all(world.join("state/probe")).unwrap(); + std::fs::write(world.join("state/probe/discover.json"), r#"{"methods":[]}"#).unwrap(); + let activate = r#"core.activate {"plugin":"notes","wasm":"candidate.wasm","force":true}"#; + let call = r#"notes.echo {"ordered":true}"#; + let mut child = binary() + .arg("run") + .arg(&world) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(format!("{activate}\n{call}\n{ending}").as_bytes()) + .await + .unwrap(); + let output = tokio::time::timeout(Duration::from_secs(20), child.wait_with_output()) + .await + .unwrap() + .unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(!stdout.contains("error:"), "{stdout}"); + assert!(stdout.contains(r#""ordered": true"#), "{stdout}"); + assert!( + stdout.find(&format!("> {activate}")).unwrap() + < stdout.find(&format!("> {call}")).unwrap(), + "{stdout}" + ); + if !ending.is_empty() { + assert!( + stdout.find(r#""ordered": true"#).unwrap() < stdout.find("> quit").unwrap(), + "{stdout}" + ); } - }) - .await - .unwrap(); - input - .write_all(format!("kill {}\n", pid.unwrap()).as_bytes()) - .await - .unwrap(); - tokio::time::timeout(Duration::from_secs(15), async { - while let Some(line) = output.next_line().await.unwrap() { - if line.starts_with("> kill ") { - break; - } - } - }) - .await - .unwrap(); - input.write_all(b"quit\n").await.unwrap(); - assert_eq!(status(&mut child).await.code(), Some(0)); + } } diff --git a/cli/tests/crates/cargo-valid/src/main.rs b/cli/tests/crates/cargo-valid/src/main.rs index aa81b93..73d7a70 100644 --- a/cli/tests/crates/cargo-valid/src/main.rs +++ b/cli/tests/crates/cargo-valid/src/main.rs @@ -99,11 +99,21 @@ mod tests { .unwrap() .map(|entry| entry.unwrap().file_name()) .collect(); - assert!(names.iter().all(|name| { - ["proc", "self", "run", "tmp"] - .iter() - .any(|allowed| name == allowed) - })); + // The runner mounts the package folder at its host path, so the first + // folder of that path also appears in `/` (for example `tmp` or `build`). + let package = env!("CARGO_MANIFEST_DIR") + .trim_start_matches('/') + .split('/') + .next() + .unwrap(); + assert!( + names.iter().all(|name| { + ["proc", "self", "run", package] + .iter() + .any(|allowed| name == allowed) + }), + "{names:?}" + ); for path in ["/proc", "/self", "/run"] { assert!(std::path::Path::new(path).is_dir()); }