vim test redux (#11709)

This cleans up the neovim-backed vim tests:
- removed exempted tests (we'll rely on bug reports to find missing edge
cases)
- moved all assertions into non-async fn's so that failures are
reporting on the right file/line
- removed the NeovimBackedBindingTestContext
- renamed a few things to make them clearer
- reduced the number of permutations tested in some cases to reduce
slowest test from 60s to 5s

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2024-05-11 12:04:05 -06:00 committed by GitHub
parent 48cba328f2
commit f550f23b97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2311 additions and 6505 deletions

View file

@ -1,4 +1,3 @@
use editor::test::editor_test_context::ContextHandle;
use gpui::{px, size, BorrowAppContext, Context};
use indoc::indoc;
use settings::SettingsStore;
@ -7,57 +6,132 @@ use std::{
panic, thread,
};
use collections::{HashMap, HashSet};
use language::language_settings::{AllLanguageSettings, SoftWrap};
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use super::{neovim_connection::NeovimConnection, VimTestContext};
use crate::state::Mode;
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
/// Enum representing features we have tests for but which don't work, yet. Used
/// to add exemptions and automatically
#[derive(PartialEq, Eq)]
pub enum ExemptionFeatures {
// MOTIONS
// When an operator completes at the end of the file, an extra newline is left
OperatorLastNewlineRemains,
// OBJECTS
// Resulting position after the operation is slightly incorrect for unintuitive reasons.
IncorrectLandingPosition,
// Operator around the text object at the end of the line doesn't remove whitespace.
AroundObjectLeavesWhitespaceAtEndOfLine,
// Sentence object on empty lines
SentenceOnEmptyLines,
// Whitespace isn't included with text objects at the start of the line
SentenceAtStartOfLineWithWhitespace,
// Whitespace around sentences is slightly incorrect when starting between sentences
AroundSentenceStartingBetweenIncludesWrongWhitespace,
// Non empty selection with text objects in visual mode
NonEmptyVisualTextObjects,
// Sentence Doesn't backtrack when its at the end of the file
SentenceAfterPunctuationAtEndOfFile,
}
impl ExemptionFeatures {
pub fn supported(&self) -> bool {
SUPPORTED_FEATURES.contains(self)
}
}
pub struct NeovimBackedTestContext {
cx: VimTestContext,
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
// bindings are exempted. If None, all bindings are ignored for that insertion text.
exemptions: HashMap<String, Option<HashSet<String>>>,
pub(crate) neovim: NeovimConnection,
last_set_state: Option<String>,
recent_keystrokes: Vec<String>,
}
is_dirty: bool,
#[derive(Default)]
pub struct SharedState {
neovim: String,
editor: String,
initial: String,
neovim_mode: Mode,
editor_mode: Mode,
recent_keystrokes: String,
}
impl SharedState {
#[track_caller]
pub fn assert_matches(&self) {
if self.neovim != self.editor || self.neovim_mode != self.editor_mode {
panic!(
indoc! {"Test failed (zed does not match nvim behaviour)
# initial state:
{}
# keystrokes:
{}
# neovim ({}):
{}
# zed ({}):
{}"},
self.initial,
self.recent_keystrokes,
self.neovim_mode,
self.neovim,
self.editor_mode,
self.editor,
)
}
}
#[track_caller]
pub fn assert_eq(&mut self, marked_text: &str) {
let marked_text = marked_text.replace('•', " ");
if self.neovim == marked_text
&& self.neovim == self.editor
&& self.neovim_mode == self.editor_mode
{
return;
}
let message = if self.neovim != marked_text {
"Test is incorrect (currently expected != neovim_state)"
} else {
"Editor does not match nvim behaviour"
};
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
{}
# currently expected:
{}
# neovim ({}):
{}
# zed ({}):
{}"},
message,
self.initial,
self.recent_keystrokes,
marked_text.replace(" \n", "\n"),
self.neovim_mode,
self.neovim.replace(" \n", "\n"),
self.editor_mode,
self.editor.replace(" \n", "\n"),
)
}
}
pub struct SharedClipboard {
neovim: String,
editor: String,
state: SharedState,
}
impl SharedClipboard {
#[track_caller]
pub fn assert_eq(&self, expected: &str) {
if expected == self.neovim && self.neovim == self.editor {
return;
}
let message = if expected == self.neovim {
"Test is incorrect (currently expected != neovim_state)"
} else {
"Editor does not match nvim behaviour"
};
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
{}
# currently expected:
{}
# neovim clipboard:
{}
# zed clipboard:
{}"},
message,
self.state.initial,
self.state.recent_keystrokes,
expected,
self.neovim,
self.editor
)
}
}
impl NeovimBackedTestContext {
@ -78,49 +152,13 @@ impl NeovimBackedTestContext {
.to_string();
Self {
cx: VimTestContext::new(cx, true).await,
exemptions: Default::default(),
neovim: NeovimConnection::new(test_name).await,
last_set_state: None,
recent_keystrokes: Default::default(),
is_dirty: false,
}
}
pub fn add_initial_state_exemptions(
&mut self,
marked_positions: &str,
missing_feature: ExemptionFeatures, // Feature required to support this exempted test case
) {
if !missing_feature.supported() {
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
for cursor_offset in cursor_offsets.iter() {
let mut marked_text = unmarked_text.clone();
marked_text.insert(*cursor_offset, 'ˇ');
// None represents all key bindings being exempted for that initial state
self.exemptions.insert(marked_text, None);
}
}
}
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
self.neovim.send_keystroke(keystroke_text).await;
self.simulate_keystroke(keystroke_text)
}
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
&mut self,
keystroke_texts: [&str; COUNT],
) {
for keystroke_text in keystroke_texts.into_iter() {
self.recent_keystrokes.push(keystroke_text.to_string());
self.neovim.send_keystroke(keystroke_text).await;
}
self.simulate_keystrokes(keystroke_texts);
}
pub async fn set_shared_state(&mut self, marked_text: &str) {
let mode = if marked_text.contains('»') {
Mode::Visual
@ -131,7 +169,21 @@ impl NeovimBackedTestContext {
self.last_set_state = Some(marked_text.to_string());
self.recent_keystrokes = Vec::new();
self.neovim.set_state(marked_text).await;
self.is_dirty = true;
}
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
for keystroke_text in keystroke_texts.split(' ') {
self.recent_keystrokes.push(keystroke_text.to_string());
self.neovim.send_keystroke(keystroke_text).await;
}
self.simulate_keystrokes(keystroke_texts);
}
#[must_use]
pub async fn simulate(&mut self, keystrokes: &str, initial_state: &str) -> SharedState {
self.set_shared_state(initial_state).await;
self.simulate_shared_keystrokes(keystrokes).await;
self.shared_state().await
}
pub async fn set_shared_wrap(&mut self, columns: u32) {
@ -186,241 +238,51 @@ impl NeovimBackedTestContext {
self.neovim.set_option(option).await;
}
pub async fn assert_shared_state(&mut self, marked_text: &str) {
self.is_dirty = false;
let marked_text = marked_text.replace('•', " ");
let neovim = self.neovim_state().await;
let neovim_mode = self.neovim_mode().await;
let editor = self.editor_state();
let editor_mode = self.mode();
if neovim == marked_text && neovim == editor && neovim_mode == editor_mode {
return;
}
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
let message = if neovim != marked_text {
"Test is incorrect (currently expected != neovim_state)"
} else {
"Editor does not match nvim behaviour"
};
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
{}
# currently expected:
{}
# neovim ({}):
{}
# zed ({}):
{}"},
message,
initial_state,
self.recent_keystrokes.join(" "),
marked_text.replace(" \n", "\n"),
neovim_mode,
neovim.replace(" \n", "\n"),
editor_mode,
editor.replace(" \n", "\n"),
)
}
pub async fn assert_shared_clipboard(&mut self, text: &str) {
let neovim = self.neovim.read_register('"').await;
let editor = self.read_from_clipboard().unwrap().text().clone();
if text == neovim && text == editor {
return;
}
let message = if neovim != text {
"Test is incorrect (currently expected != neovim)"
} else {
"Editor does not match nvim behaviour"
};
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
{}
# currently expected:
{}
# neovim clipboard:
{}
# zed clipboard:
{}"},
message,
initial_state,
self.recent_keystrokes.join(" "),
text,
neovim,
editor
)
}
pub async fn neovim_state(&mut self) -> String {
self.neovim.marked_text().await
}
pub async fn neovim_mode(&mut self) -> Mode {
self.neovim.mode().await.unwrap()
}
pub async fn assert_shared_mode(&mut self, mode: Mode) {
let neovim = self.neovim_mode().await;
let editor = self.cx.mode();
if neovim != mode || editor != mode {
panic!(
indoc! {"Test failed (zed does not match nvim behaviour)
# desired mode:
{:?}
# neovim mode:
{:?}
# zed mode:
{:?}"},
mode, neovim, editor,
)
#[must_use]
pub async fn shared_clipboard(&mut self) -> SharedClipboard {
SharedClipboard {
state: self.shared_state().await,
neovim: self.neovim.read_register('"').await,
editor: self.read_from_clipboard().unwrap().text().clone(),
}
}
pub async fn assert_state_matches(&mut self) {
self.is_dirty = false;
let neovim = self.neovim_state().await;
let neovim_mode = self.neovim_mode().await;
let editor = self.editor_state();
let editor_mode = self.mode();
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
if neovim != editor || neovim_mode != editor_mode {
panic!(
indoc! {"Test failed (zed does not match nvim behaviour)
# initial state:
{}
# keystrokes:
{}
# neovim ({}):
{}
# zed ({}):
{}"},
initial_state,
self.recent_keystrokes.join(" "),
neovim_mode,
neovim,
editor_mode,
editor,
)
#[must_use]
pub async fn shared_state(&mut self) -> SharedState {
let (mode, marked_text) = self.neovim.state().await;
SharedState {
neovim: marked_text,
neovim_mode: mode,
editor: self.editor_state(),
editor_mode: self.mode(),
initial: self
.last_set_state
.as_ref()
.cloned()
.unwrap_or("N/A".to_string()),
recent_keystrokes: self.recent_keystrokes.join(" "),
}
}
pub async fn assert_binding_matches<const COUNT: usize>(
#[must_use]
pub async fn simulate_at_each_offset(
&mut self,
keystrokes: [&str; COUNT],
initial_state: &str,
) {
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
match possible_exempted_keystrokes {
Some(exempted_keystrokes) => {
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
// This keystroke was exempted for this insertion text
return;
}
}
None => {
// All keystrokes for this insertion text are exempted
return;
}
keystrokes: &str,
marked_positions: &str,
) -> SharedState {
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
for cursor_offset in cursor_offsets.iter() {
let mut marked_text = unmarked_text.clone();
marked_text.insert(*cursor_offset, 'ˇ');
let state = self.simulate(keystrokes, &marked_text).await;
if state.neovim != state.editor || state.neovim_mode != state.editor_mode {
return state;
}
}
let _state_context = self.set_shared_state(initial_state).await;
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
self.assert_state_matches().await;
}
pub async fn assert_binding_matches_all<const COUNT: usize>(
&mut self,
keystrokes: [&str; COUNT],
marked_positions: &str,
) {
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
for cursor_offset in cursor_offsets.iter() {
let mut marked_text = unmarked_text.clone();
marked_text.insert(*cursor_offset, 'ˇ');
self.assert_binding_matches(keystrokes, &marked_text).await;
}
}
pub fn each_marked_position(&self, marked_positions: &str) -> Vec<String> {
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
let mut ret = Vec::with_capacity(cursor_offsets.len());
for cursor_offset in cursor_offsets.iter() {
let mut marked_text = unmarked_text.clone();
marked_text.insert(*cursor_offset, 'ˇ');
ret.push(marked_text)
}
ret
}
pub async fn assert_neovim_compatible<const COUNT: usize>(
&mut self,
marked_positions: &str,
keystrokes: [&str; COUNT],
) {
self.set_shared_state(&marked_positions).await;
self.simulate_shared_keystrokes(keystrokes).await;
self.assert_state_matches().await;
}
pub async fn assert_matches_neovim<const COUNT: usize>(
&mut self,
marked_positions: &str,
keystrokes: [&str; COUNT],
result: &str,
) {
self.set_shared_state(marked_positions).await;
self.simulate_shared_keystrokes(keystrokes).await;
self.assert_shared_state(result).await;
}
pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
&mut self,
keystrokes: [&str; COUNT],
marked_positions: &str,
feature: ExemptionFeatures,
) {
if SUPPORTED_FEATURES.contains(&feature) {
self.assert_binding_matches_all(keystrokes, marked_positions)
.await
}
}
pub fn binding<const COUNT: usize>(
self,
keystrokes: [&'static str; COUNT],
) -> NeovimBackedBindingTestContext<COUNT> {
NeovimBackedBindingTestContext::new(keystrokes, self)
SharedState::default()
}
}
@ -438,17 +300,6 @@ impl DerefMut for NeovimBackedTestContext {
}
}
// a common mistake in tests is to call set_shared_state when
// you mean asswert_shared_state. This notices that and lets
// you know.
impl Drop for NeovimBackedTestContext {
fn drop(&mut self) {
if self.is_dirty {
panic!("Test context was dropped after set_shared_state before assert_shared_state")
}
}
}
#[cfg(test)]
mod test {
use crate::test::NeovimBackedTestContext;
@ -457,8 +308,8 @@ mod test {
#[gpui::test]
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_state_matches().await;
cx.shared_state().await.assert_matches();
cx.set_shared_state("This is a tesˇt").await;
cx.assert_state_matches().await;
cx.shared_state().await.assert_matches();
}
}