use super::*;
use std::fs;
use std::io::Write;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn create_temp_json_file(content: &str) -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.json");
let mut file = fs::File::create(&file_path).unwrap();
file.write_all(content.as_bytes()).unwrap();
(temp_dir, file_path)
}
fn wait_for_completion(
loader: &mut FileLoader,
max_attempts: u32,
) -> Option<Result<String, JiqError>> {
for _ in 0..max_attempts {
if let Some(result) = loader.poll() {
return Some(result);
}
thread::sleep(Duration::from_millis(10));
}
None
}
#[test]
fn test_file_loader_loads_valid_json() {
let json_content = r#"{"name": "test", "value": 42}"#;
let (_temp_dir, file_path) = create_temp_json_file(json_content);
let mut loader = FileLoader::spawn_load(file_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some(), "Loader should complete");
let result = result.unwrap();
assert!(result.is_ok(), "Loading should succeed");
assert_eq!(result.unwrap(), json_content);
assert!(matches!(loader.state(), LoadingState::Complete(_)));
}
#[test]
fn test_file_loader_returns_error_for_invalid_json() {
let invalid_json = r#"{"name": "test", invalid}"#;
let (_temp_dir, file_path) = create_temp_json_file(invalid_json);
let mut loader = FileLoader::spawn_load(file_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some(), "Loader should complete");
let result = result.unwrap();
assert!(result.is_err(), "Loading should fail for invalid JSON");
assert!(matches!(result.unwrap_err(), JiqError::InvalidJson(_)));
assert!(matches!(loader.state(), LoadingState::Error(_)));
}
#[test]
fn test_file_loader_returns_error_for_missing_file() {
let missing_path = PathBuf::from("/nonexistent/path/to/file.json");
let mut loader = FileLoader::spawn_load(missing_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some(), "Loader should complete");
let result = result.unwrap();
assert!(result.is_err(), "Loading should fail for missing file");
assert!(matches!(result.unwrap_err(), JiqError::Io(_)));
assert!(matches!(loader.state(), LoadingState::Error(_)));
}
#[test]
fn test_poll_returns_none_while_loading() {
let json_content = r#"{"name": "test"}"#;
let (_temp_dir, file_path) = create_temp_json_file(json_content);
let mut loader = FileLoader::spawn_load(file_path);
let first_poll = loader.poll();
if first_poll.is_none() {
assert!(loader.is_loading() || matches!(loader.state(), LoadingState::Complete(_)));
}
}
#[test]
fn test_poll_returns_result_when_complete() {
let json_content = r#"{"name": "test"}"#;
let (_temp_dir, file_path) = create_temp_json_file(json_content);
let mut loader = FileLoader::spawn_load(file_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some(), "Poll should return Some when complete");
assert!(result.unwrap().is_ok(), "Result should be Ok");
assert_eq!(loader.poll(), None, "Subsequent polls should return None");
}
#[test]
fn test_io_errors_convert_to_jiq_error() {
let missing_path = PathBuf::from("/nonexistent/file.json");
let mut loader = FileLoader::spawn_load(missing_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some());
let err = result.unwrap().unwrap_err();
assert!(
matches!(err, JiqError::Io(_)),
"IO errors should convert to JiqError::Io"
);
}
#[test]
fn test_spawn_load_stdin_creates_loader() {
let loader = FileLoader::spawn_load_stdin();
assert!(loader.is_loading());
assert!(matches!(loader.state(), LoadingState::Loading));
}
#[test]
fn test_load_stdin_sync_detects_terminal() {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
let result = load_stdin_sync();
assert!(result.is_err(), "Should error when stdin is a terminal");
match result.unwrap_err() {
JiqError::Io(msg) => {
assert!(msg.contains("No input provided"));
assert!(msg.contains("Usage:"));
}
_ => panic!("Expected JiqError::Io"),
}
}
}
#[test]
fn test_validate_json_single_object() {
let json = r#"{"name": "test", "value": 42}"#;
let result = validate_json_or_jsonl(json);
assert!(result.is_ok(), "Single JSON object should be valid");
}
#[test]
fn test_validate_json_array() {
let json = r#"[1, 2, 3]"#;
let result = validate_json_or_jsonl(json);
assert!(result.is_ok(), "JSON array should be valid");
}
#[test]
fn test_validate_jsonl_multiple_objects() {
let jsonl = r#"{"id": 1, "name": "Alice"}
{"id": 2, "name": "Bob"}
{"id": 3, "name": "Charlie"}"#;
let result = validate_json_or_jsonl(jsonl);
assert!(
result.is_ok(),
"JSONL with multiple objects should be valid"
);
}
#[test]
fn test_validate_jsonl_with_empty_lines() {
let jsonl = r#"{"id": 1}
{"id": 2}
{"id": 3}"#;
let result = validate_json_or_jsonl(jsonl);
assert!(
result.is_ok(),
"JSONL with blank lines between values should be valid"
);
}
#[test]
fn test_validate_invalid_json() {
let invalid = r#"{"name": invalid}"#;
let result = validate_json_or_jsonl(invalid);
assert!(result.is_err(), "Invalid JSON should fail validation");
assert!(matches!(result.unwrap_err(), JiqError::InvalidJson(_)));
}
#[test]
fn test_validate_empty_input() {
let empty = "";
let result = validate_json_or_jsonl(empty);
assert!(result.is_err(), "Empty input should fail validation");
match result.unwrap_err() {
JiqError::InvalidJson(msg) => {
assert!(msg.contains("Empty input"));
}
_ => panic!("Expected JiqError::InvalidJson with 'Empty input' message"),
}
}
#[test]
fn test_validate_whitespace_only_input() {
let whitespace = " \n\t\n ";
let result = validate_json_or_jsonl(whitespace);
assert!(
result.is_err(),
"Whitespace-only input should fail validation"
);
}
#[test]
fn test_file_loader_loads_jsonl() {
let jsonl_content = r#"{"id": 1, "name": "Alice"}
{"id": 2, "name": "Bob"}"#;
let (_temp_dir, file_path) = create_temp_json_file(jsonl_content);
let mut loader = FileLoader::spawn_load(file_path);
let result = wait_for_completion(&mut loader, 100);
assert!(result.is_some(), "Loader should complete");
let result = result.unwrap();
assert!(result.is_ok(), "Loading JSONL should succeed");
assert_eq!(result.unwrap(), jsonl_content);
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn valid_json_string() -> impl Strategy<Value = String> {
prop_oneof![
Just(r#"{"key": "value"}"#.to_string()),
Just(r#"[1, 2, 3]"#.to_string()),
Just(r#"{"nested": {"data": [1, 2, 3]}}"#.to_string()),
Just(r#"{"string": "test", "number": 42, "bool": true}"#.to_string()),
Just(r#"[]"#.to_string()),
Just(r#"{}"#.to_string()),
]
}
fn invalid_path() -> impl Strategy<Value = PathBuf> {
prop_oneof![
Just(PathBuf::from("/nonexistent/path/file.json")),
Just(PathBuf::from("/tmp/nonexistent_dir_12345/file.json")),
Just(PathBuf::from("/root/protected/file.json")),
Just(PathBuf::from("/dev/null/impossible/file.json")),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_poll_none_until_complete(json in valid_json_string()) {
let (_temp_dir, file_path) = create_temp_json_file(&json);
let mut loader = FileLoader::spawn_load(file_path);
let mut got_some = false;
for _ in 0..100 {
match loader.poll() {
None => {
}
Some(result) => {
got_some = true;
prop_assert!(result.is_ok());
break;
}
}
thread::sleep(Duration::from_millis(1));
}
prop_assert!(got_some, "Should eventually return Some");
prop_assert_eq!(loader.poll(), None);
prop_assert_eq!(loader.poll(), None);
}
#[test]
fn prop_io_errors_become_jiq_errors(path in invalid_path()) {
let mut loader = FileLoader::spawn_load(path);
let result = wait_for_completion(&mut loader, 100);
prop_assert!(result.is_some(), "Loader should complete");
let result = result.unwrap();
prop_assert!(result.is_err(), "Should return error for invalid path");
match result.unwrap_err() {
JiqError::Io(_) => {
}
other => {
prop_assert!(false, "Expected JiqError::Io, got {:?}", other);
}
}
}
}
}