@@ -1,13 +1,19 @@
1−use assert_cmd::Command;
1+use assert_cmd::assert::OutputAssertExt;
2+use assert_cmd::cargo::CommandCargoExt;
23use predicates::{prelude::predicate, str::PredicateStrExt};
34use serial_test::serial;
45use std::env;
6+use std::fs::File;
57use std::path::{Path, PathBuf};
8+use std::process::{Command, Stdio};
69use std::str::from_utf8;
10+use std::time::Duration;
711
812const EXAMPLES_DIR: &str = "tests/examples";
13+const SAFE_CHILD_PROCESS_CREATION_TIME: Duration = Duration::from_millis(100);
14+const CHILD_WAIT_TIMEOUT: Duration = Duration::from_secs(15);
915
10−fn bat_with_config() -> Command {
16+fn bat_raw_command() -> Command {
1117 let mut cmd = Command::cargo_bin("bat").unwrap();
1218 cmd.current_dir("tests/examples");
1319 cmd.env_remove("PAGER");
@@ -19,7 +25,11 @@ fn bat_with_config() -> Command {
1925 cmd
2026}
2127
22−fn bat() -> Command {
28+fn bat_with_config() -> assert_cmd::Command {
29+ assert_cmd::Command::from_std(bat_raw_command())
30+}
31+
32+fn bat() -> assert_cmd::Command {
2333 let mut cmd = bat_with_config();
2434 cmd.arg("--no-config");
2535 cmd
@@ -258,6 +268,80 @@ fn line_range_multiple() {
258268 .stdout("line 1\nline 2\nline 4\n");
259269}
260270
271+#[test]
272+fn basic_io_cycle() {
273+ let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
274+ bat_raw_command()
275+ .arg("test.txt")
276+ .arg("cycle.txt")
277+ .stdout(file_out)
278+ .assert()
279+ .failure();
280+}
281+
282+#[test]
283+fn stdin_to_stdout_cycle() {
284+ let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
285+ let file_in = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
286+ bat_raw_command()
287+ .stdin(file_in)
288+ .arg("test.txt")
289+ .arg("-")
290+ .stdout(file_out)
291+ .assert()
292+ .failure();
293+}
294+
295+#[cfg(unix)]
296+#[test]
297+fn no_args_doesnt_break() {
298+ use std::io::Write;
299+ use std::os::unix::io::FromRawFd;
300+ use std::thread;
301+
302+ use clircle::nix::pty::{openpty, OpenptyResult};
303+ use wait_timeout::ChildExt;
304+
305+ // To simulate bat getting started from the shell, a process is created with stdin and stdout
306+ // as the slave end of a pseudo terminal. Although both point to the same "file", bat should
307+ // not exit, because in this case it is safe to read and write to the same fd, which is why
308+ // this test exists.
309+ let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty.");
310+ let mut master = unsafe { File::from_raw_fd(master) };
311+ let stdin = unsafe { Stdio::from_raw_fd(slave) };
312+ let stdout = unsafe { Stdio::from_raw_fd(slave) };
313+
314+ let mut child = bat_raw_command()
315+ .stdin(stdin)
316+ .stdout(stdout)
317+ .spawn()
318+ .expect("Failed to start.");
319+
320+ // Some time for the child process to start and to make sure, that we can poll the exit status.
321+ // Although this waiting period is not necessary, it is best to keep it in and be absolutely
322+ // sure, that the try_wait does not error later.
323+ thread::sleep(SAFE_CHILD_PROCESS_CREATION_TIME);
324+
325+ // The child process should be running and waiting for input,
326+ // therefore no exit status should be available.
327+ let exit_status = child
328+ .try_wait()
329+ .expect("Error polling exit status, this should never happen.");
330+ assert!(exit_status.is_none());
331+
332+ // Write Ctrl-D (end of transmission) to the pty.
333+ master
334+ .write_all(&[0x04])
335+ .expect("Couldn't write EOT character to master end.");
336+
337+ let exit_status = child
338+ .wait_timeout(CHILD_WAIT_TIMEOUT)
339+ .expect("Error polling exit status, this should never happen.")
340+ .expect("Exit status not set, but the child should have exited already.");
341+
342+ assert!(exit_status.success());
343+}
344+
261345#[test]
262346fn tabs_numbers() {
263347 bat()