#![allow(dead_code)]
pub mod editor_patterns;
pub mod ipc;
use std::{
fs,
future::Future,
path::{Path, PathBuf},
process::Command,
sync::Arc,
time::{Duration, Instant},
};
use sqry_core::{graph::unified::build::BuildConfig, project::ProjectRootMode};
use sqry_daemon::{
DaemonConfig, DaemonError, RebuildDispatcher, WorkspaceKey, WorkspaceManager,
workspace::{WorkingSetInputs, WorkspaceBuilder, working_set_estimate},
};
use tempfile::TempDir;
pub struct RealGraphBuilder {
pub plugins: Arc<sqry_core::plugin::PluginManager>,
pub cfg: BuildConfig,
}
impl std::fmt::Debug for RealGraphBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RealGraphBuilder").finish_non_exhaustive()
}
}
impl WorkspaceBuilder for RealGraphBuilder {
fn build(&self, workspace_root: &Path) -> Result<sqry_core::graph::CodeGraph, DaemonError> {
sqry_core::graph::unified::build::build_unified_graph(
workspace_root,
&self.plugins,
&self.cfg,
)
.map_err(|e| DaemonError::WorkspaceBuildFailed {
root: workspace_root.to_path_buf(),
reason: format!("test build: {e}"),
})
}
}
pub struct WatcherHarness {
pub tmp: TempDir,
pub root: PathBuf,
pub key: WorkspaceKey,
pub manager: Arc<WorkspaceManager>,
pub dispatcher: Arc<RebuildDispatcher>,
}
impl WatcherHarness {
pub async fn new() -> Self {
Self::new_with_debounce(100).await
}
pub async fn new_with_debounce(debounce_ms: u64) -> Self {
let tmp = TempDir::new().expect("tempdir");
let root = tmp.path().to_path_buf();
init_git_repo(&root);
let config = Arc::new(DaemonConfig {
debounce_ms,
..DaemonConfig::default()
});
let manager = WorkspaceManager::new_without_reaper(Arc::clone(&config));
let plugins = Arc::new(sqry_plugin_registry::create_plugin_manager());
let dispatcher = RebuildDispatcher::new(
Arc::clone(&manager),
Arc::clone(&config),
Arc::clone(&plugins),
);
let key = WorkspaceKey::new(root.clone(), ProjectRootMode::GitRoot, 0);
let builder = RealGraphBuilder {
plugins: Arc::clone(&plugins),
cfg: BuildConfig::default(),
};
let estimate = working_set_estimate(WorkingSetInputs {
new_graph_final_estimate: 64 * 1024,
staging_overhead: 32 * 1024,
interner_snapshot_bytes: 16 * 1024,
});
manager
.get_or_load(&key, &builder, estimate)
.expect("initial load must succeed");
let ws = manager.lookup(&key).expect("registered");
dispatcher
.ensure_watching(&key, ws, root.clone())
.await
.expect("ensure_watching must succeed on a fresh harness");
Self {
tmp,
root,
key,
manager,
dispatcher,
}
}
#[must_use]
pub fn settle_for_one(&self) -> Duration {
Duration::from_millis(self.manager_config_debounce_ms() * 2)
}
#[must_use]
pub fn settle_for_zero(&self) -> Duration {
Duration::from_millis(self.manager_config_debounce_ms() * 3)
}
fn manager_config_debounce_ms(&self) -> u64 {
100
}
}
pub struct DispatchHarness {
pub tmp: TempDir,
pub root: PathBuf,
pub key: WorkspaceKey,
pub manager: Arc<WorkspaceManager>,
pub dispatcher: Arc<RebuildDispatcher>,
}
impl DispatchHarness {
pub fn new() -> Self {
Self::with_debounce(100)
}
pub fn with_debounce(debounce_ms: u64) -> Self {
Self::with_debounce_and_cap(debounce_ms, 24)
}
pub fn with_debounce_and_cap(debounce_ms: u64, stale_cap_hours: u32) -> Self {
let tmp = TempDir::new().expect("tempdir");
let root = tmp.path().to_path_buf();
let config = Arc::new(DaemonConfig {
debounce_ms,
stale_serve_max_age_hours: stale_cap_hours,
..DaemonConfig::default()
});
let manager = WorkspaceManager::new_without_reaper(Arc::clone(&config));
let plugins = Arc::new(sqry_plugin_registry::create_plugin_manager());
let dispatcher = RebuildDispatcher::new(
Arc::clone(&manager),
Arc::clone(&config),
Arc::clone(&plugins),
);
let key = WorkspaceKey::new(root.clone(), ProjectRootMode::GitRoot, 0);
let builder = RealGraphBuilder {
plugins: Arc::clone(&plugins),
cfg: BuildConfig::default(),
};
let estimate = working_set_estimate(WorkingSetInputs {
new_graph_final_estimate: 64 * 1024,
staging_overhead: 32 * 1024,
interner_snapshot_bytes: 16 * 1024,
});
manager
.get_or_load(&key, &builder, estimate)
.expect("initial load must succeed");
Self {
tmp,
root,
key,
manager,
dispatcher,
}
}
}
pub struct AlwaysFailBuilder;
impl std::fmt::Debug for AlwaysFailBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AlwaysFailBuilder").finish()
}
}
impl WorkspaceBuilder for AlwaysFailBuilder {
fn build(&self, workspace_root: &Path) -> Result<sqry_core::graph::CodeGraph, DaemonError> {
Err(DaemonError::WorkspaceBuildFailed {
root: workspace_root.to_path_buf(),
reason: "AlwaysFailBuilder: synthetic failure for classify_for_serve tests".to_string(),
})
}
}
pub fn insert_workspace_in_state(
manager: &Arc<sqry_daemon::WorkspaceManager>,
key: &sqry_daemon::WorkspaceKey,
state: sqry_daemon::WorkspaceState,
) {
manager.insert_workspace_in_state_for_test(key.clone(), state);
}
pub fn init_git_repo(root: &Path) {
run_git(root, &["init", "-q", "-b", "main"]);
run_git(root, &["config", "user.email", "test@sqry.dev"]);
run_git(root, &["config", "user.name", "Sqry Test"]);
run_git(root, &["config", "commit.gpgsign", "false"]);
fs::write(root.join("seed.rs"), b"pub fn seed() {}\n").expect("write seed");
run_git(root, &["add", "seed.rs"]);
run_git(root, &["commit", "-q", "-m", "seed"]);
}
pub fn run_git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.status()
.expect("git command failed to launch");
assert!(status.success(), "git {args:?} failed in {}", dir.display());
}
pub async fn assert_exactly_one_rebuild<F>(
dispatcher: &RebuildDispatcher,
timeout: Duration,
post_settle: Duration,
f: F,
) where
F: FnOnce(),
{
let before = dispatcher.dispatched_count();
f();
let deadline = Instant::now() + timeout;
loop {
let now = dispatcher.dispatched_count();
assert!(
now <= before + 1,
"unexpected extra rebuild during poll (before={before}, now={now})"
);
if now == before + 1 {
break;
}
assert!(
Instant::now() < deadline,
"timeout waiting for 1 rebuild (before={before}, now={now}, timeout={timeout:?})"
);
tokio::time::sleep(Duration::from_millis(25)).await;
}
tokio::time::sleep(post_settle).await;
let final_count = dispatcher.dispatched_count();
assert_eq!(
final_count,
before + 1,
"unexpected extra rebuild in post-settle window (before={before}, final={final_count})"
);
}
pub async fn assert_zero_rebuilds<F>(dispatcher: &RebuildDispatcher, settle: Duration, f: F)
where
F: FnOnce(),
{
let before = dispatcher.dispatched_count();
f();
tokio::time::sleep(settle).await;
let after = dispatcher.dispatched_count();
assert_eq!(
after, before,
"unexpected rebuild during assert_zero_rebuilds (before={before}, after={after})"
);
}
pub async fn wait_until<F>(mut predicate: F, timeout: Duration) -> bool
where
F: FnMut() -> bool,
{
let deadline = Instant::now() + timeout;
loop {
if predicate() {
return true;
}
if Instant::now() >= deadline {
return false;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
pub async fn wait_until_async<F, Fut>(mut predicate: F, timeout: Duration) -> bool
where
F: FnMut() -> Fut,
Fut: Future<Output = bool>,
{
let deadline = Instant::now() + timeout;
loop {
if predicate().await {
return true;
}
if Instant::now() >= deadline {
return false;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}