use assert_cmd::Command; use predicates::{prelude::predicate, str::PredicateStrExt}; use serial_test::serial; use std::env; use std::path::{Path, PathBuf}; use std::str::from_utf8; const EXAMPLES_DIR: &str = "tests/examples"; fn bat_with_config() -> Command { let mut cmd = Command::cargo_bin("bat").unwrap(); cmd.current_dir("tests/examples"); cmd.env_remove("PAGER"); cmd.env_remove("BAT_PAGER"); cmd.env_remove("BAT_CONFIG_PATH"); cmd.env_remove("BAT_STYLE"); cmd.env_remove("BAT_THEME"); cmd.env_remove("BAT_TABS"); cmd } fn bat() -> Command { let mut cmd = bat_with_config(); cmd.arg("--no-config"); cmd } /// For some tests we want mocked versions of some pagers /// This fn returns the absolute path to the directory with these mocked pagers fn get_mocked_pagers_dir() -> PathBuf { let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("Missing CARGO_MANIFEST_DIR"); Path::new(&cargo_manifest_dir) .join("tests") .join("mocked-pagers") } /// Prepends a directory to the PATH environment variable /// Returns the original value for later restoration fn prepend_dir_to_path_env_var(dir: PathBuf) -> String { // Get current PATH let original_path = env::var("PATH").expect("No PATH?!"); // Add the new dir first let mut split_paths = env::split_paths(&original_path).collect::>(); split_paths.insert(0, dir); // Set PATH with the new dir let new_path = env::join_paths(split_paths).expect("Failed to join paths"); env::set_var("PATH", new_path); // Return the original value for later restoration of it original_path } /// Helper to restore the value of PATH fn restore_path(original_path: String) { env::set_var("PATH", original_path); } /// Allows test to run that require our mocked versions of 'more' and 'most' /// in PATH. Temporarily changes PATH while the test code runs, and then restore it /// to avoid pollution of global state fn with_mocked_versions_of_more_and_most_in_path(actual_test: fn()) { let original_path = prepend_dir_to_path_env_var(get_mocked_pagers_dir()); // Make sure our own variants of 'more' and 'most' is used Command::new("more") .assert() .success() .stdout("I am more\n"); Command::new("most") .assert() .success() .stdout("I am most\n"); // Now run the actual test actual_test(); // Make sure to restore PATH since it is global state restore_path(original_path); } #[test] fn basic() { bat() .arg("test.txt") .assert() .success() .stdout("hello world\n") .stderr(""); } #[test] fn stdin() { bat() .write_stdin("foo\nbar\n") .assert() .success() .stdout("foo\nbar\n"); } #[test] fn concatenate() { bat() .arg("test.txt") .arg("test.txt") .assert() .success() .stdout("hello world\nhello world\n"); } #[test] fn concatenate_stdin() { bat() .arg("test.txt") .arg("-") .arg("test.txt") .write_stdin("stdin\n") .assert() .success() .stdout("hello world\nstdin\nhello world\n"); } #[test] fn concatenate_empty_first() { bat() .arg("empty.txt") .arg("test.txt") .assert() .success() .stdout("hello world\n"); } #[test] fn concatenate_empty_last() { bat() .arg("test.txt") .arg("empty.txt") .assert() .success() .stdout("hello world\n"); } #[test] fn concatenate_empty_both() { bat() .arg("empty.txt") .arg("empty.txt") .assert() .success() .stdout(""); } #[test] fn concatenate_empty_between() { bat() .arg("test.txt") .arg("empty.txt") .arg("test.txt") .assert() .success() .stdout("hello world\nhello world\n"); } #[test] fn concatenate_empty_first_and_last() { bat() .arg("empty.txt") .arg("test.txt") .arg("empty.txt") .assert() .success() .stdout("hello world\n"); } #[test] fn concatenate_single_line() { bat() .arg("single-line.txt") .arg("single-line.txt") .assert() .success() .stdout("Single LineSingle Line"); } #[test] fn concatenate_single_line_empty() { bat() .arg("single-line.txt") .arg("empty.txt") .arg("single-line.txt") .assert() .success() .stdout("Single LineSingle Line"); } #[test] fn line_numbers() { bat() .arg("multiline.txt") .arg("--style=numbers") .arg("--decorations=always") .assert() .success() .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n"); } #[test] fn line_range_2_3() { bat() .arg("multiline.txt") .arg("--line-range=2:3") .assert() .success() .stdout("line 2\nline 3\n"); } #[test] fn line_range_first_two() { bat() .arg("multiline.txt") .arg("--line-range=:2") .assert() .success() .stdout("line 1\nline 2\n"); } #[test] fn line_range_last_3() { bat() .arg("multiline.txt") .arg("--line-range=2:") .assert() .success() .stdout("line 2\nline 3\nline 4\n"); } #[test] fn line_range_multiple() { bat() .arg("multiline.txt") .arg("--line-range=1:2") .arg("--line-range=4:4") .assert() .success() .stdout("line 1\nline 2\nline 4\n"); } #[test] fn tabs_numbers() { bat() .arg("tabs.txt") .arg("--tabs=4") .arg("--style=numbers") .arg("--decorations=always") .assert() .success() .stdout( " 1 1 2 3 4 2 1 ? 3 22 ? 4 333 ? 5 4444 ? 6 55555 ? 7 666666 ? 8 7777777 ? 9 88888888 ? ", ); } #[test] fn tabs_passthrough_wrapped() { bat() .arg("tabs.txt") .arg("--tabs=0") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn tabs_4_wrapped() { bat() .arg("tabs.txt") .arg("--tabs=4") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn tabs_8_wrapped() { bat() .arg("tabs.txt") .arg("--tabs=8") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn tabs_passthrough() { bat() .arg("tabs.txt") .arg("--tabs=0") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn tabs_4() { bat() .arg("tabs.txt") .arg("--tabs=4") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn tabs_8() { bat() .arg("tabs.txt") .arg("--tabs=8") .arg("--style=plain") .arg("--decorations=always") .assert() .success() .stdout( " 1 2 3 4 1 ? 22 ? 333 ? 4444 ? 55555 ? 666666 ? 7777777 ? 88888888 ? ", ); } #[test] fn fail_non_existing() { bat().arg("non-existing-file").assert().failure(); } #[test] fn fail_directory() { bat().arg("sub_directory").assert().failure(); } #[test] fn do_not_exit_directory() { bat() .arg("sub_directory") .arg("test.txt") .assert() .stdout("hello world\n") .failure(); } #[test] fn pager_basic() { bat() .env("PAGER", "echo pager-output") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("pager-output\n").normalize()); } #[test] fn pager_overwrite() { bat() .env("PAGER", "echo other-pager") .env("BAT_PAGER", "echo pager-output") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("pager-output\n").normalize()); } #[test] fn pager_disable() { bat() .env("PAGER", "echo other-pager") .env("BAT_PAGER", "") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("hello world\n").normalize()); } #[test] fn pager_value_bat() { bat() .arg("--pager=bat") .arg("--paging=always") .arg("test.txt") .assert() .failure(); } /// We shall use less instead of most if PAGER is used since PAGER /// is a generic env var #[test] #[serial] // Because of PATH fn pager_most_from_pager_env_var() { with_mocked_versions_of_more_and_most_in_path(|| { // If the output is not "I am most\n" then we know 'most' is not used bat() .env("PAGER", "most") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("hello world\n").normalize()); }); } /// If the bat-specific BAT_PAGER is used, obey the wish of the user /// and allow 'most' #[test] #[serial] // Because of PATH fn pager_most_from_bat_pager_env_var() { with_mocked_versions_of_more_and_most_in_path(|| { bat() .env("BAT_PAGER", "most") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("I am most\n").normalize()); }); } /// Same reasoning with --pager as with BAT_PAGER #[test] #[serial] // Because of PATH fn pager_most_from_pager_arg() { with_mocked_versions_of_more_and_most_in_path(|| { bat() .arg("--paging=always") .arg("--pager=most") .arg("test.txt") .assert() .success() .stdout(predicate::eq("I am most\n").normalize()); }); } /// Make sure the logic for 'most' applies even if an argument is passed #[test] #[serial] // Because of PATH fn pager_most_with_arg() { with_mocked_versions_of_more_and_most_in_path(|| { bat() .env("PAGER", "most -w") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("hello world\n").normalize()); }); } /// Sanity check that 'more' is treated like 'most' #[test] #[serial] // Because of PATH fn pager_more() { with_mocked_versions_of_more_and_most_in_path(|| { bat() .env("PAGER", "more") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("hello world\n").normalize()); }); } #[test] fn alias_pager_disable() { bat() .env("PAGER", "echo other-pager") .arg("-P") .arg("test.txt") .assert() .success() .stdout(predicate::eq("hello world\n").normalize()); } #[test] fn alias_pager_disable_long_overrides_short() { bat() .env("PAGER", "echo pager-output") .arg("-P") .arg("--paging=always") .arg("test.txt") .assert() .success() .stdout(predicate::eq("pager-output\n").normalize()); } #[test] fn config_location_test() { bat_with_config() .env("BAT_CONFIG_PATH", "bat.conf") .arg("--config-file") .assert() .success() .stdout("bat.conf\n"); } #[test] fn config_read_arguments_from_file() { bat_with_config() .env("BAT_CONFIG_PATH", "bat.conf") .arg("test.txt") .assert() .success() .stdout(predicate::eq("dummy-pager-from-config\n").normalize()); } #[test] fn utf16() { // The output will be converted to UTF-8 with a leading UTF-8 BOM bat() .arg("--plain") .arg("--decorations=always") .arg("test_UTF-16LE.txt") .assert() .success() .stdout(std::str::from_utf8(b"\xEF\xBB\xBFhello world\n").unwrap()); } #[test] fn can_print_file_named_cache() { bat_with_config() .arg("cache") .assert() .success() .stdout("test\n") .stderr(""); } #[test] fn can_print_file_named_cache_with_additional_argument() { bat_with_config() .arg("cache") .arg("test.txt") .assert() .success() .stdout("test\nhello world\n") .stderr(""); } #[test] fn can_print_file_starting_with_cache() { bat_with_config() .arg("cache.c") .assert() .success() .stdout("test\n") .stderr(""); } #[test] fn does_not_print_unwanted_file_named_cache() { bat_with_config().arg("cach").assert().failure(); } #[test] fn unicode_wrap() { bat_with_config() .arg("unicode-wrap.txt") .arg("--style=numbers,snip") .arg("--decorations=always") .arg("--terminal-width=40") .assert() .success() .stdout( " 1 ビタミンA ビタミンD ビタミンE ビ タミンK ビタミンB1 ビタミンB2 ナ イアシン パントテン酸 ビタミンB6 ビタミンB12 葉酸 ビオチン ビタ ミンC 2 3 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 고양이 4 5 1 บวก 2 บวก 3 บวก 4 บวก 5 บวก 6 บวก 7 บวก 8 บวก 9 บวก 10 บวก 11 บวก 12 บวก 13 บวก 14 บวก 15 บวก 16 บวก 17 บวก 18 บวก 19 บวก 20 6 7 Бельгия Болгария Чехия Дания Герман ия Эстония Ирландия Греция Испания Франция Хорватия Италия Кипр Латвия Литва Люксембург Венгрия Мальта Ни дерланды Австрия Польша Португалия Румыния Словения Словакия Финляндия Швеция Великобритания ", ); } #[test] fn snip() { bat() .arg("multiline.txt") .arg("--style=numbers,snip") .arg("--decorations=always") .arg("--line-range=1:2") .arg("--line-range=4:") .arg("--terminal-width=80") .assert() .success() .stdout( " 1 line 1 2 line 2 ...─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 8< ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 4 line 4 ", ); } #[test] fn empty_file_leads_to_empty_output_with_grid_enabled() { bat() .arg("empty.txt") .arg("--style=grid") .arg("--decorations=always") .arg("--terminal-width=80") .assert() .success() .stdout(""); } #[test] fn empty_file_leads_to_empty_output_with_rule_enabled() { bat() .arg("empty.txt") .arg("--style=rule") .arg("--decorations=always") .arg("--terminal-width=80") .assert() .success() .stdout(""); } #[test] fn filename_basic() { bat() .arg("test.txt") .arg("--decorations=always") .arg("--style=header") .arg("-r=0:0") .arg("--file-name=foo") .assert() .success() .stdout("File: foo\n") .stderr(""); } #[test] fn filename_binary() { bat() .arg("test.binary") .arg("--decorations=always") .arg("--style=header") .arg("-r=0:0") .arg("--file-name=foo") .assert() .success() .stdout("File: foo \n") .stderr(""); } #[test] fn filename_stdin() { bat() .arg("--decorations=always") .arg("--style=header") .arg("-r=0:0") .arg("-") .write_stdin("stdin\n") .arg("--file-name=foo") .assert() .success() .stdout("File: foo\n") .stderr(""); } #[test] fn filename_stdin_binary() { let vec = vec![0; 1]; bat_with_config() .arg("--decorations=always") .arg("--style=header") .write_stdin(vec) .arg("--file-name=foo") .assert() .success() .stdout("File: foo \n") .stderr(""); } #[test] fn filename_multiple_ok() { bat() .arg("--decorations=always") .arg("--style=header") .arg("-r=0:0") .arg("test.txt") .arg("--file-name=foo") .arg("single-line.txt") .arg("--file-name=bar") .assert() .success() .stdout("File: foo\n\nFile: bar\n") .stderr(""); } #[test] fn filename_multiple_err() { bat() .arg("--decorations=always") .arg("--style=header") .arg("-r=0:0") .arg("test.txt") .arg("--file-name=foo") .arg("single-line.txt") .assert() .failure(); } #[test] fn header_padding() { bat() .arg("--decorations=always") .arg("--style=header") .arg("test.txt") .arg("single-line.txt") .assert() .stdout("File: test.txt\nhello world\n\nFile: single-line.txt\nSingle Line\n") .stderr(""); } #[test] fn header_padding_rule() { bat() .arg("--decorations=always") .arg("--style=header,rule") .arg("--terminal-width=80") .arg("test.txt") .arg("single-line.txt") .assert() .stdout( "File: test.txt hello world ──────────────────────────────────────────────────────────────────────────────── File: single-line.txt Single Line ", ) .stderr(""); } #[test] fn grid_overrides_rule() { bat() .arg("--decorations=always") .arg("--style=grid,rule") .arg("--terminal-width=80") .arg("test.txt") .arg("single-line.txt") .assert() .stdout( "\ ──────────────────────────────────────────────────────────────────────────────── hello world ──────────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────────────── Single Line ──────────────────────────────────────────────────────────────────────────────── ", ) .stderr("\x1b[33m[bat warning]\x1b[0m: Style 'rule' is a subset of style 'grid', 'rule' will not be visible.\n"); } #[cfg(target_os = "linux")] #[test] fn file_with_invalid_utf8_filename() { use std::ffi::OsStr; use std::fs::File; use std::io::Write; use std::os::unix::ffi::OsStrExt; use tempdir::TempDir; let tmp_dir = TempDir::new("bat_test").expect("can create temporary directory"); let file_path = tmp_dir .path() .join(OsStr::from_bytes(b"test-invalid-utf8-\xC3(.rs")); { let mut file = File::create(&file_path).expect("can create temporary file"); writeln!(file, "dummy content").expect("can write to file"); } bat() .arg(file_path.as_os_str()) .assert() .success() .stdout("dummy content\n"); } #[test] fn do_not_panic_regression_tests() { for filename in &[ "issue_28.md", "issue_190.md", "issue_314.hs", "issue_914.rb", "issue_915.vue", ] { bat() .arg("--color=always") .arg(&format!("regression_tests/{}", filename)) .assert() .success(); } } #[test] fn do_not_detect_different_syntax_for_stdin_and_files() { let file = "regression_tests/issue_985.js"; let cmd_for_file = bat() .arg("--color=always") .arg("--map-syntax=*.js:Markdown") .arg(&format!("--file-name={}", file)) .arg("--style=plain") .arg(file) .assert() .success(); let cmd_for_stdin = bat() .arg("--color=always") .arg("--map-syntax=*.js:Markdown") .arg("--style=plain") .arg(&format!("--file-name={}", file)) .pipe_stdin(Path::new(EXAMPLES_DIR).join(file)) .unwrap() .assert() .success(); assert_eq!( from_utf8(&cmd_for_file.get_output().stdout).expect("output is valid utf-8"), from_utf8(&cmd_for_stdin.get_output().stdout).expect("output is valid utf-8") ); } #[test] fn show_all_mode() { bat() .arg("--show-all") .arg("nonprintable.txt") .assert() .stdout("hello·world␊\n├──┤␍␀␇␈␛") .stderr(""); } #[test] fn plain_mode_does_not_add_nonexisting_newline() { bat() .arg("--paging=never") .arg("--color=never") .arg("--decorations=always") .arg("--style=plain") .arg("single-line.txt") .assert() .success() .stdout("Single Line"); } // Regression test for https://github.com/sharkdp/bat/issues/299 #[test] fn grid_for_file_without_newline() { bat() .arg("--paging=never") .arg("--color=never") .arg("--terminal-width=80") .arg("--wrap=never") .arg("--decorations=always") .arg("--style=full") .arg("single-line.txt") .assert() .success() .stdout( "\ ───────┬──────────────────────────────────────────────────────────────────────── │ File: single-line.txt ───────┼──────────────────────────────────────────────────────────────────────── 1 │ Single Line ───────┴──────────────────────────────────────────────────────────────────────── ", ) .stderr(""); }