test.rs
use crate::{
diff, file, init, reindex_selection, repo, repo_got_change_diffs,
selection, update, ManagingRepoMsg, Msg, State,
};
use inflorescence_model::model::{
ManagingRepoSubState, ReadyState, RecordChanges, SubState,
};
use inflorescence_model::{action, model};
use inflorescence_view::view;
use libflorescence::prelude::Timestamp;
use libflorescence::testing::{
repo_path, setup_test_repo, setup_test_user_id, TestRepo, TestUserId,
DEFAULT_IGNORE_FILE, INITIAL_LOG_LEN,
};
use assert_matches::assert_matches;
use derivative::Derivative;
use iced::widget::text_editor;
use iced_utils::task::{self, await_next_msg};
use iced_utils::Task;
use test_log::test;
use tokio::fs;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
/// Test app [`init`] function
#[test(tokio::test)]
async fn test_init() {
let _id = setup_test_user_id();
let repo = setup_test_repo();
let repo_path = repo_path(&repo);
let (state, mut tasks) = init(Some(repo_path.to_path_buf()));
assert_matches!(state.model.sub, model::SubState::ManagingRepo(_));
if let model::SubState::ManagingRepo(sub) = state.model.sub {
assert_eq!(sub.repo_path, repo_path);
}
// Must receive 3 messages:
// - Msg::NoOp from opening a window
// - Msg::LoadedId
// - Msg::FromRepo(repo::MsgOut::Init)
let msg_0 = task::await_next_msg(&mut tasks).await;
let msg_1 = task::await_next_msg(&mut tasks).await;
let msg_2 = task::await_next_msg(&mut tasks).await;
let mut window_opened = false;
let mut loaded_id = false;
let mut inited_repo = false;
for msg in [msg_0, msg_1, msg_2] {
if !window_opened && matches!(msg, Msg::NoOp) {
window_opened = true;
continue;
}
if !loaded_id
&& matches!(
msg,
Msg::ManagingRepo(ManagingRepoMsg::LoadedIdentities(_))
)
{
loaded_id = true;
continue;
}
if !inited_repo
&& matches!(
msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(
repo::MsgOut::Init(_)
))
)
{
inited_repo = true;
continue;
}
}
let all = [window_opened, loaded_id, inited_repo];
if !all.iter().all(|success| *success) {
panic!("Some task didn't complete {all:#?}")
}
}
/// Test making of records
#[test(tokio::test)]
async fn test_making_records() {
let TestState {
mut state,
mut tasks,
mut fs_watch_task,
id: _,
repo,
} = setup_state().await;
let repo_path = repo_path(&repo);
{
let ready_state = get_ready_state(&state);
assert_eq!(ready_state.repo.short_log.len(), INITIAL_LOG_LEN);
}
let to_path = |raw: &str| file::Path {
raw: raw.to_string(),
is_dir: false,
};
// _________________________________________________________________________
// Case: start -> edit -> save
// Add an untracked file
let file_to_record = "funky.rs";
fs::write(repo_path.join(file_to_record), "music")
.await
.unwrap();
// Wait for the FS watch to pick it up
let msg = await_next_msg(&mut fs_watch_task).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
path
)) if path == &repo_path.join(file_to_record)
);
// Update the state with it to send it to repo
let _task = update(&mut state, msg);
// Wait for response from repo
let msg = await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
invalidate_logs: true,
..
}))
);
let _task = update(&mut state, msg);
// Select it
update(
&mut state,
Msg::View(view::Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::UntrackedFile {
ix: 0,
path: to_path(file_to_record),
},
),
)),
);
// Selection triggers `LoadedSrcFile`
let _msg = task::await_next_msg(&mut tasks).await;
// Add it to tracked files
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
model::SubMenu::Add { recursive: false },
))),
);
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
);
// Wait for it to be added
let msg = task::await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile { result:_, path })) if path == file_to_record
);
// Start a record
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
}
// Edit the record msg
let record_msg = "Added funky music";
let _task = update(
&mut state,
Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing { .. }
);
if let RecordChanges::Typing { msg, desc } =
ready_state.record_changes.as_ref().unwrap()
{
assert_eq!(msg.as_str(), record_msg);
assert_eq!(&desc.text(), "");
}
}
// Save the record
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::SaveRecord)),
);
let msg = task::await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
invalidate_logs: true,
..
}))
);
let _task = update(&mut state, msg);
// Check the log
{
let ready_state = get_ready_state(&state);
assert_eq!(ready_state.repo.short_log.len(), INITIAL_LOG_LEN + 1);
}
// _________________________________________________________________________
// Case: start -> edit -> abandon
// Add an untracked file
let file_to_record = "soul.rs";
fs::write(repo_path.join(file_to_record), "music")
.await
.unwrap();
// Wait for the FS watch to pick it up
let msg = await_next_msg(&mut fs_watch_task).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
path
)) if path == &repo_path.join(file_to_record)
);
// Update the state with it to send it to repo
let _task = update(&mut state, msg);
// Wait for response from repo
let msg = await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(
repo::MsgOut::Refreshed { .. }
))
);
let _task = update(&mut state, msg);
// Select it
update(
&mut state,
Msg::View(view::Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::UntrackedFile {
ix: 0,
path: to_path(file_to_record),
},
),
)),
);
// Selection triggers `LoadedSrcFile`
let msg = task::await_next_msg(&mut tasks).await;
let id = file::Id {
path: to_path(file_to_record),
file_kind: file::Kind::Untracked,
};
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
if *loaded_id == id
);
// Add it to tracked files
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
model::SubMenu::Add { recursive: false },
))),
);
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
);
// Wait for it to be added
let msg = task::await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile {result:_, path })) if path == file_to_record
);
// Start a record
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
}
// Edit the record msg
let record_msg = "Added soul music";
let _task = update(
&mut state,
Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing { .. }
);
if let RecordChanges::Typing { msg, desc } =
ready_state.record_changes.as_ref().unwrap()
{
assert_eq!(msg.as_str(), record_msg);
assert_eq!(&desc.text(), "");
}
}
// Abandon it
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_none());
}
// _________________________________________________________________________
// Case: start -> edit msg -> defer -> start -> abandon
// Start a record - there's an an added file from previous case
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
}
// Edit the record msg
let record_msg = "Added soul music";
let _task = update(
&mut state,
Msg::View(view::Msg::EditRecordMsg(record_msg.to_string())),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing { .. }
);
if let RecordChanges::Typing { msg, desc } =
ready_state.record_changes.as_ref().unwrap()
{
assert_eq!(msg.as_str(), record_msg);
assert_eq!(&desc.text(), "");
}
}
// Defer the record
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::PostponeRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Canceled { old_msg, old_desc: _ } if old_msg == record_msg
);
}
// Start again
// Start a record - there's an an added file from previous case
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing{ msg, desc: _ } if msg.as_str() == record_msg
);
}
// Abandon it
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_none());
}
// _________________________________________________________________________
// Case: start -> edit desc -> defer -> start
// Start a record - there's an an added file from previous case
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
}
// Edit the record desc
let record_desc = "Added honky tonk music";
let _task = update(
&mut state,
Msg::View(view::Msg::EditRecordDesc(text_editor::Action::Edit(
text_editor::Edit::Paste(Arc::new(record_desc.to_string())),
))),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing { .. }
);
if let RecordChanges::Typing { msg, desc } =
ready_state.record_changes.as_ref().unwrap()
{
assert_eq!(msg.as_str(), "");
assert_eq!(&desc.text(), record_desc);
}
}
// Defer the record
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::PostponeRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Canceled { old_msg: _, old_desc } if old_desc == record_desc
);
}
// Start again
// Start a record - there's an an added file from previous case
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_some());
assert!(ready_state.record_changes.is_some());
assert_matches!(
ready_state.record_changes.as_ref().unwrap(),
RecordChanges::Typing{ msg: _, desc } if desc.text() == record_desc
);
}
// Abandon it
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::DiscardRecord)),
);
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_none());
}
}
/// When there's nothing to record, trying to start recording does nothing
#[test(tokio::test)]
async fn test_start_record_when_nothing_to_record() {
let TestState {
mut state,
tasks: _,
fs_watch_task: _,
id: _,
repo: _repo,
} = setup_state().await;
let task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::StartRecord)),
);
assert!(task.is_none());
{
let ready_state = get_ready_state(&state);
assert!(ready_state.record_changes.is_none());
}
}
/// Test add and rm untracked file
#[test(tokio::test)]
async fn test_add_and_rm_untracked_file() {
let TestState {
mut state,
mut tasks,
mut fs_watch_task,
id: _,
repo,
} = setup_state().await;
let repo_path = repo_path(&repo);
{
let ready_state = get_ready_state(&state);
let repo_state = &ready_state.repo;
assert!(repo_state.untracked_files.is_empty());
assert!(repo_state.changed_files.is_empty());
}
let to_path = |raw: &str| file::Path {
raw: raw.to_string(),
is_dir: false,
};
// Add an untracked file
let file_to_record = "new_file.rs";
fs::write(repo_path.join(file_to_record), "some contents")
.await
.unwrap();
// Wait for the FS watch to pick it up
let msg = await_next_msg(&mut fs_watch_task).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::WatchedFileChange(
path
)) if path == &repo_path.join(file_to_record)
);
// Update the state with it to send it to repo
let _task = update(&mut state, msg);
// Wait for response from repo
let msg = await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Refreshed {
invalidate_logs: true,
..
}))
);
let _task = update(&mut state, msg);
{
let ready_state = get_ready_state(&state);
let repo_state = &ready_state.repo;
assert_eq!(repo_state.untracked_files.len(), 1);
assert!(repo_state
.untracked_files
.contains(&to_path(file_to_record)));
assert!(repo_state.changed_files.is_empty());
}
// Select it
update(
&mut state,
Msg::View(view::Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::UntrackedFile {
ix: 0,
path: to_path(file_to_record),
},
),
)),
);
// Selection triggers `LoadedSrcFile`
let msg = task::await_next_msg(&mut tasks).await;
let id = file::Id {
path: to_path(file_to_record),
file_kind: file::Kind::Untracked,
};
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
if *loaded_id == id
);
// Add it to tracked files
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::EnterSubMenu(
model::SubMenu::Add { recursive: false },
))),
);
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::Confirm)),
);
// Wait for it to be added
let msg = task::await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::AddedUntrackedFile { result:_, path }))
if path == file_to_record
);
{
let ready_state = get_ready_state(&state);
let repo_state = &ready_state.repo;
assert!(repo_state.untracked_files.is_empty());
assert_eq!(repo_state.changed_files.len(), 1);
assert!(repo_state
.changed_files
.contains_key(&to_path(file_to_record)));
assert_eq!(
repo_state
.changed_files
.get(&to_path(file_to_record))
.unwrap(),
&BTreeSet::from_iter([repo::ChangedFileDiff::Add {
contents: None
}])
);
}
// Select the added file
update(
&mut state,
Msg::View(view::Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(selection::Select::ChangedFile {
ix: 0,
path: to_path(file_to_record),
}),
)),
);
// Selection triggers `LoadedSrcFile`
let msg = task::await_next_msg(&mut tasks).await;
let id = file::Id {
path: to_path(file_to_record),
file_kind: file::Kind::Changed,
};
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::File(crate::file::Msg::LoadedSrcFile { id: loaded_id, .. }))
if *loaded_id == id
);
// Remove it to get it back into untracked files
let _task = update(
&mut state,
Msg::View(view::Msg::Action(action::FilteredMsg::RmChange)),
);
// Wait for it to be rm'd
let msg = task::await_next_msg(&mut tasks).await;
assert_matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::RmedAddedFile { result:_, path }))
if path == file_to_record
);
{
let ready_state = get_ready_state(&state);
let repo_state = &ready_state.repo;
assert_eq!(repo_state.untracked_files.len(), 1);
assert!(repo_state
.untracked_files
.contains(&to_path(file_to_record)));
assert!(repo_state.changed_files.is_empty());
}
}
/// When the repo is refreshed we try to re-index the selection if it's still
/// present.
///
/// If the selection is still present we must perform different actions
/// depending on the selection type:
///
/// - untracked file: We should start loading the file
/// - changed file: Load the file if the diff has any contents
/// - log change: Request the diffs
///
/// If the selection is not present any more, no action must be taken.
#[test(tokio::test)]
async fn test_reindex_selection() {
let TestState {
mut state,
tasks: _tasks,
fs_watch_task: _,
id: _,
repo: _,
} = setup_state().await;
let SubState::ManagingRepo(sub) = &mut state.model.sub else {
panic!("Unexpected state: {:?}", state.model.sub)
};
let ManagingRepoSubState::Ready(ready_state) = &mut sub.sub else {
panic!("Unexpected state: {:?}", sub.sub)
};
let to_path = |raw: &str| file::Path {
raw: raw.to_string(),
is_dir: false,
};
// _________________________________________________________________________
// Case: no current selection
assert!(ready_state.selection.status.is_none());
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_none());
assert!(state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.is_empty());
assert!(task.is_none());
// _________________________________________________________________________
// Case: untracked file selection present after refresh, index is increased
ready_state.repo.untracked_files = BTreeSet::from_iter([
to_path("untracked_0.rs"),
to_path("untracked_1.rs"),
to_path("untracked_2.rs"),
]);
ready_state.selection.status = Some(selection::Status::UntrackedFile {
ix: 0,
path: to_path("untracked_1.rs"),
diff_selected: false,
});
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_some());
assert_matches!(
ready_state.selection.status.as_ref().unwrap(),
selection::Status::UntrackedFile { ix, path, diff_selected }
if *ix == 1 && path.raw == "untracked_1.rs" && !diff_selected
);
assert!(task.is_none());
assert_eq!(
state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.len(),
1
);
assert_matches!(
state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.peek(&file::id_parts_hash(
&to_path("untracked_1.rs"),
file::Kind::Untracked
)),
Some(file::Diff::Loading)
);
// Clear the cache for next test case
state
.managing_repo
.as_mut()
.unwrap()
.files
.diffs_cache
.inner
.clear();
// _________________________________________________________________________
// Case: untracked file selection not present after refresh
ready_state.selection.status = Some(selection::Status::UntrackedFile {
ix: 0,
path: to_path("untracked_gone.rs"),
diff_selected: false,
});
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_none());
assert!(task.is_none());
assert!(state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.is_empty());
// _________________________________________________________________________
// Case: changed file selection present after refresh, index is decreased,
// file has diffs with contents
ready_state.repo.changed_files = repo::ChangedFiles::from_iter([
(
to_path("changed_0.rs"),
BTreeSet::from_iter([repo::ChangedFileDiff::Add {
contents: None,
}]),
),
(to_path("changed_1.rs"), BTreeSet::new()),
(to_path("changed_2.rs"), BTreeSet::new()),
]);
ready_state.selection.status = Some(selection::Status::ChangedFile {
ix: 1,
path: to_path("changed_0.rs"),
diff_selected: false,
});
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_some());
assert_matches!(
ready_state.selection.status.as_ref().unwrap(),
selection::Status::ChangedFile { ix, path, diff_selected }
if *ix == 0 && path.raw == "changed_0.rs" && !diff_selected
);
assert!(task.is_none());
assert_eq!(
state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.len(),
1
);
assert_matches!(
state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.peek(&file::id_parts_hash(
&to_path("changed_0.rs"),
file::Kind::Changed
)),
Some(file::Diff::Loading)
);
// Clear the cache for next test case
state
.managing_repo
.as_mut()
.unwrap()
.files
.diffs_cache
.inner
.clear();
// _________________________________________________________________________
// Case: changed file selection present after refresh, index is the same,
// but file has no diffs with contents
ready_state.selection.status = Some(selection::Status::ChangedFile {
ix: 1,
path: to_path("changed_1.rs"),
diff_selected: false,
});
// // Add a diff that would make `reindex_selection` expect that the file
// doesn't exist
{
let changed_file = BTreeSet::from_iter([repo::ChangedFileDiff::Del {
contents: None,
}]);
assert!(!diff::should_file_exist(&changed_file));
ready_state
.repo
.changed_files
.insert(to_path("changed_1.rs"), changed_file);
}
let mut task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_some());
assert_matches!(
ready_state.selection.status.as_ref().unwrap(),
selection::Status::ChangedFile { ix, path, diff_selected }
if *ix == 1 && path.raw == "changed_1.rs" && !diff_selected
);
assert!(task.is_some());
let msg = await_next_msg(&mut task).await;
assert_matches!(
&msg,
crate::ManagingRepoMsg::File(file::Msg::SrcFileDoesntExist { .. })
);
// The file will be assembled from the diff
assert_matches!(
state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.peek(&file::id_parts_hash(
&to_path("changed_1.rs"),
file::Kind::Changed
)),
Some(file::Diff::Loaded(_))
);
// Clear the cache for next test case
state
.managing_repo
.as_mut()
.unwrap()
.files
.diffs_cache
.inner
.clear();
// _________________________________________________________________________
// Case: changed file selection not present after refresh
ready_state.selection.status = Some(selection::Status::ChangedFile {
ix: 0,
path: to_path("changed_gone.rs"),
diff_selected: false,
});
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_none());
assert!(task.is_none());
assert!(state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.is_empty());
// _________________________________________________________________________
// Case: log selection present after refresh
let change_hash_0 = repo::hash_bytes(&[0]);
let change_hash_1 = repo::hash_bytes(&[1]);
let change_hash_2 = repo::hash_bytes(&[2]);
ready_state.repo.short_log = vec![
repo::LogEntry {
hash: change_hash_0,
message: "".to_string(),
description: None,
timestamp: Timestamp::now(),
file_paths: vec![],
},
repo::LogEntry {
hash: change_hash_1,
message: "".to_string(),
description: None,
timestamp: Timestamp::now(),
file_paths: vec![],
},
repo::LogEntry {
hash: change_hash_2,
message: "".to_string(),
description: None,
timestamp: Timestamp::now(),
file_paths: vec![],
},
];
ready_state.selection.status =
Some(selection::Status::LogChange(selection::LogChange {
ix: 0,
hash: change_hash_2,
message: "".to_string(),
file: None,
}));
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_some());
assert_matches!(
ready_state.selection.status.as_ref().unwrap(),
selection::Status::LogChange(selection::LogChange { ix, hash, .. })
if *ix == 2 && *hash == change_hash_2
);
assert!(task.is_some());
assert!(state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.is_empty());
// _________________________________________________________________________
// Case: log selection not present after refresh
ready_state.selection.status = Some(selection::Status::UntrackedFile {
ix: 0,
path: to_path("log_gone.rs"),
diff_selected: false,
});
let task = reindex_selection(
&ready_state.repo,
&mut ready_state.selection,
&mut state.managing_repo.as_mut().unwrap().files,
&ready_state.logs,
);
assert!(ready_state.selection.status.is_none());
assert!(task.is_none());
assert!(state
.managing_repo
.as_ref()
.unwrap()
.files
.diffs_cache
.inner
.is_empty());
}
/// When a repo loads a change diff, it should only be used if the same change
/// is still selected
#[test(tokio::test)]
async fn test_repo_got_change_diffs() {
let TestState {
mut state,
tasks: _tasks,
fs_watch_task: _,
id: _,
repo: _,
} = setup_state().await;
let change_hash_0 = repo::hash_bytes(&[0]);
let change_hash_1 = repo::hash_bytes(&[1]);
// _________________________________________________________________________
// Case: nothing is selected
{
let ready_state = get_ready_state(&state);
assert!(ready_state.selection.status.is_none());
}
let diffs = BTreeMap::from_iter([]);
let task = {
let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
panic!("Unexpected state {:?}", state.model.sub)
};
repo_got_change_diffs(model, change_hash_0, diffs)
};
assert!(task.is_none());
{
let ready_state = get_ready_state(&state);
assert!(ready_state.selection.status.is_none());
}
// _________________________________________________________________________
// Case: selection is changed and doesn't match the loaded diff
{
let ready_state = get_ready_state_mut(&mut state);
ready_state.selection.status =
Some(selection::Status::LogChange(selection::LogChange {
ix: 0,
hash: change_hash_0,
message: "".to_string(),
file: None,
}));
}
let diffs = BTreeMap::from_iter([]);
let task = {
let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
panic!("Unexpected state {:?}", state.model.sub)
};
repo_got_change_diffs(model, change_hash_1, diffs)
};
assert!(task.is_none());
{
let ready_state = get_ready_state(&state);
assert!(ready_state.selection.status.is_some());
}
// _________________________________________________________________________
// Case: selection is still the same and matches the loaded diff
{
let ready_state = get_ready_state_mut(&mut state);
ready_state.selection.status =
Some(selection::Status::LogChange(selection::LogChange {
ix: 0,
hash: change_hash_1,
message: "".to_string(),
file: None,
}));
}
let diffs = BTreeMap::from_iter([]);
let task = {
let model::SubState::ManagingRepo(model) = &mut state.model.sub else {
panic!("Unexpected state {:?}", state.model.sub)
};
repo_got_change_diffs(model, change_hash_1, diffs)
};
assert!(task.is_none());
{
let ready_state = get_ready_state(&state);
assert!(ready_state.selection.status.is_some());
}
}
async fn setup_state() -> TestState {
let id = setup_test_user_id();
let repo = setup_test_repo();
let repo_path = repo_path(&repo);
let (mut state, mut tasks) = init(Some(repo_path.to_path_buf()));
// Wait for the first 3 msgs:
// - Msg::WindowOpened
// - Msg::LoadedId
// - Msg::FromRepo(repo::MsgOut::Init)
let mut fs_watch_task: Task<Msg> = Task::none();
for _ in 0..3 {
let msg = task::await_next_msg(&mut tasks).await;
let is_init = matches!(
&msg,
Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Init(_)))
); // Upate state with the msg received from task to init the repo
let task = update(&mut state, msg);
if is_init {
fs_watch_task = task;
}
}
let ready_state = get_ready_state(&state);
// Expect 2 changes in the log:
// - Add root
// - Add ".ignore" file
let log = &ready_state.repo.short_log;
assert_eq!(log.len(), 2);
assert_eq!(log[0].file_paths, [DEFAULT_IGNORE_FILE]);
assert_eq!(log[1].file_paths, ["/"]);
TestState {
state,
tasks,
fs_watch_task,
id,
repo,
}
}
#[derive(Derivative)]
#[derivative(Debug)]
struct TestState {
pub state: State,
/// Tasks to open window, set icon, tasks replies from repo, load user id
/// and files
#[derivative(Debug = "ignore")]
pub tasks: Task<Msg>,
#[derivative(Debug = "ignore")]
pub fs_watch_task: Task<Msg>,
pub id: TestUserId,
pub repo: TestRepo,
}
#[track_caller]
fn get_ready_state(state: &State) -> &ReadyState {
let SubState::ManagingRepo(state) = &state.model.sub else {
panic!("Unexpected state: {:?}", state.model.sub)
};
let ManagingRepoSubState::Ready(ready_state) = &state.sub else {
panic!("Unexpected state: {:?}", state.sub)
};
ready_state
}
#[track_caller]
fn get_ready_state_mut(state: &mut State) -> &mut ReadyState {
let state_dbg = format!("{:?}", state.model.sub);
let SubState::ManagingRepo(state) = &mut state.model.sub else {
panic!("Unexpected state: {state_dbg}")
};
let state_dbg = format!("{:?}", state.sub);
let ManagingRepoSubState::Ready(ready_state) = &mut state.sub else {
panic!("Unexpected state: {state_dbg}")
};
ready_state
}