use std::{ffi::OsStr, fs};
use rstest_bdd_macros::{given, then, when};
#[path = "git_selection/fixture.rs"]
mod fixture;
pub use fixture::GitSelectionState;
use fixture::{
CLEAN,
RAGGED,
before_bytes,
commit_file,
git,
git_raw,
last_run,
repo_path,
run_directory,
run_once,
snapshot,
write_fixture,
};
#[given("a Git repository containing a committed file {name:string} with a broken table")]
fn repository_with_committed_file(state: &GitSelectionState, name: String) {
let repo = repo_path(state);
fs::write(repo.join(".gitignore"), "build/\n").expect("write .gitignore");
git(&repo, &["init", "--quiet", "-b", "main"]);
git(&repo, &["add", "--", ".gitignore"]);
git(
&repo,
&["commit", "-m", "initialise the fixture repository"],
);
commit_file(state, &name, RAGGED);
}
#[given("a committed file {name:string} with a broken table")]
fn committed_file(state: &GitSelectionState, name: String) { commit_file(state, &name, RAGGED); }
#[given("an untracked file {name:string} with a broken table")]
fn untracked_file(state: &GitSelectionState, name: String) {
write_fixture(&repo_path(state).join(&name), RAGGED);
}
#[given("an ignored file {name:string} with a broken table")]
fn ignored_file(state: &GitSelectionState, name: String) {
let repo = repo_path(state);
write_fixture(&repo.join(&name), RAGGED);
assert!(
git_raw(&repo, &["check-ignore", "--quiet", "--", &name])
.status
.success(),
"{name} must be ignored, or the scenario proves nothing"
);
}
#[cfg(unix)]
#[given("a committed symlink {name:string} pointing at {target:string}")]
fn committed_symlink(state: &GitSelectionState, name: String, target: String) {
let repo = repo_path(state);
let path = repo.join(&name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create the fixture's directory");
}
std::os::unix::fs::symlink(&target, &path).expect("create the fixture symlink");
git(&repo, &["add", "--", &name]);
git(&repo, &["commit", "-m", &format!("add {name}")]);
}
#[given("the file {name:string} is deleted from the working tree")]
fn deleted_from_working_tree(state: &GitSelectionState, name: String) {
fs::remove_file(repo_path(state).join(&name)).expect("delete the fixture");
}
#[given("the directory {name:string} is replaced by a regular file")]
fn directory_replaced_by_a_file(state: &GitSelectionState, name: String) {
let path = repo_path(state).join(&name);
fs::remove_dir_all(&path).expect("remove the fixture directory");
fs::write(&path, "not a directory\n").expect("write the fixture file");
}
#[given("an unresolved merge conflict in the tracked file {name:string}")]
fn unresolved_merge_conflict(state: &GitSelectionState, name: String) {
let repo = repo_path(state);
git(&repo, &["branch", "side"]);
write_fixture(&repo.join(&name), "|A|B|\n|---|---|\n|1|9|\n");
git(&repo, &["commit", "-am", "the main side"]);
git(&repo, &["checkout", "--quiet", "side"]);
write_fixture(&repo.join(&name), "|A|B|\n|---|---|\n|1|3|\n");
git(&repo, &["commit", "-am", "the side branch"]);
git(&repo, &["checkout", "--quiet", "main"]);
let merge = git_raw(&repo, &["merge", "side"]);
assert!(
!merge.status.success(),
"the fixture's merge must conflict for the scenario to mean anything"
);
let conflicted = fs::read_to_string(repo.join(&name)).expect("read the conflicted fixture");
for marker in ["<<<<<<<", "=======", ">>>>>>>"] {
assert!(
conflicted.lines().any(|line| line.starts_with(marker)),
"git must have written a {marker} marker at the start of a line: {conflicted:?}"
);
}
}
#[given("every tracked Markdown file is already formatted")]
fn every_tracked_file_is_formatted(state: &GitSelectionState) {
commit_file(state, "docs/guide.md", CLEAN);
}
#[given("a Git repository containing only the committed file {name:string}")]
fn repository_containing_only(state: &GitSelectionState, name: String) {
let repo = repo_path(state);
for entry in fs::read_dir(&repo).expect("read the fixture repository") {
let entry = entry.expect("read a fixture directory entry");
if entry.file_name() == OsStr::new(".git") {
continue;
}
let path = entry.path();
let removed = if path.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
};
removed.expect("empty the working tree");
}
fs::write(repo.join(".gitignore"), "build/\n").expect("write .gitignore");
write_fixture(&repo.join(&name), RAGGED);
git(&repo, &["add", "-A"]);
git(&repo, &["commit", "-m", "only the named file"]);
}
#[given("the working directory is not inside a Git repository")]
fn outside_a_repository(state: &GitSelectionState) {
let elsewhere = state
.elsewhere
.get_or_insert_with(|| tempfile::tempdir().expect("create temporary directory"));
state.run_dir.set(elsewhere.path().to_path_buf());
}
#[when("I run mdtablefix with {flags:string}")]
fn runs_with(state: &GitSelectionState, flags: String) {
let directory = run_directory(state);
state.before.set(snapshot(&directory));
state.run.set(run_once(&directory, &flags));
}
#[when("I run mdtablefix from {directory:string} with {flags:string}")]
fn runs_from(state: &GitSelectionState, directory: String, flags: String) {
let root = repo_path(state);
let nested = root.join(&directory);
state.before.set(snapshot(&root));
state.run.set(run_once(&nested, &flags));
}
#[then("the command succeeds")]
fn command_succeeds(state: &GitSelectionState) {
let run = last_run(state);
assert_eq!(
run.status, 0,
"the command must succeed, stderr: {}",
run.stderr
);
}
#[then("the command fails")]
fn command_fails(state: &GitSelectionState) {
let run = last_run(state);
assert_ne!(
run.status, 0,
"the command must fail, stdout: {}",
run.stdout
);
}
#[then("the exit status is {expected:i32}")]
fn exit_status_is(state: &GitSelectionState, expected: i32) {
let run = last_run(state);
assert_eq!(run.status, expected, "exit status, stderr: {}", run.stderr);
}
#[then("the command exits with status {expected:i32}")]
fn command_exits_with_status(state: &GitSelectionState, expected: i32) {
exit_status_is(state, expected);
}
#[then("stdout is empty")]
fn stdout_is_empty(state: &GitSelectionState) {
let run = last_run(state);
assert!(
run.stdout.is_empty(),
"standard output was {:?}",
run.stdout
);
}
#[then("stdout is exactly {expected:string}")]
fn stdout_is(state: &GitSelectionState, expected: String) {
let run = last_run(state);
assert_eq!(run.stdout, format!("{expected}\n"), "standard output");
}
#[then("stdout names {name:string}")]
fn stdout_names(state: &GitSelectionState, name: String) {
let run = last_run(state);
assert!(
run.stdout.contains(&name),
"standard output must name {name}: {:?}",
run.stdout
);
}
#[then("stdout contains {text:string}")]
fn stdout_contains(state: &GitSelectionState, text: String) {
let run = last_run(state);
assert!(
run.stdout.contains(&text),
"standard output must contain {text:?}: {:?}",
run.stdout
);
}
#[then("stderr contains {text:string}")]
fn stderr_contains(state: &GitSelectionState, text: String) {
let run = last_run(state);
assert!(
run.stderr.contains(&text),
"standard error must contain {text:?}: {:?}",
run.stderr
);
}
#[then("standard input was not read")]
fn standard_input_was_not_read(state: &GitSelectionState) {
let run = last_run(state);
assert!(
run.stdout.is_empty(),
"standard input carried a document mdtablefix would have printed had it read standard \
input: {:?}",
run.stdout
);
}
#[then("the file {name:string} has a reflowed table")]
fn file_has_a_reflowed_table(state: &GitSelectionState, name: String) {
let path = repo_path(state).join(&name);
let content = fs::read_to_string(&path).expect("read the rewritten fixture");
assert_eq!(content, CLEAN, "{name} must hold the formatted table");
assert_ne!(
content.as_bytes(),
before_bytes(state, &name).as_slice(),
"{name} must actually have changed, or the assertion is vacuous"
);
}
#[then("the file {name:string} is rewritten with its conflict markers intact")]
fn rewritten_with_markers(state: &GitSelectionState, name: String) {
let path = repo_path(state).join(&name);
let content = fs::read(&path).expect("read the rewritten fixture");
assert_ne!(
content,
before_bytes(state, &name),
"{name} must actually have been rewritten, or the scenario proves nothing"
);
let text = String::from_utf8(content).expect("the rewritten fixture is UTF-8");
for marker in ["<<<<<<<", "=======", ">>>>>>>"] {
let lines = text.lines().filter(|line| line.starts_with(marker)).count();
assert_eq!(
lines, 1,
"{name} must still carry exactly one {marker} line: {text:?}"
);
}
}
#[then("the file {name:string} is unchanged")]
fn file_is_unchanged(state: &GitSelectionState, name: String) {
let path = repo_path(state).join(&name);
let content = fs::read(&path).expect("read the fixture");
let expected = before_bytes(state, &name);
assert_eq!(
content,
expected,
"{name} must be byte-identical:\nactual: {:?}\nexpected: {:?}",
String::from_utf8_lossy(&content),
String::from_utf8_lossy(&expected)
);
}
#[then("the file {name:string} is still a symlink")]
fn file_is_still_a_symlink(state: &GitSelectionState, name: String) {
let path = repo_path(state).join(&name);
let metadata = fs::symlink_metadata(&path).expect("read the fixture's metadata");
assert!(
metadata.file_type().is_symlink(),
"{name} must remain a symbolic link, not be replaced by a regular file"
);
}