// these tests don't work on Windows for some reason #![cfg(not(target_os = "windows"))] use assert_cmd::prelude::*; use assert_fs::prelude::{FileWriteBin, FileWriteStr}; use predicates::reflection::{Case, Parameter, PredicateReflection}; use pomsky::diagnose::DiagnosticCode; use pomsky_bin::{CompilationResult, Diagnostic, Kind, Severity, Span, Timings, Version}; use std::{fmt, process::Command}; pub struct Output { ignore_visual: bool, expected: CompilationResult, } impl Output { pub fn new(expected: CompilationResult) -> Self { Output { ignore_visual: true, expected } } pub fn ignore_visual(mut self, ignore_visual: bool) -> Self { self.ignore_visual = ignore_visual; self } } impl fmt::Display for Output { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", serde_json::to_string_pretty(&self.expected).unwrap()) } } impl predicates::Predicate<[u8]> for Output { fn eval(&self, variable: &[u8]) -> bool { match serde_json::from_slice::(variable) { Ok(mut res) => { res.timings.all = 0; res.timings.tests = 0; if self.ignore_visual { for d in &mut res.diagnostics { d.visual = String::new(); } } self.expected == res } Err(_) => false, } } fn find_case(&self, expected: bool, variable: &[u8]) -> Option { let actual = self.eval(variable); if expected == actual { Some(Case::new(Some(self), actual)) } else { None } } } impl PredicateReflection for Output { fn parameters<'a>(&'a self) -> Box> + 'a> { let params = [Parameter::new("expected output", self)]; Box::new(params.into_iter()) } } const RED: &str = "\u{1b}[31m"; // const RED_BOLD: &str = "\u{1b}[31;1m"; const RESET: &str = "\u{1b}[0m"; const ERROR: &str = "error:\n × "; const ERROR_COLOR: &str = "\u{1b}[31;1merror\u{1b}[0m:\n \u{1b}[31m×\u{1b}[0m "; const USAGE: &str = r#" USAGE: pomsky [OPTIONS] pomsky [OPTIONS] --path command | pomsky [OPTIONS] For more information try `--help` "#; const USAGE_COLOR: &str = "\n\ \u{1b}[33mUSAGE\u{1b}[0m:\n \ pomsky [OPTIONS] \n \ pomsky [OPTIONS] --path \n \ command | pomsky [OPTIONS]\n\ For more information try \u{1b}[36m--help\u{1b}[0m\n"; fn command(args: &[&str]) -> Command { let mut cmd = Command::cargo_bin("pomsky").unwrap(); for arg in args { cmd.arg(arg); } cmd } fn command_color(args: &[&str]) -> Command { let mut cmd = Command::cargo_bin("pomsky").unwrap(); for arg in args { cmd.arg(arg); } cmd.env("FORCE_COLOR", "1"); cmd } #[test] fn version() { let mut cmd = command(&["-V"]); cmd.assert().success().stderr("").stdout(format!("pomsky {}\n", env!("CARGO_PKG_VERSION"))); } #[test] fn help() { let mut cmd = command(&["-h"]); cmd.assert().success().stderr("").stdout(format!(r#"pomsky {} Compile pomsky expressions, a new regular expression language Use `-h` for short descriptions and `--help` for more details. USAGE: pomsky [OPTIONS] pomsky [OPTIONS] --path command | pomsky [OPTIONS] ARGS: Pomsky expression to compile OPTIONS: --allowed-features ... Comma-separated list of allowed features [default: all enabled] -f, --flavor Regex flavor [default: `pcre`] -h, --help Print help information --list shorthands Show all available character class shorthands -n, --no-new-line Don't print a new-line after the output -p, --path File containing the pomsky expression to compile --test Execute unit tests and report failures -V, --version Print version information -W, --warnings Disable certain warnings (disable all with `-W0`) "#, env!("CARGO_PKG_VERSION"))); } #[test] fn unknown_flag() { let mut cmd = command(&["-k", "test/file/doesnt/exist"]); cmd.assert().failure().stderr(format!( "{ERROR}invalid option '-k' USAGE: pomsky [OPTIONS] pomsky [OPTIONS] --path command | pomsky [OPTIONS] For more information try `--help` " )); } #[test] fn file_doesnt_exist() { let mut cmd = command(&["-p", "test/file/doesnt/exist"]); cmd.assert().failure().stderr(format!("{ERROR}No such file or directory (os error 2)\n")); let mut cmd = command_color(&["-p", "test/file/doesnt/exist"]); cmd.assert().failure().stderr(format!("{ERROR_COLOR}No such file or directory (os error 2)\n")); } #[test] fn empty_input() { let mut cmd = command(&[]); cmd.assert().success().stdout("\n").stderr(""); } #[test] fn pretty_print() { let mut cmd = command(&[ "let x = >> 'test'?; x{2} | x{3,5} | . C ![w d s n r t a e f] ['a'-'f'] | range '0'-'7F' base 16 |\ :x() ::x | (!<< 'a')+ | regex '['", "--debug", ]); cmd.assert() .success() .stdout( "(?=(?:test)?){2}|(?=(?:test)?){3,5}|.[\\s\\S]\ [^\\w\\d\\s\\n\\r\\t\\x07\\x1B\\f][a-f]|\ 0|[1-7][0-9a-fA-F]?|[8-9a-fA-F]|(?P)(?:\\1)|(?> "test"{0,1} ); | x{2} | x{3,5} | . C ![word digit space n r t a e f] ['a'-'f'] | range '0'-'7F' base 16 | :x( "" ) ::x | ( !<< "a" ){1,} | regex "[" "#, ); } #[test] fn arg_input() { let mut cmd = command(&[":foo('test')+"]); cmd.assert().success().stdout("(?Ptest)+\n").stderr(""); } #[test] fn arg_input_with_flavor() { let mut cmd = command(&[":foo('test')+", "-f", "js"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); let mut cmd = command(&[":foo('test')+", "-fjs"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); let mut cmd = command(&[":foo('test')+", "-f=js"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); let mut cmd = command(&[":foo('test')+", "--flavor=js"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); let mut cmd = command(&[":foo('test')+", "--flavor", "js"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); let mut cmd = command(&[":foo('test')+", "-f", "jS"]); cmd.assert().success().stdout("(?test)+\n").stderr(""); } #[test] fn invalid_flavor() { let mut cmd = command(&[":foo('test')+", "-f", "jsx"]); cmd.assert().failure().stderr(format!( "{ERROR}'jsx' isn't a valid flavor │ possible values: pcre, python, java, javascript, dotnet, ruby, rust {USAGE}" )); let mut cmd = command_color(&[":foo('test')+", "-f", "jsx"]); cmd.assert().failure().stderr(format!( "{ERROR_COLOR}'jsx' isn't a valid flavor {RED}│{RESET} possible values: pcre, python, java, javascript, dotnet, ruby, rust {USAGE_COLOR}" )); } #[test] fn flavor_used_multiple_times() { let mut cmd = command(&[":foo('test')+", "-fjs", "-f", "rust"]); cmd.assert().failure().stderr(format!( "{ERROR}The argument '--flavor' was provided more than once, but cannot be used │ multiple times {USAGE}" )); } #[test] fn input_and_path() { let mut cmd = command(&[":foo('test')+", "-p", "foo"]); cmd.assert().failure().stderr(format!( "{ERROR}You can only provide an input or a path, but not both {USAGE}" )); } #[test] fn path() { let file = assert_fs::NamedTempFile::new("sample.txt").unwrap(); file.write_str(":foo('test')+").unwrap(); let path = file.path().to_str().unwrap(); let mut cmd = command(&["-p", path]); cmd.assert().success().stdout("(?Ptest)+\n"); let mut cmd = command(&["-p", path, "-fJS"]); cmd.assert().success().stdout("(?test)+\n"); let mut cmd = command(&["-fJS", "-p", path]); cmd.assert().success().stdout("(?test)+\n"); file.write_binary(b"\xC3\x28").unwrap(); let path = file.path().to_str().unwrap(); let mut cmd = command(&["-fJS", "-p", path]); cmd.assert() .failure() .stdout("") .stderr(format!("{ERROR}stream did not contain valid UTF-8\n")); } #[test] fn no_newline() { let mut cmd = command(&[":foo('test')+", "--no-new-line"]); cmd.assert().success().stdout("(?Ptest)+").stderr(""); let mut cmd = command(&[":foo('test')+", "-n"]); cmd.assert().success().stdout("(?Ptest)+").stderr(""); let mut cmd = command(&["-n", ":foo('test')+"]); cmd.assert().success().stdout("(?Ptest)+").stderr(""); let mut cmd = command(&["-n", ":foo('test')+", "-n"]); cmd.assert().failure().stderr(format!( r#"{ERROR}The argument '--no-new-line' was provided more than once, but cannot be │ used multiple times {USAGE}"# )); } #[test] fn disable_warnings() { let mut cmd = command(&["<< 'test'", "-W0", "-fJS"]); cmd.assert().success().stdout("(?<=test)\n").stderr(""); let mut cmd = command(&["<< 'test'", "-Wcompat=0", "-fJS"]); cmd.assert().success().stdout("(?<=test)\n").stderr(""); let mut cmd = command(&["<< 'test'", "-Wdeprecated=0", "-fJS"]); cmd.assert().success().stdout("(?<=test)\n").stderr( r#"warning P0400(compat): ⚠ Lookbehind is not supported in all browsers, e.g. Safari ╭──── 1 │ << 'test' · ────┬──── · ╰── warning originated here ╰──── help: Avoid lookbehind if the regex should work in different browsers "#, ); } #[test] fn wrong_order() { let mut cmd = command(&["-pf", "file.txt", "rust"]); cmd.assert().failure().stderr(format!("{ERROR}unexpected argument \"rust\"\n{USAGE}")); let mut cmd = command(&["-p", "-W0", "file.txt"]); cmd.assert() .failure() .stderr(format!("{ERROR}You can only provide an input or a path, but not both\n{USAGE}")); } #[test] fn specify_features() { let mut cmd = command(&[ ":(.)", "--allowed-features", "variables,boundaries,dot,atomic-groups,lazy-mode,named-groups", ]); cmd.assert().failure().stderr( r#"error P0302(syntax): × Numbered capturing groups aren't supported ╭──── 1 │ :(.) · ──┬─ · ╰── error occurred here ╰──── "#, ); } #[test] fn test_output() { let mut cmd = command(&[ r#"test { match "test"; match in "testicles"; match "test" in "testicles"; reject in "fastest"; match "fanta" in "fantastic"; # wrong match "test" as { 1: "" } in "testament"; match "test" as { 1: "?" } in "test!"; # wrong match "test" as { foo: "!" } in "test!"; # wrong } % 'test' :('!')?"#, "--test=pcre2", ]); cmd.assert().failure().stderr( r#"error P0501(test): × The regex did not find this match within the test string ╭─[5:1] 5 │ reject in "fastest"; 6 │ match "fanta" in "fantastic"; # wrong · ───┬─── · ╰── error occurred here 7 │ match "test" as { 1: "" } in "testament"; ╰──── error P0505(test): × The regex match does not have the expected capture group ╭─[6:1] 6 │ match "fanta" in "fantastic"; # wrong 7 │ match "test" as { 1: "" } in "testament"; · ┬ · ╰── error occurred here 8 │ match "test" as { 1: "?" } in "test!"; # wrong ╰──── error P0503(test): × The regex found a different match in the test string ╭─[7:1] 7 │ match "test" as { 1: "" } in "testament"; 8 │ match "test" as { 1: "?" } in "test!"; # wrong · ───┬── · ╰── error occurred here 9 │ match "test" as { foo: "!" } in "test!"; # wrong ╰──── help: The actual match is "test!" error P0503(test): × The regex found a different match in the test string ╭─[8:1] 8 │ match "test" as { 1: "?" } in "test!"; # wrong 9 │ match "test" as { foo: "!" } in "test!"; # wrong · ───┬── · ╰── error occurred here 10 │ } ╰──── help: The actual match is "test!" "#, ); } #[test] fn json_output() { let mut cmd = command(&["..[word]", "--json"]); cmd.assert() .success() .stdout(Output::new(CompilationResult { version: Version::V1, success: true, output: Some("..\\w".into()), diagnostics: vec![], timings: Timings { all: 0, tests: 0 }, })) .stderr(""); } #[test] fn json_output_warnings() { let mut cmd = command(&[". (<< 'a') (<< 'a')", "--json", "-fJS"]); cmd.assert() .success() .stdout(Output::new(CompilationResult { version: Version::V1, success: true, output: Some(".(?<=a)(?<=a)".into()), diagnostics: vec![ Diagnostic { severity: Severity::Warning, kind: Kind::Compat, code: Some(DiagnosticCode::PossiblyUnsupported), spans: vec![Span { start: 3, end: 9, label: None }], description: "Lookbehind is not supported in all browsers, e.g. Safari".into(), help: vec![ "Avoid lookbehind if the regex should work in different browsers".into() ], fixes: vec![], visual: String::new(), }, Diagnostic { severity: Severity::Warning, kind: Kind::Compat, code: Some(DiagnosticCode::PossiblyUnsupported), spans: vec![Span { start: 12, end: 18, label: None }], description: "Lookbehind is not supported in all browsers, e.g. Safari".into(), help: vec![ "Avoid lookbehind if the regex should work in different browsers".into() ], fixes: vec![], visual: String::new(), }, ], timings: Timings { all: 0, tests: 0 }, })) .stderr(""); } #[test] fn json_output_errors() { let mut cmd = command(&["[.][^test]", "--json"]); cmd.assert() .failure() .stdout( Output::new(CompilationResult { version: Version::V1, success: false, output: None, diagnostics: vec![Diagnostic { severity: Severity::Error, kind: Kind::Deprecated, code: Some(DiagnosticCode::DeprecatedSyntax), spans: vec![Span { start: 1, end: 2, label: None }], description: "`[.]` is deprecated".into(), help: vec!["Use `.` without brackets instead".into()], fixes: vec![], visual: String::from( "error P0105(deprecated): × `[.]` is deprecated ╭──── 1 │ [.][^test] · ┬ · ╰── error occurred here ╰──── help: Use `.` without brackets instead ", ), }], timings: Timings { all: 0, tests: 0 }, }) .ignore_visual(false), ) .stderr(""); }