use anyhow::Result;
use std::io::Write;
use std::path::PathBuf;
use std::time::Duration;
use tempfile::TempDir;
#[cfg(all(test, feature = "integration-tests"))]
mod e2e_tests {
use super::*;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Instant;
const CLI_BINARY: &str = "cqlite";
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
fn run_cli_with_timeout(args: &[&str], timeout: Duration) -> Result<std::process::Output> {
let mut cmd = Command::new("cargo");
cmd.args(["run", "--bin", CLI_BINARY, "--"]).args(args);
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
println!("Running command: cargo run --bin {CLI_BINARY} -- {args:?}");
let start = Instant::now();
let mut child = cmd.spawn()?;
loop {
if child.try_wait()?.is_some() {
let output = Command::new("cargo")
.args(["run", "--bin", CLI_BINARY, "--"])
.args(args)
.output()?;
println!(
"Command completed. stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return Ok(output);
}
if start.elapsed() >= timeout {
let _ = child.kill();
anyhow::bail!("Timed out after {timeout:?} running CLI with args {args:?}");
}
thread::sleep(Duration::from_millis(50));
}
}
fn create_test_sstable_structure(temp_dir: &TempDir) -> Result<PathBuf> {
let sstable_dir = temp_dir
.path()
.join("users-46436710673711f0b2cf19d64e7cbecb");
std::fs::create_dir_all(&sstable_dir)?;
let data_file = sstable_dir.join("nb-1-big-Data.db");
let toc_file = sstable_dir.join("nb-1-big-TOC.txt");
let statistics_file = sstable_dir.join("nb-1-big-Statistics.db");
std::fs::write(&data_file, b"test data")?;
std::fs::write(&toc_file, "Data.db\nStatistics.db\nTOC.txt")?;
std::fs::write(&statistics_file, b"test stats")?;
Ok(sstable_dir)
}
fn create_test_schema_files(temp_dir: &TempDir) -> Result<(PathBuf, PathBuf)> {
let json_schema = temp_dir.path().join("schema.json");
let cql_schema = temp_dir.path().join("schema.cql");
let json_content = r#"{
"keyspace": "test_keyspace",
"table": "users",
"partition_keys": [
{
"name": "id",
"data_type": "uuid",
"position": 0
}
],
"clustering_keys": [
{
"name": "created_at",
"data_type": "timestamp",
"position": 0,
"order": "ASC"
}
],
"columns": [
{
"name": "id",
"data_type": "uuid",
"nullable": false,
"default": null
},
{
"name": "name",
"data_type": "text",
"nullable": true,
"default": null
},
{
"name": "email",
"data_type": "text",
"nullable": true,
"default": null
},
{
"name": "created_at",
"data_type": "timestamp",
"nullable": false,
"default": null
}
],
"comments": {}
}"#;
let cql_content = r#"CREATE TABLE test_keyspace.users (
id uuid,
name text,
email text,
created_at timestamp,
PRIMARY KEY (id, created_at)
);"#;
std::fs::write(&json_schema, json_content)?;
std::fs::write(&cql_schema, cql_content)?;
Ok((json_schema, cql_schema))
}
#[test]
#[ignore = "Long e2e; run with --ignored or set E2E=1"]
fn test_complete_database_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("workflow.db");
let (json_schema, _cql_schema) = create_test_schema_files(&temp_dir)?;
let create_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"schema",
"create",
json_schema.to_str().unwrap(),
],
TEST_TIMEOUT,
)?;
println!("Create table output: {create_output:?}");
let list_output = run_cli_with_timeout(
&["--database", db_path.to_str().unwrap(), "schema", "list"],
TEST_TIMEOUT,
)?;
println!("List tables output: {list_output:?}");
let insert_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"query",
"INSERT INTO test_keyspace.users (id, name, email, created_at) VALUES (uuid(), 'John Doe', 'john@example.com', toTimestamp(now()))",
],
TEST_TIMEOUT,
)?;
println!("Insert data output: {insert_output:?}");
for format in &["table", "json", "csv"] {
let query_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"--format",
format,
"query",
"SELECT * FROM test_keyspace.users",
],
TEST_TIMEOUT,
)?;
println!("Query ({format}) output: {query_output:?}");
}
let info_output = run_cli_with_timeout(
&["--database", db_path.to_str().unwrap(), "admin", "info"],
TEST_TIMEOUT,
)?;
println!("Database info output: {info_output:?}");
let bench_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"bench",
"read",
"--operations",
"5",
"--concurrency",
"1",
],
TEST_TIMEOUT,
)?;
println!("Benchmark output: {bench_output:?}");
Ok(())
}
#[test]
fn test_sstable_reading_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let sstable_dir = create_test_sstable_structure(&temp_dir)?;
let (json_schema, _cql_schema) = create_test_schema_files(&temp_dir)?;
let info_output =
run_cli_with_timeout(&["info", sstable_dir.to_str().unwrap()], TEST_TIMEOUT)?;
println!("SSTable info output: {info_output:?}");
let detailed_info_output = run_cli_with_timeout(
&["info", sstable_dir.to_str().unwrap(), "--detailed"],
TEST_TIMEOUT,
)?;
println!("Detailed SSTable info output: {detailed_info_output:?}");
let read_output = run_cli_with_timeout(
&[
"read",
sstable_dir.to_str().unwrap(),
"--schema",
json_schema.to_str().unwrap(),
"--limit",
"10",
],
TEST_TIMEOUT,
)?;
println!("SSTable read output: {read_output:?}");
for format in &["json", "csv"] {
let format_output = run_cli_with_timeout(
&[
"--format",
format,
"read",
sstable_dir.to_str().unwrap(),
"--schema",
json_schema.to_str().unwrap(),
"--limit",
"5",
],
TEST_TIMEOUT,
)?;
println!("SSTable read ({format}) output: {format_output:?}");
}
let auto_detect_output = run_cli_with_timeout(
&[
"--auto-detect",
"--cassandra-version",
"5.0",
"info",
sstable_dir.to_str().unwrap(),
],
TEST_TIMEOUT,
)?;
println!("Auto-detect output: {auto_detect_output:?}");
Ok(())
}
#[test]
fn test_schema_validation_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let (json_schema, cql_schema) = create_test_schema_files(&temp_dir)?;
let json_validation = run_cli_with_timeout(
&["schema", "validate", json_schema.to_str().unwrap()],
TEST_TIMEOUT,
)?;
println!("JSON schema validation: {json_validation:?}");
let cql_validation = run_cli_with_timeout(
&["schema", "validate", cql_schema.to_str().unwrap()],
TEST_TIMEOUT,
)?;
println!("CQL schema validation: {cql_validation:?}");
let invalid_schema = temp_dir.path().join("invalid.json");
std::fs::write(&invalid_schema, "{ invalid json syntax }")?;
let invalid_validation = run_cli_with_timeout(
&["schema", "validate", invalid_schema.to_str().unwrap()],
TEST_TIMEOUT,
)?;
println!("Invalid schema validation: {invalid_validation:?}");
Ok(())
}
#[test]
fn test_import_export_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("import_export.db");
let json_data = temp_dir.path().join("test_data.json");
let csv_data = temp_dir.path().join("test_data.csv");
std::fs::write(
&json_data,
r#"[
{"id": "123e4567-e89b-12d3-a456-426614174000", "name": "Alice", "email": "alice@example.com"},
{"id": "123e4567-e89b-12d3-a456-426614174001", "name": "Bob", "email": "bob@example.com"}
]"#,
)?;
std::fs::write(
&csv_data,
"id,name,email\n123e4567-e89b-12d3-a456-426614174000,Alice,alice@example.com\n123e4567-e89b-12d3-a456-426614174001,Bob,bob@example.com",
)?;
let json_import = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"import",
json_data.to_str().unwrap(),
"--format",
"json",
"--table",
"users",
],
TEST_TIMEOUT,
)?;
println!("JSON import: {json_import:?}");
let csv_import = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"import",
csv_data.to_str().unwrap(),
"--format",
"csv",
"--table",
"users",
],
TEST_TIMEOUT,
)?;
println!("CSV import: {csv_import:?}");
let export_file = temp_dir.path().join("exported.json");
let export_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"export",
"users",
export_file.to_str().unwrap(),
"--format",
"json",
],
TEST_TIMEOUT,
)?;
println!("Export output: {export_output:?}");
Ok(())
}
#[test]
fn test_configuration_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let config_file = temp_dir.path().join("test_config.toml");
let db_path = temp_dir.path().join("config_test.db");
std::fs::write(
&config_file,
r#"
[connection]
timeout_ms = 30000
retry_attempts = 3
pool_size = 10
[output]
max_rows = 1000
colors = true
timestamp_format = "%Y-%m-%d %H:%M:%S"
[performance]
cache_size_mb = 128
query_timeout_ms = 45000
memory_limit_mb = 512
[logging]
level = "debug"
format = "Pretty"
[repl]
enable_history = true
enable_completion = true
enable_colors = true
show_timing = false
page_size = 50
enable_paging = true
max_history_size = 1000
prompt = "cqlite> "
prompt_continuation = " -> "
default_database = "default.db"
"#,
)?;
let config_output = run_cli_with_timeout(
&[
"--config",
config_file.to_str().unwrap(),
"--database",
db_path.to_str().unwrap(),
"--verbose",
"admin",
"info",
],
TEST_TIMEOUT,
)?;
println!("Configuration test output: {config_output:?}");
let quiet_output = run_cli_with_timeout(
&[
"--config",
config_file.to_str().unwrap(),
"--database",
db_path.to_str().unwrap(),
"--quiet",
"admin",
"info",
],
TEST_TIMEOUT,
)?;
println!("Quiet mode output: {quiet_output:?}");
Ok(())
}
#[test]
fn test_error_recovery_workflow() -> Result<()> {
let temp_dir = TempDir::new()?;
let non_existent_db = temp_dir.path().join("nonexistent.db");
let no_db_output = run_cli_with_timeout(
&[
"--database",
non_existent_db.to_str().unwrap(),
"query",
"SELECT 1",
],
TEST_TIMEOUT,
)?;
println!("Non-existent database output: {no_db_output:?}");
let db_path = temp_dir.path().join("error_test.db");
let invalid_query_output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"query",
"INVALID SQL SYNTAX HERE",
],
TEST_TIMEOUT,
)?;
println!("Invalid query output: {invalid_query_output:?}");
let no_sstable_output =
run_cli_with_timeout(&["info", "/tmp/nonexistent/sstable/path"], TEST_TIMEOUT)?;
println!("Non-existent SSTable output: {no_sstable_output:?}");
let invalid_version_output = run_cli_with_timeout(
&["--cassandra-version", "99.0", "info", "/tmp/test"],
TEST_TIMEOUT,
)?;
println!("Invalid version output: {invalid_version_output:?}");
Ok(())
}
#[test]
#[ignore = "Performance-heavy; run with --ignored or set E2E=1"]
fn test_performance_under_load() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("performance.db");
let large_benchmark = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"bench",
"mixed",
"--operations",
"100",
"--concurrency",
"2",
"--read-pct",
"80",
],
Duration::from_secs(60),
)?;
println!("Large benchmark output: {large_benchmark:?}");
let query_performance = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"query",
"--timing",
"SELECT * FROM system.local",
],
TEST_TIMEOUT,
)?;
println!("Query performance output: {query_performance:?}");
Ok(())
}
#[test]
#[ignore = "REPL mode requires interactive input - skipping in automated tests"]
fn test_interactive_mode_simulation() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("interactive.db");
let output = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"query",
"SELECT 1 as column_0",
],
TEST_TIMEOUT,
)?;
println!("Non-interactive test output: {output:?}");
assert!(output.status.success(), "Command should succeed");
Ok(())
}
#[test]
fn test_cross_format_compatibility() -> Result<()> {
let temp_dir = TempDir::new()?;
let (json_schema, cql_schema) = create_test_schema_files(&temp_dir)?;
let json_validation = run_cli_with_timeout(
&["schema", "validate", json_schema.to_str().unwrap()],
TEST_TIMEOUT,
)?;
let cql_validation = run_cli_with_timeout(
&["schema", "validate", cql_schema.to_str().unwrap()],
TEST_TIMEOUT,
)?;
println!("JSON validation: {json_validation:?}");
println!("CQL validation: {cql_validation:?}");
Ok(())
}
#[test]
fn test_memory_usage_patterns() -> Result<()> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("memory_test.db");
let memory_test = run_cli_with_timeout(
&[
"--database",
db_path.to_str().unwrap(),
"bench",
"write",
"--operations",
"50",
"--concurrency",
"1",
],
TEST_TIMEOUT,
)?;
println!("Memory usage test: {memory_test:?}");
let post_info = run_cli_with_timeout(
&["--database", db_path.to_str().unwrap(), "admin", "info"],
TEST_TIMEOUT,
)?;
println!("Post-operation info: {post_info:?}");
Ok(())
}
}
#[cfg(test)]
mod e2e_helpers {
use super::*;
#[allow(dead_code)]
pub fn validate_output_contains(output: &std::process::Output, patterns: &[&str]) -> bool {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{stdout}{stderr}");
patterns.iter().all(|pattern| combined.contains(pattern))
}
#[allow(dead_code)]
pub fn validate_success(output: &std::process::Output) -> bool {
output.status.success()
}
#[allow(dead_code)]
pub fn validate_failure(output: &std::process::Output) -> bool {
!output.status.success()
}
#[allow(dead_code)]
pub fn extract_timing_info(output: &std::process::Output) -> Option<Duration> {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("executed in") && line.contains("ms") {
if let Some(ms_part) = line.split("in ").nth(1) {
if let Some(ms_str) = ms_part.split("ms").next() {
if let Ok(ms) = ms_str.trim().parse::<f64>() {
return Some(Duration::from_millis(ms as u64));
}
}
}
}
}
None
}
#[allow(dead_code)]
pub fn create_large_test_dataset(temp_dir: &TempDir, size: usize) -> Result<PathBuf> {
let data_file = temp_dir.path().join("large_dataset.csv");
let mut file = std::fs::File::create(&data_file)?;
writeln!(file, "id,name,email,age,city")?;
for i in 0..size {
writeln!(
file,
"{},User{},user{}@example.com,{},City{}",
i,
i,
i,
20 + (i % 50),
i % 100
)?;
}
Ok(data_file)
}
}