@@ -1,13 +1,19 @@
1−use assert_cmd::Command;
1+use assert_cmd::assert::OutputAssertExt;
2+use assert_cmd::cargo::CommandCargoExt;
2use predicates::{prelude::predicate, str::PredicateStrExt};
3use predicates::{prelude::predicate, str::PredicateStrExt};
3use serial_test::serial;
4use serial_test::serial;
4use std::env;
5use std::env;
5use std::path::{Path, PathBuf};
7use std::path::{Path, PathBuf};
8+use std::process::{Command, Stdio};
6use std::str::from_utf8;
9use std::str::from_utf8;
10+use std::time::Duration;
8const EXAMPLES_DIR: &str = "tests/examples";
12const 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);
10−fn bat_with_config() -> Command {
16+fn bat_raw_command() -> Command {
11 let mut cmd = Command::cargo_bin("bat").unwrap();
17 let mut cmd = Command::cargo_bin("bat").unwrap();
12 cmd.current_dir("tests/examples");
18 cmd.current_dir("tests/examples");
13 cmd.env_remove("PAGER");
19 cmd.env_remove("PAGER");
@@ -19,7 +25,11 @@ fn bat_with_config() -> Command {
28+fn bat_with_config() -> assert_cmd::Command {
29+ assert_cmd::Command::from_std(bat_raw_command())
22−fn bat() -> Command {
32+fn bat() -> assert_cmd::Command {
23 let mut cmd = bat_with_config();
33 let mut cmd = bat_with_config();
24 cmd.arg("--no-config");
34 cmd.arg("--no-config");
@@ -258,6 +268,80 @@ fn line_range_multiple() {
258 .stdout("line 1\nline 2\nline 4\n");
268 .stdout("line 1\nline 2\nline 4\n");
272+fn basic_io_cycle() {
273+ let file_out = Stdio::from(File::open("tests/examples/cycle.txt").unwrap());
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());
297+fn no_args_doesnt_break() {
299+ use std::os::unix::io::FromRawFd;
302+ use clircle::nix::pty::{openpty, OpenptyResult};
303+ use wait_timeout::ChildExt;
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) };
314+ let mut child = bat_raw_command()
318+ .expect("Failed to start.");
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);
325+ // The child process should be running and waiting for input,
326+ // therefore no exit status should be available.
327+ let exit_status = child
329+ .expect("Error polling exit status, this should never happen.");
330+ assert!(exit_status.is_none());
332+ // Write Ctrl-D (end of transmission) to the pty.
335+ .expect("Couldn't write EOT character to master end.");
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.");
342+ assert!(exit_status.success());
262fn tabs_numbers() {
346fn tabs_numbers() {