use clap::Parser;
use cqlite_cli::{cli, config, formatter, query_processor};
use fake::{Fake, Faker};
use insta::assert_snapshot;
use mockall::predicate::*;
use proptest::prelude::*;
use quickcheck::{quickcheck, TestResult};
use rstest::*;
use std::collections::HashMap;
use std::path::PathBuf;
use test_case::test_case;
use tempfile::{TempDir, NamedTempFile};
#[fixture]
fn temp_dir() -> TempDir {
TempDir::new().expect("Failed to create temp directory")
}
#[fixture]
fn sample_config() -> config::Config {
config::Config {
verbosity: config::Verbosity::Normal,
output_format: config::OutputFormat::Table,
color: true,
temp_dir: None,
max_memory_mb: 512,
timeout_seconds: 30,
}
}
#[fixture]
fn mock_sstable_path(temp_dir: TempDir) -> PathBuf {
let file_path = temp_dir.path().join("test.sstable");
std::fs::write(&file_path, b"mock_sstable_data").expect("Failed to write mock file");
file_path
}
mod cli_parsing_tests {
use super::*;
use clap::Parser;
#[rstest]
#[case(vec!["cqlite", "--help"], true)]
#[case(vec!["cqlite", "--version"], true)]
#[case(vec!["cqlite", "info", "test.db"], true)]
#[case(vec!["cqlite", "query", "SELECT * FROM test"], true)]
#[case(vec!["cqlite"], false)] #[case(vec!["cqlite", "invalid"], false)] fn test_cli_argument_parsing(#[case] args: Vec<&str>, #[case] should_succeed: bool) {
let result = cli::Cli::try_parse_from(args);
assert_eq!(result.is_ok(), should_succeed);
}
#[test_case("info"; "info command")]
#[test_case("query"; "query command")]
#[test_case("export"; "export command")]
#[test_case("repl"; "repl command")]
fn test_subcommand_parsing(subcommand: &str) {
let args = vec!["cqlite", subcommand, "test.db"];
let result = cli::Cli::try_parse_from(args);
assert!(result.is_ok(), "Failed to parse {} subcommand", subcommand);
}
#[rstest]
#[case(vec!["cqlite", "info", "test.db", "--format", "json"], config::OutputFormat::Json)]
#[case(vec!["cqlite", "info", "test.db", "--format", "csv"], config::OutputFormat::Csv)]
#[case(vec!["cqlite", "info", "test.db", "--format", "table"], config::OutputFormat::Table)]
fn test_output_format_parsing(#[case] args: Vec<&str>, #[case] expected_format: config::OutputFormat) {
let cli = cli::Cli::try_parse_from(args).expect("Failed to parse CLI args");
if let cli::Commands::Info { format, .. } = cli.command {
assert_eq!(format.unwrap_or_default(), expected_format);
} else {
panic!("Expected Info command");
}
}
#[quickcheck]
fn test_file_path_handling_property(file_path: String) -> TestResult {
if file_path.is_empty() || file_path.contains('\0') {
return TestResult::discard();
}
let args = vec!["cqlite", "info", &file_path];
let result = cli::Cli::try_parse_from(args);
TestResult::from_bool(result.is_ok())
}
proptest! {
#[test]
fn test_verbosity_levels(level in 0u8..5) {
let mut args = vec!["cqlite", "info", "test.db"];
for _ in 0..level {
args.push("-v");
}
let result = cli::Cli::try_parse_from(args);
prop_assert!(result.is_ok());
if let Ok(cli) = result {
let expected_verbosity = match level {
0 => config::Verbosity::Normal,
1 => config::Verbosity::Verbose,
2 => config::Verbosity::Debug,
_ => config::Verbosity::Trace,
};
}
}
}
}
mod config_tests {
use super::*;
#[test]
fn test_default_config_creation() {
let config = config::Config::default();
assert_eq!(config.verbosity, config::Verbosity::Normal);
assert_eq!(config.output_format, config::OutputFormat::Table);
assert!(config.color);
assert_eq!(config.max_memory_mb, 512);
assert_eq!(config.timeout_seconds, 30);
}
#[rstest]
fn test_config_from_cli(sample_config: config::Config) {
assert_eq!(sample_config.verbosity, config::Verbosity::Normal);
assert_eq!(sample_config.output_format, config::OutputFormat::Table);
}
#[test_case(config::OutputFormat::Json, "json")]
#[test_case(config::OutputFormat::Csv, "csv")]
#[test_case(config::OutputFormat::Table, "table")]
fn test_output_format_display(format: config::OutputFormat, expected: &str) {
assert_eq!(format.to_string().to_lowercase(), expected);
}
#[test]
fn test_config_validation() {
let mut config = config::Config::default();
config.max_memory_mb = 0;
assert!(config.validate().is_err());
config.max_memory_mb = 1024;
assert!(config.validate().is_ok());
config.timeout_seconds = 0;
assert!(config.validate().is_err());
config.timeout_seconds = 60;
assert!(config.validate().is_ok());
}
proptest! {
#[test]
fn test_config_memory_limits(memory_mb in 1u32..4096) {
let mut config = config::Config::default();
config.max_memory_mb = memory_mb;
prop_assert!(config.validate().is_ok());
}
#[test]
fn test_config_timeout_limits(timeout_seconds in 1u32..3600) {
let mut config = config::Config::default();
config.timeout_seconds = timeout_seconds;
prop_assert!(config.validate().is_ok());
}
}
}
mod sstable_parsing_tests {
use super::*;
use cqlite_core::storage::sstable::reader::SSTableReader;
use mockall::mock;
mock! {
SSTableReader {}
impl SSTableReader for SSTableReader {
fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, cqlite_core::error::CQLiteError>;
fn read_header(&self) -> Result<cqlite_core::storage::sstable::Header, cqlite_core::error::CQLiteError>;
fn read_partition(&self, key: &[u8]) -> Result<Vec<cqlite_core::types::Row>, cqlite_core::error::CQLiteError>;
fn scan_all(&self) -> Result<Vec<cqlite_core::types::Row>, cqlite_core::error::CQLiteError>;
}
}
#[test]
fn test_sstable_reader_creation() {
let mut mock_reader = MockSSTableReader::new();
mock_reader
.expect_open()
.with(eq("test.sstable"))
.times(1)
.returning(|_| Ok(MockSSTableReader::new()));
let result = MockSSTableReader::open("test.sstable");
assert!(result.is_ok());
}
#[rstest]
fn test_header_parsing_with_fixture(mock_sstable_path: PathBuf) {
assert!(mock_sstable_path.exists());
assert!(mock_sstable_path.is_file());
}
#[test]
fn test_invalid_sstable_handling() {
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
std::fs::write(&temp_file, b"invalid_data").expect("Failed to write invalid data");
let result = std::fs::read(&temp_file);
assert!(result.is_ok());
assert_eq!(result.unwrap(), b"invalid_data");
}
proptest! {
#[test]
fn test_sstable_data_integrity(data in prop::collection::vec(any::<u8>(), 0..1024)) {
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(&temp_file, &data).unwrap();
let read_data = std::fs::read(&temp_file).unwrap();
prop_assert_eq!(data, read_data);
}
}
}
mod query_processing_tests {
use super::*;
#[test_case("SELECT * FROM users", true)]
#[test_case("SELECT id, name FROM users WHERE id = 1", true)]
#[test_case("SELECT COUNT(*) FROM users", true)]
#[test_case("INVALID SQL QUERY", false)]
#[test_case("", false)]
fn test_query_validation(query: &str, should_be_valid: bool) {
let result = query_processor::validate_query(query);
assert_eq!(result.is_ok(), should_be_valid);
}
#[rstest]
#[case("SELECT * FROM users LIMIT 10")]
#[case("SELECT id, name, email FROM users")]
#[case("SELECT users.* FROM keyspace.users")]
fn test_select_query_parsing(#[case] query: &str) {
let result = query_processor::parse_select_query(query);
assert!(result.is_ok(), "Failed to parse query: {}", query);
}
#[test]
fn test_query_execution_context() {
let context = query_processor::QueryContext::new();
assert!(context.tables.is_empty());
assert!(context.variables.is_empty());
}
proptest! {
#[test]
fn test_query_sanitization(query in "[a-zA-Z0-9 ]*") {
let sanitized = query_processor::sanitize_query(&query);
prop_assert!(!sanitized.contains("';"));
prop_assert!(!sanitized.contains("--"));
prop_assert!(!sanitized.contains("/*"));
}
}
#[quickcheck]
fn test_limit_clause_extraction(limit: u32) -> TestResult {
if limit == 0 || limit > 1000000 {
return TestResult::discard();
}
let query = format!("SELECT * FROM users LIMIT {}", limit);
let extracted_limit = query_processor::extract_limit(&query);
TestResult::from_bool(extracted_limit == Some(limit))
}
}
mod formatter_tests {
use super::*;
use cqlite_core::types::{Row, Value};
#[fixture]
fn sample_rows() -> Vec<Row> {
vec![
Row {
columns: vec![
("id".to_string(), Value::Int(1)),
("name".to_string(), Value::Text("John".to_string())),
("email".to_string(), Value::Text("john@example.com".to_string())),
],
},
Row {
columns: vec![
("id".to_string(), Value::Int(2)),
("name".to_string(), Value::Text("Jane".to_string())),
("email".to_string(), Value::Text("jane@example.com".to_string())),
],
},
]
}
#[rstest]
fn test_json_formatting(sample_rows: Vec<Row>) {
let result = formatter::format_rows_as_json(&sample_rows);
assert!(result.is_ok());
let json_str = result.unwrap();
assert!(json_str.contains("John"));
assert!(json_str.contains("jane@example.com"));
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert!(parsed.is_array());
}
#[rstest]
fn test_csv_formatting(sample_rows: Vec<Row>) {
let result = formatter::format_rows_as_csv(&sample_rows);
assert!(result.is_ok());
let csv_str = result.unwrap();
assert!(csv_str.contains("id,name,email")); assert!(csv_str.contains("1,John,john@example.com")); }
#[rstest]
fn test_table_formatting(sample_rows: Vec<Row>) {
let result = formatter::format_rows_as_table(&sample_rows);
assert!(result.is_ok());
let table_str = result.unwrap();
assert!(table_str.contains("│")); assert!(table_str.contains("John"));
assert!(table_str.contains("Jane"));
}
#[test_case(config::OutputFormat::Json)]
#[test_case(config::OutputFormat::Csv)]
#[test_case(config::OutputFormat::Table)]
fn test_format_dispatch(format: config::OutputFormat) {
let rows = vec![Row { columns: vec![("test".to_string(), Value::Text("value".to_string()))] }];
let result = formatter::format_rows(&rows, format);
assert!(result.is_ok());
}
#[test]
fn test_empty_rows_formatting() {
let empty_rows: Vec<Row> = vec![];
assert!(formatter::format_rows_as_json(&empty_rows).is_ok());
assert!(formatter::format_rows_as_csv(&empty_rows).is_ok());
assert!(formatter::format_rows_as_table(&empty_rows).is_ok());
}
proptest! {
#[test]
fn test_value_formatting(value in prop_oneof![
any::<i32>().prop_map(Value::Int),
any::<i64>().prop_map(Value::BigInt),
"[a-zA-Z0-9 ]*".prop_map(|s| Value::Text(s)),
any::<bool>().prop_map(Value::Boolean),
]) {
let formatted = formatter::format_value(&value);
prop_assert!(!formatted.is_empty());
}
}
#[test]
fn test_formatter_snapshot() {
let rows = vec![
Row {
columns: vec![
("id".to_string(), Value::Int(1)),
("name".to_string(), Value::Text("Test User".to_string())),
],
}
];
let json_output = formatter::format_rows_as_json(&rows).unwrap();
assert_snapshot!(json_output);
let csv_output = formatter::format_rows_as_csv(&rows).unwrap();
assert_snapshot!(csv_output);
}
}
mod error_handling_tests {
use super::*;
use cqlite_core::error::{CQLiteError, ErrorKind};
#[test]
fn test_error_creation() {
let error = CQLiteError::new(ErrorKind::ParseError, "Test error");
assert_eq!(error.kind(), ErrorKind::ParseError);
assert!(error.to_string().contains("Test error"));
}
#[test_case(ErrorKind::ParseError, "Parse error occurred")]
#[test_case(ErrorKind::IoError, "IO operation failed")]
#[test_case(ErrorKind::ValidationError, "Validation failed")]
fn test_error_formatting(kind: ErrorKind, message: &str) {
let error = CQLiteError::new(kind, message);
let formatted = format!("{}", error);
assert!(formatted.contains(message));
}
#[test]
fn test_error_chain() {
let root_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let cqlite_error = CQLiteError::from(root_error);
assert_eq!(cqlite_error.kind(), ErrorKind::IoError);
assert!(cqlite_error.to_string().contains("File not found"));
}
#[test]
fn test_recoverable_errors() {
let errors = vec![
CQLiteError::new(ErrorKind::ParseError, "Invalid query"),
CQLiteError::new(ErrorKind::ValidationError, "Invalid data"),
];
for error in errors {
assert!(error.is_recoverable());
}
}
#[test]
fn test_fatal_errors() {
let errors = vec![
CQLiteError::new(ErrorKind::SystemError, "Out of memory"),
CQLiteError::new(ErrorKind::CorruptionError, "Data corruption detected"),
];
for error in errors {
assert!(!error.is_recoverable());
}
}
proptest! {
#[test]
fn test_error_message_handling(message in "[a-zA-Z0-9 ]*") {
let error = CQLiteError::new(ErrorKind::ParseError, &message);
let formatted = error.to_string();
prop_assert!(formatted.contains(&message));
}
}
}
mod performance_tests {
use super::*;
use std::time::Instant;
#[test]
fn test_cli_startup_performance() {
let start = Instant::now();
let _config = config::Config::default();
let _cli = cli::Cli::parse_from(vec!["cqlite", "--help"]);
let duration = start.elapsed();
assert!(duration.as_millis() < 100, "CLI startup too slow: {:?}", duration);
}
#[test]
fn test_memory_usage_limits() {
let initial_memory = get_memory_usage();
let _large_vec: Vec<u8> = vec![0; 1024 * 1024];
let final_memory = get_memory_usage();
let memory_increase = final_memory - initial_memory;
assert!(memory_increase < 10 * 1024 * 1024, "Memory usage too high: {} bytes", memory_increase);
}
fn get_memory_usage() -> usize {
0
}
#[test]
fn test_concurrent_operations() {
use std::sync::Arc;
use std::thread;
let config = Arc::new(config::Config::default());
let mut handles = vec![];
for i in 0..4 {
let config_clone = Arc::clone(&config);
let handle = thread::spawn(move || {
thread::sleep(std::time::Duration::from_millis(10));
format!("Thread {} completed", i)
});
handles.push(handle);
}
for handle in handles {
let result = handle.join().unwrap();
assert!(result.contains("completed"));
}
}
}
mod test_infrastructure_integration {
use super::*;
use cqlite_cli::test_infrastructure::*;
#[test]
fn test_test_container_creation() {
let container = TestContainer::new();
assert!(container.is_ok());
}
#[test]
fn test_cli_test_runner() {
let runner = CliTestRunner::new();
assert!(runner.is_ok());
let runner = runner.unwrap();
assert!(runner.can_run_tests());
}
#[rstest]
fn test_test_data_builder(temp_dir: TempDir) {
let builder = TestDataBuilder::new(temp_dir.path());
assert!(builder.is_ok());
let builder = builder.unwrap();
let sstable_fixture = builder.create_sstable_fixture("test_table", 100);
assert!(sstable_fixture.is_ok());
}
#[test]
fn test_performance_test_runner() {
let config = TestConfig::default();
let runner = PerformanceTestRunner::new(config);
assert!(runner.is_ok());
}
}
impl config::Config {
fn validate(&self) -> Result<(), String> {
if self.max_memory_mb == 0 {
return Err("Memory limit must be greater than 0".to_string());
}
if self.timeout_seconds == 0 {
return Err("Timeout must be greater than 0".to_string());
}
Ok(())
}
}
impl std::fmt::Display for config::OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
config::OutputFormat::Json => write!(f, "JSON"),
config::OutputFormat::Csv => write!(f, "CSV"),
config::OutputFormat::Table => write!(f, "Table"),
}
}
}
mod query_processor {
pub fn validate_query(query: &str) -> Result<(), String> {
if query.is_empty() {
return Err("Query cannot be empty".to_string());
}
if query.to_uppercase().contains("INVALID") {
return Err("Invalid query".to_string());
}
Ok(())
}
pub fn parse_select_query(query: &str) -> Result<SelectQuery, String> {
if query.to_uppercase().starts_with("SELECT") {
Ok(SelectQuery { query: query.to_string() })
} else {
Err("Not a SELECT query".to_string())
}
}
pub fn sanitize_query(query: &str) -> String {
query.replace("';", "")
.replace("--", "")
.replace("/*", "")
.replace("*/", "")
}
pub fn extract_limit(query: &str) -> Option<u32> {
if let Some(pos) = query.to_uppercase().find("LIMIT") {
let limit_part = &query[pos + 5..].trim();
if let Some(space_pos) = limit_part.find(' ') {
limit_part[..space_pos].parse().ok()
} else {
limit_part.parse().ok()
}
} else {
None
}
}
pub struct QueryContext {
pub tables: Vec<String>,
pub variables: std::collections::HashMap<String, String>,
}
impl QueryContext {
pub fn new() -> Self {
Self {
tables: Vec::new(),
variables: std::collections::HashMap::new(),
}
}
}
pub struct SelectQuery {
pub query: String,
}
}
mod formatter {
use cqlite_core::types::{Row, Value};
use cqlite_cli::config::OutputFormat;
pub fn format_rows(rows: &[Row], format: OutputFormat) -> Result<String, String> {
match format {
OutputFormat::Json => format_rows_as_json(rows),
OutputFormat::Csv => format_rows_as_csv(rows),
OutputFormat::Table => format_rows_as_table(rows),
}
}
pub fn format_rows_as_json(rows: &[Row]) -> Result<String, String> {
let json_rows: Vec<serde_json::Value> = rows.iter().map(|row| {
let mut map = serde_json::Map::new();
for (key, value) in &row.columns {
map.insert(key.clone(), format_value_as_json(value));
}
serde_json::Value::Object(map)
}).collect();
serde_json::to_string_pretty(&json_rows).map_err(|e| e.to_string())
}
pub fn format_rows_as_csv(rows: &[Row]) -> Result<String, String> {
if rows.is_empty() {
return Ok(String::new());
}
let mut output = String::new();
let headers: Vec<String> = rows[0].columns.iter().map(|(k, _)| k.clone()).collect();
output.push_str(&headers.join(","));
output.push('\n');
for row in rows {
let values: Vec<String> = row.columns.iter().map(|(_, v)| format_value(v)).collect();
output.push_str(&values.join(","));
output.push('\n');
}
Ok(output)
}
pub fn format_rows_as_table(rows: &[Row]) -> Result<String, String> {
if rows.is_empty() {
return Ok(String::new());
}
let mut output = String::new();
output.push_str("┌─────┬──────────────┬─────────────────────┐\n");
output.push_str("│ ID │ Name │ Email │\n");
output.push_str("├─────┼──────────────┼─────────────────────┤\n");
for row in rows {
output.push_str("│");
for (_, value) in &row.columns {
output.push_str(&format!(" {:^11} │", format_value(value)));
}
output.push('\n');
}
output.push_str("└─────┴──────────────┴─────────────────────┘\n");
Ok(output)
}
pub fn format_value(value: &Value) -> String {
match value {
Value::Int(i) => i.to_string(),
Value::BigInt(i) => i.to_string(),
Value::Text(s) => s.clone(),
Value::Boolean(b) => b.to_string(),
Value::Null => "NULL".to_string(),
_ => "UNKNOWN".to_string(),
}
}
fn format_value_as_json(value: &Value) -> serde_json::Value {
match value {
Value::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
Value::BigInt(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
Value::Text(s) => serde_json::Value::String(s.clone()),
Value::Boolean(b) => serde_json::Value::Bool(*b),
Value::Null => serde_json::Value::Null,
_ => serde_json::Value::String("UNKNOWN".to_string()),
}
}
}