#![allow(clippy::expect_used)]
use std::error::Error as StdError;
use libtmux::{Error, ReleaseSuffix, ReleaseVersion, TmuxVersion};
use static_assertions::assert_impl_all;
assert_impl_all!(Error: StdError, Send, Sync);
assert_impl_all!(ReleaseSuffix: Send, Sync);
assert_impl_all!(ReleaseVersion: Send, Sync);
assert_impl_all!(TmuxVersion: Send, Sync);
#[test]
fn parses_release_and_development_versions() {
let patch_a = ReleaseSuffix::patch('a').expect("a is a lowercase patch suffix");
let patch_b = ReleaseSuffix::patch('b').expect("b is a lowercase patch suffix");
let patch_c = ReleaseSuffix::patch('c').expect("c is a lowercase patch suffix");
let cases: &[(&[u8], &str, Option<ReleaseVersion>)] = &[
(
b"tmux 3.2\n",
"3.2",
Some(ReleaseVersion::new(3, 2, ReleaseSuffix::FINAL)),
),
(
b"renamed-tmux 3.2a\n",
"3.2a",
Some(ReleaseVersion::new(3, 2, patch_a)),
),
(
b"\xfftmux 3.2a\n",
"3.2a",
Some(ReleaseVersion::new(3, 2, patch_a)),
),
(
b"tmux 3.2a\n",
"3.2a",
Some(ReleaseVersion::new(3, 2, patch_a)),
),
(
b"tmux 3.1c\n",
"3.1c",
Some(ReleaseVersion::new(3, 1, patch_c)),
),
(
b"tmux 3.7b\n",
"3.7b",
Some(ReleaseVersion::new(3, 7, patch_b)),
),
(b"tmux master\n", "master", None),
(b"tmux next-3.8\n", "next-3.8", None),
];
for (output, raw, release) in cases {
let parsed = TmuxVersion::parse_output(output).expect("fixture is valid");
assert_eq!(parsed.raw(), *raw);
assert_eq!(parsed.release(), release.as_ref());
assert_eq!(parsed.is_development(), release.is_none());
}
}
#[test]
fn parses_release_suffixes_without_losing_order() {
let rc = TmuxVersion::parse_output(b"tmux 3.7-rc\n").unwrap();
let rc1 = TmuxVersion::parse_output(b"tmux 3.7-rc1\n").unwrap();
let rc2 = TmuxVersion::parse_output(b"tmux 3.7-rc2\n").unwrap();
let rc3 = TmuxVersion::parse_output(b"tmux 3.7-rc3\n").unwrap();
let rc5 = TmuxVersion::parse_output(b"tmux 3.2-rc5\n").unwrap();
let rc_max = TmuxVersion::parse_output(b"tmux 3.7-rc65535\n").unwrap();
let release = TmuxVersion::parse_output(b"tmux 3.7\n").unwrap();
let patch_a = TmuxVersion::parse_output(b"tmux 3.7a\n").unwrap();
let patch_b = TmuxVersion::parse_output(b"tmux 3.7b\n").unwrap();
assert_eq!(rc, rc1);
assert!(rc1 < rc2);
assert!(rc2 < rc3);
assert!(rc3 < release);
assert!(release < patch_a);
assert!(patch_a < patch_b);
assert_eq!(
rc5.release()
.expect("fixture is a release")
.suffix()
.release_candidate_number(),
Some(5),
);
assert_eq!(
rc_max
.release()
.expect("fixture is a release")
.suffix()
.release_candidate_number(),
Some(u16::MAX),
);
}
#[test]
fn compares_numeric_release_components() {
let one_nine = TmuxVersion::parse_output(b"tmux 1.9\n").unwrap();
let one_ten = TmuxVersion::parse_output(b"tmux 1.10\n").unwrap();
assert!(one_nine < one_ten);
}
#[test]
fn release_display_uses_canonical_suffixes() {
let cases: &[(&[u8], &str)] = &[
(b"tmux 3.7-rc\n", "3.7-rc"),
(b"tmux 3.7-rc1\n", "3.7-rc"),
(b"tmux 3.7-rc2\n", "3.7-rc2"),
(b"tmux 3.7\n", "3.7"),
(b"tmux 3.7a\n", "3.7a"),
(b"tmux 1.10\n", "1.10"),
];
for (output, expected) in cases {
let version = TmuxVersion::parse_output(output).expect("fixture is valid");
let release = version.release().expect("fixture is a numbered release");
assert_eq!(release.to_string(), *expected);
}
}
#[test]
fn development_versions_do_not_invent_release_ordering() {
let master = TmuxVersion::parse_output(b"tmux master\n").unwrap();
let release = TmuxVersion::parse_output(b"tmux 3.7\n").unwrap();
assert_eq!(master.partial_cmp(&release), None);
assert_eq!(release.partial_cmp(&master), None);
}
#[test]
fn rejects_malformed_output_and_reports_only_its_length() {
let cases: &[&[u8]] = &[
b"3.7\n",
b"tmux 3.7\nextra\n",
b"tmux \xff\n",
b"tmux 65536.1\n",
b"tmux 3.65536\n",
b"tmux 03.2\n",
b"tmux 3.02\n",
b"tmux 3.7A\n",
b"tmux 3.7a trailing\n",
b"tmux 3.7aa\n",
b"tmux 3.3rc1\n",
b"tmux 3.3a-rc1\n",
b"tmux next-3.3a\n",
b"tmux 3.3a-extra\n",
b"tmux 3.3a-master\n",
b"tmux 3.7-rc0\n",
b"tmux 3.7-rc01\n",
b"tmux 3.7-rc65536\n",
b"tmux 3.7\r\n",
b"tmux 3.7",
b"tmux \n",
b" 3.7\n",
b"tmux 3.7\n",
b"tmux\t3.7\n",
b"tmux 3.7 extra\n",
b"tmux 3.7 \n",
b"tm\x01ux 3.7\n",
b"tmux \x013.7\n",
b"tmux unknown\n",
b"tmux next-\n",
b"tmux next-3\n",
b"tmux next-3.8a\n",
b"tmux next-3..8\n",
b"tmux next--3.8\n",
b"tmux next-3-8\n",
b"tmux next-65536.8\n",
b"tmux next-3.65536\n",
b"tmux next-03.3\n",
b"tmux next-3.03\n",
b"tmux next-3.\xc3\xa9\n",
];
for output in cases {
let error = TmuxVersion::parse_output(output).expect_err("fixture is malformed");
assert_eq!(error.invalid_version_output_len(), Some(output.len()));
}
}
#[test]
fn invalid_output_is_redacted_from_diagnostics() {
let output = b"tmux secret-SENTINEL\n";
let error = TmuxVersion::parse_output(output).expect_err("fixture is malformed");
assert_eq!(error.invalid_version_output_len(), Some(output.len()));
let mut cause: Option<&(dyn StdError + 'static)> = Some(&error);
while let Some(current) = cause {
assert!(!current.to_string().contains("SENTINEL"));
assert!(!format!("{current:?}").contains("SENTINEL"));
cause = current.source();
}
}
#[test]
fn enforces_the_minimum_without_promoting_development_versions() {
let old = TmuxVersion::parse_output(b"tmux 3.3\n").unwrap();
let minimum = TmuxVersion::parse_output(b"tmux 3.3a\n").unwrap();
let master = TmuxVersion::parse_output(b"tmux master\n").unwrap();
let next_3_2 = TmuxVersion::parse_output(b"tmux next-3.2\n").unwrap();
let next_3_4 = TmuxVersion::parse_output(b"tmux next-3.4\n").unwrap();
let next_3_8 = TmuxVersion::parse_output(b"tmux next-3.8\n").unwrap();
let newer_requirement = ReleaseVersion::new(
3,
7,
ReleaseSuffix::patch('b').expect("b is a lowercase patch suffix"),
);
assert!(!old.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(minimum.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(master.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(!master.meets(&newer_requirement));
assert!(!next_3_2.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(next_3_4.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(!next_3_4.meets(&newer_requirement));
assert!(next_3_8.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(!next_3_8.meets(&newer_requirement));
let error = old.ensure_supported().expect_err("3.3 is below 3.3a");
match &error {
Error::UnsupportedTmuxVersion { found, minimum } => {
assert_eq!(found, &old);
assert_eq!(minimum, &TmuxVersion::MIN_SUPPORTED);
}
_ => panic!("expected an unsupported tmux version error"),
}
assert_eq!(error.found_version(), Some(&old));
assert_eq!(error.minimum_version(), Some(&TmuxVersion::MIN_SUPPORTED));
assert!(minimum.ensure_supported().is_ok());
}
#[test]
fn has_behavior_and_meets_disagree_above_the_supported_floor() {
let master = TmuxVersion::parse_output(b"tmux master\n").unwrap();
let next_3_9 = TmuxVersion::parse_output(b"tmux next-3.9\n").unwrap();
let next_3_2 = TmuxVersion::parse_output(b"tmux next-3.2\n").unwrap();
let numbered = TmuxVersion::parse_output(b"tmux 3.7b\n").unwrap();
let capture_trim = ReleaseVersion::new(3, 4, ReleaseSuffix::FINAL);
assert!(!master.meets(&capture_trim));
assert!(!next_3_9.meets(&capture_trim));
assert!(!next_3_2.meets(&capture_trim));
assert!(master.has_behavior(&capture_trim));
assert!(next_3_9.has_behavior(&capture_trim));
assert!(!next_3_2.has_behavior(&capture_trim));
assert_eq!(
numbered.meets(&capture_trim),
numbered.has_behavior(&capture_trim)
);
assert!(numbered.has_behavior(&capture_trim));
}
#[test]
fn release_value_getters_preserve_components() {
let suffix = ReleaseSuffix::patch('c').expect("c is a lowercase patch suffix");
let release = ReleaseVersion::new(3, 1, suffix);
assert_eq!(release.major(), 3);
assert_eq!(release.minor(), 1);
assert_eq!(release.suffix(), suffix);
assert_eq!(suffix.patch_letter(), Some('c'));
assert!(!suffix.is_final());
assert!(!suffix.is_release_candidate());
assert!(ReleaseSuffix::FINAL.is_final());
assert!(ReleaseSuffix::RELEASE_CANDIDATE.is_release_candidate());
assert_eq!(
ReleaseSuffix::release_candidate(2)
.expect("two is a valid release candidate")
.release_candidate_number(),
Some(2),
);
assert_eq!(
ReleaseSuffix::RELEASE_CANDIDATE.release_candidate_number(),
Some(1),
);
assert_eq!(ReleaseSuffix::release_candidate(0), None);
assert_eq!(ReleaseSuffix::patch('A'), None);
assert_eq!(ReleaseSuffix::patch('\u{00e9}'), None);
}
#[test]
fn error_is_a_thread_safe_static_standard_error() {
fn assert_error<T: StdError + Send + Sync + 'static>() {}
assert_error::<Error>();
}
#[test]
fn meets_is_only_ever_asked_about_the_floor() {
let mut offenders = Vec::new();
for entry in walk(std::path::Path::new("src")) {
let source = std::fs::read_to_string(&entry).expect("a readable source file");
for (number, line) in source.lines().enumerate() {
let code = line.trim_start();
if code.starts_with("///") || code.starts_with("//!") || code.starts_with("//") {
continue;
}
if code.contains(".meets(") && !code.contains("MIN_SUPPORTED") {
offenders.push(format!(
"{}:{}: {}",
entry.display(),
number + 1,
code.trim()
));
}
}
}
assert!(
offenders.is_empty(),
"`meets` gates a capability here; use `has_behavior` or `require`:\n{}",
offenders.join("\n"),
);
}
fn walk(directory: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut found = Vec::new();
let entries = std::fs::read_dir(directory).expect("a readable directory");
for entry in entries {
let path = entry.expect("a readable entry").path();
if path.is_dir() {
found.extend(walk(&path));
} else if path.extension().is_some_and(|extension| extension == "rs") {
found.push(path);
}
}
found
}
#[test]
fn undated_diagnostics_do_not_qualify_hard_version_check() {
let version = TmuxVersion::parse_output(b"tmux master\n").unwrap();
assert!(version.meets(&TmuxVersion::MIN_SUPPORTED));
assert!(matches!(
version.ensure_supported(),
Err(Error::UnrecognizedTmuxVersion { .. })
));
}
#[test]
fn no_since_gate_sits_at_or_below_the_supported_floor() {
let source = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/version.rs"))
.expect("version.rs is readable");
let module = source
.split("pub mod since {")
.nth(1)
.expect("the since module exists");
let mut gates = 0;
for piece in module.split("ReleaseVersion::new(").skip(1) {
let mut numbers = piece
.split(',')
.take(2)
.map(|number| number.trim().parse::<u8>().expect("a numeric release"));
let (major, minor) = (numbers.next().unwrap(), numbers.next().unwrap());
assert!(
(major, minor) > (3, 3),
"a gate for {major}.{minor} is always true on a 3.3a floor"
);
gates += 1;
}
assert!(gates > 0, "the scan found no gates");
}