From d79e97cf3b86c0816904bfe41642af0238b33dfd Mon Sep 17 00:00:00 2001 From: Slack Coder Date: Tue, 4 Apr 2023 16:38:29 -0500 Subject: [PATCH] cli: Note '--version' output as API Consider our command line version output part of our API via a function comment. Test the first two fields of the version output are the program name and a semantic version. --- radicle-cli/src/main.rs | 96 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs index 4ad334c0..a31ed19f 100644 --- a/radicle-cli/src/main.rs +++ b/radicle-cli/src/main.rs @@ -1,4 +1,5 @@ use std::ffi::OsString; +use std::io; use std::{io::ErrorKind, iter, process}; use anyhow::anyhow; @@ -62,16 +63,20 @@ fn parse_args() -> anyhow::Result { Ok(command.unwrap_or_else(|| Command::Other(vec![]))) } -fn print_version() { +/// Print the Radicle CLI's version. +/// +/// Third party applications use it to parse Radicle Cli's version. +fn print_version(mut w: impl std::io::Write) -> anyhow::Result<()> { if VERSION.contains("-dev") { - println!("{NAME} {VERSION}+{GIT_HEAD}") + write!(w, "{NAME} {VERSION}+{GIT_HEAD}")?; } else { - println!("{NAME} {VERSION} ({GIT_HEAD})") + write!(w, "{NAME} {VERSION} ({GIT_HEAD})")?; } + Ok(()) } fn print_help() -> anyhow::Result<()> { - print_version(); + print_version(&mut io::stdout())?; println!("{DESCRIPTION}"); println!(); @@ -81,7 +86,7 @@ fn print_help() -> anyhow::Result<()> { fn run(command: Command) -> Result<(), Option> { match command { Command::Version => { - print_version(); + print_version(&mut io::stdout())?; } Command::Help => { print_help()?; @@ -346,3 +351,84 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option> } Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + fn is_dot_separated_identifier(s: &str) -> bool { + let vs: Vec<_> = s.split('.').collect(); + + if Some(&"") == vs.first() || Some(&"") == vs.last() { + return false; + } + for v in vs { + if v.is_empty() || v.contains(|c: char| !(c.is_ascii_alphanumeric() || c == '-')) { + return false; + } + } + true + } + + /// https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + fn is_semantic_version(s: &str) -> bool { + let (s, build) = s.split_once('+').unwrap_or((s, "")); + let (version_core, pre_release) = s.split_once('-').unwrap_or((s, "")); + + let versions: Vec<_> = version_core.split('.').collect(); + if versions.len() != 3 { + return false; + } + for v in versions { + if v != "0" && (v.get(0..1) == Some("0") || v.parse::().is_err()) { + return false; + } + } + + (pre_release.is_empty() || is_dot_separated_identifier(pre_release)) + && (build.is_empty() || is_dot_separated_identifier(build)) + } + + #[test] + fn test_is_semantic_version() { + assert!(is_semantic_version("0.0.1")); + assert!(is_semantic_version("1.0.0-alpha.1")); + assert!(is_semantic_version("1.0.0-0.3.7")); + assert!(is_semantic_version("1.0.0-x.7.z.92")); + assert!(is_semantic_version("1.0.0-alpha+001")); + assert!(is_semantic_version("1.0.0+20130313144700")); + assert!(is_semantic_version("1.0.0-beta+exp.sha.5114f85")); + assert!(is_semantic_version("1.0.0+21AF26D3----117B344092BD")); + + assert!(!is_semantic_version(""), "empty"); + assert!(!is_semantic_version("1.0"), "too little versions"); + assert!(!is_semantic_version("1.0.01"), "no leading zeroes"); + assert!( + !is_semantic_version("1.0.0-beta+exp..sha.5114f85"), + "dot separated value must be non-empty" + ); + assert!( + !is_semantic_version("1.0.0-beta+exp.sha.5114f85."), + "dot separated value must be non-empty" + ); + assert!( + !is_semantic_version("1.0.0-alpha+001+002"), + "only one '+' allowed" + ); + } + + /// Ensure version output is consistent for consumption by third parties. + #[test] + fn test_version() { + let mut buffer = Vec::new(); + print_version(&mut buffer).unwrap(); + let str = std::str::from_utf8(&buffer).unwrap(); + + let mut strs = str.split(' '); + assert_eq!("rad", strs.next().unwrap_or_default(), "program name"); + assert!( + is_semantic_version(strs.next().unwrap_or_default()), + "semantic version" + ); + } +}