diff --git a/.gitignore b/.gitignore index 94835bc44..f32da18cb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ squawk/ # Auto generated treesitter files crates/pgt_treesitter_grammar/src/grammar.json crates/pgt_treesitter_grammar/src/node-types.json -crates/pgt_treesitter_grammar/src/parser.c \ No newline at end of file +crates/pgt_treesitter_grammar/src/parser.c +crates/pgt_treesitter_grammar/src/parser.c.codex-session-id +.codex-session-id diff --git a/Cargo.lock b/Cargo.lock index 8d488c4af..4f7434f16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,15 +1408,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - [[package]] name = "flume" version = "0.11.1" @@ -2018,6 +2009,7 @@ dependencies = [ "linked-hash-map", "once_cell", "pin-project", + "serde", "similar", ] @@ -2393,12 +2385,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "ntest" version = "0.9.3" @@ -2697,6 +2683,7 @@ dependencies = [ "crossbeam", "dashmap 5.5.3", "hdrhistogram", + "insta", "libc", "mimalloc", "path-absolutize", @@ -2709,7 +2696,6 @@ dependencies = [ "pgt_lsp", "pgt_text_edit", "pgt_workspace", - "predicates", "quick-junit", "rayon", "rustc-hash 2.1.0", @@ -3337,10 +3323,7 @@ checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", - "float-cmp", - "normalize-line-endings", "predicates-core", - "regex", ] [[package]] diff --git a/crates/pgt_cli/Cargo.toml b/crates/pgt_cli/Cargo.toml index fb20036fe..0e6093cbb 100644 --- a/crates/pgt_cli/Cargo.toml +++ b/crates/pgt_cli/Cargo.toml @@ -53,7 +53,7 @@ tikv-jemallocator = "0.6.0" [dev-dependencies] assert_cmd = "2.0.16" -predicates = "3.1.3" +insta = { workspace = true, features = ["yaml"] } [lib] doctest = false diff --git a/crates/pgt_cli/tests/assert_check.rs b/crates/pgt_cli/tests/assert_check.rs new file mode 100644 index 000000000..eaf77be33 --- /dev/null +++ b/crates/pgt_cli/tests/assert_check.rs @@ -0,0 +1,219 @@ +use assert_cmd::Command; +use insta::assert_snapshot; +use std::path::Path; +use std::process::ExitStatus; + +const BIN: &str = "postgres-language-server"; +const CONFIG_PATH: &str = "tests/fixtures/postgres-language-server.jsonc"; + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_default_reporter_snapshot() { + assert_snapshot!(run_check(&["tests/fixtures/test.sql"])); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_github_reporter_snapshot() { + assert_snapshot!(run_check(&[ + "--reporter", + "github", + "tests/fixtures/test.sql" + ])); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_gitlab_reporter_snapshot() { + assert_snapshot!(run_check(&[ + "--reporter", + "gitlab", + "tests/fixtures/test.sql" + ])); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_junit_reporter_snapshot() { + assert_snapshot!(run_check(&[ + "--reporter", + "junit", + "tests/fixtures/test.sql" + ])); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_stdin_snapshot() { + assert_snapshot!(run_check_with( + &[ + "--config-path", + CONFIG_PATH, + "--stdin-file-path", + "virtual.sql" + ], + Some("alter tqjable stdin drop column id;\n"), + None + )); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn check_directory_traversal_snapshot() { + let project_dir = Path::new("tests/fixtures/traversal"); + assert_snapshot!(run_check_with( + &["--diagnostic-level", "info", "."], + None, + Some(project_dir) + )); +} + +fn run_check(args: &[&str]) -> String { + let mut full_args = vec!["--config-path", CONFIG_PATH]; + full_args.extend_from_slice(args); + run_check_with(&full_args, None, None) +} + +fn run_check_with(args: &[&str], stdin: Option<&str>, cwd: Option<&Path>) -> String { + let mut cmd = Command::cargo_bin(BIN).expect("binary not built"); + if let Some(dir) = cwd { + cmd.current_dir(dir); + } + if let Some(input) = stdin { + cmd.write_stdin(input); + } + + let mut full_args = vec!["check"]; + full_args.extend_from_slice(args); + let output = cmd.args(full_args).output().expect("failed to run CLI"); + + normalize_output( + output.status, + &String::from_utf8_lossy(&output.stdout), + &String::from_utf8_lossy(&output.stderr), + ) +} + +fn normalize_durations(input: &str) -> String { + let mut content = input.to_owned(); + + let mut search_start = 0; + while let Some(relative) = content[search_start..].find(" in ") { + let start = search_start + relative + 4; + if let Some(end_rel) = content[start..].find('.') { + let end = start + end_rel; + if content[start..end].chars().any(|c| c.is_ascii_digit()) { + content.replace_range(start..end, ""); + search_start = start + "".len() + 1; + continue; + } + search_start = end + 1; + } else { + break; + } + } + + let mut time_search = 0; + while let Some(relative) = content[time_search..].find("time=\"") { + let start = time_search + relative + 6; + if let Some(end_rel) = content[start..].find('"') { + let end = start + end_rel; + if content[start..end].chars().any(|c| c.is_ascii_digit()) { + content.replace_range(start..end, ""); + } + time_search = end + 1; + } else { + break; + } + } + + content +} + +fn normalize_output(status: ExitStatus, stdout: &str, stderr: &str) -> String { + let normalized_stdout = normalize_durations(stdout); + let normalized_stderr = normalize_diagnostics(stderr); + let status_label = if status.success() { + "success" + } else { + "failure" + }; + format!( + "status: {status_label}\nstdout:\n{}\nstderr:\n{}\n", + normalized_stdout.trim_end(), + normalized_stderr.trim_end() + ) +} + +fn normalize_diagnostics(input: &str) -> String { + let normalized = normalize_durations(input); + let mut lines = normalized.lines().peekable(); + let mut diagnostic_sections: Vec = Vec::new(); + let mut other_lines: Vec = Vec::new(); + + while let Some(line) = lines.next() { + if is_path_line(line) { + let mut block = String::from(line); + while let Some(&next) = lines.peek() { + if is_path_line(next) || next.starts_with("check ") { + break; + } + block.push('\n'); + block.push_str(next); + lines.next(); + } + diagnostic_sections.push(trim_trailing_newlines(block)); + } else { + other_lines.push(line.to_string()); + } + } + + diagnostic_sections.sort(); + + let mut parts = Vec::new(); + if !diagnostic_sections.is_empty() { + parts.push(diagnostic_sections.join("\n\n")); + } + + let rest = trim_trailing_newlines(other_lines.join("\n")); + if rest.trim().is_empty() { + parts.join("\n\n") + } else if parts.is_empty() { + rest + } else { + parts.push(rest); + parts.join("\n\n") + } +} + +fn is_path_line(line: &str) -> bool { + let trimmed = line.trim_start(); + (trimmed.starts_with("./") || trimmed.starts_with("tests/")) + && trimmed.contains(':') + && trimmed.contains(" syntax") +} + +fn trim_trailing_newlines(mut value: String) -> String { + while value.ends_with('\n') { + value.pop(); + } + value +} diff --git a/crates/pgt_cli/tests/assert_cmd.rs b/crates/pgt_cli/tests/assert_cmd.rs deleted file mode 100644 index a7ddc17fb..000000000 --- a/crates/pgt_cli/tests/assert_cmd.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::path::PathBuf; - -use assert_cmd::Command; -use predicates::prelude::*; - -#[test] -fn test_cli_check_command() { - let mut cmd = Command::cargo_bin("postgrestools").unwrap(); - - let test_sql_path = PathBuf::from("tests/fixtures/test.sql"); - - cmd.args(["check", test_sql_path.to_str().unwrap()]) - .assert() - .failure() - .stdout(predicate::str::contains("Found 1 error")); -} diff --git a/crates/pgt_cli/tests/commands/check.rs b/crates/pgt_cli/tests/commands/check.rs deleted file mode 100644 index ce0de03da..000000000 --- a/crates/pgt_cli/tests/commands/check.rs +++ /dev/null @@ -1,24 +0,0 @@ -use bpaf::Args; -use std::path::Path; - -use crate::run_cli; -use pgt_console::BufferConsole; -use pgt_fs::MemoryFileSystem; -use pgt_workspace::DynRef; - -#[test] -fn syntax_error() { - let mut fs = MemoryFileSystem::default(); - let mut console = BufferConsole::default(); - - let file_path = Path::new("test.sql"); - fs.insert(file_path.into(), "select 1".as_bytes()); - - let result = run_cli( - DynRef::Borrowed(&mut fs), - &mut console, - Args::from([("check"), file_path.as_os_str().to_str().unwrap()].as_slice()), - ); - - assert!(result.is_ok(), "run_cli returned {result:?}"); -} diff --git a/crates/pgt_cli/tests/commands/mod.rs b/crates/pgt_cli/tests/commands/mod.rs deleted file mode 100644 index be0c6a3ea..000000000 --- a/crates/pgt_cli/tests/commands/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod check; diff --git a/crates/pgt_cli/tests/fixtures/postgres-language-server.jsonc b/crates/pgt_cli/tests/fixtures/postgres-language-server.jsonc new file mode 100644 index 000000000..95c6dfa04 --- /dev/null +++ b/crates/pgt_cli/tests/fixtures/postgres-language-server.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "https://pg-language-server.com/schema/postgres-language-server.schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignore": [] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/crates/pgt_cli/tests/fixtures/traversal/another_bad.sql b/crates/pgt_cli/tests/fixtures/traversal/another_bad.sql new file mode 100644 index 000000000..9dae41f29 --- /dev/null +++ b/crates/pgt_cli/tests/fixtures/traversal/another_bad.sql @@ -0,0 +1 @@ +alter tqjable another drop column id; diff --git a/crates/pgt_cli/tests/fixtures/traversal/bad.sql b/crates/pgt_cli/tests/fixtures/traversal/bad.sql new file mode 100644 index 000000000..6cca86c02 --- /dev/null +++ b/crates/pgt_cli/tests/fixtures/traversal/bad.sql @@ -0,0 +1 @@ +alter tqjable bad drop column id; diff --git a/crates/pgt_cli/tests/fixtures/traversal/postgres-language-server.jsonc b/crates/pgt_cli/tests/fixtures/traversal/postgres-language-server.jsonc new file mode 100644 index 000000000..771c82670 --- /dev/null +++ b/crates/pgt_cli/tests/fixtures/traversal/postgres-language-server.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "https://pg-language-server.com/schema/postgres-language-server.schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/crates/pgt_cli/tests/main.rs b/crates/pgt_cli/tests/main.rs deleted file mode 100644 index 4ab061727..000000000 --- a/crates/pgt_cli/tests/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -mod commands; - -use bpaf::ParseFailure; -use pgt_cli::{CliDiagnostic, CliSession, pgt_command}; -use pgt_console::{Console, ConsoleExt, markup}; -use pgt_fs::FileSystem; -use pgt_workspace::{App, DynRef}; - -/// Create an [App] instance using the provided [FileSystem] and [Console] -/// instance, and using an in-process "remote" instance of the workspace -pub(crate) fn run_cli<'app>( - fs: DynRef<'app, dyn FileSystem>, - console: &'app mut dyn Console, - args: bpaf::Args, -) -> Result<(), CliDiagnostic> { - use pgt_cli::SocketTransport; - use pgt_lsp::ServerFactory; - use pgt_workspace::{WorkspaceRef, workspace}; - use tokio::{ - io::{duplex, split}, - runtime::Runtime, - }; - - let factory = ServerFactory::default(); - let connection = factory.create(None); - - let runtime = Runtime::new().expect("failed to create runtime"); - - let (client, server) = duplex(4096); - let (stdin, stdout) = split(server); - runtime.spawn(connection.accept(stdin, stdout)); - - let (client_read, client_write) = split(client); - let transport = SocketTransport::open(runtime, client_read, client_write); - - let workspace = workspace::client(transport).unwrap(); - let app = App::new(fs, console, WorkspaceRef::Owned(workspace)); - - let mut session = CliSession { app }; - let command = pgt_command().run_inner(args); - match command { - Ok(command) => session.run(command), - Err(failure) => { - if let ParseFailure::Stdout(help, _) = &failure { - let console = &mut session.app.console; - console.log(markup! {{help.to_string()}}); - Ok(()) - } else { - Err(CliDiagnostic::parse_error_bpaf(failure)) - } - } - } -} diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_default_reporter_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_default_reporter_snapshot.snap new file mode 100644 index 000000000..6293dc01f --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_default_reporter_snapshot.snap @@ -0,0 +1,22 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check(&[\"tests/fixtures/test.sql\"])" +snapshot_kind: text +--- +status: failure +stdout: +Checked 1 file in . No fixes applied. +Found 1 error. +stderr: +tests/fixtures/test.sql:1:1 syntax ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Invalid statement: syntax error at or near "tqjable" + + > 1 │ alter tqjable test drop column id; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ + + +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_directory_traversal_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_directory_traversal_snapshot.snap new file mode 100644 index 000000000..cec4b985a --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_directory_traversal_snapshot.snap @@ -0,0 +1,31 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check_with(&[\"--diagnostic-level\", \"info\", \".\"], None, Some(project_dir))" +snapshot_kind: text +--- +status: failure +stdout: +Checked 2 files in . No fixes applied. +Found 2 errors. +stderr: +./another_bad.sql:1:1 syntax ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Invalid statement: syntax error at or near "tqjable" + + > 1 │ alter tqjable another drop column id; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ + + +./bad.sql:1:1 syntax ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Invalid statement: syntax error at or near "tqjable" + + > 1 │ alter tqjable bad drop column id; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ + + +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_github_reporter_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_github_reporter_snapshot.snap new file mode 100644 index 000000000..8410450f9 --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_github_reporter_snapshot.snap @@ -0,0 +1,12 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check(&[\"--reporter\", \"github\", \"tests/fixtures/test.sql\"])" +snapshot_kind: text +--- +status: failure +stdout: +::error title=syntax,file=tests/fixtures/test.sql,line=1,endLine=1,col=1,endColumn=35::Invalid statement: syntax error at or near "tqjable" +stderr: +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_gitlab_reporter_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_gitlab_reporter_snapshot.snap new file mode 100644 index 000000000..66516538f --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_gitlab_reporter_snapshot.snap @@ -0,0 +1,25 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check(&[\"--reporter\", \"gitlab\", \"tests/fixtures/test.sql\"])" +snapshot_kind: text +--- +status: failure +stdout: +[ + { + "description": "Invalid statement: syntax error at or near \"tqjable\"", + "check_name": "syntax", + "fingerprint": "15806099827984337215", + "severity": "critical", + "location": { + "path": "tests/fixtures/test.sql", + "lines": { + "begin": 1 + } + } + } +] +stderr: +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_junit_reporter_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_junit_reporter_snapshot.snap new file mode 100644 index 000000000..243b265cc --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_junit_reporter_snapshot.snap @@ -0,0 +1,19 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check(&[\"--reporter\", \"junit\", \"tests/fixtures/test.sql\"])" +snapshot_kind: text +--- +status: failure +stdout: + + + + + line 0, col 0, Invalid statement: syntax error at or near "tqjable" + + + +stderr: +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. diff --git a/crates/pgt_cli/tests/snapshots/assert_check__check_stdin_snapshot.snap b/crates/pgt_cli/tests/snapshots/assert_check__check_stdin_snapshot.snap new file mode 100644 index 000000000..33f48f349 --- /dev/null +++ b/crates/pgt_cli/tests/snapshots/assert_check__check_stdin_snapshot.snap @@ -0,0 +1,9 @@ +--- +source: crates/pgt_cli/tests/assert_check.rs +expression: "run_check_with(&[\"--config-path\", CONFIG_PATH, \"--stdin-file-path\",\n\"virtual.sql\"], Some(\"alter tqjable stdin drop column id;\\n\"), None)" +snapshot_kind: text +--- +status: success +stdout: +alter tqjable stdin drop column id; +stderr: