on master help sign in
Merge remote-tracking branch 'origin/master' into fix-1063
commit46487b201ff0f4cdcbfdea9d328cbac6d29a22d1 authorMartin Nordholts <enselic@gmail.com> dateWed 6 Jan 2021, 14:30 (6 years ago) parent478233fb600f62
6 files changed+143−3
Cargo.lock+26 Cargo.toml+2 src/controller.rs+14 src/input.rs+14 tests/examples/cycle.txt tests/integration_tests.rs+87−3
Cargo.lock+26
@@ -99,6 +99,7 @@ dependencies = [
9999 "assert_cmd",
100100 "atty",
101101 "clap",
102+ "clircle",
102103 "console",
103104 "content_inspector",
104105 "dirs",
@@ -117,6 +118,7 @@ dependencies = [
117118 "syntect",
118119 "tempdir",
119120 "unicode-width",
121+ "wait-timeout",
120122 "wild",
121123]
122124
@@ -251,6 +253,18 @@ dependencies = [
251253 "vec_map",
252254]
253255
256+[[package]]
257+name = "clircle"
258+version = "0.2.0"
259+source = "registry+https://github.com/rust-lang/crates.io-index"
260+checksum = "e27a01e782190a8314e65cc94274d9bbcd52e05a9d15b437fe2b31259b854b0d"
261+dependencies = [
262+ "cfg-if 1.0.0",
263+ "nix",
264+ "serde",
265+ "winapi",
266+]
267+
254268[[package]]
255269name = "console"
256270version = "0.14.0"
@@ -691,6 +705,18 @@ dependencies = [
691705 "autocfg",
692706]
693707
708+[[package]]
709+name = "nix"
710+version = "0.19.1"
711+source = "registry+https://github.com/rust-lang/crates.io-index"
712+checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
713+dependencies = [
714+ "bitflags",
715+ "cc",
716+ "cfg-if 1.0.0",
717+ "libc",
718+]
719+
694720[[package]]
695721name = "normalize-line-endings"
696722version = "0.3.0"
Cargo.toml+2
@@ -50,6 +50,7 @@ serde = { version = "1.0", features = ["derive"] }
5050serde_yaml = "0.8"
5151semver = "0.11"
5252path_abs = { version = "0.5", default-features = false }
53+clircle = "0.2.0"
5354
5455[dependencies.git2]
5556version = "0.13"
@@ -76,6 +77,7 @@ tempdir = "0.3"
7677assert_cmd = "1.0.2"
7778serial_test = "0.5.1"
7879predicates = "1.0.6"
80+wait-timeout = "0.2.0"
7981
8082[build-dependencies]
8183clap = { version = "2.33", optional = true }
src/controller.rs+14
@@ -1,3 +1,4 @@
1+use std::convert::TryFrom;
12use std::io::{self, Write};
23
34use crate::assets::HighlightingAssets;
@@ -66,6 +67,14 @@ impl<'b> Controller<'b> {
6667 }
6768
6869 let attached_to_pager = output_type.is_pager();
70+ let rw_cycle_detected = !attached_to_pager && {
71+ let identifiers: Vec<_> = inputs
72+ .iter()
73+ .flat_map(clircle::Identifier::try_from)
74+ .collect();
75+ clircle::stdout_among_inputs(&identifiers)
76+ };
77+
6978 let writer = output_type.handle()?;
7079 let mut no_errors: bool = true;
7180
@@ -78,6 +87,11 @@ impl<'b> Controller<'b> {
7887 }
7988 };
8089
90+ if rw_cycle_detected {
91+ print_error(&"The output file is also an input!".into(), writer);
92+ return Ok(false);
93+ }
94+
8195 for (index, input) in inputs.into_iter().enumerate() {
8296 match input.open(io::stdin().lock()) {
8397 Err(error) => {
src/input.rs+14
@@ -1,6 +1,8 @@
1+use std::convert::TryFrom;
12use std::ffi::{OsStr, OsString};
23use std::fs::File;
34use std::io::{self, BufRead, BufReader, Read};
5+use std::path::Path;
46
57use content_inspector::{self, ContentType};
68
@@ -191,6 +193,18 @@ impl<'a> Input<'a> {
191193 }
192194}
193195
196+impl TryFrom<&'_ Input<'_>> for clircle::Identifier {
197+ type Error = ();
198+
199+ fn try_from(input: &Input) -> std::result::Result<clircle::Identifier, ()> {
200+ match input.kind {
201+ InputKind::OrdinaryFile(ref path) => Self::try_from(Path::new(path)).map_err(|_| ()),
202+ InputKind::StdIn => Self::try_from(clircle::Stdio::Stdin).map_err(|_| ()),
203+ InputKind::CustomReader(_) => Err(()),
204+ }
205+ }
206+}
207+
194208pub(crate) struct InputReader<'a> {
195209 inner: Box<dyn BufRead + 'a>,
196210 pub(crate) first_line: Vec<u8>,
tests/examples/cycle.txtnew file

No changes to the content.

tests/integration_tests.rs+87−3
@@ -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()