mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 11:17:27 +00:00
test(backend): add unit tests for caching, usage tracking, and snippet management
This commit is contained in:
parent
e2f40553ac
commit
75182dd4aa
3 changed files with 360 additions and 8 deletions
|
@ -86,3 +86,97 @@ impl AppCache {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn setup_temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"raycast_test_cache_{}_{}",
|
||||
name,
|
||||
rand::random::<u32>()
|
||||
));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_file_roundtrip() {
|
||||
let temp_dir = setup_temp_dir("roundtrip");
|
||||
let cache_path = temp_dir.join("test_cache.bincode");
|
||||
|
||||
let mut dir_mod_times = HashMap::new();
|
||||
dir_mod_times.insert(PathBuf::from("/test/path"), SystemTime::now());
|
||||
|
||||
let original_cache = AppCache {
|
||||
apps: vec![App::new("TestApp".to_string())],
|
||||
dir_mod_times,
|
||||
};
|
||||
|
||||
original_cache.write_to_file(&cache_path).unwrap();
|
||||
|
||||
let read_cache = AppCache::read_from_file(&cache_path).unwrap();
|
||||
|
||||
assert_eq!(original_cache.apps.len(), read_cache.apps.len());
|
||||
assert_eq!(original_cache.apps[0].name, read_cache.apps[0].name);
|
||||
assert_eq!(original_cache.dir_mod_times, read_cache.dir_mod_times);
|
||||
|
||||
fs::remove_dir_all(temp_dir).unwrap();
|
||||
}
|
||||
|
||||
fn get_mock_app_directories(mock_dir: PathBuf) -> Vec<PathBuf> {
|
||||
vec![mock_dir]
|
||||
}
|
||||
|
||||
fn is_stale_mock(cache: &AppCache, mock_dir: PathBuf) -> bool {
|
||||
get_mock_app_directories(mock_dir).into_iter().any(|dir| {
|
||||
let current_mod_time = fs::metadata(&dir).ok().and_then(|m| m.modified().ok());
|
||||
let cached_mod_time = cache.dir_mod_times.get(&dir);
|
||||
match (current_mod_time, cached_mod_time) {
|
||||
(Some(current), Some(cached)) => current > *cached,
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_stale_logic() {
|
||||
let temp_dir = setup_temp_dir("is_stale");
|
||||
|
||||
let mod_time_before = fs::metadata(&temp_dir).unwrap().modified().unwrap();
|
||||
let mut dir_mod_times = HashMap::new();
|
||||
dir_mod_times.insert(temp_dir.clone(), mod_time_before);
|
||||
let cache = AppCache {
|
||||
apps: vec![],
|
||||
dir_mod_times,
|
||||
};
|
||||
assert!(!is_stale_mock(&cache, temp_dir.clone()));
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let mut file = fs::File::create(temp_dir.join("test.txt")).unwrap();
|
||||
file.write_all(b"Hello, world!").unwrap();
|
||||
drop(file);
|
||||
assert!(is_stale_mock(&cache, temp_dir.clone()));
|
||||
|
||||
let mod_time_after = fs::metadata(&temp_dir).unwrap().modified().unwrap();
|
||||
let mut new_dir_mod_times = HashMap::new();
|
||||
new_dir_mod_times.insert(temp_dir.clone(), mod_time_after);
|
||||
let cache_updated = AppCache {
|
||||
apps: vec![],
|
||||
dir_mod_times: new_dir_mod_times,
|
||||
};
|
||||
assert!(!is_stale_mock(&cache_updated, temp_dir.clone()));
|
||||
|
||||
let cache_missing_entry = AppCache {
|
||||
apps: vec![],
|
||||
dir_mod_times: HashMap::new(),
|
||||
};
|
||||
assert!(is_stale_mock(&cache_missing_entry, temp_dir.clone()));
|
||||
|
||||
fs::remove_dir_all(temp_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,16 @@ impl FrecencyManager {
|
|||
Ok(Self { store })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test() -> Result<Self, AppError> {
|
||||
let store = Store::new_in_memory()?;
|
||||
store.init_table(FRECENCY_SCHEMA)?;
|
||||
store.init_table(HIDDEN_ITEMS_SCHEMA)?;
|
||||
Ok(Self { store })
|
||||
}
|
||||
|
||||
pub fn record_usage(&self, item_id: String) -> Result<(), AppError> {
|
||||
let now = Utc::now().timestamp();
|
||||
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
|
||||
self.store.execute(
|
||||
"INSERT INTO frecency (item_id, use_count, last_used_at) VALUES (?, 1, ?)
|
||||
ON CONFLICT(item_id) DO UPDATE SET
|
||||
|
@ -84,3 +92,102 @@ impl FrecencyManager {
|
|||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_record_usage_new_item() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let item_id = "new_item".to_string();
|
||||
|
||||
manager.record_usage(item_id.clone()).unwrap();
|
||||
|
||||
let data = manager.get_frecency_data().unwrap();
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].item_id, item_id);
|
||||
assert_eq!(data[0].use_count, 1);
|
||||
assert!(data[0].last_used_at > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_usage_existing_item() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let item_id = "existing_item".to_string();
|
||||
|
||||
manager.record_usage(item_id.clone()).unwrap();
|
||||
let data1 = manager.get_frecency_data().unwrap();
|
||||
let time1 = data1[0].last_used_at;
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
manager.record_usage(item_id.clone()).unwrap();
|
||||
let data2 = manager.get_frecency_data().unwrap();
|
||||
let time2 = data2[0].last_used_at;
|
||||
|
||||
assert_eq!(data2.len(), 1);
|
||||
assert_eq!(data2[0].use_count, 2);
|
||||
assert!(time2 > time1, "last_used_at should be updated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_frecency_data_empty() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let data = manager.get_frecency_data().unwrap();
|
||||
assert!(data.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_frecency_entry() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let item_id = "to_delete".to_string();
|
||||
manager.record_usage(item_id.clone()).unwrap();
|
||||
assert_eq!(manager.get_frecency_data().unwrap().len(), 1);
|
||||
|
||||
manager.delete_frecency_entry(item_id).unwrap();
|
||||
assert!(manager.get_frecency_data().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_non_existent_entry() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let result = manager.delete_frecency_entry("non_existent".to_string());
|
||||
assert!(result.is_ok());
|
||||
assert!(manager.get_frecency_data().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hide_item_and_get_ids() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let item1 = "hidden1".to_string();
|
||||
let item2 = "hidden2".to_string();
|
||||
|
||||
assert!(manager.get_hidden_item_ids().unwrap().is_empty());
|
||||
|
||||
manager.hide_item(item1.clone()).unwrap();
|
||||
let hidden = manager.get_hidden_item_ids().unwrap();
|
||||
assert_eq!(hidden.len(), 1);
|
||||
assert_eq!(hidden[0], item1);
|
||||
|
||||
manager.hide_item(item2.clone()).unwrap();
|
||||
let hidden = manager.get_hidden_item_ids().unwrap();
|
||||
assert_eq!(hidden.len(), 2);
|
||||
assert!(hidden.contains(&item1));
|
||||
assert!(hidden.contains(&item2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hide_item_is_idempotent() {
|
||||
let manager = FrecencyManager::new_for_test().unwrap();
|
||||
let item1 = "hidden1".to_string();
|
||||
|
||||
manager.hide_item(item1.clone()).unwrap();
|
||||
assert_eq!(manager.get_hidden_item_ids().unwrap().len(), 1);
|
||||
|
||||
manager.hide_item(item1.clone()).unwrap();
|
||||
assert_eq!(manager.get_hidden_item_ids().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,10 +30,10 @@ impl Storable for Snippet {
|
|||
name: row.get(1)?,
|
||||
keyword: row.get(2)?,
|
||||
content: row.get(3)?,
|
||||
created_at: DateTime::from_timestamp(created_at_ts, 0).unwrap_or_default(),
|
||||
updated_at: DateTime::from_timestamp(updated_at_ts, 0).unwrap_or_default(),
|
||||
created_at: DateTime::from_timestamp_nanos(created_at_ts),
|
||||
updated_at: DateTime::from_timestamp_nanos(updated_at_ts),
|
||||
times_used: row.get(6)?,
|
||||
last_used_at: DateTime::from_timestamp(last_used_at_ts, 0).unwrap_or_default(),
|
||||
last_used_at: DateTime::from_timestamp_nanos(last_used_at_ts),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ impl SnippetManager {
|
|||
keyword: String,
|
||||
content: String,
|
||||
) -> Result<i64, AppError> {
|
||||
let now = Utc::now().timestamp();
|
||||
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
|
||||
self.store.execute(
|
||||
"INSERT INTO snippets (name, keyword, content, created_at, updated_at, times_used, last_used_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?4, 0, 0)",
|
||||
|
@ -129,7 +129,7 @@ impl SnippetManager {
|
|||
keyword: String,
|
||||
content: String,
|
||||
) -> Result<(), AppError> {
|
||||
let now = Utc::now().timestamp();
|
||||
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
|
||||
self.store.execute(
|
||||
"UPDATE snippets SET name = ?1, keyword = ?2, content = ?3, updated_at = ?4 WHERE id = ?5",
|
||||
params![name, keyword, content, now, id],
|
||||
|
@ -144,7 +144,7 @@ impl SnippetManager {
|
|||
}
|
||||
|
||||
pub fn snippet_was_used(&self, id: i64) -> Result<(), AppError> {
|
||||
let now = Utc::now().timestamp();
|
||||
let now = Utc::now().timestamp_nanos_opt().unwrap_or_default();
|
||||
self.store.execute(
|
||||
"UPDATE snippets SET times_used = times_used + 1, last_used_at = ?1 WHERE id = ?2",
|
||||
params![now, id],
|
||||
|
@ -152,7 +152,7 @@ impl SnippetManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
|
||||
pub fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
|
||||
self.store.query_row(
|
||||
"SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets WHERE keyword = ?1",
|
||||
params![keyword],
|
||||
|
@ -166,3 +166,154 @@ impl SnippetManager {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn test_create_and_list_snippets() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
manager
|
||||
.create_snippet(
|
||||
"Test Snippet".into(),
|
||||
"testkey".into(),
|
||||
"This is a test.".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let snippets = manager.list_snippets(None).unwrap();
|
||||
assert_eq!(snippets.len(), 1);
|
||||
assert_eq!(snippets[0].name, "Test Snippet");
|
||||
assert_eq!(snippets[0].keyword, "testkey");
|
||||
assert_eq!(snippets[0].content, "This is a test.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_snippet_with_duplicate_keyword_fails() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
manager
|
||||
.create_snippet("First".into(), "dupkey".into(), "content1".into())
|
||||
.unwrap();
|
||||
|
||||
let result = manager.create_snippet("Second".into(), "dupkey".into(), "content2".into());
|
||||
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
AppError::Rusqlite(rusqlite::Error::SqliteFailure(e, _)) => {
|
||||
assert_eq!(e.code, rusqlite::ErrorCode::ConstraintViolation);
|
||||
}
|
||||
_ => panic!("Expected a database constraint violation"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_snippets_with_search() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
manager
|
||||
.create_snippet(
|
||||
"Email Signature".into(),
|
||||
"sig".into(),
|
||||
"Best regards".into(),
|
||||
)
|
||||
.unwrap();
|
||||
manager
|
||||
.create_snippet(
|
||||
"Boilerplate".into(),
|
||||
"bp".into(),
|
||||
"Some email content".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manager.list_snippets(Some("email".into())).unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(manager.list_snippets(Some("sig".into())).unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
manager.list_snippets(Some("regards".into())).unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
manager.list_snippets(Some("nothing".into())).unwrap().len(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_snippet() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
let id = manager
|
||||
.create_snippet("Original".into(), "orig".into(), "original content".into())
|
||||
.unwrap();
|
||||
|
||||
manager
|
||||
.update_snippet(
|
||||
id,
|
||||
"Updated".into(),
|
||||
"updated".into(),
|
||||
"updated content".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let snippet = manager.find_snippet_by_keyword("updated").unwrap().unwrap();
|
||||
assert_eq!(snippet.id, id);
|
||||
assert_eq!(snippet.name, "Updated");
|
||||
assert_eq!(snippet.content, "updated content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_snippet() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
let id = manager
|
||||
.create_snippet("To Delete".into(), "del".into(), "delete me".into())
|
||||
.unwrap();
|
||||
assert_eq!(manager.list_snippets(None).unwrap().len(), 1);
|
||||
manager.delete_snippet(id).unwrap();
|
||||
assert!(manager.list_snippets(None).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snippet_was_used() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
let id = manager
|
||||
.create_snippet("Test".into(), "test".into(), "test content".into())
|
||||
.unwrap();
|
||||
|
||||
let snippet1 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
|
||||
assert_eq!(snippet1.times_used, 0);
|
||||
|
||||
manager.snippet_was_used(id).unwrap();
|
||||
let snippet2 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
|
||||
assert_eq!(snippet2.times_used, 1);
|
||||
assert!(snippet2.last_used_at.timestamp() > 0);
|
||||
|
||||
manager.snippet_was_used(id).unwrap();
|
||||
let snippet3 = manager.find_snippet_by_keyword("test").unwrap().unwrap();
|
||||
assert_eq!(snippet3.times_used, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_snippet_by_name() {
|
||||
let manager = SnippetManager::new_for_test().unwrap();
|
||||
manager
|
||||
.create_snippet("Unique Name".into(), "key1".into(), "content1".into())
|
||||
.unwrap();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
manager
|
||||
.create_snippet("Shared Name".into(), "key2".into(), "content2".into())
|
||||
.unwrap();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let newest_id = manager
|
||||
.create_snippet("Shared Name".into(), "key3".into(), "newest".into())
|
||||
.unwrap();
|
||||
|
||||
let found = manager.find_snippet_by_name("Shared Name").unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().id, newest_id);
|
||||
|
||||
let not_found = manager.find_snippet_by_name("Non Existent").unwrap();
|
||||
assert!(not_found.is_none());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue