refactor(test): promote shared server-lifecycle test helpers into fabro-test

Move wait_for_path, wait_for_log_line, stop_pid, server_log_files, and
isolated_storage_dir out of the three integration test files that duplicated
them and into fabro-test's public surface next to apply_test_isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-19 16:43:26 -04:00
parent a256c14a77
commit f8c560a9a6
No known key found for this signature in database
5 changed files with 76 additions and 122 deletions

View file

@ -2,66 +2,10 @@ use std::process::Stdio;
use std::sync::{Arc, Barrier};
use std::time::{Duration, Instant};
use fabro_test::{apply_test_isolation, fabro_snapshot, test_context};
fn isolated_storage_dir() -> tempfile::TempDir {
let root = tempfile::tempdir_in("/tmp").unwrap();
std::fs::create_dir_all(root.path().join("storage")).unwrap();
root
}
fn server_log_files(logs_dir: &std::path::Path) -> Vec<std::path::PathBuf> {
let Ok(entries) = std::fs::read_dir(logs_dir) else {
return Vec::new();
};
entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("server.") && name.ends_with(".log"))
})
.collect()
}
fn wait_for_path(path: &std::path::Path) {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if path.exists() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("timed out waiting for {}", path.display());
}
fn wait_for_log_line(path: &std::path::Path, needle: &str) {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if std::fs::read_to_string(path)
.ok()
.is_some_and(|contents| contents.contains(needle))
{
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("timed out waiting for {needle:?} in {}", path.display());
}
fn stop_pid(pid: u32) {
fabro_proc::sigterm(pid);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if !fabro_proc::process_alive(pid) {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
fabro_proc::sigkill(pid);
}
use fabro_test::{
apply_test_isolation, fabro_snapshot, isolated_storage_dir, server_log_files, stop_pid,
test_context, wait_for_log_line, wait_for_path,
};
#[test]
fn help() {

View file

@ -1,33 +1,4 @@
use fabro_test::{fabro_snapshot, test_context};
fn isolated_storage_dir() -> tempfile::TempDir {
let root = tempfile::tempdir_in("/tmp").unwrap();
std::fs::create_dir_all(root.path().join("storage")).unwrap();
root
}
fn wait_for_path(path: &std::path::Path) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if path.exists() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
panic!("timed out waiting for {}", path.display());
}
fn stop_pid(pid: u32) {
fabro_proc::sigterm(pid);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if !fabro_proc::process_alive(pid) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
fabro_proc::sigkill(pid);
}
use fabro_test::{fabro_snapshot, isolated_storage_dir, stop_pid, test_context, wait_for_path};
#[test]
fn help() {

View file

@ -1,10 +1,4 @@
use fabro_test::{fabro_snapshot, test_context};
fn isolated_storage_dir() -> tempfile::TempDir {
let root = tempfile::tempdir_in("/tmp").unwrap();
std::fs::create_dir_all(root.path().join("storage")).unwrap();
root
}
use fabro_test::{fabro_snapshot, isolated_storage_dir, test_context};
#[test]
fn help() {

View file

@ -1,7 +1,6 @@
use std::fs;
use std::time::{Duration, Instant};
use fabro_test::{fabro_snapshot, test_context};
use fabro_test::{fabro_snapshot, stop_pid, test_context, wait_for_path};
use serde_json::Value;
#[test]
@ -38,29 +37,6 @@ fn command_with_no_fabro_home(context: &fabro_test::TestContext) -> assert_cmd::
cmd
}
fn wait_for_path(path: &std::path::Path) {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if path.exists() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("timed out waiting for {}", path.display());
}
fn stop_pid(pid: u32) {
fabro_proc::sigterm(pid);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if !fabro_proc::process_alive(pid) {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
fabro_proc::sigkill(pid);
}
#[test]
fn not_installed_prints_message() {
let context = test_context!();

View file

@ -150,6 +150,75 @@ fn apply_test_isolation_with_lookup(
cmd.env(TEST_IN_MEMORY_STORE_ENV, "1");
}
/// Create a fresh tempdir containing an empty `storage/` subdirectory, for
/// isolating server lifecycle tests from the shared nextest session storage.
#[must_use]
pub fn isolated_storage_dir() -> tempfile::TempDir {
let root = tempfile::tempdir_in("/tmp").expect("tempdir under /tmp");
std::fs::create_dir_all(root.path().join("storage")).expect("create storage dir");
root
}
/// Poll up to 5s for a path to appear; panic on timeout.
pub fn wait_for_path(path: &Path) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if path.exists() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
panic!("timed out waiting for {}", path.display());
}
/// Poll up to 5s for `needle` to appear in the contents of `path`; panic on
/// timeout.
pub fn wait_for_log_line(path: &Path, needle: &str) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if std::fs::read_to_string(path)
.ok()
.is_some_and(|contents| contents.contains(needle))
{
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
panic!("timed out waiting for {needle:?} in {}", path.display());
}
/// SIGTERM the pid, wait up to 5s for it to exit, then SIGKILL if still alive.
pub fn stop_pid(pid: u32) {
fabro_proc::sigterm(pid);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if !fabro_proc::process_alive(pid) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
fabro_proc::sigkill(pid);
}
/// List any `server.*.log` files under `logs_dir`. Used by server-lifecycle
/// tests to assert that no server logs leak into the home logs directory.
#[must_use]
pub fn server_log_files(logs_dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(logs_dir) else {
return Vec::new();
};
entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("server.") && name.ends_with(".log"))
})
.collect()
}
/// A test context for running fabro CLI commands.
///
/// Each context gets isolated home/temp directories. The storage directory is