libtmux 0.1.0-alpha.13

Async typed tmux client and object model (alpha)
Documentation
//! Integration tests for tmux version parsing and ordering.

// Helpers outside a test function are not covered by clippy.toml's in-test
// exemptions, and this file has one.
#![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.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.2\n").unwrap();
    let minimum = TmuxVersion::parse_output(b"tmux 3.2a\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_3 = TmuxVersion::parse_output(b"tmux next-3.3\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_3.meets(&TmuxVersion::MIN_SUPPORTED));
    assert!(!next_3_3.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.2 is below 3.2a");
    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());
}

/// `meets` and `has_behavior` are two different, deliberately non-identical
/// rules -- documented at each -- so a test predicting a capability gate must
/// call `has_behavior`, the one [`TmuxVersion::require`] actually refuses
/// with.
///
/// `meets`'s own doc comment says a development identifier "meets only
/// requirements at or below the crate's minimum supported release; they are
/// not promoted to an invented release" -- and that clamp fires for *every*
/// development identifier, `next-X.Y` included, not only a bare `master`.
/// `has_behavior` instead reads `next-X.Y` as the real release `X.Y`, and a
/// bare `master` (no release parses from it at all) as "not yet known to
/// lack this". So `next-3.9` -- the version string this workspace's own
/// tmux-matrix probe binary reports -- disagrees with `meets` for exactly
/// the capabilities this crate gates above the floor: `meets` refuses every
/// one of them, `has_behavior` grants whichever `next-3.9` numerically
/// contains. This is the rule the `commands.rs` tests that predicted a
/// `require`-gated branch with `meets` were reproducing, unnoticed, always
/// taking the "unsupported" branch against that probe.
#[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 prompt_history = ReleaseVersion::new(3, 3, ReleaseSuffix::FINAL);

    // `meets` clamps every development identifier to "no more than the
    // floor", so it refuses a capability above 3.2a regardless of what the
    // build's own next-release number is.
    assert!(!master.meets(&prompt_history));
    assert!(!next_3_9.meets(&prompt_history));
    assert!(!next_3_2.meets(&prompt_history));

    // `has_behavior` computes the real answer instead: `next-3.9` already
    // contains 3.3's behavior, `next-3.2` does not yet, and a bare `master`
    // is taken at its word rather than assumed to lack it.
    assert!(master.has_behavior(&prompt_history));
    assert!(next_3_9.has_behavior(&prompt_history));
    assert!(!next_3_2.has_behavior(&prompt_history));

    // A numbered release is the one shape where the two rules cannot
    // disagree: both reduce to the same `release >= required` comparison.
    assert_eq!(
        numbered.meets(&prompt_history),
        numbered.has_behavior(&prompt_history)
    );
    assert!(numbered.has_behavior(&prompt_history));
}

#[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>();
}

/// `meets` answers the floor question and clamps a development identifier to
/// it, so gating a capability on it refuses a tmux that already has the
/// capability. Its own rustdoc has warned about that from the start, and
/// `SavedLayout::classify` gated the mirrored layout presets on it anyway:
/// on `next-3.9` both presets vanished and an ambiguous prefix read as
/// unique. Prose did not prevent that, so this does. A capability question
/// belongs to `has_behavior` or `require`.
#[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();
            // Doc examples show the trap on purpose.
            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
}