refactor(app): clarify application and binary roles

Rename the application Cargo package from irtt-cli to irtt-rs
(crates/irtt-cli -> crates/irtt-app), and hard-rename its scriptable
client binary from irtt-cli to irtt-client, now that the application
also ships a first-class server and the old name reads as an umbrella
CLI for both roles.

Give each dedicated binary (irtt-client, irtt-tui, irtt-server) its
own entry point instead of funneling through the multicall
dispatcher's argv0/subcommand logic, so a dedicated binary only links
the applet code for the role it implements and always runs that role
regardless of the name it's invoked or copied under. irtt-rs remains
the feature-selectable multicall dispatcher and is now the only binary
that inspects argv0 or subcommands.

Move future application releases to the irtt-rs/vX.Y.Z tag namespace
(regenerated via `dist generate`), and bump the application package to
0.6.0 as a breaking distribution change. Library crate versions are
unaffected.
This commit is contained in:
Nils Andreas Svee committed 2026-08-20 11:44:35 +07:00
1 parent 7129ee0bc5
commit 7bea3dd54e
49 files changed
+365 -213

No files matched your search

+300
View File
@@ -0,0 +1,300 @@
use std::process::{Command, Output};
fn irtt_rs() -> Command {
Command::new(env!("CARGO_BIN_EXE_irtt-rs"))
}
#[cfg(unix)]
fn irtt_rs_with_arg0(arg0: &str) -> Command {
use std::os::unix::process::CommandExt;
let mut command = irtt_rs();
command.arg0(arg0);
command
}
/// Copies a dedicated binary to `name` in a temp dir and returns a `Command`
/// invoking it under that name, proving dedicated binaries decide their role
/// from the entry point they were built with, not from argv0.
#[cfg(all(unix, any(feature = "client", feature = "server")))]
fn copied_binary(cargo_bin_exe: &str, name: &str) -> Command {
let dir = std::env::temp_dir().join(format!(
"irtt-copied-binary-test-{}-{}",
std::process::id(),
name
));
std::fs::create_dir_all(&dir).unwrap();
let dest = dir.join(name);
std::fs::copy(cargo_bin_exe, &dest).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dest).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest, perms).unwrap();
}
Command::new(dest)
}
fn output_text(output: &Output) -> String {
let mut text = String::new();
text.push_str(&String::from_utf8_lossy(&output.stdout));
text.push_str(&String::from_utf8_lossy(&output.stderr));
text
}
#[test]
fn canonical_help_shows_dispatcher_help() {
let output = irtt_rs().arg("--help").output().unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("IRTT-compatible multi-applet dispatcher"));
assert!(text.contains("client"));
assert!(text.contains("tui"));
assert!(text.contains("server"));
}
#[test]
fn canonical_without_applet_errors() {
let output = irtt_rs().output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("choose an applet"), "{text}");
assert!(text.contains("client"));
assert!(text.contains("tui"));
assert!(text.contains("server"));
}
#[cfg(feature = "client")]
#[test]
fn canonical_client_subcommand_dispatches_to_client() {
let output = irtt_rs().args(["client", "--help"]).output().unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
assert!(text.contains("--format <FORMAT>"));
assert!(text.contains("--columns <COLUMNS>"));
}
#[cfg(not(feature = "client"))]
#[test]
fn canonical_client_subcommand_reports_unavailable_when_disabled() {
let output = irtt_rs().args(["client", "--help"]).output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(
text.contains("client applet is not available; rebuild with the client feature"),
"{text}"
);
}
#[cfg(feature = "server")]
#[test]
fn canonical_server_subcommand_dispatches_to_server() {
let output = irtt_rs().args(["server", "--help"]).output().unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible UDP server"));
assert!(text.contains("--bind <ADDR>"));
assert!(text.contains("--max-sessions <COUNT>"));
assert!(text.contains("--idle-timeout <DURATION>"));
assert!(text.contains("--timestamp-allowance <MODE>"));
assert!(text.contains("--no-dscp"));
}
#[cfg(not(feature = "server"))]
#[test]
fn canonical_server_subcommand_reports_unavailable_when_disabled() {
let output = irtt_rs().args(["server", "--help"]).output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(
text.contains("server applet is not available; rebuild with the server feature"),
"{text}"
);
}
#[cfg(feature = "client")]
#[test]
fn dedicated_client_binary_always_enters_the_client_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
assert!(text.contains("--format <FORMAT>"));
}
#[cfg(feature = "tui")]
#[test]
fn dedicated_tui_binary_always_enters_the_tui_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-tui"))
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible TUI client"));
assert!(text.contains("--duration <DURATION>"));
}
#[cfg(feature = "server")]
#[test]
fn dedicated_server_binary_always_enters_the_server_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-server"))
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible UDP server"));
assert!(text.contains("--bind <ADDR>"));
}
#[cfg(all(unix, feature = "client"))]
#[test]
fn renaming_the_dedicated_client_binary_does_not_change_its_role() {
let output = copied_binary(env!("CARGO_BIN_EXE_irtt-client"), "whatever")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
}
#[cfg(all(unix, feature = "server"))]
#[test]
fn renaming_the_dedicated_server_binary_does_not_change_its_role() {
let output = copied_binary(env!("CARGO_BIN_EXE_irtt-server"), "irtt-cli")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible UDP server"));
}
#[cfg(unix)]
#[cfg(feature = "client")]
#[test]
fn client_applet_name_dispatches_to_client() {
let output = irtt_rs_with_arg0("irtt-client")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
assert!(text.contains("--format <FORMAT>"));
assert!(text.contains("--columns <COLUMNS>"));
}
#[cfg(all(unix, not(feature = "client")))]
#[test]
fn client_applet_name_reports_unavailable_when_disabled() {
let output = irtt_rs_with_arg0("irtt-client")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(
text.contains("client applet is not available; rebuild with the client feature"),
"{text}"
);
}
#[cfg(all(unix, feature = "tui"))]
#[test]
fn tui_applet_name_dispatches_to_tui_when_enabled() {
let output = irtt_rs_with_arg0("irtt-tui")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible TUI client"));
assert!(text.contains("--duration <DURATION>"));
}
#[cfg(all(unix, not(feature = "tui")))]
#[test]
fn tui_applet_name_reports_unavailable_when_disabled() {
let output = irtt_rs_with_arg0("irtt-tui")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("TUI applet is not available"), "{text}");
}
#[cfg(all(unix, feature = "server"))]
#[test]
fn server_applet_name_dispatches_to_server_when_enabled() {
let output = irtt_rs_with_arg0("irtt-server")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible UDP server"));
assert!(text.contains("--bind <ADDR>"));
}
#[cfg(all(unix, not(feature = "server")))]
#[test]
fn server_applet_name_reports_unavailable_when_disabled() {
let output = irtt_rs_with_arg0("irtt-server")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("server applet is not available"), "{text}");
}
#[cfg(unix)]
#[test]
fn unknown_irtt_applet_name_errors() {
let output = irtt_rs_with_arg0("irtt-typo").output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("unknown applet name 'irtt-typo'"), "{text}");
assert!(text.contains("irtt-client"));
assert!(text.contains("irtt-tui"));
assert!(text.contains("irtt-server"));
}
#[cfg(unix)]
#[test]
fn old_irtt_cli_binary_name_is_no_longer_a_recognized_applet() {
let output = irtt_rs_with_arg0("irtt-cli").output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("unknown applet name 'irtt-cli'"), "{text}");
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,74 @@
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::mpsc,
thread::{self, JoinHandle},
time::Duration,
};
use irtt_server::{Server, ServerConfig};
use tokio::sync::oneshot;
const STARTUP_TIMEOUT: Duration = Duration::from_secs(2);
pub struct InTreeServer {
pub addr: SocketAddr,
shutdown: Option<oneshot::Sender<()>>,
done: Option<JoinHandle<Result<(), irtt_server::ServerRuntimeError>>>,
}
impl InTreeServer {
pub fn start() -> Self {
let (startup_tx, startup_rx) = mpsc::sync_channel(1);
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let done = thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build in-tree test server runtime");
runtime.block_on(async move {
let bind_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let mut server = Server::bind(bind_addr, ServerConfig::default()).await?;
startup_tx
.send(server.local_addr()?)
.expect("test stopped waiting for in-tree server startup");
server
.run(async {
let _ = shutdown_rx.await;
})
.await
})
});
let addr = match startup_rx.recv_timeout(STARTUP_TIMEOUT) {
Ok(addr) => addr,
Err(error) => {
let _ = shutdown_tx.send(());
let result = done.join().expect("in-tree test server thread panicked");
panic!("in-tree test server failed before startup ({error}): {result:?}");
}
};
Self {
addr,
shutdown: Some(shutdown_tx),
done: Some(done),
}
}
fn stop(&mut self) {
if let Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
if let Some(done) = self.done.take() {
done.join()
.expect("in-tree test server thread panicked")
.expect("in-tree test server stopped with an error");
}
}
}
impl Drop for InTreeServer {
fn drop(&mut self) {
self.stop();
}
}