From b9b0d20e8d73db0183a356a49230b6dd23672a12 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 22 May 2023 00:10:33 +0200 Subject: [PATCH] test: Add golden tests infrastructure (#4395) --- Cargo.lock | 44 +++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/test_utils.rs | 66 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3a006e0d..d186d0c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -977,6 +977,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1183,6 +1193,7 @@ dependencies = [ "parking_lot", "percent-encoding", "pgp", + "pretty_assertions", "pretty_env_logger", "proptest", "qrcodegen", @@ -1400,6 +1411,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -3201,6 +3218,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -3545,6 +3571,18 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -5744,6 +5782,12 @@ dependencies = [ "libc", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yasna" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 0ff02606f..02a35fed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,7 @@ proptest = { version = "1", default-features = false, features = ["std"] } tempfile = "3" testdir = "0.7.3" tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] } +pretty_assertions = "1.3.0" [workspace] members = [ diff --git a/src/test_utils.rs b/src/test_utils.rs index 4dc780371..d6278e206 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -3,8 +3,10 @@ //! This private module is only compiled for test runs. #![allow(clippy::indexing_slicing)] use std::collections::BTreeMap; +use std::fmt::Write; use std::ops::{Deref, DerefMut}; use std::panic; +use std::path::Path; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -12,11 +14,12 @@ use ansi_term::Color; use async_channel::{self as channel, Receiver, Sender}; use chat::ChatItem; use once_cell::sync::Lazy; +use pretty_assertions::assert_eq; use rand::Rng; use tempfile::{tempdir, TempDir}; use tokio::runtime::Handle; use tokio::sync::RwLock; -use tokio::task; +use tokio::{fs, task}; use crate::chat::{ self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, MessageListOptions, @@ -285,7 +288,7 @@ impl TestContext { println!("\n========== Chats of {}: ==========", self.name()); if let Ok(chats) = Chatlist::try_load(self, 0, None, None).await { for (chat, _) in chats.iter() { - self.print_chat(*chat).await; + print!("{}", self.display_chat(*chat).await); } } println!(); @@ -624,6 +627,28 @@ impl TestContext { res } + #[allow(unused)] + pub async fn golden_test_chat(&self, chat_id: ChatId, filename: &str) { + let filename = Path::new("test-data/golden/").join(filename); + + let actual = self.display_chat(chat_id).await; + + // We're using `unwrap_or_default()` here so that if the file doesn't exist, + // it can be created using `write` below. + let expected = fs::read(&filename).await.unwrap_or_default(); + let expected = String::from_utf8(expected).unwrap(); + if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected { + fs::write(&filename, &actual) + .await + .unwrap_or_else(|e| panic!("Error writing {filename:?}: {e}")); + } else { + assert_eq!( + actual, expected, + "To update the expected value, run `UPDATE_GOLDEN_TESTS=1 cargo test`" + ); + } + } + /// Prints out the entire chat to stdout. /// /// You can use this to debug your test by printing the entire chat conversation. @@ -631,7 +656,9 @@ impl TestContext { // merge them to a public function in the `deltachat` crate. #[allow(dead_code)] #[allow(clippy::indexing_slicing)] - pub async fn print_chat(&self, chat_id: ChatId) { + async fn display_chat(&self, chat_id: ChatId) -> String { + let mut res = String::new(); + let msglist = chat::get_chat_msgs_ex( self, chat_id, @@ -662,7 +689,8 @@ impl TestContext { } else { format!("{} member(s)", members.len()) }; - println!( + writeln!( + res, "{}#{}: {} [{}]{}{}{} {}", sel_chat.typ, sel_chat.get_id(), @@ -686,32 +714,38 @@ impl TestContext { } else { "" }, - ); + ) + .unwrap(); let mut lines_out = 0; for msg_id in msglist { if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) { - println!( + writeln!(res, "--------------------------------------------------------------------------------" - ); + ) + .unwrap(); lines_out += 1 } else if !msg_id.is_special() { if lines_out == 0 { - println!( + writeln!(res, "--------------------------------------------------------------------------------", - ); + ).unwrap(); lines_out += 1 } let msg = Message::load_from_db(self, msg_id).await.unwrap(); - log_msg(self, "", &msg).await; + write_msg(self, "", &msg, &mut res).await; } } if lines_out > 0 { - println!( + writeln!( + res, "--------------------------------------------------------------------------------" - ); + ) + .unwrap(); } + + res } pub async fn create_group_with_members( @@ -1041,7 +1075,7 @@ fn print_event(event: &Event) { /// Logs an individual message to stdout. /// /// This includes a bunch of the message meta-data as well. -async fn log_msg(context: &Context, prefix: &str, msg: &Message) { +async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut String) { let contact = match Contact::get_by_id(context, msg.get_from_id()).await { Ok(contact) => contact, Err(e) => { @@ -1061,7 +1095,8 @@ async fn log_msg(context: &Context, prefix: &str, msg: &Message) { _ => "", }; let msgtext = msg.get_text(); - println!( + writeln!( + buf, "{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}", prefix, msg.get_id(), @@ -1095,7 +1130,8 @@ async fn log_msg(context: &Context, prefix: &str, msg: &Message) { "" }, statestr, - ); + ) + .unwrap(); } #[cfg(test)]