mod common;
use assert_cmd::Command;
use clap::Parser;
use predicates::prelude::*;
use serial_test::serial;
use sqry_cli::args::{Cli, Command as CliCommand, WorkspaceCommand};
use std::path::PathBuf;
use common::sqry_bin;
struct EnvGuard {
key: &'static str,
prior: Option<std::ffi::OsString>,
}
impl EnvGuard {
fn set(key: &'static str, value: &std::ffi::OsStr) -> Self {
let prior = std::env::var_os(key);
unsafe {
std::env::set_var(key, value);
}
Self { key, prior }
}
fn remove(key: &'static str) -> Self {
let prior = std::env::var_os(key);
unsafe {
std::env::remove_var(key);
}
Self { key, prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.prior.take() {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}
macro_rules! large_stack_test {
($(#[$attr:meta])* fn $name:ident() $body:block) => {
$(#[$attr])*
fn $name() {
let result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || $body)
.expect("spawn test thread")
.join();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
};
}
fn sqry_cmd_clean_env() -> Command {
let mut cmd = Command::new(sqry_bin());
cmd.env_remove("SQRY_WORKSPACE_FILE");
cmd.env("NO_COLOR", "1");
cmd
}
large_stack_test! {
#[test]
#[serial]
fn global_workspace_flag_parses_and_propagates() {
let cli = Cli::try_parse_from(["sqry", "--workspace", "/tmp/ws", "index"])
.expect("parse --workspace before subcommand");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/ws"))
);
assert!(matches!(cli.command.as_deref(), Some(CliCommand::Index { .. })));
let cli = Cli::try_parse_from(["sqry", "index", "--workspace", "/tmp/ws"])
.expect("parse --workspace after subcommand");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/ws"))
);
let cli = Cli::try_parse_from(["sqry", "--workspace", "/tmp/ws", "query", "kind:function"])
.expect("parse with query subcommand");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/ws"))
);
assert!(matches!(cli.command.as_deref(), Some(CliCommand::Query { .. })));
let cli = Cli::try_parse_from(["sqry", "--workspace", "/tmp/ws", "main"])
.expect("parse with shorthand search");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/ws"))
);
assert_eq!(cli.pattern.as_deref(), Some("main"));
let cli = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/ws",
"graph",
"stats",
])
.expect("parse with graph subcommand");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/ws"))
);
}
}
#[test]
#[serial]
fn env_var_resolves_identically() {
let result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
let _guard = EnvGuard::set(
"SQRY_WORKSPACE_FILE",
std::ffi::OsStr::new("/tmp/sqry-step8-env-path"),
);
let cli = Cli::try_parse_from(["sqry", "index"]).expect("parse with env-only");
assert_eq!(
cli.workspace,
Some(PathBuf::from("/tmp/sqry-step8-env-path")),
"env var must populate `cli.workspace` identically to --workspace"
);
assert_eq!(
cli.workspace_path(),
Some(std::path::Path::new("/tmp/sqry-step8-env-path")),
"workspace_path() helper must return the env-supplied path"
);
let positional = if let Some(CliCommand::Index { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Index variant");
};
assert_eq!(positional, None);
let resolved = cli
.resolve_subcommand_path(positional)
.expect("env workspace path is UTF-8");
assert_eq!(
resolved, "/tmp/sqry-step8-env-path",
"resolver must surface the env-supplied path as the effective search path"
);
})
.expect("spawn test thread")
.join();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
#[test]
#[serial]
fn cli_flag_wins_over_env_var_on_conflict() {
let result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
let _guard = EnvGuard::set(
"SQRY_WORKSPACE_FILE",
std::ffi::OsStr::new("/tmp/sqry-step8-from-env"),
);
let cli =
Cli::try_parse_from(["sqry", "--workspace", "/tmp/sqry-step8-from-flag", "index"])
.expect("parse with both env and flag");
assert_eq!(
cli.workspace,
Some(PathBuf::from("/tmp/sqry-step8-from-flag")),
"explicit --workspace flag must override SQRY_WORKSPACE_FILE"
);
assert_ne!(
cli.workspace,
Some(PathBuf::from("/tmp/sqry-step8-from-env")),
"env var must NOT win over the explicit --workspace flag"
);
let positional = if let Some(CliCommand::Index { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Index variant");
};
assert_eq!(positional, None);
let resolved = cli
.resolve_subcommand_path(positional)
.expect("flag path is UTF-8");
assert_eq!(
resolved, "/tmp/sqry-step8-from-flag",
"CLI flag path must be the effective search path on conflict"
);
})
.expect("spawn test thread")
.join();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
#[test]
#[serial]
fn env_var_alone_resolves_for_query_subcommand() {
let result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
let _guard = EnvGuard::set(
"SQRY_WORKSPACE_FILE",
std::ffi::OsStr::new("/tmp/sqry-step8-env-query"),
);
let cli = Cli::try_parse_from(["sqry", "query", "kind:function"])
.expect("parse query with env-only");
assert_eq!(
cli.workspace,
Some(PathBuf::from("/tmp/sqry-step8-env-query"))
);
let positional = if let Some(CliCommand::Query { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Query variant");
};
assert_eq!(positional, None);
let resolved = cli
.resolve_subcommand_path(positional)
.expect("env workspace path is UTF-8");
assert_eq!(resolved, "/tmp/sqry-step8-env-query");
})
.expect("spawn test thread")
.join();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
#[test]
#[serial]
fn help_text_advertises_env_var_binding() {
sqry_cmd_clean_env()
.args(["-h"])
.assert()
.success()
.stdout(predicate::str::contains("--workspace"))
.stdout(predicate::str::contains("SQRY_WORKSPACE_FILE"));
}
#[test]
#[serial]
fn cli_flag_wins_over_env_var_e2e_collision_diagnostic() {
sqry_cmd_clean_env()
.env("SQRY_WORKSPACE_FILE", "/tmp/sqry-step8-from-env")
.args([
"--workspace",
"/tmp/sqry-step8-from-flag",
"workspace",
"status",
"/tmp/positional",
])
.assert()
.failure()
.stderr(predicate::str::contains("--workspace").and(
predicate::str::contains("workspace").and(predicate::str::contains("subcommand")),
));
}
large_stack_test! {
#[test]
#[serial]
fn positional_path_wins_over_workspace_for_index() {
let cli = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/ws-flag",
"index",
"/tmp/positional-path",
])
.expect("parse");
let positional = if let Some(CliCommand::Index { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Index variant");
};
assert_eq!(positional, Some("/tmp/positional-path"));
let resolved = cli
.resolve_subcommand_path(positional)
.expect("positional path is UTF-8");
assert_eq!(resolved, "/tmp/positional-path");
let cli_no_positional = Cli::try_parse_from(["sqry", "--workspace", "/tmp/ws-flag", "index"])
.expect("parse without positional");
let positional = if let Some(CliCommand::Index { path, .. }) = cli_no_positional.command.as_deref() {
path.as_deref()
} else {
panic!("expected Index variant");
};
assert_eq!(positional, None);
let resolved = cli_no_positional
.resolve_subcommand_path(positional)
.expect("workspace flag is UTF-8");
assert_eq!(resolved, "/tmp/ws-flag");
}
}
large_stack_test! {
#[test]
#[serial]
fn positional_path_wins_over_workspace_for_query() {
let cli = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/ws-flag",
"query",
"kind:function",
"/tmp/positional-path",
])
.expect("parse");
let positional = if let Some(CliCommand::Query { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Query variant");
};
assert_eq!(positional, Some("/tmp/positional-path"));
let resolved = cli
.resolve_subcommand_path(positional)
.expect("positional path is UTF-8");
assert_eq!(resolved, "/tmp/positional-path");
let cli_no_positional = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/ws-flag",
"query",
"kind:function",
])
.expect("parse without positional");
let positional = if let Some(CliCommand::Query { path, .. }) =
cli_no_positional.command.as_deref()
{
path.as_deref()
} else {
panic!("expected Query variant");
};
assert_eq!(positional, None);
let resolved = cli_no_positional
.resolve_subcommand_path(positional)
.expect("workspace flag is UTF-8");
assert_eq!(resolved, "/tmp/ws-flag");
}
}
large_stack_test! {
#[test]
#[serial]
fn resolver_falls_back_to_default_when_neither_set() {
let _guard = EnvGuard::remove("SQRY_WORKSPACE_FILE");
let cli = Cli::try_parse_from(["sqry", "index"]).expect("parse");
let positional = if let Some(CliCommand::Index { path, .. }) = cli.command.as_deref() {
path.as_deref()
} else {
panic!("expected Index variant");
};
assert_eq!(positional, None);
assert_eq!(
cli.resolve_subcommand_path(positional)
.expect("default `.` is UTF-8"),
"."
);
}
}
large_stack_test! {
#[test]
#[serial]
fn workspace_subcommand_parses_with_global_workspace_flag() {
let cli = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/global-ws",
"workspace",
"stats",
"/tmp/positional-ws",
])
.expect("parser must accept the combination so dispatch can render a clear error");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/global-ws"))
);
if let Some(CliCommand::Workspace {
action: WorkspaceCommand::Stats { workspace },
}) = cli.command.as_deref()
{
assert_eq!(workspace, "/tmp/positional-ws");
} else {
panic!("expected WorkspaceCommand::Stats");
}
}
}
#[test]
#[serial]
fn workspace_subcommand_with_global_workspace_errors_clearly() {
sqry_cmd_clean_env()
.args([
"--workspace",
"/tmp/global-ws",
"workspace",
"stats",
"/tmp/positional-ws",
])
.assert()
.failure()
.stderr(
predicate::str::contains("--workspace")
.and(predicate::str::contains("subcommand"))
.and(
predicate::str::contains("SQRY_WORKSPACE_FILE")
.or(predicate::str::contains("positional")),
),
);
}
#[test]
#[serial]
fn env_var_with_workspace_subcommand_errors_clearly() {
sqry_cmd_clean_env()
.env("SQRY_WORKSPACE_FILE", "/tmp/from-env-ws")
.args(["workspace", "stats", "/tmp/positional-ws"])
.assert()
.failure()
.stderr(
predicate::str::contains("--workspace").and(predicate::str::contains("subcommand")),
);
}
large_stack_test! {
#[test]
#[serial]
fn internal_bench_workspace_flag_does_not_collide() {
let cli = Cli::try_parse_from([
"sqry",
"--workspace",
"/tmp/sqry-cli-ws",
"index",
"/tmp/positional",
])
.expect("parse");
assert_eq!(
cli.workspace.as_deref(),
Some(std::path::Path::new("/tmp/sqry-cli-ws"))
);
if let Some(CliCommand::Index { path, .. }) = cli.command.as_deref() {
assert_eq!(path.as_deref(), Some("/tmp/positional"));
} else {
panic!("expected Index variant");
}
}
}
large_stack_test! {
#[test]
#[serial]
fn workspace_path_helper_returns_some_when_flag_set() {
let cli = Cli::try_parse_from(["sqry", "--workspace", "/tmp/x", "index"]).unwrap();
assert_eq!(cli.workspace_path(), Some(std::path::Path::new("/tmp/x")));
}
}
large_stack_test! {
#[test]
#[serial]
fn workspace_path_helper_returns_none_when_unset() {
let _guard = EnvGuard::remove("SQRY_WORKSPACE_FILE");
let cli = Cli::try_parse_from(["sqry", "index"]).unwrap();
assert!(cli.workspace_path().is_none());
}
}
#[cfg(unix)]
#[test]
#[serial]
fn non_utf8_workspace_path_surfaces_as_error() {
use std::os::unix::ffi::OsStrExt;
let result = std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(move || {
let _env_guard = EnvGuard::remove("SQRY_WORKSPACE_FILE");
let bad_path = PathBuf::from(std::ffi::OsStr::from_bytes(b"\xff/sqry-step8-non-utf8"));
assert!(
bad_path.to_str().is_none(),
"fixture must be non-UTF-8 to exercise the regression"
);
let mut cli = Cli::try_parse_from(["sqry", "index"]).expect("parse baseline");
cli.workspace = Some(bad_path);
let positional: Option<&str> = None;
let err = cli
.resolve_subcommand_path(positional)
.expect_err("non-UTF-8 workspace must error, not silently fall back");
let msg = err.to_string();
assert!(
msg.contains("UTF-8"),
"error must mention UTF-8 violation; got: {msg}"
);
assert!(
msg.contains("--workspace") || msg.contains("SQRY_WORKSPACE_FILE"),
"error must reference the flag/env that supplied the bad path; got: {msg}"
);
let resolved = cli
.resolve_subcommand_path(Some("/tmp/explicit-positional"))
.expect("positional bypasses the workspace branch");
assert_eq!(resolved, "/tmp/explicit-positional");
})
.expect("spawn test thread")
.join();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}