#![cfg(test)]
use crate::board::{BoardStore, Ticket, TicketParams, TicketPhase};
use std::path::PathBuf;
use std::sync::OnceLock;
static TEST_ROOT: OnceLock<PathBuf> = OnceLock::new();
pub fn env_lock() -> &'static std::sync::Mutex<()> {
static ENV_LOCK: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
ENV_LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
fn test_root() -> &'static PathBuf {
TEST_ROOT.get_or_init(|| {
let tmp = tempfile::TempDir::new().expect("failed to create test temp dir");
let path = tmp.path().to_path_buf();
std::mem::forget(tmp);
path
})
}
#[macro_export]
macro_rules! open_test_store {
($store:ty, $store_name:expr) => {{
let tmp = ::tempfile::TempDir::new().expect("temp dir for test store");
let store = <$store>::open(tmp.path()).await.unwrap_or_else(|e| {
::std::panic!(
"failed to open test {store_name} store: {e:?}",
store_name = $store_name
)
});
(store, tmp)
}};
}
pub async fn expect_ticket(store: &BoardStore, id: &str) -> Ticket {
store
.get_ticket(id)
.await
.expect("BoardStore::get_ticket query failed")
.expect("expected ticket to exist")
}
pub async fn expect_ticket_status(store: &BoardStore, id: &str) -> TicketPhase {
store
.get_ticket_status(id)
.await
.expect("BoardStore::get_ticket_status query failed")
.expect("expected ticket status to exist")
}
pub fn assert_superseded_ticket(ticket: &Ticket) {
assert_eq!(ticket.status, TicketPhase::Cancelled);
assert!(
ticket.assigned_to.is_none(),
"superseded ticket should have no assignee"
);
assert!(
ticket.is_archived,
"superseded ticket should be archived immediately"
);
}
pub(crate) struct TicketBuilder<'a> {
store: &'a BoardStore,
ws: crate::Workspace,
title: String,
desc: String,
phase: TicketPhase,
prereqs: Vec<String>,
reporter: String,
embedding: Option<Vec<u8>>,
}
impl<'a> TicketBuilder<'a> {
pub(crate) fn new(store: &'a BoardStore, ws: crate::Workspace) -> Self {
Self {
store,
ws,
title: String::new(),
desc: "desc".into(),
phase: TicketPhase::Backlog,
prereqs: Vec::new(),
reporter: "test".into(),
embedding: None,
}
}
pub(crate) fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub(crate) fn desc(mut self, desc: impl Into<String>) -> Self {
self.desc = desc.into();
self
}
pub(crate) fn phase(mut self, phase: TicketPhase) -> Self {
self.phase = phase;
self
}
pub(crate) fn prereqs(mut self, prereqs: &[String]) -> Self {
self.prereqs = prereqs.to_vec();
self
}
pub(crate) fn reporter(mut self, reporter: impl Into<String>) -> Self {
self.reporter = reporter.into();
self
}
pub(crate) fn embedding(mut self, blob: &[u8]) -> Self {
self.embedding = Some(blob.to_vec());
self
}
pub(crate) async fn create(self) -> anyhow::Result<String> {
let (store, params) = self.into_parts();
store.create_ticket(¶ms).await
}
pub(crate) async fn supersede(self, supersede_id: &str) -> anyhow::Result<String> {
let (store, params) = self.into_parts();
store.supersede_and_create(supersede_id, ¶ms).await
}
fn into_parts(self) -> (&'a BoardStore, TicketParams) {
(
self.store,
TicketParams {
title: self.title,
description: self.desc,
workspace_name: self.ws.name,
phase: self.phase,
prerequisites: self.prereqs,
reporter: self.reporter,
embedding: self.embedding,
},
)
}
}
pub async fn init_test_stores() {
use crate::chat_history::ChatHistoryStore;
use crate::config_db::ConfigStore;
use crate::session::SessionStore;
use crate::stats::StatsStore;
use crate::users::UserStore;
use crate::workspace::WorkspaceStore;
static INIT: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
INIT.get_or_init(|| async {
let _ = crate::config::CONFIG.try_set_storage_root(test_root().clone());
crate::search_engine::init_global();
crate::ticket_buffer::init_global();
macro_rules! init_test_store {
($cell:expr, $store:ty) => {
$cell
.set(<$store>::open(test_root()).await.expect(concat!(
"failed to create ",
stringify!($store),
" for tests"
)))
.expect(concat!(
stringify!($cell),
" already initialized by another path"
));
};
}
init_test_store!(crate::session::SESSIONS, SessionStore);
init_test_store!(crate::board::BOARD, BoardStore);
init_test_store!(crate::workspace::WORKSPACES, WorkspaceStore);
init_test_store!(crate::users::USER_STORE, UserStore);
init_test_store!(crate::config_db::CONFIG_STORE, ConfigStore);
init_test_store!(crate::stats::STATS_STORE, StatsStore);
init_test_store!(crate::chat_history::CHAT_HISTORY, ChatHistoryStore);
})
.await;
}
pub(crate) fn init_temp_repo() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("create temp dir");
let repo_path = dir.path().to_path_buf();
let status = std::process::Command::new("git")
.args(["init"])
.current_dir(&repo_path)
.status()
.expect("git init");
assert!(status.success());
for (key, value) in [("user.name", "Test"), ("user.email", "test@test.com")] {
let status = std::process::Command::new("git")
.args(["config", key, value])
.current_dir(&repo_path)
.status()
.expect("git config");
assert!(status.success());
}
std::fs::write(repo_path.join("test.txt"), b"line1\nline2\nline3\n").expect("write test file");
let status = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(&repo_path)
.status()
.expect("git add");
assert!(status.success());
let status = std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(&repo_path)
.status()
.expect("git commit");
assert!(status.success());
(dir, repo_path)
}