mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-17 23:52:34 +00:00
test(nextest): share one session server by default
Eagerly start one shared test server per nextest session and point default TestContext commands at that session socket instead of leaking per-test daemons keyed by FABRO_STORAGE_DIR. Add isolated_server() for tests that need an explicit separate daemon, and tighten the ps filtering test so it still proves the contract without timing out under full-suite load.
This commit is contained in:
parent
c9d1b92725
commit
9d9ebd529f
13 changed files with 518 additions and 54 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1992,6 +1992,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
"twin-github",
|
||||
"twin-openai",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--web-url <WEB_URL> Base URL for the web UI (used for OAuth callback URLs) [default: http://localhost:3000]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::support::unique_run_id;
|
||||
|
||||
use super::support::{fixture, setup_completed_fast_dry_run, setup_created_fast_dry_run};
|
||||
use super::support::{setup_completed_fast_dry_run, setup_created_fast_dry_run};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -111,8 +111,28 @@ fn ps_quiet_outputs_run_ids_only() {
|
|||
#[test]
|
||||
fn ps_filters_by_workflow_and_label() {
|
||||
let context = test_context!();
|
||||
let simple = fixture("simple.fabro");
|
||||
let branching = fixture("branching.fabro");
|
||||
let simple = context.temp_dir.join("simple.fabro");
|
||||
let branching = context.temp_dir.join("branching.fabro");
|
||||
context.write_temp(
|
||||
"simple.fabro",
|
||||
r#"digraph Simple {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
run [shape=parallelogram, script="true"]
|
||||
start -> run -> exit
|
||||
}
|
||||
"#,
|
||||
);
|
||||
context.write_temp(
|
||||
"branching.fabro",
|
||||
r#"digraph Branching {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
run [shape=parallelogram, script="true"]
|
||||
start -> run -> exit
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
context
|
||||
.run_cmd()
|
||||
|
|
@ -127,7 +147,7 @@ fn ps_filters_by_workflow_and_label() {
|
|||
.assert()
|
||||
.success();
|
||||
context
|
||||
.run_cmd()
|
||||
.create_cmd()
|
||||
.args([
|
||||
"--dry-run",
|
||||
"--auto-approve",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn help() {
|
|||
--json
|
||||
Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR>
|
||||
Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug
|
||||
Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--foreground
|
||||
|
|
@ -99,17 +99,21 @@ fn start_already_running_exits_with_error() {
|
|||
#[test]
|
||||
fn start_without_bind_uses_home_socket_instead_of_storage_socket() {
|
||||
let context = test_context!();
|
||||
let storage_root = isolated_storage_dir();
|
||||
let storage_dir = storage_root.path().join("storage");
|
||||
let expected_socket = context.home_dir.join(".fabro").join("fabro.sock");
|
||||
let storage_socket = context.storage_dir.join("fabro.sock");
|
||||
let storage_socket = storage_dir.join("fabro.sock");
|
||||
|
||||
context
|
||||
.command()
|
||||
.env("FABRO_STORAGE_DIR", &storage_dir)
|
||||
.args(["server", "start", "--dry-run"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let output = context
|
||||
.command()
|
||||
.env("FABRO_STORAGE_DIR", &storage_dir)
|
||||
.args(["server", "status", "--json"])
|
||||
.assert()
|
||||
.success()
|
||||
|
|
@ -123,11 +127,81 @@ fn start_without_bind_uses_home_socket_instead_of_storage_socket() {
|
|||
|
||||
context
|
||||
.command()
|
||||
.env("FABRO_STORAGE_DIR", &storage_dir)
|
||||
.args(["server", "stop"])
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_test_contexts_share_one_eager_session_server() {
|
||||
let context_a = test_context!();
|
||||
let context_b = test_context!();
|
||||
|
||||
assert_eq!(
|
||||
context_a.storage_dir, context_b.storage_dir,
|
||||
"default test contexts in one session should share storage owned by one server"
|
||||
);
|
||||
|
||||
let output_a = context_a
|
||||
.command()
|
||||
.args(["server", "status", "--json"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let output_b = context_b
|
||||
.command()
|
||||
.args(["server", "status", "--json"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let status_a: serde_json::Value = serde_json::from_slice(&output_a).unwrap();
|
||||
let status_b: serde_json::Value = serde_json::from_slice(&output_b).unwrap();
|
||||
|
||||
assert_eq!(status_a["status"].as_str(), Some("running"));
|
||||
assert_eq!(status_a["pid"], status_b["pid"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_server_switches_context_to_separate_daemon() {
|
||||
let mut context = test_context!();
|
||||
let shared_storage_dir = context.storage_dir.clone();
|
||||
let shared_status = context
|
||||
.command()
|
||||
.args(["server", "status", "--json"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let shared_status: serde_json::Value = serde_json::from_slice(&shared_status).unwrap();
|
||||
|
||||
context.isolated_server();
|
||||
|
||||
assert_ne!(
|
||||
context.storage_dir, shared_storage_dir,
|
||||
"isolated_server should move the context onto a separate server-owned storage dir"
|
||||
);
|
||||
|
||||
let isolated_status = context
|
||||
.command()
|
||||
.args(["server", "status", "--json"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let isolated_status: serde_json::Value = serde_json::from_slice(&isolated_status).unwrap();
|
||||
|
||||
assert_eq!(isolated_status["status"].as_str(), Some("running"));
|
||||
assert_ne!(isolated_status["pid"], shared_status["pid"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_autostart_converges_on_one_shared_daemon_and_cleans_up() {
|
||||
fn run_ps_json(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fn help() {
|
|||
Usage: fabro server status [OPTIONS]
|
||||
|
||||
Options:
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--json Output as JSON
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--timeout <TIMEOUT> Seconds to wait for graceful shutdown before SIGKILL [default: 10]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
-o, --output <OUTPUT> Output directory (must not exist or be empty)
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ fn help() {
|
|||
|
||||
Options:
|
||||
--json Output as JSON [env: FABRO_JSON=]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
|
||||
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
|
||||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||||
--before <BEFORE> Only include runs started before this date (YYYY-MM-DD prefix match)
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@ serde = { workspace = true }
|
|||
serde_json = { workspace = true }
|
||||
tempfile = "3"
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
twin-openai = { workspace = true }
|
||||
twin-github = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ static INSTA_FILTERS: &[(&str, &str)] = &[
|
|||
(r"\\([\w\d])", "/$1"),
|
||||
];
|
||||
|
||||
const MANAGED_STORAGE_MARKER: &str = "# fabro-test managed storage_dir";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TestMode {
|
||||
#[default]
|
||||
|
|
@ -103,14 +105,24 @@ pub struct TestContext {
|
|||
session_root: PathBuf,
|
||||
fabro_bin: PathBuf,
|
||||
filters: Vec<(String, String)>,
|
||||
active_socket_path: PathBuf,
|
||||
isolated_server: Option<ServerPaths>,
|
||||
managed_storage_dirs: Vec<PathBuf>,
|
||||
_context_root: tempfile::TempDir,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SessionPaths {
|
||||
struct ServerPaths {
|
||||
root: PathBuf,
|
||||
storage_dir: PathBuf,
|
||||
socket_path: PathBuf,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SessionPaths {
|
||||
root: PathBuf,
|
||||
server: ServerPaths,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -171,7 +183,12 @@ fn session_paths() -> (SessionMode, String, SessionPaths) {
|
|||
SessionMode::Nextest,
|
||||
run_id,
|
||||
SessionPaths {
|
||||
storage_dir: root.join("storage"),
|
||||
server: ServerPaths {
|
||||
root: root.clone(),
|
||||
storage_dir: root.join("storage"),
|
||||
socket_path: root.join("fabro.sock"),
|
||||
config_path: root.join("settings.toml"),
|
||||
},
|
||||
root,
|
||||
},
|
||||
);
|
||||
|
|
@ -184,7 +201,12 @@ fn session_paths() -> (SessionMode, String, SessionPaths) {
|
|||
SessionMode::Process,
|
||||
process_id,
|
||||
SessionPaths {
|
||||
storage_dir: root.join("storage"),
|
||||
server: ServerPaths {
|
||||
root: root.clone(),
|
||||
storage_dir: root.join("storage"),
|
||||
socket_path: root.join("fabro.sock"),
|
||||
config_path: root.join("settings.toml"),
|
||||
},
|
||||
root,
|
||||
},
|
||||
)
|
||||
|
|
@ -297,15 +319,257 @@ fn write_marker(root: &Path) {
|
|||
.unwrap_or_else(|err| panic!("failed to write {}: {err}", marker_path.display()));
|
||||
}
|
||||
|
||||
fn stop_session_server(storage_dir: &Path) {
|
||||
let record_path = storage_dir.join("server.json");
|
||||
let Ok(content) = std::fs::read_to_string(&record_path) else {
|
||||
fn managed_storage_settings(storage_dir: &Path, rest: &str) -> String {
|
||||
format!(
|
||||
"{MANAGED_STORAGE_MARKER}\nstorage_dir = \"{}\"\n{rest}",
|
||||
storage_dir.display()
|
||||
)
|
||||
}
|
||||
|
||||
fn strip_managed_storage_settings(contents: &str) -> &str {
|
||||
if !contents.starts_with(MANAGED_STORAGE_MARKER) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
let after_marker = contents
|
||||
.strip_prefix(MANAGED_STORAGE_MARKER)
|
||||
.and_then(|rest| rest.strip_prefix('\n'))
|
||||
.unwrap_or("");
|
||||
let (first_line, mut rest) = after_marker.split_once('\n').unwrap_or((after_marker, ""));
|
||||
if !first_line.starts_with("storage_dir = ") {
|
||||
return after_marker;
|
||||
}
|
||||
if let Some((maybe_target, tail)) = rest.split_once('\n') {
|
||||
if maybe_target.starts_with("server.target = ") {
|
||||
rest = tail;
|
||||
}
|
||||
}
|
||||
rest
|
||||
}
|
||||
|
||||
fn settings_storage_dir(settings_path: &Path) -> Option<PathBuf> {
|
||||
let content = std::fs::read_to_string(settings_path).ok()?;
|
||||
let value = toml::from_str::<toml::Value>(strip_managed_storage_settings(&content)).ok()?;
|
||||
value
|
||||
.get("storage_dir")
|
||||
.or_else(|| value.get("data_dir"))
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
fn home_settings_path(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".fabro/settings.toml")
|
||||
}
|
||||
|
||||
fn write_settings_file(path: &Path, storage_dir: &Path, rest: &str) {
|
||||
ensure_parent_dir(path);
|
||||
std::fs::write(
|
||||
path,
|
||||
format!("storage_dir = \"{}\"\n{rest}", storage_dir.display()),
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("failed to write {}: {err}", path.display()));
|
||||
}
|
||||
|
||||
fn parse_settings_table(contents: &str, source: &Path) -> toml::map::Map<String, toml::Value> {
|
||||
let stripped = strip_managed_storage_settings(contents);
|
||||
let value = toml::from_str::<toml::Value>(stripped)
|
||||
.unwrap_or_else(|err| panic!("failed to parse {}: {err}", source.display()));
|
||||
let Some(table) = value.as_table() else {
|
||||
panic!("expected {} to contain a TOML table", source.display());
|
||||
};
|
||||
table.clone()
|
||||
}
|
||||
|
||||
fn write_settings_table(path: &Path, table: &toml::map::Map<String, toml::Value>) {
|
||||
ensure_parent_dir(path);
|
||||
let mut contents = toml::to_string(table)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize {}: {err}", path.display()));
|
||||
if !contents.ends_with('\n') {
|
||||
contents.push('\n');
|
||||
}
|
||||
std::fs::write(path, contents)
|
||||
.unwrap_or_else(|err| panic!("failed to write {}: {err}", path.display()));
|
||||
}
|
||||
|
||||
fn server_target_from_table(table: &toml::map::Map<String, toml::Value>) -> Option<String> {
|
||||
table
|
||||
.get("server")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|server| server.get("target"))
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn set_server_target(table: &mut toml::map::Map<String, toml::Value>, socket_path: &Path) {
|
||||
let server_entry = table
|
||||
.entry("server".to_string())
|
||||
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
|
||||
let Some(server_table) = server_entry.as_table_mut() else {
|
||||
panic!("expected [server] to be a TOML table");
|
||||
};
|
||||
server_table.insert(
|
||||
"target".to_string(),
|
||||
toml::Value::String(socket_path.display().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn clear_server_target(table: &mut toml::map::Map<String, toml::Value>) {
|
||||
let Some(server_entry) = table.get_mut("server") else {
|
||||
return;
|
||||
};
|
||||
let Some(server_table) = server_entry.as_table_mut() else {
|
||||
return;
|
||||
};
|
||||
server_table.remove("target");
|
||||
if server_table.is_empty() {
|
||||
table.remove("server");
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_home_settings(
|
||||
settings_path: &Path,
|
||||
storage_dir: &Path,
|
||||
socket_path: &Path,
|
||||
force_server_target: bool,
|
||||
) {
|
||||
let (mut table, had_explicit_storage, had_explicit_target) =
|
||||
match std::fs::read_to_string(settings_path) {
|
||||
Ok(contents) => {
|
||||
let had_managed_storage = contents.starts_with(MANAGED_STORAGE_MARKER);
|
||||
let table = parse_settings_table(&contents, settings_path);
|
||||
let had_explicit_storage = !had_managed_storage
|
||||
&& (table.contains_key("storage_dir") || table.contains_key("data_dir"));
|
||||
let had_explicit_target = server_target_from_table(&table).is_some();
|
||||
(table, had_explicit_storage, had_explicit_target)
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
(toml::map::Map::new(), false, false)
|
||||
}
|
||||
Err(err) => panic!("failed to read {}: {err}", settings_path.display()),
|
||||
};
|
||||
|
||||
if !had_explicit_storage {
|
||||
table.insert(
|
||||
"storage_dir".to_string(),
|
||||
toml::Value::String(storage_dir.display().to_string()),
|
||||
);
|
||||
table.remove("data_dir");
|
||||
}
|
||||
|
||||
if force_server_target || (!had_explicit_storage && !had_explicit_target) {
|
||||
set_server_target(&mut table, socket_path);
|
||||
} else if had_explicit_storage && !had_explicit_target {
|
||||
clear_server_target(&mut table);
|
||||
}
|
||||
|
||||
if !had_explicit_storage {
|
||||
let mut rest = table.clone();
|
||||
rest.remove("storage_dir");
|
||||
let managed_target = !had_explicit_target && !force_server_target;
|
||||
if managed_target {
|
||||
clear_server_target(&mut rest);
|
||||
}
|
||||
let rest = toml::to_string(&rest)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize {}: {err}", settings_path.display()));
|
||||
let mut contents = managed_storage_settings(storage_dir, &rest);
|
||||
if managed_target {
|
||||
contents = format!(
|
||||
"{MANAGED_STORAGE_MARKER}\nstorage_dir = \"{}\"\nserver.target = \"{}\"\n{rest}",
|
||||
storage_dir.display(),
|
||||
socket_path.display()
|
||||
);
|
||||
}
|
||||
ensure_parent_dir(settings_path);
|
||||
std::fs::write(settings_path, contents)
|
||||
.unwrap_or_else(|err| panic!("failed to write {}: {err}", settings_path.display()));
|
||||
return;
|
||||
}
|
||||
|
||||
write_settings_table(settings_path, &table);
|
||||
}
|
||||
|
||||
fn server_record_path(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("server.json")
|
||||
}
|
||||
|
||||
fn server_record_pid(storage_dir: &Path) -> Option<u32> {
|
||||
let record_path = server_record_path(storage_dir);
|
||||
let Ok(content) = std::fs::read_to_string(&record_path) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(record) = serde_json::from_str::<serde_json::Value>(&content) else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
let Some(pid) = record["pid"].as_u64().and_then(|p| u32::try_from(p).ok()) else {
|
||||
record["pid"]
|
||||
.as_u64()
|
||||
.and_then(|pid| u32::try_from(pid).ok())
|
||||
}
|
||||
|
||||
fn server_running(server: &ServerPaths) -> bool {
|
||||
server_record_pid(&server.storage_dir).is_some_and(|pid| fabro_proc::process_alive(pid))
|
||||
}
|
||||
|
||||
fn wait_for_server_running(server: &ServerPaths) {
|
||||
let poll = std::time::Duration::from_millis(50);
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let mut elapsed = std::time::Duration::ZERO;
|
||||
while elapsed < timeout {
|
||||
if server_running(server) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(poll);
|
||||
elapsed += poll;
|
||||
}
|
||||
panic!(
|
||||
"timed out waiting for test server record in {}",
|
||||
server.storage_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_server_running(fabro_bin: &Path, server: &ServerPaths, config_path: &Path) {
|
||||
if server_running(server) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_parent_dir(&server.socket_path);
|
||||
ensure_parent_dir(config_path);
|
||||
std::fs::create_dir_all(&server.storage_dir)
|
||||
.unwrap_or_else(|err| panic!("failed to create {}: {err}", server.storage_dir.display()));
|
||||
let _ = std::fs::remove_file(server_record_path(&server.storage_dir));
|
||||
let _ = std::fs::remove_file(&server.socket_path);
|
||||
|
||||
let output = std::process::Command::new(fabro_bin)
|
||||
.env("NO_COLOR", "1")
|
||||
.env("FABRO_NO_UPGRADE_CHECK", "true")
|
||||
.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64")
|
||||
.env("FABRO_HOME", &server.root)
|
||||
.args(["server", "start"])
|
||||
.arg("--storage-dir")
|
||||
.arg(&server.storage_dir)
|
||||
.arg("--bind")
|
||||
.arg(&server.socket_path)
|
||||
.arg("--config")
|
||||
.arg(config_path)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("failed to execute {}: {err}", fabro_bin.display()));
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !output.status.success() && !stderr.contains("Server already running") {
|
||||
panic!(
|
||||
"failed to start test server:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
wait_for_server_running(server);
|
||||
}
|
||||
|
||||
fn stop_test_server(server: &ServerPaths) {
|
||||
let record_path = server_record_path(&server.storage_dir);
|
||||
let Some(pid) = server_record_pid(&server.storage_dir) else {
|
||||
let _ = std::fs::remove_file(&server.socket_path);
|
||||
let _ = std::fs::remove_file(&record_path);
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
@ -322,16 +586,62 @@ fn stop_session_server(storage_dir: &Path) {
|
|||
fabro_proc::sigkill(pid);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(&server.socket_path);
|
||||
let _ = std::fs::remove_file(&record_path);
|
||||
}
|
||||
|
||||
fn cleanup_session_root(root: &Path, storage_dir: &Path) {
|
||||
fn shared_server_paths(root: &Path) -> ServerPaths {
|
||||
ServerPaths {
|
||||
root: root.to_path_buf(),
|
||||
storage_dir: root.join("storage"),
|
||||
socket_path: root.join("fabro.sock"),
|
||||
config_path: root.join("settings.toml"),
|
||||
}
|
||||
}
|
||||
|
||||
fn isolated_server_paths(
|
||||
root: &Path,
|
||||
test_case_id: &str,
|
||||
storage_dir: Option<PathBuf>,
|
||||
) -> ServerPaths {
|
||||
let server_root = root.join("isolated").join(test_case_id);
|
||||
ServerPaths {
|
||||
root: server_root.clone(),
|
||||
storage_dir: storage_dir.unwrap_or_else(|| server_root.join("storage")),
|
||||
socket_path: server_root.join("fabro.sock"),
|
||||
config_path: server_root.join("settings.toml"),
|
||||
}
|
||||
}
|
||||
|
||||
fn reap_isolated_servers(root: &Path) {
|
||||
let isolated_root = root.join("isolated");
|
||||
let Ok(entries) = std::fs::read_dir(&isolated_root) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
let server_root = entry.path();
|
||||
if !server_root.is_dir() {
|
||||
continue;
|
||||
}
|
||||
stop_test_server(&ServerPaths {
|
||||
root: server_root.clone(),
|
||||
storage_dir: server_root.join("storage"),
|
||||
socket_path: server_root.join("fabro.sock"),
|
||||
config_path: server_root.join("settings.toml"),
|
||||
});
|
||||
let _ = std::fs::remove_dir_all(server_root);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_session_root(root: &Path) {
|
||||
with_session_lock(root, || {
|
||||
let marker_path = session_marker_path(root, current_pid());
|
||||
let _ = std::fs::remove_file(&marker_path);
|
||||
let live_count = live_marker_count(root);
|
||||
if live_count == 0 {
|
||||
stop_session_server(storage_dir);
|
||||
stop_test_server(&shared_server_paths(root));
|
||||
reap_isolated_servers(root);
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
});
|
||||
|
|
@ -361,8 +671,8 @@ fn reap_stale_session_roots(mode: SessionMode) {
|
|||
}
|
||||
with_session_lock(&root, || {
|
||||
if live_marker_count(&root) == 0 {
|
||||
let storage_dir = root.join("storage");
|
||||
stop_session_server(&storage_dir);
|
||||
stop_test_server(&shared_server_paths(&root));
|
||||
reap_isolated_servers(&root);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
});
|
||||
|
|
@ -402,32 +712,40 @@ impl TestContext {
|
|||
)
|
||||
},
|
||||
);
|
||||
std::fs::create_dir_all(&session_paths.storage_dir).unwrap_or_else(|err| {
|
||||
std::fs::create_dir_all(&session_paths.server.storage_dir).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to create {}: {err}",
|
||||
session_paths.storage_dir.display()
|
||||
session_paths.server.storage_dir.display()
|
||||
)
|
||||
});
|
||||
write_settings_file(
|
||||
&session_paths.server.config_path,
|
||||
&session_paths.server.storage_dir,
|
||||
"",
|
||||
);
|
||||
if fabro_bin.exists() {
|
||||
ensure_server_running(
|
||||
&fabro_bin,
|
||||
&session_paths.server,
|
||||
&session_paths.server.config_path,
|
||||
);
|
||||
}
|
||||
write_marker(&session_paths.root);
|
||||
});
|
||||
|
||||
let temp_dir = root_path.join("temp");
|
||||
let home_dir = root_path.join("home");
|
||||
let storage_dir = root_path.join("storage");
|
||||
let storage_dir = session_paths.server.storage_dir.clone();
|
||||
let test_case_id = test_case_id();
|
||||
|
||||
std::fs::create_dir_all(&temp_dir).expect("failed to create temp_dir");
|
||||
std::fs::create_dir_all(&home_dir).expect("failed to create home_dir");
|
||||
std::fs::create_dir_all(&storage_dir).expect("failed to create storage_dir");
|
||||
let settings_path = home_dir.join(".fabro/settings.toml");
|
||||
if let Some(parent) = settings_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("failed to create fabro settings dir");
|
||||
}
|
||||
std::fs::write(
|
||||
&settings_path,
|
||||
format!("storage_dir = \"{}\"\n", storage_dir.display()),
|
||||
)
|
||||
.expect("failed to write default fabro settings");
|
||||
sync_home_settings(
|
||||
&home_settings_path(&home_dir),
|
||||
&storage_dir,
|
||||
&session_paths.server.socket_path,
|
||||
false,
|
||||
);
|
||||
|
||||
let filters = vec![
|
||||
(
|
||||
|
|
@ -472,6 +790,8 @@ impl TestContext {
|
|||
session_root: session_paths.root,
|
||||
fabro_bin,
|
||||
filters,
|
||||
active_socket_path: session_paths.server.socket_path,
|
||||
isolated_server: None,
|
||||
managed_storage_dirs: Vec::new(),
|
||||
_context_root: context_root,
|
||||
}
|
||||
|
|
@ -529,7 +849,6 @@ impl TestContext {
|
|||
cmd.env("NO_COLOR", "1");
|
||||
cmd.env("HOME", &self.home_dir);
|
||||
cmd.env("FABRO_NO_UPGRADE_CHECK", "true");
|
||||
cmd.env("FABRO_STORAGE_DIR", &self.storage_dir);
|
||||
cmd.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64");
|
||||
cmd
|
||||
}
|
||||
|
|
@ -702,17 +1021,50 @@ impl TestContext {
|
|||
std::fs::create_dir_all(parent).expect("failed to create parent dirs");
|
||||
}
|
||||
let content = content.as_ref();
|
||||
let effective_content = if path == std::path::Path::new(".fabro/settings.toml")
|
||||
&& !String::from_utf8_lossy(content).contains("storage_dir")
|
||||
{
|
||||
let mut merged =
|
||||
format!("storage_dir = \"{}\"\n", self.storage_dir.display()).into_bytes();
|
||||
merged.extend_from_slice(content);
|
||||
merged
|
||||
if path == std::path::Path::new(".fabro/settings.toml") {
|
||||
let contents =
|
||||
std::str::from_utf8(content).expect("settings.toml should be valid UTF-8");
|
||||
let table = parse_settings_table(contents, &full);
|
||||
write_settings_table(&full, &table);
|
||||
sync_home_settings(
|
||||
&full,
|
||||
&self.storage_dir,
|
||||
&self.active_socket_path,
|
||||
self.isolated_server.is_some(),
|
||||
);
|
||||
} else {
|
||||
content.to_vec()
|
||||
};
|
||||
std::fs::write(&full, effective_content).expect("failed to write file");
|
||||
std::fs::write(&full, content).expect("failed to write file");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn server_target(&self) -> String {
|
||||
self.active_socket_path.display().to_string()
|
||||
}
|
||||
|
||||
pub fn isolated_server(&mut self) -> &mut Self {
|
||||
if self.isolated_server.is_some() {
|
||||
return self;
|
||||
}
|
||||
|
||||
let settings_path = home_settings_path(&self.home_dir);
|
||||
let storage_dir_override = settings_storage_dir(&settings_path);
|
||||
let server =
|
||||
isolated_server_paths(&self.session_root, &self.test_case_id, storage_dir_override);
|
||||
std::fs::create_dir_all(&server.root)
|
||||
.unwrap_or_else(|err| panic!("failed to create {}: {err}", server.root.display()));
|
||||
sync_home_settings(
|
||||
&settings_path,
|
||||
&server.storage_dir,
|
||||
&server.socket_path,
|
||||
true,
|
||||
);
|
||||
if fabro_bin_exists(&self.fabro_bin) {
|
||||
ensure_server_running(&self.fabro_bin, &server, &settings_path);
|
||||
}
|
||||
self.storage_dir = server.storage_dir.clone();
|
||||
self.active_socket_path = server.socket_path.clone();
|
||||
self.isolated_server = Some(server);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -783,10 +1135,24 @@ impl TestContext {
|
|||
}
|
||||
}
|
||||
|
||||
fn fabro_bin_exists(path: &Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
for storage_dir in &self.managed_storage_dirs {
|
||||
stop_session_server(storage_dir);
|
||||
stop_test_server(&ServerPaths {
|
||||
root: storage_dir.clone(),
|
||||
storage_dir: storage_dir.clone(),
|
||||
socket_path: PathBuf::new(),
|
||||
config_path: PathBuf::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(server) = &self.isolated_server {
|
||||
stop_test_server(server);
|
||||
let _ = std::fs::remove_dir_all(&server.root);
|
||||
}
|
||||
|
||||
let is_last_ref = {
|
||||
|
|
@ -807,7 +1173,7 @@ impl Drop for TestContext {
|
|||
return;
|
||||
}
|
||||
|
||||
cleanup_session_root(&self.session_root, &self.storage_dir);
|
||||
cleanup_session_root(&self.session_root);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1326,7 +1692,8 @@ mod tests {
|
|||
let (_, run_id, paths) = session_paths();
|
||||
assert_eq!(run_id, "nextest-run-123");
|
||||
assert!(paths.root.ends_with(Path::new("fx").join("n-nextestrun12")));
|
||||
assert_eq!(paths.storage_dir, paths.root.join("storage"));
|
||||
assert_eq!(paths.server.storage_dir, paths.root.join("storage"));
|
||||
assert_eq!(paths.server.socket_path, paths.root.join("fabro.sock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1340,7 +1707,8 @@ mod tests {
|
|||
.root
|
||||
.ends_with(Path::new("fx").join(format!("p-{}", current_pid())))
|
||||
);
|
||||
assert_eq!(paths.storage_dir, paths.root.join("storage"));
|
||||
assert_eq!(paths.server.storage_dir, paths.root.join("storage"));
|
||||
assert_eq!(paths.server.socket_path, paths.root.join("fabro.sock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue