Improve release and benchmark tooling
This commit is contained in:
+139
-11
@@ -2,6 +2,8 @@ use anyhow::{Context, Result, anyhow};
|
||||
use clap::Parser;
|
||||
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -63,6 +65,12 @@ struct Args {
|
||||
/// Emit machine-readable JSON (one object per metric, with raw samples).
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
/// Emit a publishable Markdown benchmark report.
|
||||
#[arg(long)]
|
||||
markdown: bool,
|
||||
/// Write the report to a file instead of stdout.
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
/// Optional label printed in summary/JSON output (e.g. machine/OS identifier).
|
||||
#[arg(long)]
|
||||
label: Option<String>,
|
||||
@@ -179,11 +187,14 @@ fn main() -> Result<()> {
|
||||
results.push(MetricSamples::new("mosh_start_true_ms", mosh_times));
|
||||
}
|
||||
|
||||
if args.json {
|
||||
print_json(&args, &results);
|
||||
let report = if args.json {
|
||||
render_json(&args, &results)
|
||||
} else if args.markdown {
|
||||
render_markdown(&args, &results)
|
||||
} else {
|
||||
print_table(&args, &results);
|
||||
}
|
||||
render_table(&args, &results)
|
||||
};
|
||||
write_report(&args, &report)?;
|
||||
|
||||
run_assertions(&args, &results)?;
|
||||
Ok(())
|
||||
@@ -560,17 +571,20 @@ fn percentile(sorted: &[f64], pct: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_table(args: &Args, results: &[MetricSamples]) {
|
||||
fn render_table(args: &Args, results: &[MetricSamples]) -> String {
|
||||
let mut out = String::new();
|
||||
if let Some(label) = &args.label {
|
||||
println!("# label: {label}");
|
||||
let _ = writeln!(out, "# label: {label}");
|
||||
}
|
||||
println!(
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"{:<24} {:>6} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
||||
"metric", "n", "min", "median", "p95", "mean", "max"
|
||||
);
|
||||
for metric in results {
|
||||
let s = metric.stats();
|
||||
println!(
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"{:<24} {:>6} {:>9.2} {:>9.2} {:>9.2} {:>9.2} {:>9.2}",
|
||||
metric.name, s.count, s.min, s.median, s.p95, s.mean, s.max
|
||||
);
|
||||
@@ -578,11 +592,12 @@ fn print_table(args: &Args, results: &[MetricSamples]) {
|
||||
// Raw per-iteration samples, so published numbers can include raw data.
|
||||
for metric in results {
|
||||
let ms: Vec<String> = metric.ms().iter().map(|v| format!("{v:.2}")).collect();
|
||||
println!("{} samples_ms=[{}]", metric.name, ms.join(", "));
|
||||
let _ = writeln!(out, "{} samples_ms=[{}]", metric.name, ms.join(", "));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn print_json(args: &Args, results: &[MetricSamples]) {
|
||||
fn render_json(args: &Args, results: &[MetricSamples]) -> String {
|
||||
let mut entries = Vec::new();
|
||||
for metric in results {
|
||||
let s = metric.stats();
|
||||
@@ -603,11 +618,58 @@ fn print_json(args: &Args, results: &[MetricSamples]) {
|
||||
Some(label) => format!("\"{}\"", label.replace('"', "\\\"")),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
println!(
|
||||
format!(
|
||||
"{{\"label\":{label},\"iterations\":{},\"metrics\":[{}]}}",
|
||||
args.iterations.max(1),
|
||||
entries.join(",")
|
||||
)
|
||||
}
|
||||
|
||||
fn render_markdown(args: &Args, results: &[MetricSamples]) -> String {
|
||||
let mut out = String::new();
|
||||
let label = args.label.as_deref().unwrap_or("Dosh benchmark");
|
||||
let _ = writeln!(out, "# {label}\n");
|
||||
let _ = writeln!(out, "- server: `{}`", args.server);
|
||||
let _ = writeln!(out, "- iterations: `{}`", args.iterations.max(1));
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"- generated_by: `dosh-bench {}`\n",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"| metric | n | min ms | median ms | p95 ms | mean ms | max ms |"
|
||||
);
|
||||
let _ = writeln!(out, "|---|---:|---:|---:|---:|---:|---:|");
|
||||
for metric in results {
|
||||
let s = metric.stats();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"| `{}` | {} | {:.2} | {:.2} | {:.2} | {:.2} | {:.2} |",
|
||||
metric.name, s.count, s.min, s.median, s.p95, s.mean, s.max
|
||||
);
|
||||
}
|
||||
let _ = writeln!(out, "\n## Raw Samples\n");
|
||||
for metric in results {
|
||||
let ms: Vec<String> = metric.ms().iter().map(|v| format!("{v:.2}")).collect();
|
||||
let _ = writeln!(out, "- `{}`: [{}]", metric.name, ms.join(", "));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn write_report(args: &Args, report: &str) -> Result<()> {
|
||||
if let Some(path) = &args.output {
|
||||
if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
fs::write(path, report).with_context(|| format!("write {}", path.display()))?;
|
||||
println!("wrote {}", path.display());
|
||||
} else {
|
||||
println!("{report}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metric_mean(results: &[MetricSamples], name: &str) -> Option<f64> {
|
||||
@@ -677,3 +739,69 @@ fn default_client_path() -> PathBuf {
|
||||
.and_then(|path| path.parent().map(|parent| parent.join("dosh-client")))
|
||||
.unwrap_or_else(|| PathBuf::from("dosh-client"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn args_for_render() -> Args {
|
||||
Args {
|
||||
server: "local".to_string(),
|
||||
session: "default".to_string(),
|
||||
ssh_port: 22,
|
||||
dosh_port: 50000,
|
||||
dosh_host: None,
|
||||
iterations: 2,
|
||||
local_auth: true,
|
||||
cold_native: false,
|
||||
cached_ticket: false,
|
||||
resume: false,
|
||||
client: None,
|
||||
ssh_auth_command: "~/.local/bin/dosh-auth".to_string(),
|
||||
ssh_key: None,
|
||||
ssh_known_hosts: None,
|
||||
ssh_control_path: None,
|
||||
controlmaster: false,
|
||||
no_cache: false,
|
||||
warm_cache: false,
|
||||
skip_ssh_baseline: true,
|
||||
include_mosh: false,
|
||||
mosh: PathBuf::from("mosh"),
|
||||
mosh_server_command: "mosh-server".to_string(),
|
||||
mosh_port: None,
|
||||
json: false,
|
||||
markdown: true,
|
||||
output: None,
|
||||
label: Some("test host".to_string()),
|
||||
assert_ssh_plus_ms: None,
|
||||
assert_mosh_minus_ms: None,
|
||||
assert_dosh_max_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_report_contains_summary_and_samples() {
|
||||
let args = args_for_render();
|
||||
let metrics = vec![MetricSamples::new(
|
||||
"dosh_cached_attach_ms",
|
||||
vec![Duration::from_millis(3), Duration::from_millis(5)],
|
||||
)];
|
||||
let report = render_markdown(&args, &metrics);
|
||||
assert!(report.contains("# test host"));
|
||||
assert!(report.contains("| `dosh_cached_attach_ms` | 2 | 3.00"));
|
||||
assert!(report.contains("- `dosh_cached_attach_ms`: [3.00, 5.00]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_report_contains_metrics() {
|
||||
let args = args_for_render();
|
||||
let metrics = vec![MetricSamples::new(
|
||||
"dosh_local_attach_ms",
|
||||
vec![Duration::from_millis(7)],
|
||||
)];
|
||||
let report = render_json(&args, &metrics);
|
||||
assert!(report.contains("\"label\":\"test host\""));
|
||||
assert!(report.contains("\"metric\":\"dosh_local_attach_ms\""));
|
||||
assert!(report.contains("\"samples_ms\":[7.0000]"));
|
||||
}
|
||||
}
|
||||
|
||||
+92
-6
@@ -67,11 +67,11 @@ fn terminal_size() -> (u16, u16) {
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "dosh-client")]
|
||||
#[command(name = "dosh-client", version)]
|
||||
struct Args {
|
||||
#[arg()]
|
||||
server: Option<String>,
|
||||
#[arg()]
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
command: Vec<String>,
|
||||
#[arg(long)]
|
||||
session: Option<String>,
|
||||
@@ -192,7 +192,7 @@ async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
let config = load_client_config(None).unwrap_or_default();
|
||||
if args.server.as_deref() == Some("update") {
|
||||
return run_update(&config);
|
||||
return run_update(&config, update_check_requested(&args.command)?);
|
||||
}
|
||||
if args.server.as_deref() == Some("import-ssh") {
|
||||
return run_import_ssh(&config, &args.command);
|
||||
@@ -1079,13 +1079,75 @@ fn run_sessions_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_update(config: &dosh::config::ClientConfig) -> Result<()> {
|
||||
fn update_check_requested(command: &[String]) -> Result<bool> {
|
||||
match command {
|
||||
[] => Ok(false),
|
||||
[flag] if flag == "--check" || flag == "-n" => Ok(true),
|
||||
_ => Err(anyhow!("usage: dosh update [--check]")),
|
||||
}
|
||||
}
|
||||
|
||||
fn release_artifact_name() -> &'static str {
|
||||
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("macos", "aarch64") => "dosh-macos-aarch64.tar.gz",
|
||||
("macos", "x86_64") => "dosh-macos-x86_64.tar.gz",
|
||||
("linux", "aarch64") => "dosh-linux-aarch64.tar.gz",
|
||||
("linux", "x86_64") => "dosh-linux-x86_64.tar.gz",
|
||||
("windows", "x86_64") => "dosh-windows-x86_64.zip",
|
||||
("windows", "aarch64") => "dosh-windows-aarch64.zip",
|
||||
_ => "dosh-unknown.tar.gz",
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
|
||||
let web = repo
|
||||
.strip_suffix(".git")
|
||||
.unwrap_or(repo)
|
||||
.trim_end_matches('/');
|
||||
if !web.starts_with("http://") && !web.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
Some(format!("{web}/releases/latest/download/{artifact}"))
|
||||
}
|
||||
|
||||
fn url_reachable(url: &str) -> Result<bool> {
|
||||
let status = Command::new("curl")
|
||||
.arg("-fsIL")
|
||||
.arg(url)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.with_context(|| format!("check {url}"))?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<()> {
|
||||
let repo = config
|
||||
.update_repo
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
|
||||
let raw_base = raw_base_from_repo(&repo);
|
||||
let installer = format!("{raw_base}/raw/branch/main/install.sh");
|
||||
let artifact = release_artifact_name();
|
||||
let binary_url = latest_release_download_url(&repo, artifact);
|
||||
if check_only {
|
||||
println!("dosh {}", env!("CARGO_PKG_VERSION"));
|
||||
println!("repo: {repo}");
|
||||
println!("installer: {installer}");
|
||||
if let Some(binary_url) = &binary_url {
|
||||
let status = if url_reachable(binary_url)? {
|
||||
"available"
|
||||
} else {
|
||||
"missing"
|
||||
};
|
||||
println!("prebuilt: {status} ({artifact})");
|
||||
println!("prebuilt_url: {binary_url}");
|
||||
} else {
|
||||
println!("prebuilt: unavailable for non-HTTP repo");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let update_cache = dirs::cache_dir()
|
||||
.unwrap_or_else(|| expand_tilde("~/.cache"))
|
||||
.join("dosh")
|
||||
@@ -4286,12 +4348,12 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
||||
mod tests {
|
||||
use super::{
|
||||
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
||||
RemoteForward, SshConfig, StatusAction, auth_allows,
|
||||
RemoteForward, SshConfig, StatusAction, auth_allows, latest_release_download_url,
|
||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward,
|
||||
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
|
||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
||||
toml_bare_key_or_quoted,
|
||||
toml_bare_key_or_quoted, update_check_requested,
|
||||
};
|
||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||
use dosh::native::EnvVar;
|
||||
@@ -4839,6 +4901,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_check_accepts_only_check_flag() {
|
||||
assert!(!update_check_requested(&[]).unwrap());
|
||||
assert!(update_check_requested(&["--check".to_string()]).unwrap());
|
||||
assert!(update_check_requested(&["-n".to_string()]).unwrap());
|
||||
assert!(update_check_requested(&["--wat".to_string()]).is_err());
|
||||
assert!(update_check_requested(&["--check".to_string(), "extra".to_string()]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_download_url_uses_latest_release_asset() {
|
||||
assert_eq!(
|
||||
latest_release_download_url(
|
||||
"https://git.palav.dev/Palav/dosh.git",
|
||||
"dosh-linux-x86_64.tar.gz"
|
||||
)
|
||||
.unwrap(),
|
||||
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-linux-x86_64.tar.gz"
|
||||
);
|
||||
assert!(
|
||||
latest_release_download_url("git@git.palav.dev:Palav/dosh.git", "artifact").is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_startup_command_expands_global_extension() {
|
||||
let mut config = ClientConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user