use assert_cmd::Command;
use cmdhub_cli::db::{init_db, open_db};
use rusqlite::Connection;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn setup_test_db(data_dir: &std::path::Path) -> Connection {
unsafe {
type SqliteVecInitFn = unsafe extern "C" fn();
let init_fn: SqliteVecInitFn = sqlite_vec::sqlite3_vec_init;
#[allow(clippy::missing_transmute_annotations)]
let _ = rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute(init_fn)));
}
std::env::set_var("XDG_DATA_HOME", data_dir);
let conn = open_db().unwrap();
init_db(&conn).unwrap();
conn.execute(
"INSERT INTO apps (app_id, name, install_instructions) VALUES ('org.test.concurrency', 'ConcurrencyApp', '{}')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO arguments (cmd_path, app_id, node_name, node_type, description, risk_level) \
VALUES ('concurrency.cmd', 'org.test.concurrency', 'cmd', 'root', 'concurrency test command', 'safe')",
[],
).unwrap();
conn.execute(
"INSERT INTO apps_fts (cmd_path, name, capabilities) VALUES ('concurrency.cmd', 'ConcurrencyApp', 'concurrency test command')",
[],
).unwrap();
conn
}
#[test]
fn test_concurrency_wal_mode_reader_safety() {
let tmp = TempDir::new().unwrap();
let data_dir = tmp.path().to_path_buf();
let _conn = setup_test_db(&data_dir);
let data_dir_clone = Arc::new(data_dir.clone());
let data_dir_for_writer = Arc::clone(&data_dir_clone);
let writer_handle = thread::spawn(move || {
let db_path = data_dir_for_writer.join("cmdhub").join("cmdhub.db");
let mut conn = Connection::open(&db_path).unwrap();
conn.execute_batch("PRAGMA journal_mode = WAL;").unwrap();
let tx = conn
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.unwrap();
tx.execute(
"INSERT INTO apps (app_id, name, install_instructions) VALUES ('org.test.tx', 'TxApp', '{}')",
[],
)
.unwrap();
thread::sleep(Duration::from_millis(1000));
tx.commit().unwrap();
});
thread::sleep(Duration::from_millis(200));
let mut reader_handles = vec![];
for _ in 0..10 {
let dir = Arc::clone(&data_dir_clone);
let handle = thread::spawn(move || {
let mut cmd = Command::cargo_bin("cmdh").unwrap();
cmd.env("XDG_DATA_HOME", &*dir)
.env("XDG_CONFIG_HOME", &*dir)
.arg("search")
.arg("concurrency");
let assert = cmd.assert().success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = json.as_array().unwrap();
assert!(!arr.is_empty(), "Reader should have returned results");
assert_eq!(arr[0]["cmd_path"], "concurrency.cmd");
});
reader_handles.push(handle);
}
for handle in reader_handles {
handle.join().unwrap();
}
writer_handle.join().unwrap();
let mut cmd = Command::cargo_bin("cmdh").unwrap();
cmd.env("XDG_DATA_HOME", &data_dir)
.env("XDG_CONFIG_HOME", &data_dir)
.arg("search")
.arg("TxApp");
let assert = cmd.assert().success();
let _stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let db_path = data_dir.join("cmdhub").join("cmdhub.db");
let conn = Connection::open(&db_path).unwrap();
let app_exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM apps WHERE app_id = 'org.test.tx')",
[],
|row| row.get(0),
)
.unwrap();
assert!(
app_exists,
"New app should have been successfully committed"
);
}