Add randomized tests for incremental diff

Antonio Scandurra created

Change summary

Cargo.lock                |  4 ++
crates/ai/Cargo.toml      |  5 +++
crates/ai/src/ai.rs       |  8 ++++
crates/ai/src/diff.rs     | 67 ++++++++++++++++++++++++++++++++++++----
crates/ai/src/refactor.rs |  7 +--
5 files changed, 80 insertions(+), 11 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -102,15 +102,19 @@ dependencies = [
  "anyhow",
  "chrono",
  "collections",
+ "ctor",
  "editor",
+ "env_logger 0.9.3",
  "fs",
  "futures 0.3.28",
  "gpui",
  "indoc",
  "isahc",
  "language",
+ "log",
  "menu",
  "project",
+ "rand 0.8.5",
  "regex",
  "schemars",
  "search",

crates/ai/Cargo.toml 🔗

@@ -37,3 +37,8 @@ tiktoken-rs = "0.4"
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+log.workspace = true
+rand.workspace = true

crates/ai/src/ai.rs 🔗

@@ -283,3 +283,11 @@ pub async fn stream_completion(
         }
     }
 }
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}

crates/ai/src/diff.rs 🔗

@@ -204,14 +204,67 @@ impl Diff {
 
 #[cfg(test)]
 mod tests {
+    use std::env;
+
     use super::*;
+    use rand::prelude::*;
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_diffs(mut rng: StdRng) {
+        let old_text_len = env::var("OLD_TEXT_LEN")
+            .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
+            .unwrap_or(10);
+        let new_text_len = env::var("NEW_TEXT_LEN")
+            .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
+            .unwrap_or(10);
+
+        let old = util::RandomCharIter::new(&mut rng)
+            .take(old_text_len)
+            .collect::<String>();
+        log::info!("old text: {:?}", old);
+
+        let mut diff = Diff::new(old.clone());
+        let mut hunks = Vec::new();
+        let mut new_len = 0;
+        let mut new = String::new();
+        while new_len < new_text_len {
+            let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
+            let new_chunk = util::RandomCharIter::new(&mut rng)
+                .take(new_len)
+                .collect::<String>();
+            log::info!("new chunk: {:?}", new_chunk);
+            new_len += new_chunk_len;
+            new.push_str(&new_chunk);
+            let new_hunks = diff.push_new(&new_chunk);
+            log::info!("hunks: {:?}", new_hunks);
+            hunks.extend(new_hunks);
+        }
+        let final_hunks = diff.finish();
+        log::info!("final hunks: {:?}", final_hunks);
+        hunks.extend(final_hunks);
 
-    #[test]
-    fn test_diff() {
-        let mut diff = Diff::new("hello world".to_string());
-        dbg!(diff.push_new("hello"));
-        dbg!(diff.push_new(" ciaone"));
-        // dbg!(diff.push_new(" world"));
-        dbg!(diff.finish());
+        log::info!("new text: {:?}", new);
+        let mut old_ix = 0;
+        let mut new_ix = 0;
+        let mut patched = String::new();
+        for hunk in hunks {
+            match hunk {
+                Hunk::Keep { len } => {
+                    assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
+                    patched.push_str(&old[old_ix..old_ix + len]);
+                    old_ix += len;
+                    new_ix += len;
+                }
+                Hunk::Remove { len } => {
+                    old_ix += len;
+                }
+                Hunk::Insert { text } => {
+                    assert_eq!(text, &new[new_ix..new_ix + text.len()]);
+                    patched.push_str(&text);
+                    new_ix += text.len();
+                }
+            }
+        }
+        assert_eq!(patched, new);
     }
 }

crates/ai/src/refactor.rs 🔗

@@ -78,15 +78,14 @@ impl RefactoringAssistant {
                             }
 
                             let hunks = diff.push_new(&new_text);
-                            hunks_tx.send((hunks, new_text)).await?;
+                            hunks_tx.send(hunks).await?;
                         }
-
-                        hunks_tx.send((diff.finish(), String::new())).await?;
+                        hunks_tx.send(diff.finish()).await?;
 
                         anyhow::Ok(())
                     });
 
-                    while let Some((hunks, new_text)) = hunks_rx.next().await {
+                    while let Some(hunks) = hunks_rx.next().await {
                         editor.update(&mut cx, |editor, cx| {
                             editor.buffer().update(cx, |buffer, cx| {
                                 buffer.start_transaction(cx);