290 lines
9.7 KiB
Rust
290 lines
9.7 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use vamp::{
|
|
diff::{Category, Phase, RevisionTarget},
|
|
editor::Input,
|
|
session::EditorSession,
|
|
};
|
|
|
|
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
|
|
struct Fixture {
|
|
root: PathBuf,
|
|
target: String,
|
|
decoy: String,
|
|
}
|
|
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"vamp-diff-search-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed),
|
|
));
|
|
fs::create_dir_all(root.join("repo")).unwrap();
|
|
let mut fixture = Self {
|
|
root,
|
|
target: String::new(),
|
|
decoy: String::new(),
|
|
};
|
|
fixture.git(&["init", "-q", "--initial-branch=fixture-main"]);
|
|
fixture.commit("foundation", "base");
|
|
fixture.target = fixture.commit("Über parser target", "target");
|
|
fixture.decoy = fixture.commit(
|
|
&format!("release archaeology mentioning {}", fixture.target),
|
|
"decoy",
|
|
);
|
|
for name in [
|
|
"a-r_e_l_e_a_s_e",
|
|
"release-candidate",
|
|
"zrelease",
|
|
"tie-a",
|
|
"tie-b",
|
|
] {
|
|
fixture.git(&["branch", name, &fixture.decoy]);
|
|
fixture.git(&["tag", name, &fixture.decoy]);
|
|
}
|
|
for name in ["release", "feature/parser-rewrite", "feature/Über-parser"] {
|
|
fixture.git(&["branch", name, &fixture.target]);
|
|
fixture.git(&["tag", name, &fixture.target]);
|
|
}
|
|
fixture
|
|
}
|
|
|
|
fn git(&self, args: &[&str]) -> String {
|
|
let output = Command::new("git")
|
|
.args(args)
|
|
.current_dir(self.root.join("repo"))
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
|
.env("GIT_TERMINAL_PROMPT", "0")
|
|
.env("GIT_AUTHOR_NAME", "Diff Search Fixture")
|
|
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
|
|
.env("GIT_COMMITTER_NAME", "Diff Search Fixture")
|
|
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
|
|
.env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z")
|
|
.env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"git {args:?}: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
String::from_utf8(output.stdout).unwrap().trim().to_owned()
|
|
}
|
|
|
|
fn commit(&self, message: &str, content: &str) -> String {
|
|
fs::write(self.root.join("repo/source.txt"), content).unwrap();
|
|
self.git(&["add", "source.txt"]);
|
|
self.git(&["commit", "-qm", message]);
|
|
self.git(&["rev-parse", "HEAD"])
|
|
}
|
|
|
|
fn session(&self) -> EditorSession {
|
|
let config = self.root.join("config.rb");
|
|
fs::write(
|
|
&config,
|
|
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\nend\n",
|
|
)
|
|
.unwrap();
|
|
let mut session = EditorSession::open(
|
|
Some(self.root.join("repo/source.txt")),
|
|
vec![],
|
|
Some(&config),
|
|
)
|
|
.unwrap();
|
|
assert!(session.editor().startup_diagnostic().is_none());
|
|
session.execute(":diff");
|
|
wait_until(&mut session, |session| {
|
|
session
|
|
.editor()
|
|
.diff()
|
|
.is_some_and(|diff| diff.catalog.is_some() && !diff.loading)
|
|
});
|
|
session
|
|
}
|
|
}
|
|
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.root);
|
|
}
|
|
}
|
|
|
|
fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> bool) {
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while !ready(session) {
|
|
session.poll();
|
|
assert!(Instant::now() < deadline, "{:?}", session.editor().diff());
|
|
if let Some(diff) = session.editor().diff() {
|
|
assert!(diff.error.is_none(), "{:?}", diff.error);
|
|
}
|
|
thread::sleep(Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
fn query(session: &mut EditorSession, text: &str) {
|
|
session.handle(Input::Ctrl('u'));
|
|
for ch in text.chars() {
|
|
session.handle(Input::Char(ch));
|
|
}
|
|
}
|
|
|
|
fn category(session: &mut EditorSession, category: Category) {
|
|
query(session, "");
|
|
for _ in 0..3 {
|
|
let diff = session.editor().diff().unwrap();
|
|
if diff.catalog.as_ref().unwrap().kind.categories()[diff.category] == category {
|
|
return;
|
|
}
|
|
session.handle(Input::Char('\t'));
|
|
}
|
|
panic!("category unavailable: {category:?}");
|
|
}
|
|
|
|
fn labels(session: &EditorSession) -> Vec<String> {
|
|
let diff = session.editor().diff().unwrap();
|
|
assert_eq!(
|
|
diff.entries().next().unwrap().target,
|
|
RevisionTarget::Working
|
|
);
|
|
diff.entries()
|
|
.skip(1)
|
|
.map(|entry| entry.label.clone())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn branch_and_tag_fuzzy_ranking_and_empty_order_match_in_both_selectors() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.session();
|
|
for phase in [Phase::Before, Phase::After] {
|
|
assert_eq!(session.editor().diff().unwrap().phase, phase);
|
|
for kind in [Category::Branches, Category::Tags] {
|
|
category(&mut session, kind);
|
|
let catalogue_order = labels(&session);
|
|
query(&mut session, " ");
|
|
assert_eq!(labels(&session), catalogue_order);
|
|
|
|
query(&mut session, "release");
|
|
assert_eq!(
|
|
labels(&session),
|
|
[
|
|
"release",
|
|
"release-candidate",
|
|
"zrelease",
|
|
"a-r_e_l_e_a_s_e"
|
|
]
|
|
);
|
|
query(&mut session, "ftprw");
|
|
assert_eq!(labels(&session), ["feature/parser-rewrite"]);
|
|
query(&mut session, "tie");
|
|
let ties: Vec<_> = catalogue_order
|
|
.iter()
|
|
.filter(|label| label.starts_with("tie-"))
|
|
.cloned()
|
|
.collect();
|
|
assert_eq!(&labels(&session)[..ties.len()], ties);
|
|
query(&mut session, "no-such-reference-anywhere");
|
|
assert!(labels(&session).is_empty());
|
|
assert_eq!(session.editor().diff().unwrap().selected, 0);
|
|
}
|
|
if phase == Phase::Before {
|
|
// Even an unmatched query leaves Working selectable and pinned first.
|
|
session.handle(Input::Enter);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_tokens_and_commit_ids_search_fields_without_cross_field_subsequences() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.session();
|
|
for phase in [Phase::Before, Phase::After] {
|
|
assert_eq!(session.editor().diff().unwrap().phase, phase);
|
|
category(&mut session, Category::Branches);
|
|
for text in [
|
|
format!("{} ÜBPR", &fixture.target[..10]),
|
|
format!("übpr {}", &fixture.target[..10]),
|
|
] {
|
|
query(&mut session, &text);
|
|
assert_eq!(labels(&session), ["feature/Über-parser"]);
|
|
}
|
|
|
|
category(&mut session, Category::Commits);
|
|
for text in [&fixture.target[..], &fixture.target[..10]] {
|
|
query(&mut session, text);
|
|
assert_eq!(
|
|
labels(&session),
|
|
[fixture.target.clone(), fixture.decoy.clone()]
|
|
);
|
|
}
|
|
query(&mut session, "ÜB PRSR");
|
|
assert_eq!(labels(&session), [fixture.target.clone()]);
|
|
// The ID and description can satisfy separate tokens, but not one token
|
|
// that would require a subsequence crossing between unrelated fields.
|
|
query(&mut session, &format!("{} übpr", &fixture.target[..10]));
|
|
assert_eq!(labels(&session), [fixture.target.clone()]);
|
|
query(&mut session, &format!("{}über", fixture.target));
|
|
assert!(labels(&session).is_empty());
|
|
if phase == Phase::Before {
|
|
session.handle(Input::Enter);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn selecting_ranked_branch_uses_its_actual_commit_in_before_and_after() {
|
|
let fixture = Fixture::new();
|
|
for ranked_phase in [Phase::Before, Phase::After] {
|
|
let mut session = fixture.session();
|
|
if ranked_phase == Phase::After {
|
|
session.handle(Input::Enter);
|
|
}
|
|
category(&mut session, Category::Branches);
|
|
query(&mut session, "release");
|
|
assert_eq!(labels(&session)[0], "release");
|
|
session.handle(Input::Down);
|
|
session.handle(Input::Enter);
|
|
if ranked_phase == Phase::Before {
|
|
// Working selects snapshot mode rather than merge-base mode.
|
|
session.handle(Input::Enter);
|
|
}
|
|
wait_until(&mut session, |session| {
|
|
session
|
|
.editor()
|
|
.diff()
|
|
.is_some_and(|diff| diff.phase == Phase::View && diff.selected_file_loaded())
|
|
});
|
|
let diff = session.editor().diff().unwrap();
|
|
let ranked = if ranked_phase == Phase::Before {
|
|
diff.before.as_ref().unwrap()
|
|
} else {
|
|
diff.after.as_ref().unwrap()
|
|
};
|
|
assert_eq!(ranked.label, "release");
|
|
assert_eq!(
|
|
ranked.target,
|
|
RevisionTarget::Commit(fixture.target.clone())
|
|
);
|
|
let comparison = diff.comparison.as_ref().unwrap();
|
|
assert_eq!(comparison.files.len(), 1);
|
|
assert_eq!(comparison.files[0].path, Path::new("source.txt"));
|
|
let (before, after) = if ranked_phase == Phase::Before {
|
|
(["target"], ["decoy"])
|
|
} else {
|
|
(["decoy"], ["target"])
|
|
};
|
|
assert_eq!(comparison.files[0].before, before);
|
|
assert_eq!(comparison.files[0].after, after);
|
|
}
|
|
}
|