Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 29 additions & 73 deletions packages/rust/tests/cli_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@

#![cfg(feature = "cli")]

#[path = "common/server.rs"]
mod server;

use std::ffi::OsStr;
use std::fs;
use std::io::{BufRead, BufReader};
use std::net::TcpListener;
use std::process::{Child, Command as StdCommand, Stdio};
use std::process::Child;
use std::time::{Duration, Instant};

use assert_cmd::prelude::*;
use reqwest::{StatusCode, blocking::Client};
use serde_json::{Value, json};
use server::ServerGuard;
use tempfile::TempDir;

/// Write a two-post blog fixture into a fresh tempdir and return it.
Expand Down Expand Up @@ -80,41 +85,19 @@ fn free_port() -> u16 {
.port()
}

/// Spawn `dirsql` as a subprocess bound to `--port <port>` in `dir`.
/// The child inherits stderr so failures surface in test output.
fn spawn_dirsql(dir: &std::path::Path, port: u16) -> Child {
let mut cmd: StdCommand = std::process::Command::cargo_bin("dirsql")
.expect("`dirsql` binary must be built by `cargo test` with --features cli");
// #662: the HTTP server moved behind the `server` subcommand; `--host` /
// `--port` are now server-local flags.
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.current_dir(dir)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
cmd.spawn().expect("spawning dirsql failed")
/// Spawn `dirsql server` bound to `--port <port>` in `dir`.
fn spawn_dirsql(dir: &std::path::Path, port: u16) -> ServerGuard {
spawn_dirsql_with_args::<&str>(dir, port, &[])
}

/// Spawn `dirsql server` bound to `--port <port>` in `dir` with `extra` args
/// appended (e.g. `--persist`). Mirrors [`spawn_dirsql`] otherwise.
fn spawn_dirsql_with_args(dir: &std::path::Path, port: u16, extra: &[&str]) -> Child {
let mut cmd: StdCommand = std::process::Command::cargo_bin("dirsql")
.expect("`dirsql` binary must be built by `cargo test` with --features cli");
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost");
for a in extra {
cmd.arg(a);
}
cmd.current_dir(dir)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
cmd.spawn().expect("spawning dirsql failed")
/// appended (e.g. `--persist`).
fn spawn_dirsql_with_args<S: AsRef<OsStr>>(
dir: &std::path::Path,
port: u16,
extra: &[S],
) -> ServerGuard {
server::spawn_server(dir, port, extra)
}

/// Block until the server answers `GET /query` (or times out).
Expand All @@ -135,7 +118,7 @@ fn wait_until_ready(port: u16, timeout: Duration) {
panic!("dirsql server did not become ready on port {port} within {timeout:?}");
}

fn kill_and_wait(mut child: Child) {
fn kill_and_wait(mut child: ServerGuard) {
// Prefer polite shutdown; fall back to kill if the child hangs.
#[cfg(unix)]
{
Expand Down Expand Up @@ -1382,19 +1365,9 @@ on-file = "printf '[{}]'"
.unwrap();

let port = free_port();
let mut cmd: StdCommand =
std::process::Command::cargo_bin("dirsql").expect("binary must exist");
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.arg("--config")
.arg(elsewhere.path().join(".dirsql.toml"))
.current_dir(cwd.path())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let child = cmd.spawn().expect("spawn");
let config = elsewhere.path().join(".dirsql.toml");
let child =
spawn_dirsql_with_args(cwd.path(), port, &["--config".as_ref(), config.as_os_str()]);
wait_until_ready(port, Duration::from_secs(10));

let resp = Client::new()
Expand All @@ -1421,19 +1394,12 @@ fn explicit_config_flag_loads_the_named_config() {
let elsewhere = TempDir::new().unwrap();

let port = free_port();
let mut cmd: StdCommand =
std::process::Command::cargo_bin("dirsql").expect("binary must exist");
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.arg("--config")
.arg(fixture.path().join(".dirsql.toml"))
.current_dir(elsewhere.path())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let child = cmd.spawn().expect("spawn");
let config = fixture.path().join(".dirsql.toml");
let child = spawn_dirsql_with_args(
elsewhere.path(),
port,
&["--config".as_ref(), config.as_os_str()],
);
wait_until_ready(port, Duration::from_secs(10));

let resp = Client::new()
Expand All @@ -1453,19 +1419,9 @@ fn short_config_flag_loads_the_named_config() {
let elsewhere = TempDir::new().unwrap();

let port = free_port();
let mut cmd: StdCommand =
std::process::Command::cargo_bin("dirsql").expect("binary must exist");
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.arg("-c")
.arg(fixture.path().join(".dirsql.toml"))
.current_dir(elsewhere.path())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let child = cmd.spawn().expect("spawn");
let config = fixture.path().join(".dirsql.toml");
let child =
spawn_dirsql_with_args(elsewhere.path(), port, &["-c".as_ref(), config.as_os_str()]);
wait_until_ready(port, Duration::from_secs(10));

let resp = Client::new()
Expand Down
34 changes: 15 additions & 19 deletions packages/rust/tests/cli_repeatable_config_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
//! Gated behind `--features cli` like the sibling `cli_e2e.rs`.
#![cfg(feature = "cli")]

#[path = "common/server.rs"]
mod server;

use std::fs;
use std::net::TcpListener;
use std::path::Path;
use std::process::{Child, Command as StdCommand, Stdio};
use std::process::{Child, Command as StdCommand};
use std::time::{Duration, Instant};

use assert_cmd::prelude::*;
Expand Down Expand Up @@ -83,22 +86,16 @@ fn server_serves_tables_from_two_config_flags() {
let cfg_b_path = write_table_config(cfg_b.path(), "beta", "b.json");

let port = free_port();
let mut cmd: StdCommand = std::process::Command::cargo_bin("dirsql")
.expect("`dirsql` binary must be built by `cargo test` with --features cli");
// #662: server flags live under the `server` subcommand now.
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.arg("-c")
.arg(&cfg_a_path)
.arg("-c")
.arg(&cfg_b_path)
.current_dir(data.path())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().expect("spawning dirsql failed");
let mut child = server::spawn_server(
data.path(),
port,
&[
"-c".as_ref(),
cfg_a_path.as_os_str(),
"-c".as_ref(),
cfg_b_path.as_os_str(),
],
);

wait_until_ready_or_exit(&mut child, port, Duration::from_secs(10));

Expand All @@ -118,8 +115,7 @@ fn server_serves_tables_from_two_config_flags() {
assert_eq!(body, vec![json!({"n": 1})], "table {table} must be indexed");
}

let _ = child.kill();
let _ = child.wait();
drop(child);
}

#[test]
Expand Down
71 changes: 71 additions & 0 deletions packages/rust/tests/common/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Spawn `dirsql server` as a child that cannot outlive its test.
//!
//! The guard reaps the server when the test unwinds. A SIGKILLed test process
//! never unwinds, so on Linux the child also asks the kernel to SIGKILL it
//! when its parent thread dies (`PR_SET_PDEATHSIG`).

use std::ffi::OsStr;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::process::{Child, Command, Stdio};

use assert_cmd::prelude::*;

/// A running `dirsql server`, killed and reaped on drop so a panicking test
/// never leaks a process that holds the inherited stderr pipe open.
pub struct ServerGuard(Child);

impl Deref for ServerGuard {
type Target = Child;

fn deref(&self) -> &Child {
&self.0
}
}

impl DerefMut for ServerGuard {
fn deref_mut(&mut self) -> &mut Child {
&mut self.0
}
}

impl Drop for ServerGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}

/// Spawn `dirsql server --port <port> --host localhost` in `cwd` with `extra`
/// args appended. stdout is piped; stderr is inherited so failures surface in
/// test output.
pub fn spawn_server<S: AsRef<OsStr>>(cwd: &Path, port: u16, extra: &[S]) -> ServerGuard {
let mut cmd: Command = Command::cargo_bin("dirsql")
.expect("`dirsql` binary must be built by `cargo test` with --features cli");
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost")
.args(extra)
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
#[cfg(target_os = "linux")]
{
use std::os::unix::process::CommandExt;
#[expect(
unsafe_code,
reason = "pre_exec runs between fork and exec; prctl is async-signal-safe"
)]
unsafe {
cmd.pre_exec(|| {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
ServerGuard(cmd.spawn().expect("spawning dirsql failed"))
}
54 changes: 18 additions & 36 deletions packages/rust/tests/on_file_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@

#![cfg(all(feature = "cli", unix))]

#[path = "common/server.rs"]
mod server;

use std::fs;
use std::net::TcpListener;
use std::process::{Child, Command as StdCommand, Stdio};
use std::time::{Duration, Instant};

use assert_cmd::prelude::*;
use reqwest::{StatusCode, blocking::Client};
use serde_json::{Value, json};
use server::ServerGuard;
use tempfile::TempDir;

fn free_port() -> u16 {
Expand All @@ -28,7 +30,7 @@ fn free_port() -> u16 {
.port()
}

fn spawn_dirsql(dir: &std::path::Path, port: u16) -> Child {
fn spawn_dirsql(dir: &std::path::Path, port: u16) -> ServerGuard {
// Bare `dirsql` no longer auto-loads a cwd `.dirsql.toml` (#602); these
// fixtures place their config at the index root, so pass it explicitly.
let config = dir.join(".dirsql.toml");
Expand All @@ -37,22 +39,15 @@ fn spawn_dirsql(dir: &std::path::Path, port: u16) -> Child {

/// Spawn `dirsql` from working directory `cwd`, optionally pointing `--config`
/// at a config file elsewhere (so the config dir can differ from the cwd).
fn spawn_dirsql_with(cwd: &std::path::Path, config: Option<&std::path::Path>, port: u16) -> Child {
let mut cmd: StdCommand = std::process::Command::cargo_bin("dirsql")
.expect("`dirsql` binary must be built with --features cli");
// #662: the HTTP server is behind the `server` subcommand now.
cmd.arg("server")
.arg("--port")
.arg(port.to_string())
.arg("--host")
.arg("localhost");
if let Some(config) = config {
cmd.arg("--config").arg(config);
fn spawn_dirsql_with(
cwd: &std::path::Path,
config: Option<&std::path::Path>,
port: u16,
) -> ServerGuard {
match config {
Some(config) => server::spawn_server(cwd, port, &["--config".as_ref(), config.as_os_str()]),
None => server::spawn_server::<&str>(cwd, port, &[]),
}
cmd.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
cmd.spawn().expect("spawning dirsql failed")
}

fn wait_until_ready(port: u16, timeout: Duration) {
Expand All @@ -71,21 +66,8 @@ fn wait_until_ready(port: u16, timeout: Duration) {
panic!("dirsql server did not become ready on port {port} within {timeout:?}");
}

fn kill_and_wait(mut child: Child) {
let _ = child.kill();
let _ = child.wait();
}

/// Kills the spawned server on drop, so a panicking test never leaks a process
/// that keeps the inherited stderr pipe open (which would stall the CI step
/// waiting for EOF).
struct ServerGuard(Child);

impl Drop for ServerGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
fn kill_and_wait(child: ServerGuard) {
drop(child);
}

#[test]
Expand Down Expand Up @@ -162,7 +144,7 @@ on-file = "sh abscheck.sh {path}"
.unwrap();

let port = free_port();
let _server = ServerGuard(spawn_dirsql(root.path(), port));
let _server = spawn_dirsql(root.path(), port);
wait_until_ready(port, Duration::from_secs(10));

let resp = Client::new()
Expand Down Expand Up @@ -209,11 +191,11 @@ on-file = "sh abscheck.sh {path}"
.unwrap();

let port = free_port();
let _server = ServerGuard(spawn_dirsql_with(
let _server = spawn_dirsql_with(
project.path(),
Some(&configdir.path().join(".dirsql.toml")),
port,
));
);
wait_until_ready(port, Duration::from_secs(10));

let resp = Client::new()
Expand Down
Loading
Loading