a73x

tests/trailer_test.rs

Ref:   Size: 4.6 KiB   History

//! One set of tests for the one git-trailer parser.
//!
//! `Issue:` (commit linking) and `Patch:` (merge recording) share
//! `trailer::parse_trailers`; every case below is run against both tokens so a
//! change that fixes one and breaks the other cannot pass.

use git_collab::trailer::parse_trailers;

/// Run one parser case against both tokens that use it. `message` is written
/// with `{}` where the token goes, and `expected` is the list of values the
/// parser must return.
fn both_tokens(message_template: &str, expected: &[&str]) {
    for token in ["issue", "patch"] {
        // Capitalized in the message, lowercase as the token argument: the
        // match is case-insensitive.
        let capitalized = format!("{}{}", token[..1].to_uppercase(), &token[1..]);
        let message = message_template.replace("{}", &capitalized);
        let got = parse_trailers(&message, token);
        assert_eq!(
            got,
            expected.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
            "token {} on message {:?}",
            token,
            message
        );
    }
}

#[test]
fn no_trailer_block() {
    both_tokens("Just a plain commit", &[]);
}

#[test]
fn empty_message() {
    both_tokens("", &[]);
}

#[test]
fn single_trailer_in_pure_block() {
    both_tokens(
        "Fix thing\n\nSome context in the body.\n\n{}: abc",
        &["abc"],
    );
}

#[test]
fn case_and_spacing_variants() {
    for token in ["issue", "patch"] {
        let upper = token.to_uppercase();
        assert_eq!(
            parse_trailers(&format!("subject\n\n{}: abc", token), token),
            vec!["abc".to_string()]
        );
        assert_eq!(
            parse_trailers(&format!("subject\n\n{} : abc", upper), token),
            vec!["abc".to_string()]
        );
        assert_eq!(
            parse_trailers(&format!("subject\n\n  {}:  abc  ", upper), token),
            vec!["abc".to_string()]
        );
    }
}

#[test]
fn two_trailers_in_pure_block() {
    both_tokens("subject\n\n{}: abc\n{}: def", &["abc", "def"]);
}

#[test]
fn trailer_present_but_not_in_final_paragraph() {
    // The final paragraph is the signed-off-by block. It is a valid trailer
    // block, but it carries no matching key, so nothing is extracted.
    both_tokens(
        "subject\n\n{}: abc\n\nSigned-off-by: alice <a@example.com>",
        &[],
    );
}

#[test]
fn wrong_key() {
    for token in ["issue", "patch"] {
        // `Issues:` / `Patchs:` — trailer-shaped, but not our token.
        let msg = format!("subject\n\n{}s: abc", token);
        assert_eq!(parse_trailers(&msg, token), Vec::<String>::new());
    }
}

#[test]
fn prose_mention() {
    for token in ["issue", "patch"] {
        let msg = format!("subject\n\nthis fixes {} abc in the body", token);
        assert_eq!(parse_trailers(&msg, token), Vec::<String>::new());
    }
}

#[test]
fn single_paragraph_whole_message_is_trailer_block() {
    both_tokens("{}: abc", &["abc"]);
}

#[test]
fn mixed_final_paragraph_rejects_all() {
    // A prose line in the final paragraph disqualifies the whole paragraph.
    both_tokens("subject\n\nThanks to Bob for the catch.\n{}: a3f9", &[]);
}

#[test]
fn trailing_whitespace_paragraph_does_not_shadow_trailer_block() {
    both_tokens("subject\n\n{}: abc\n\n   \n", &["abc"]);
}

#[test]
fn pure_block_with_mixed_keys() {
    both_tokens(
        "subject\n\nSigned-off-by: alice <a@example.com>\n{}: abc",
        &["abc"],
    );
}

#[test]
fn rejects_value_with_interior_whitespace() {
    // `Patch: abc merged by me` must parse to *nothing*, not silently to
    // `abc`. Silently truncating would record a merge the author did not
    // name; parsing to nothing makes the ignored commentary visible.
    both_tokens("subject\n\n{}: abc merged by me", &[]);
    both_tokens("subject\n\n{}: abc fixes thing", &[]);
}

#[test]
fn rejects_empty_value() {
    both_tokens("subject\n\n{}:   ", &[]);
}

#[test]
fn a_trailer_for_the_other_token_is_not_matched() {
    // The two tokens genuinely select: an `Issue:` line is not a `Patch:` one.
    let msg = "subject\n\nIssue: abc\nPatch: def";
    assert_eq!(parse_trailers(msg, "issue"), vec!["abc".to_string()]);
    assert_eq!(parse_trailers(msg, "patch"), vec!["def".to_string()]);
}

#[test]
fn squashed_message_keeps_only_the_final_trailer_block() {
    // `git merge --squash` concatenates the source messages. Only the last
    // commit's trailer block is the final paragraph — which is fine, because
    // every commit of a patch carries the same id.
    let msg = "\
Squashed commit of the following:

first commit

Patch: aaaa1111

second commit

Patch: aaaa1111";
    assert_eq!(parse_trailers(msg, "patch"), vec!["aaaa1111".to_string()]);
}