0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2024-12-04 18:58:00 +00:00
libsql/libsql-wal/tests/oracle.rs
2024-09-18 20:31:14 +02:00

291 lines
9.3 KiB
Rust

//! This test suite runs about 30k test files against sqlite and our test_suite, compares the
//! results, and then compares the database files.
use std::ffi::c_char;
use std::fmt::Display;
use std::mem::size_of;
use std::os::unix::prelude::FileExt;
use std::path::Path;
use std::sync::Arc;
use libsql_sys::ffi::{sqlite3_finalize, sqlite3_prepare, Sqlite3DbHeader};
use libsql_sys::name::NamespaceName;
use libsql_sys::rusqlite::OpenFlags;
use libsql_sys::wal::{Sqlite3WalManager, Wal, WalManager};
use libsql_sys::Connection;
use libsql_wal::registry::WalRegistry;
use libsql_wal::storage::TestStorage;
use libsql_wal::test::{seal_current_segment, wait_current_durable};
use libsql_wal::wal::LibsqlWalManager;
use once_cell::sync::Lazy;
use rand::Rng;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regex::{Captures, Regex};
use tempfile::tempdir;
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
#[tokio::test]
async fn test_oracle() {
let manifest_path: &Path = env!("CARGO_MANIFEST_DIR").as_ref();
let test_samples_path = manifest_path.join("tests/assets/fixtures");
let filter = std::env::var("LIBSQL_WAL_FILTER");
let dir = walkdir::WalkDir::new(test_samples_path);
for entry in dir {
let entry = entry.unwrap();
if let Ok(ref filter) = filter {
if entry.path().file_name().unwrap().to_str().unwrap() != filter {
continue;
}
}
if entry.file_type().is_file() {
run_test_sample(entry.path()).await.unwrap();
}
}
}
async fn run_test_sample(path: &Path) -> Result {
println!("test: {:?}", path.file_name().unwrap());
let curdir = std::env::current_dir().unwrap();
let tmp = tempdir()?;
std::fs::create_dir_all(tmp.path().join("test")).unwrap();
std::env::set_current_dir(tmp.path().join("test")).unwrap();
let script = std::fs::read_to_string(path).unwrap();
let sqlite_conn = libsql_sys::Connection::open(
tmp.path().join("test/data"),
OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
Sqlite3WalManager::default(),
1000,
None,
)
.unwrap();
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
let before = std::time::Instant::now();
let sqlite_results = run_script(
&sqlite_conn,
&script,
&mut rng,
Sqlite3WalManager::default(),
)
.collect::<Vec<_>>();
println!("ran sqlite in {:?}", before.elapsed());
drop(sqlite_conn);
std::fs::rename(tmp.path().join("test/data"), tmp.path().join("sqlite-data")).unwrap();
std::fs::remove_dir_all(tmp.path().join("test")).unwrap();
std::fs::create_dir_all(tmp.path().join("test")).unwrap();
std::env::set_current_dir(tmp.path().join("test")).unwrap();
let resolver = |path: &Path| {
if path.file_name().unwrap() != "data" {
return NamespaceName::from_string(
path.file_name().unwrap().to_str().unwrap().to_string(),
);
}
let name = path
.parent()
.unwrap()
.file_name()
.unwrap()
.to_str()
.unwrap();
NamespaceName::from_string(name.to_string())
};
let (sender, _receiver) = tokio::sync::mpsc::channel(64);
let registry = Arc::new(WalRegistry::new(TestStorage::new().into(), sender).unwrap());
let wal_manager = LibsqlWalManager::new(registry.clone(), Arc::new(resolver));
tokio::fs::create_dir_all(tmp.path().join("test"))
.await
.unwrap();
let db_path = tmp.path().join("test/data").clone();
let libsql_conn = libsql_sys::Connection::open(
&db_path,
OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
wal_manager.clone(),
100000,
None,
)
.unwrap();
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
let before = std::time::Instant::now();
let libsql_results =
run_script(&libsql_conn, &script, &mut rng, wal_manager.clone()).collect::<Vec<_>>();
println!("ran libsql in {:?}", before.elapsed());
for ((a, _), (b, _)) in sqlite_results.iter().zip(libsql_results.iter()) {
if a != b {
panic!(
"sqlite and libsql output differ:\n{}",
PrintScript(sqlite_results, libsql_results)
);
}
}
drop(libsql_conn);
let shared = registry.clone().open(&db_path, &"test".into()).unwrap();
seal_current_segment(&shared);
wait_current_durable(&shared).await;
shared.checkpoint().await.unwrap();
std::env::set_current_dir(curdir).unwrap();
match std::panic::catch_unwind(|| {
compare_db_files(
&tmp.path().join("sqlite-data"),
&tmp.path().join("test/data"),
);
}) {
Ok(_) => (),
Err(e) => {
let path = tmp.into_path();
std::fs::rename(path, "./failure").unwrap();
std::panic::resume_unwind(e)
}
}
Ok(())
}
struct PrintScript(Vec<(String, String)>, Vec<(String, String)>);
impl Display for PrintScript {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for ((a, cmd), (b, _)) in self.0.iter().zip(self.1.iter()) {
writeln!(f, "stmt: {}", cmd.trim())?;
if a != b {
writeln!(f, "sqlite: {}", a)?;
writeln!(f, "libsql: {}", b)?;
} else {
writeln!(f, "OK")?;
}
}
Ok(())
}
}
fn compare_db_files(sqlite: &Path, libsql: &Path) {
let db1 = std::fs::File::open(sqlite).unwrap();
let db2 = std::fs::File::open(libsql).unwrap();
let len1 = db1.metadata().unwrap().len();
let len2 = db2.metadata().unwrap().len();
// sqlite file may contain a footer, compare only the data portions of the db files.
assert_eq!(len1, 4096 * (len2 / 4096));
let n_pages = len1 / 4096;
let mut buf1 = [0; 4096];
let mut buf2 = [0; 4096];
for i in 0..n_pages {
db1.read_exact_at(&mut buf1, i * 4096).unwrap();
db2.read_exact_at(&mut buf2, i * 4096).unwrap();
if i == 0 {
// todo!(page 1 differ)
assert_eq!(
buf1[size_of::<Sqlite3DbHeader>()..],
buf2[size_of::<Sqlite3DbHeader>()..],
"page 1 differ"
);
continue;
} else {
assert_eq!(buf1, buf2, "page {} differ", i + 1);
}
}
}
fn run_script<'a, T: Wal>(
conn: &'a Connection<T>,
script: &'a str,
rng: &'a mut ChaCha8Rng,
wal_manager: impl WalManager + Clone + 'static,
) -> impl Iterator<Item = (String, String)> + 'a {
let mut stmts = split_statements(conn, script);
std::iter::from_fn(move || {
let stmt_str = patch_randomness(stmts.next()?, rng);
if stmt_str.trim_start().starts_with("ATTACH") {
patch_attach(&stmt_str, wal_manager.clone());
}
let mut stmt = conn.prepare(&stmt_str).unwrap();
let ret = stmt
.query(())
.unwrap()
.mapped(|r| Ok(format!("{r:?}")))
.map(|r| {
// let _ = r.as_ref().map_err(|e| assert!(!matches!(e.sqlite_error_code().unwrap(), ErrorCode::DatabaseCorrupt)));
r.unwrap_or_else(|e| e.to_string())
})
.collect::<String>();
Some((ret, stmt_str.to_string()))
})
}
fn patch_attach(s: &str, wal: impl WalManager) {
let mut split = s.split_whitespace();
let name = split.nth(1).unwrap();
let name = name.trim_matches('\'');
let _ = libsql_sys::Connection::open(
name,
OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE,
wal,
100000,
None,
)
.unwrap();
}
fn patch_randomness<'a>(s: &'a str, rng: &'a mut ChaCha8Rng) -> String {
static RE_RANDOM_BLOB: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"randomblob\((\d+)\)"#).unwrap());
static RE_RANDOM: Lazy<Regex> = Lazy::new(|| Regex::new(r#"random\(\)"#).unwrap());
// static RE_STR: Lazy<Regex> = Lazy::new(|| Regex::new(r#"randomstr\((\d+), (\d+)\)"#).unwrap());
let s = RE_RANDOM_BLOB.replace_all(s, |cap: &Captures| {
let len = cap[1].parse::<usize>().unwrap();
let mut data = vec![0; len];
rng.fill_bytes(&mut data);
format!("X'{}'", hex::encode(data))
});
RE_RANDOM
.replace_all(&s, |_cap: &Captures| {
let rand: u64 = rng.gen();
format!("{rand}")
})
.to_string()
}
// shenanigans to split statments
fn split_statements<'a, T: Wal>(
conn: &'a Connection<T>,
script: &'a str,
) -> impl Iterator<Item = &'a str> + 'a {
let mut tail = script.as_ptr() as *const c_char;
let mut stmt = std::ptr::null_mut();
let mut len = script.len();
std::iter::from_fn(move || unsafe {
let previous = tail;
sqlite3_prepare(conn.handle(), previous, len as _, &mut stmt, &mut tail);
sqlite3_finalize(stmt);
if stmt.is_null() {
None
} else {
let start = previous as usize - (script.as_ptr() as usize);
let end = tail as usize - (script.as_ptr() as usize);
let s = std::str::from_utf8(&script.as_bytes()[start..end]).unwrap();
len -= s.len();
Some(s)
}
})
}