use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
struct TestFixture {
test_dir: PathBuf,
rom_path: PathBuf,
storage_dir: PathBuf,
emulator_path: PathBuf,
}
impl TestFixture {
fn new(test_name: &str) -> Self {
let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("test-tmp")
.join(test_name);
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).expect("Failed to create test directory");
let storage_dir = test_dir.join("storage");
fs::create_dir_all(&storage_dir).expect("Failed to create storage directory");
let rom_path = test_dir.join("db.bin");
let emulator_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("emulator")
.join("retroshield");
Self {
test_dir,
rom_path,
storage_dir,
emulator_path,
}
}
fn generate_rom(&self) {
use kz80_db::DatabaseCodeGen;
let mut codegen = DatabaseCodeGen::new();
codegen.generate();
let rom = codegen.into_rom();
let mut file = File::create(&self.rom_path).expect("Failed to create ROM file");
file.write_all(&rom).expect("Failed to write ROM");
}
fn create_test_db(&self, name: &str, records: &[(&str, &str)]) {
let filename = if name.contains('.') {
name.to_string()
} else {
format!("{}.DBF", name)
};
let db_path = self.storage_dir.join(&filename);
let mut header = vec![0u8; 8];
header[0] = 0x02; header[1] = records.len() as u8; header[2] = 0; header[3] = 12; header[4] = 20; header[5] = 24; header[6] = 0; header[7] = 28;
let mut fields = vec![0u8; 33];
fields[0..4].copy_from_slice(b"NAME");
fields[11] = b'C';
fields[12] = 15;
fields[16..21].copy_from_slice(b"PHONE");
fields[27] = b'C';
fields[28] = 12;
fields[32] = 0x0D;
let mut record_data = Vec::new();
for (name, phone) in records {
let mut record = vec![b' '; 28]; let name_bytes = name.as_bytes();
let phone_bytes = phone.as_bytes();
record[1..1 + name_bytes.len().min(15)].copy_from_slice(&name_bytes[..name_bytes.len().min(15)]);
record[16..16 + phone_bytes.len().min(12)].copy_from_slice(&phone_bytes[..phone_bytes.len().min(12)]);
record_data.extend(record);
}
let mut file = File::create(&db_path).expect("Failed to create test DB");
file.write_all(&header).expect("Failed to write header");
file.write_all(&fields).expect("Failed to write fields");
file.write_all(&record_data).expect("Failed to write records");
}
fn run_with_timeout(&self, input: &str, timeout_secs: u64) -> Option<String> {
let mut child = Command::new("timeout")
.arg(timeout_secs.to_string())
.arg(&self.emulator_path)
.arg("-s")
.arg(&self.storage_dir)
.arg(&self.rom_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("Failed to spawn emulator");
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(input.as_bytes()).ok();
}
let output = child.wait_with_output().expect("Failed to wait for emulator");
if output.status.success() || output.status.code() == Some(0) {
Some(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Some(String::from_utf8_lossy(&output.stdout).to_string())
}
}
}
impl Drop for TestFixture {
fn drop(&mut self) {
}
}
fn check_emulator() -> bool {
let emulator_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("emulator")
.join("retroshield");
emulator_path.exists()
}
#[test]
fn test_use_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("use_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout("USE MYDB\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Database opened"), "Should confirm database opened");
}
#[test]
fn test_list_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("list_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout("USE MYDB\nLIST\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("John Smith"), "Should show first record");
assert!(output.contains("Jane Doe"), "Should show second record");
assert!(output.contains("555-1234"), "Should show first phone");
assert!(output.contains("555-5678"), "Should show second phone");
}
#[test]
fn test_go_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("go_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
("Bob Jones", "555-9999"),
]);
let output = fixture.run_with_timeout("USE MYDB\nGO 2\nDISPLAY\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Jane Doe"), "Should show second record");
assert!(output.contains("555-5678"), "Should show second phone");
}
#[test]
fn test_delete_and_recall() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("delete_recall");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout("USE MYDB\nDELETE 2\nLIST\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Record deleted"), "Should confirm deletion");
assert!(output.contains("*Jane Doe"), "Should show deletion marker");
let output = fixture.run_with_timeout("USE MYDB\nRECALL 2\nLIST\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Record recalled"), "Should confirm recall");
}
#[test]
fn test_count_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("count_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
("Bob Jones", "555-9999"),
]);
let output = fixture.run_with_timeout("USE MYDB\nCOUNT\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("3") && output.contains("records"), "Should show 3 records");
}
#[test]
fn test_skip_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("skip_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout("USE MYDB\nSKIP\nDISPLAY\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Jane Doe"), "Should show second record after skip");
}
#[test]
fn test_pack_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("pack_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
("Bob Jones", "555-9999"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\nDELETE 2\nPACK\nLIST\nQUIT\n",
15,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("2 records copied") || output.contains("00002 records copied"),
"Should report 2 records copied: {}", output);
assert!(output.contains("John Smith"), "Should still have John Smith");
assert!(output.contains("Bob Jones"), "Should still have Bob Jones");
assert!(!output.contains("Jane Doe") || output.contains("*Jane Doe"),
"Jane Doe should be removed or only shown as deleted");
}
#[test]
fn test_no_database_error() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("no_db_error");
fixture.generate_rom();
let output = fixture.run_with_timeout("LIST\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("No database") || output.contains("no database"),
"Should show error when no database is open");
}
#[test]
fn test_invalid_record_error() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("invalid_record");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
]);
let output = fixture.run_with_timeout("USE MYDB\nGO 99\nQUIT\n", 3);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Invalid") || output.contains("invalid"),
"Should show error for invalid record number");
}
#[test]
fn test_replace_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("replace_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\nGO 2\nREPLACE\nNAME\nAlice Wonder\nLIST\nQUIT\n",
5,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Record updated"), "Should confirm update");
assert!(output.contains("Alice Wonder"), "Should show updated name");
}
#[test]
fn test_locate_command() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("locate_command");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
("Bob Jones", "555-9999"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\nLOCATE\nBob\nQUIT\n",
5,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Bob Jones"), "Should find and display Bob Jones: {}", output);
}
#[test]
fn test_locate_not_found() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("locate_not_found");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("John Smith", "555-1234"),
("Jane Doe", "555-5678"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\nLOCATE\nZzzz\nQUIT\n",
5,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Not found") || output.contains("not found"),
"Should show not found message: {}", output);
}
#[test]
fn test_multiple_operations() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("multiple_ops");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("Alice", "111-1111"),
("Bob", "222-2222"),
("Carol", "333-3333"),
("David", "444-4444"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\n\
DELETE 2\n\
COUNT\n\
GO 4\n\
DISPLAY\n\
QUIT\n",
5,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("Record deleted"), "Should delete record");
assert!(output.contains("4") && output.contains("records"), "Should count 4 records: {}", output);
assert!(output.contains("David"), "Should show David at record 4: {}", output);
}
#[test]
fn test_boundary_record_access() {
if !check_emulator() {
eprintln!("Skipping integration test: emulator not found");
return;
}
let fixture = TestFixture::new("boundary_access");
fixture.generate_rom();
fixture.create_test_db("MYDB", &[
("First", "111-1111"),
("Middle", "222-2222"),
("Last", "333-3333"),
]);
let output = fixture.run_with_timeout(
"USE MYDB\n\
GO 1\n\
DISPLAY\n\
GO 3\n\
DISPLAY\n\
GO 4\n\
QUIT\n",
5,
);
assert!(output.is_some(), "Emulator should complete");
let output = output.unwrap();
assert!(output.contains("First"), "Should access first record");
assert!(output.contains("Last"), "Should access last record");
assert!(output.contains("Invalid"), "Should reject GO 4 (out of bounds)");
}