diff --git a/Cargo.lock b/Cargo.lock index 77a3582..3432a9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,7 +233,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fb" -version = "0.2.2" +version = "0.2.3" dependencies = [ "dirs", "gumdrop", diff --git a/Cargo.toml b/Cargo.toml index 1137f45..ac2a217 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fb" -version = "0.2.2" +version = "0.2.3" edition = "2021" license = "Apache-2.0" diff --git a/src/main.rs b/src/main.rs index c07d990..45df4b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ async fn main() -> Result<(), Box> { eprintln!("Press Ctrl+D to exit."); } let mut buffer: String = String::new(); + let mut has_error = false; loop { let prompt = if !is_tty { // No prompt when stdout is not a terminal (e.g., piped) @@ -115,7 +116,9 @@ async fn main() -> Result<(), Box> { rl.append_history(&history_path)?; for q in queries { - let _ = query(&mut context, q).await; + if query(&mut context, q).await.is_err() { + has_error = true; + } } buffer.clear(); @@ -135,7 +138,9 @@ async fn main() -> Result<(), Box> { for q in queries { rl.add_history_entry(q.trim())?; rl.append_history(&history_path)?; - let _ = query(&mut context, q).await; + if query(&mut context, q).await.is_err() { + has_error = true; + } } } } @@ -144,6 +149,7 @@ async fn main() -> Result<(), Box> { } Err(err) => { eprintln!("Error: {:?}", err); + has_error = true; break; } } @@ -153,5 +159,9 @@ async fn main() -> Result<(), Box> { eprintln!("Saved history to {:?}", history_path) } - Ok(()) + if has_error { + Err("One or more queries failed".into()) + } else { + Ok(()) + } } diff --git a/src/query.rs b/src/query.rs index efc7a9a..d13043b 100644 --- a/src/query.rs +++ b/src/query.rs @@ -130,6 +130,8 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box< })) }; + let mut query_failed = false; + select! { _ = signal::ctrl_c() => { finish_token.cancel(); @@ -139,6 +141,7 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box< if !context.args.concise { eprintln!("^C"); } + query_failed = true; } response = async_resp => { let elapsed = start.elapsed(); @@ -189,8 +192,15 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box< eprintln!("URL: {}", context.url); } + let status = resp.status(); + let body = resp.text().await?; + // on stdout, on purpose - println!("{}", resp.text().await?); + print!("{}", body); + + if !status.is_success() { + query_failed = true; + } } Err(error) => { if context.args.verbose { @@ -198,6 +208,7 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box< } else { eprintln!("Failed to send the request: {}", error.to_string()); } + query_failed = true; }, }; @@ -212,7 +223,11 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box< } }; - Ok(()) + if query_failed { + Err("Query failed".into()) + } else { + Ok(()) + } } #[derive(Parser)] @@ -247,16 +262,18 @@ mod tests { use crate::args::get_args; #[tokio::test] - async fn test_query() { + async fn test_query_connection_error() { let mut args = get_args().unwrap(); args.host = "localhost:8123".to_string(); args.database = "test_db".to_string(); + args.concise = true; // suppress output let mut context = Context::new(args); let query_text = "select 42".to_string(); + // Query should fail when server is not available let result = query(&mut context, query_text).await; - assert!(result.is_ok()); + assert!(result.is_err()); } #[test] diff --git a/tests/cli.rs b/tests/cli.rs index 5ec85c7..35166a4 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -26,7 +26,7 @@ fn test_set_format() { // First set format to TSV let (success, stdout, _) = run_fb(&["--core", "--concise", "-f", "TabSeparatedWithNamesAndTypes", "SELECT 42;"]); assert!(success); - assert_eq!(stdout, "?column?\nint\n42\n\n"); + assert_eq!(stdout, "?column?\nint\n42\n"); } #[test] @@ -88,16 +88,17 @@ fn test_params_escaping() { assert!(output.status.success()); let mut lines = stdout.lines(); + // First query result assert_eq!(lines.next().unwrap(), "?column?"); - lines.next(); + assert_eq!(lines.next().unwrap(), "text"); assert_eq!(lines.next().unwrap(), "a=}&"); - lines.next(); + // Second query result assert_eq!(lines.next().unwrap(), "?column?"); - lines.next(); + assert_eq!(lines.next().unwrap(), "text"); assert_eq!(lines.next().unwrap(), "a=}&"); - lines.next(); + // Third query result assert_eq!(lines.next().unwrap(), "?column?"); - lines.next(); + assert_eq!(lines.next().unwrap(), "text"); assert_eq!(lines.next().unwrap(), "b=}&"); } @@ -235,3 +236,55 @@ fn test_json_output_fully_parseable() { ); } } + +#[test] +fn test_exit_code_on_connection_error() { + // Test that exit code is non-zero when server is not available + let (success, _, stderr) = run_fb(&["--host", "localhost:59999", "--concise", "SELECT 1"]); + + assert!(!success, "Exit code should be non-zero when connection fails"); + assert!( + stderr.contains("Failed to send the request"), + "stderr should contain connection error message, got: {}", + stderr + ); +} + +#[test] +fn test_exit_code_on_query_error() { + // Test that exit code is non-zero when query returns an error (e.g., syntax error) + let (success, stdout, _) = run_fb(&["--core", "--concise", "SELEC INVALID SYNTAX"]); + + assert!(!success, "Exit code should be non-zero when query fails"); + // The server should return an error message in the response + assert!( + stdout.to_lowercase().contains("error") || stdout.to_lowercase().contains("exception"), + "stdout should contain error message from server, got: {}", + stdout + ); +} + +#[test] +fn test_exit_code_on_query_error_interactive() { + // Test that exit code is non-zero when any query fails in interactive mode + let mut child = Command::new(env!("CARGO_BIN_EXE_fb")) + .args(&["--core", "--concise"]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = child.stdin.take().unwrap(); + writeln!(stdin, "SELECT 1;").unwrap(); // Valid query + writeln!(stdin, "SELEC INVALID;").unwrap(); // Invalid query + writeln!(stdin, "SELECT 2;").unwrap(); // Valid query + drop(stdin); + + let output = child.wait_with_output().unwrap(); + + assert!( + !output.status.success(), + "Exit code should be non-zero when any query in session fails" + ); +}