use super::BranchCreationCommand::Branch;
use super::{BranchCreationRegion, parse_branch_creation_region};
use crate::command_branch_table::BRANCH_TABLE;
use crate::command_checkout_table::{CHECKOUT_TABLE, SWITCH_TABLE};
use crate::command_options::OptionError;
use crate::command_test_completion::{git_completion, render_completion};
use crate::command_test_support::{
assert_table_invariants, fixture, git, git_status, git_with_input, os_arguments, output_text,
remove, repository,
};
use std::path::{Path, PathBuf};
use std::process::Output;
const OPTIONS: &str = ";-v;-q;-t;--track=inherit;--no-track;--set-upstream;--no-set-upstream;\
-u main;--set-upstream-to=main;--no-set-upstream-to;--unset-upstream;--color;\
--color=never;-r;-a;--contains;--contains=main;--no-contains=main;--with=main;\
--without=main;--abbrev;--abbrev=7;-d;-D;--no-delete;-m;-M;--no-move;--omit-empty;-c;-C;\
--no-copy;-l;--no-list;--show-current;--create-reflog;--edit-description;\
--delete-merged=nomatch;--dry-run;--no-dry-run;-f;--merged;--merged=main;\
--no-merged=main;--forked=main;--column;--sort=refname;--points-at=main;--no-points-at;\
-i;--recurse-submodules;--no-recurse-submodules;--format=x;--bogus;--no-remotes;-x";
const EVERY_COUNT: &str = ";-q;-t;-f;-c;-C;-l;-d;-m;--show-current";
const INTERACTING: &str = "-t;--no-track;--set-upstream;--no-set-upstream;-u main;\
--no-set-upstream-to;-r;-a;--contains=main;-d;-D;--no-delete;-m;--no-move;-c;-C;\
--no-copy;-l;--no-list;--show-current;--edit-description;--unset-upstream;\
--delete-merged=nomatch;--dry-run;--no-dry-run;-f;--points-at=main;--no-points-at;\
--recurse-submodules;--no-recurse-submodules;-x";
const PARTNERS: &str = "-c;-a;--set-upstream;--dry-run;--recurse-submodules";
struct Tally {
runs: usize,
created: usize,
refused: usize,
}
#[test]
fn tables_match_git() {
let directory: PathBuf = fixture("branch-tables");
let root: PathBuf = repository(directory.as_path(), "repository");
for (table, command) in [
(BRANCH_TABLE, "branch"),
(CHECKOUT_TABLE, "checkout"),
(SWITCH_TABLE, "switch"),
] {
assert_table_invariants(table);
assert_eq!(
render_completion(table),
git_completion(root.as_path(), &[command]),
"{command}"
);
}
remove(directory.as_path());
}
fn heads(root: &Path) -> Vec<String> {
let mut names: Vec<String> = Vec::<String>::new();
for entry in std::fs::read_dir(root.join(".git/refs/heads")).expect("branch directory") {
let name: std::ffi::OsString = entry.expect("branch entry").file_name();
names.push(name.to_string_lossy().into_owned());
}
names.sort();
return names;
}
fn branch_fixture(name: &str) -> (PathBuf, PathBuf, String) {
let directory: PathBuf = fixture(name);
let root: PathBuf = repository(directory.as_path(), "repository");
git(root.as_path(), &["branch", "other"]);
git(
root.as_path(),
&["commit", "--quiet", "--allow-empty", "--message=second"],
);
git(
root.as_path(),
&["config", "submodule.propagateBranches", "true"],
);
let restore: String = format!(
"update refs/heads/main {}\nupdate refs/heads/other {}\n",
output_text(&git(root.as_path(), &["rev-parse", "main"])),
output_text(&git(root.as_path(), &["rev-parse", "other"])),
);
assert_eq!(heads(root.as_path()), ["main", "other"]);
return (directory, root, restore);
}
fn observe(root: &Path, restore: &str, arguments: &[&str]) -> (bool, Option<i32>) {
let mut full: Vec<&str> = vec!["branch"];
full.extend_from_slice(arguments);
let output: Output = git_status(root, full.as_slice());
let after: Vec<String> = heads(root);
if after == ["main", "other"] {
return (false, output.status.code());
}
let mut script: String = restore.to_owned();
for name in &after {
if name != "main" && name != "other" {
script.push_str(format!("delete refs/heads/{name}\n").as_str());
}
}
let kept: bool =
after.contains(&String::from("main")) && after.contains(&String::from("other"));
if !kept {
git(root, &["symbolic-ref", "HEAD", "refs/heads/main"]);
}
let restored: Output = git_with_input(
root,
os_arguments(&["update-ref", "--stdin"]).as_slice(),
script.as_bytes(),
);
assert!(
restored.status.success(),
"{}",
String::from_utf8_lossy(&restored.stderr)
);
return (kept, output.status.code());
}
fn check(arguments: &[&str], created: bool, code: Option<i32>, tally: &mut Tally) {
let parsed: Result<BranchCreationRegion, OptionError> =
parse_branch_creation_region(Branch, os_arguments(arguments).as_slice(), &[]);
tally.runs += 1;
match parsed {
Ok(region) => {
assert_eq!(region.creates_branch, created, "{arguments:?}");
if created {
tally.created += 1;
}
}
Err(error) => {
assert!(!created, "{arguments:?}");
assert_eq!(code, Some(129), "{arguments:?}: {error}");
tally.refused += 1;
}
}
}
fn joined<'a>(first: &[&'a str], second: &[&'a str]) -> Vec<&'a str> {
let mut all: Vec<&'a str> = first.to_vec();
all.extend_from_slice(second);
return all;
}
fn compare(root: &Path, restore: &str, arguments: &[&str], tally: &mut Tally) {
let (created, code): (bool, Option<i32>) = observe(root, restore, arguments);
check(arguments, created, code, tally);
}
fn compare_two_names(root: &Path, restore: &str, options: &[&str], tally: &mut Tally) {
let forward: Vec<&str> = joined(options, &["fresh", "other"]);
let backward: Vec<&str> = joined(options, &["other", "fresh"]);
let (created_forward, code_forward): (bool, Option<i32>) =
observe(root, restore, forward.as_slice());
let (created_backward, code_backward): (bool, Option<i32>) =
observe(root, restore, backward.as_slice());
let created: bool = created_forward || created_backward;
check(forward.as_slice(), created, code_forward, tally);
check(backward.as_slice(), created, code_backward, tally);
}
#[test]
fn single_branch_options_match_git() {
let (directory, root, restore): (PathBuf, PathBuf, String) = branch_fixture("branch-single");
let mut tally: Tally = Tally {
runs: 0,
created: 0,
refused: 0,
};
let entries: Vec<&str> = OPTIONS.split(';').collect();
for entry in &entries {
let options: Vec<&str> = entry.split_whitespace().collect();
let before: Vec<&str> = joined(options.as_slice(), &["fresh"]);
let after: Vec<&str> = joined(&["fresh"], options.as_slice());
compare(
root.as_path(),
restore.as_str(),
before.as_slice(),
&mut tally,
);
compare(
root.as_path(),
restore.as_str(),
after.as_slice(),
&mut tally,
);
compare_two_names(
root.as_path(),
restore.as_str(),
options.as_slice(),
&mut tally,
);
}
for entry in EVERY_COUNT.split(';') {
let options: Vec<&str> = entry.split_whitespace().collect();
let three: Vec<&str> = joined(options.as_slice(), &["fresh", "other", "extra"]);
compare(
root.as_path(),
restore.as_str(),
options.as_slice(),
&mut tally,
);
compare(
root.as_path(),
restore.as_str(),
three.as_slice(),
&mut tally,
);
assert!(entries.contains(&entry), "{entry}");
}
assert_eq!((tally.runs, tally.created, tally.refused), (248, 116, 12));
remove(directory.as_path());
}
#[test]
fn paired_branch_options_match_git() {
let (directory, root, restore): (PathBuf, PathBuf, String) = branch_fixture("branch-paired");
let mut tally: Tally = Tally {
runs: 0,
created: 0,
refused: 0,
};
for partner in PARTNERS.split(';') {
for entry in INTERACTING.split(';') {
let options: Vec<&str> = entry.split_whitespace().collect();
let before: Vec<&str> = joined(&[partner], options.as_slice());
let after: Vec<&str> = joined(options.as_slice(), &[partner]);
for pair in [before, after] {
let arguments: Vec<&str> = joined(pair.as_slice(), &["fresh"]);
compare(
root.as_path(),
restore.as_str(),
arguments.as_slice(),
&mut tally,
);
}
}
}
assert_eq!((tally.runs, tally.created, tally.refused), (310, 72, 10));
remove(directory.as_path());
}