use super::{TestContainer, TestResult};
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug)]
pub struct CliTestRunner {
container: TestContainer,
binary_name: String,
timeout: std::time::Duration,
}
#[derive(Debug)]
pub struct CliTestBuilder {
command: String,
args: Vec<String>,
env_vars: Vec<(String, String)>,
working_dir: Option<PathBuf>,
stdin_data: Option<String>,
timeout: Option<std::time::Duration>,
}
#[derive(Debug)]
pub struct CommandAssertion {
cmd: assert_cmd::Command,
}
impl CliTestRunner {
pub fn new(container: TestContainer) -> Self {
Self {
container,
binary_name: "cqlite".to_string(),
timeout: std::time::Duration::from_secs(30),
}
}
pub fn with_binary<S: Into<String>>(mut self, binary_name: S) -> Self {
self.binary_name = binary_name.into();
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = timeout;
self
}
pub fn command<S: Into<String>>(&self, command: S) -> CliTestBuilder {
CliTestBuilder::new(command)
.with_timeout(self.timeout)
.with_working_dir(self.container.environment().temp_dir.clone())
}
pub fn run(&self, args: &[&str]) -> TestResult<CommandAssertion> {
let mut cmd = Command::cargo_bin(&self.binary_name)?;
cmd.args(args);
cmd.current_dir(&self.container.environment().temp_dir);
let env = self.container.environment();
cmd.env("CQLITE_CONFIG", env.config_path);
cmd.env("CQLITE_DB", env.db_path);
Ok(CommandAssertion {
cmd: assert_cmd::Command::from(cmd),
})
}
pub fn test_help(&self) -> TestResult<()> {
self.run(&["--help"])?
.assert_success()?
.stdout_contains("CQLite - High-performance embedded database")?;
Ok(())
}
pub fn test_version(&self) -> TestResult<()> {
self.run(&["--version"])?
.assert_success()?
.stdout_contains(env!("CARGO_PKG_VERSION"))?;
Ok(())
}
pub fn test_info(&self) -> TestResult<()> {
let sstable_fixture = self
.container
.environment()
.fixtures_dir
.join("test.sstable");
let schema_fixture = self
.container
.environment()
.fixtures_dir
.join("test.schema");
std::fs::write(&sstable_fixture, "dummy sstable data")?;
std::fs::write(&schema_fixture, "{\"tables\": []}")?;
self.run(&["info", sstable_fixture.to_str().unwrap()])?
.assert_success()?;
Ok(())
}
}
impl CliTestBuilder {
pub fn new<S: Into<String>>(command: S) -> Self {
Self {
command: command.into(),
args: Vec::new(),
env_vars: Vec::new(),
working_dir: None,
stdin_data: None,
timeout: None,
}
}
pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
self.args.push(arg.into());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args.extend(args.into_iter().map(|s| s.into()));
self
}
pub fn env<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.env_vars.push((key.into(), value.into()));
self
}
pub fn with_working_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
self.working_dir = Some(dir.into());
self
}
pub fn with_stdin<S: Into<String>>(mut self, data: S) -> Self {
self.stdin_data = Some(data.into());
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn execute(self) -> TestResult<CommandAssertion> {
let mut cmd = Command::cargo_bin("cqlite")?;
cmd.arg(&self.command);
cmd.args(&self.args);
for (key, value) in &self.env_vars {
cmd.env(key, value);
}
if let Some(ref dir) = self.working_dir {
cmd.current_dir(dir);
}
let mut assert_cmd = assert_cmd::Command::from(cmd);
if let Some(ref stdin) = self.stdin_data {
assert_cmd.write_stdin(stdin.as_str());
}
if let Some(timeout) = self.timeout {
assert_cmd.timeout(timeout);
}
Ok(CommandAssertion { cmd: assert_cmd })
}
}
impl CommandAssertion {
pub fn assert_success(mut self) -> TestResult<Self> {
self.cmd.assert().success();
Ok(self)
}
pub fn assert_failure(mut self) -> TestResult<Self> {
self.cmd.assert().failure();
Ok(self)
}
pub fn stdout_contains<S: AsRef<str>>(mut self, expected: S) -> TestResult<Self> {
self.cmd
.assert()
.stdout(predicate::str::contains(expected.as_ref()));
Ok(self)
}
pub fn stderr_contains<S: AsRef<str>>(mut self, expected: S) -> TestResult<Self> {
self.cmd
.assert()
.stderr(predicate::str::contains(expected.as_ref()));
Ok(self)
}
pub fn stdout_equals<S: AsRef<str>>(mut self, expected: S) -> TestResult<Self> {
let expected_string = expected.as_ref().to_string();
self.cmd.assert().stdout(expected_string);
Ok(self)
}
pub fn stderr_equals<S: AsRef<str>>(mut self, expected: S) -> TestResult<Self> {
let expected_string = expected.as_ref().to_string();
self.cmd.assert().stderr(expected_string);
Ok(self)
}
pub fn stdout_empty(mut self) -> TestResult<Self> {
self.cmd.assert().stdout(predicate::str::is_empty());
Ok(self)
}
pub fn stderr_empty(mut self) -> TestResult<Self> {
self.cmd.assert().stderr(predicate::str::is_empty());
Ok(self)
}
pub fn exit_code(mut self, code: i32) -> TestResult<Self> {
self.cmd.assert().code(code);
Ok(self)
}
pub fn raw_command(self) -> assert_cmd::Command {
self.cmd
}
}
pub struct CliTestScenarios;
impl CliTestScenarios {
pub fn test_basic_commands(runner: &CliTestRunner) -> TestResult<()> {
runner.test_help()?;
runner.test_version()?;
println!("✅ Basic CLI commands test passed");
Ok(())
}
pub fn test_query_commands(runner: &CliTestRunner) -> TestResult<()> {
let env = runner.container.environment();
let schema_file = env.fixtures_dir.join("test_schema.json");
let sstable_file = env.fixtures_dir.join("test_sstable");
std::fs::write(
&schema_file,
r#"{"tables": [{"name": "users", "columns": []}]}"#,
)?;
std::fs::write(&sstable_file, "test sstable data")?;
runner
.run(&[
"read",
sstable_file.to_str().unwrap(),
"--schema",
schema_file.to_str().unwrap(),
])?
.assert_success()?;
runner
.run(&["info", sstable_file.to_str().unwrap()])?
.assert_success()?;
println!("✅ Query commands test passed");
Ok(())
}
pub fn test_error_handling(runner: &CliTestRunner) -> TestResult<()> {
runner
.run(&["invalid_command"])?
.assert_failure()?
.stderr_contains("error:")?;
runner
.run(&["read-sstable"])?
.assert_failure()?
.stderr_contains("required")?;
runner
.run(&[
"read-sstable",
"/non/existent/file",
"--schema",
"/non/existent/schema",
])?
.assert_failure()?;
println!("✅ Error handling test passed");
Ok(())
}
pub fn test_config_handling(runner: &CliTestRunner) -> TestResult<()> {
let env = runner.container.environment();
runner
.run(&["--config", env.config_path.to_str().unwrap(), "--help"])?
.assert_success()?;
runner.run(&["-v", "--help"])?.assert_success()?;
runner.run(&["-q", "--help"])?.assert_success()?;
println!("✅ Configuration handling test passed");
Ok(())
}
pub fn run_all(runner: &CliTestRunner) -> TestResult<()> {
Self::test_basic_commands(runner)?;
Self::test_query_commands(runner)?;
Self::test_error_handling(runner)?;
Self::test_config_handling(runner)?;
println!("🎉 All CLI test scenarios passed!");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_infrastructure::TestContainer;
#[tokio::test]
async fn test_cli_runner_creation() {
let container = TestContainer::new().unwrap();
let runner = CliTestRunner::new(container)
.with_binary("cqlite")
.with_timeout(std::time::Duration::from_secs(10));
assert_eq!(runner.binary_name, "cqlite");
assert_eq!(runner.timeout, std::time::Duration::from_secs(10));
}
#[test]
fn test_command_builder() {
let builder = CliTestBuilder::new("read")
.arg("test.sstable")
.arg("--schema")
.arg("test.schema")
.env("TEST_VAR", "test_value")
.with_timeout(std::time::Duration::from_secs(30));
assert_eq!(builder.command, "read");
assert_eq!(builder.args.len(), 3);
assert_eq!(builder.env_vars.len(), 1);
assert_eq!(builder.timeout, Some(std::time::Duration::from_secs(30)));
}
}