use super::*;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
fn parent(port: u16, finish: &str) -> String {
let descendant = format!(
"import socket,sys;s=socket.create_connection(('127.0.0.1',{port}));s.sendall(b'ready');sys.stdout.buffer.write(b'ready');sys.stdout.buffer.flush();s.recv(1);s.sendall(b'alive')"
);
format!(
"import os,subprocess,sys;child=subprocess.Popen([sys.executable,'-c',{descendant:?}],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL);ready=child.stdout.read(5);assert ready==b'ready',repr(ready);print('synthetic-cli-token',flush=True);{finish}"
)
}
async fn ready(listener: &TcpListener) -> TcpStream {
let (mut stream, _) = tokio::time::timeout(DEADLINE, listener.accept())
.await
.expect("descendant must connect")
.expect("accept descendant");
let mut bytes = [0; 5];
tokio::time::timeout(DEADLINE, stream.read_exact(&mut bytes))
.await
.expect("descendant must report readiness")
.expect("read descendant readiness");
assert_eq!(&bytes, b"ready");
stream
}
#[tokio::test]
async fn private_capture_reaps_descendants_on_completion_and_timeout() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut control = tokio::process::Command::new("python3");
control.args(["-c", &parent(listener.local_addr().unwrap().port(), "pass")]);
let (status, mut stream) = tokio::join!(control.status(), ready(&listener));
assert!(status.unwrap().success());
assert_eq!(
stream.try_read(&mut [0; 1]).unwrap_err().kind(),
std::io::ErrorKind::WouldBlock,
"unmanaged child must still hold its connection"
);
stream.write_all(b"x").await.unwrap();
let mut survived = Vec::new();
tokio::time::timeout(DEADLINE, stream.read_to_end(&mut survived))
.await
.expect("released control must finish")
.unwrap();
assert_eq!(survived, b"alive");
for (finish, refuses) in [
("pass", false),
("sys.stdout.close();os.close(1);child.wait()", false),
("child.wait()", true),
] {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut command = tokio::process::Command::new("python3");
command.args(["-c", &parent(listener.local_addr().unwrap().port(), finish)]);
let (result, mut stream) = tokio::join!(capture(command, DEADLINE), ready(&listener));
assert_eq!(result.is_err(), refuses, "parent: {finish}");
let mut survivor = Vec::new();
let closed = tokio::time::timeout(DEADLINE, stream.read_to_end(&mut survivor))
.await
.expect("capture must close the reached descendant's connection");
assert!(
match &closed {
Ok(_) => true,
Err(error) => error.kind() == std::io::ErrorKind::ConnectionReset,
},
"descendant connection must end with EOF or reset: {closed:?}"
);
assert!(survivor.is_empty(), "captured child must not survive");
}
}