#![cfg(test)]
#![allow(dead_code)]
#![allow(unused_imports)]
use anyhow::Result;
use common::event::{Event, EventHub, Origin, UndoRedoEvent};
use common::undo_redo::{
CompositeCommand, UNTRACKED_STACK_ID, UndoLabel, UndoRedoCommand, UndoRedoManager, UndoStatus,
};
use std::any::Any;
use std::sync::{Arc, Mutex};
struct TestCommand {
counter: Arc<Mutex<i32>>,
value: i32,
}
impl TestCommand {
fn new(counter: Arc<Mutex<i32>>, value: i32) -> Self {
TestCommand { counter, value }
}
}
impl UndoRedoCommand for TestCommand {
fn undo(&mut self) -> Result<()> {
let mut counter = self.counter.lock().unwrap();
*counter -= self.value;
Ok(())
}
fn redo(&mut self) -> Result<()> {
let mut counter = self.counter.lock().unwrap();
*counter += self.value;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MergeableCommand {
counter: Arc<Mutex<i32>>,
value: i32,
}
impl MergeableCommand {
fn new(counter: Arc<Mutex<i32>>, value: i32) -> Self {
MergeableCommand { counter, value }
}
}
impl UndoRedoCommand for MergeableCommand {
fn undo(&mut self) -> Result<()> {
let mut counter = self.counter.lock().unwrap();
*counter -= self.value;
Ok(())
}
fn redo(&mut self) -> Result<()> {
let mut counter = self.counter.lock().unwrap();
*counter += self.value;
Ok(())
}
fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
other.as_any().downcast_ref::<Self>().is_some()
}
fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
if let Some(other_cmd) = other.as_any().downcast_ref::<Self>() {
self.value += other_cmd.value;
return true;
}
false
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_new() {
let manager = UndoRedoManager::new();
assert!(!manager.can_undo(None));
assert!(!manager.can_redo(None));
}
#[test]
fn test_add_command() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
manager.add_command(Box::new(TestCommand::new(counter.clone(), 5)));
assert!(manager.can_undo(None));
assert!(!manager.can_redo(None));
assert_eq!(*counter.lock().unwrap(), 0); }
#[test]
fn test_undo() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd = TestCommand::new(counter.clone(), 5);
cmd.redo().unwrap();
manager.add_command(Box::new(cmd));
}
assert_eq!(*counter.lock().unwrap(), 5);
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 0);
assert!(!manager.can_undo(None));
assert!(manager.can_redo(None));
}
#[test]
fn test_redo() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd = TestCommand::new(counter.clone(), 5);
cmd.redo().unwrap();
manager.add_command(Box::new(cmd));
}
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 0);
manager.redo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 5);
assert!(manager.can_undo(None));
assert!(!manager.can_redo(None));
}
#[test]
fn test_undo_redo_multiple() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
assert_eq!(*counter.lock().unwrap(), 8);
manager.undo(None).unwrap(); assert_eq!(*counter.lock().unwrap(), 5);
manager.undo(None).unwrap(); assert_eq!(*counter.lock().unwrap(), 0);
manager.redo(None).unwrap(); assert_eq!(*counter.lock().unwrap(), 5);
manager.redo(None).unwrap(); assert_eq!(*counter.lock().unwrap(), 8);
}
#[test]
fn test_composite_command() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
manager.begin_composite(None).unwrap();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
manager.end_composite();
assert_eq!(*counter.lock().unwrap(), 8);
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 0);
manager.redo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 8);
}
#[test]
fn test_empty_composite() {
let mut manager = UndoRedoManager::new();
manager.begin_composite(None).unwrap();
manager.end_composite();
assert!(!manager.can_undo(None));
}
#[test]
fn test_nested_composite() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
manager.begin_composite(None).unwrap();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
}
manager.begin_composite(None).unwrap();
{
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
manager.end_composite();
{
let mut cmd3 = TestCommand::new(counter.clone(), 2);
cmd3.redo().unwrap();
manager.add_command(Box::new(cmd3));
}
manager.end_composite();
assert_eq!(*counter.lock().unwrap(), 10);
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 0);
}
#[test]
fn test_command_merging() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd1 = MergeableCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
}
{
let mut cmd2 = MergeableCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
assert_eq!(*counter.lock().unwrap(), 8);
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 0);
manager.redo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 8);
}
#[test]
fn test_redo_stack_cleared_on_new_command() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
}
manager.undo(None).unwrap();
assert!(manager.can_redo(None));
{
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
assert!(!manager.can_redo(None));
assert_eq!(*counter.lock().unwrap(), 3);
}
#[test]
fn test_undo_with_empty_stack() {
let mut manager = UndoRedoManager::new();
let result = manager.undo(None);
assert!(result.is_ok());
}
#[test]
fn test_redo_with_empty_stack() {
let mut manager = UndoRedoManager::new();
let result = manager.redo(None);
assert!(result.is_ok());
}
#[test]
fn test_clear() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
assert_eq!(*counter.lock().unwrap(), 8);
assert!(manager.can_undo(None));
assert!(!manager.can_redo(None));
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 5);
assert!(manager.can_undo(None));
assert!(manager.can_redo(None));
manager.clear_all_stacks();
assert!(!manager.can_undo(None));
assert!(!manager.can_redo(None));
{
let mut cmd = TestCommand::new(counter.clone(), 10);
cmd.redo().unwrap();
manager.add_command(Box::new(cmd));
}
assert!(manager.can_undo(None));
assert!(!manager.can_redo(None));
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 5); assert!(!manager.can_undo(None));
assert!(manager.can_redo(None));
}
#[test]
fn test_clear_with_in_progress_composite() {
let counter = Arc::new(Mutex::new(0));
let mut manager = UndoRedoManager::new();
manager.begin_composite(None).unwrap();
{
let mut cmd1 = TestCommand::new(counter.clone(), 5);
cmd1.redo().unwrap();
manager.add_command(Box::new(cmd1));
let mut cmd2 = TestCommand::new(counter.clone(), 3);
cmd2.redo().unwrap();
manager.add_command(Box::new(cmd2));
}
assert_eq!(*counter.lock().unwrap(), 8);
manager.clear_all_stacks();
assert!(!manager.can_undo(None));
assert!(!manager.can_redo(None));
{
let mut cmd = TestCommand::new(counter.clone(), 10);
cmd.redo().unwrap();
manager.add_command(Box::new(cmd));
}
assert!(manager.can_undo(None));
manager.undo(None).unwrap();
assert_eq!(*counter.lock().unwrap(), 8); assert!(!manager.can_undo(None));
assert!(manager.can_redo(None));
}
struct NamedCommand {
value: i32,
}
impl UndoRedoCommand for NamedCommand {
fn undo(&mut self) -> Result<()> {
self.value -= 1;
Ok(())
}
fn redo(&mut self) -> Result<()> {
self.value += 1;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn label(&self) -> Option<UndoLabel> {
Some(UndoLabel::new("widget", "update"))
}
}
#[test]
fn test_untracked_stack_records_nothing() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
assert!(manager.last_pushed_seq().is_some());
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(UNTRACKED_STACK_ID),
)
.unwrap();
assert_eq!(manager.get_stack_size(stack), 1);
assert_eq!(manager.get_stack_size(0), 0);
assert!(!manager.can_undo(None));
assert_eq!(
manager.last_pushed_seq(),
None,
"an untracked write records nothing, so it must not leave the register \
naming the entry below it — which is still the head, and which \
`undo_if_head` would therefore undo"
);
}
#[test]
fn test_untracked_composite_is_refused() {
let mut manager = UndoRedoManager::new();
assert!(
manager.begin_composite(Some(UNTRACKED_STACK_ID)).is_err(),
"a group whose commands are dropped could never be undone"
);
}
#[test]
fn test_push_mints_a_sequence_and_head_reports_it() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
let first = manager.last_pushed_seq().expect("a push mints a sequence");
assert_eq!(manager.head_seq(Some(stack)), Some(first));
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
let second = manager.last_pushed_seq().unwrap();
assert_ne!(first, second, "sequences are unique");
assert_eq!(manager.head_seq(Some(stack)), Some(second));
}
#[test]
fn test_undo_if_head_refuses_a_superseded_entry() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
let target = manager.last_pushed_seq().unwrap();
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
assert_eq!(
manager.undo_if_head(Some(stack), target).unwrap(),
UndoStatus::Superseded
);
assert_eq!(
manager.get_stack_size(stack),
2,
"a superseded request must not undo the newer command instead"
);
manager.undo(Some(stack)).unwrap();
assert_eq!(
manager.undo_if_head(Some(stack), target).unwrap(),
UndoStatus::Undone
);
assert_eq!(manager.get_stack_size(stack), 0);
assert_eq!(
manager.undo_if_head(Some(stack), target).unwrap(),
UndoStatus::Empty
);
}
#[test]
fn test_merging_keeps_the_sequence_of_the_entry_it_merged_into() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
let counter = Arc::new(Mutex::new(0));
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 1)),
Some(stack),
)
.unwrap();
let first = manager.last_pushed_seq().unwrap();
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 2)),
Some(stack),
)
.unwrap();
assert_eq!(manager.get_stack_size(stack), 1, "the two merged");
assert_eq!(
manager.last_pushed_seq(),
Some(first),
"a merged push reports the entry it landed in, not a fresh number — a \
caller holding that sequence still names the burst it contributed to"
);
assert_eq!(manager.head_seq(Some(stack)), Some(first));
}
#[test]
fn test_labels_describe_the_head_of_each_stack() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
assert_eq!(
manager.undo_label(Some(stack)),
None,
"an empty stack names nothing"
);
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
assert_eq!(
manager.undo_label(Some(stack)),
Some(UndoLabel::new("widget", "update"))
);
assert_eq!(manager.redo_label(Some(stack)), None);
manager.undo(Some(stack)).unwrap();
assert_eq!(manager.undo_label(Some(stack)), None);
assert_eq!(
manager.redo_label(Some(stack)),
Some(UndoLabel::new("widget", "update")),
"what a redo would re-apply"
);
}
#[test]
fn test_a_composite_reports_its_own_name_not_a_constituent_s() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.begin_composite_labeled(Some(stack), Some(UndoLabel::act("capture_character")))
.unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager.end_composite();
assert_eq!(manager.get_stack_size(stack), 1);
assert_eq!(
manager.undo_label(Some(stack)),
Some(UndoLabel::act("capture_character")),
"the group names itself; `widget/update` describes only its least \
interesting part"
);
assert_eq!(
manager.last_pushed_seq(),
manager.head_seq(Some(stack)),
"closing a group mints one sequence, so a toast can name the group — \
and it must be the group's own, not any older entry's"
);
assert_eq!(
manager
.undo_if_head(Some(stack), manager.last_pushed_seq().unwrap())
.unwrap(),
UndoStatus::Undone,
"the sequence a toast was handed must still address the group"
);
}
#[test]
fn test_undo_limit_drops_the_oldest_entries() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager.set_undo_limit(Some(2));
assert_eq!(manager.undo_limit(), Some(2));
for _ in 0..5 {
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(stack),
)
.unwrap();
}
assert_eq!(manager.get_stack_size(stack), 2, "the cap holds");
let last = manager.last_pushed_seq().unwrap();
assert_eq!(manager.head_seq(Some(stack)), Some(last));
}
#[test]
fn test_stacks_are_isolated_from_one_another() {
let mut manager = UndoRedoManager::new();
let a = manager.create_new_stack();
let b = manager.create_new_stack();
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(a),
)
.unwrap();
assert!(manager.can_undo(Some(a)));
assert!(!manager.can_undo(Some(b)));
assert!(!manager.can_undo(None), "the global stack is untouched");
manager.undo(Some(b)).unwrap();
assert_eq!(
manager.get_stack_size(a),
1,
"undoing an empty stack must not reach into a sibling"
);
}
#[test]
fn test_seal_head_stops_the_next_command_merging_into_it() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
let counter = Arc::new(Mutex::new(0));
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 1)),
Some(stack),
)
.unwrap();
manager.seal_head(Some(stack));
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 2)),
Some(stack),
)
.unwrap();
assert_eq!(
manager.get_stack_size(stack),
2,
"a sealed entry must not absorb the command after it"
);
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 3)),
Some(stack),
)
.unwrap();
assert_eq!(
manager.get_stack_size(stack),
2,
"sealing one entry must not stop everything merging forever"
);
}
#[test]
fn test_seal_head_is_harmless_on_an_empty_or_missing_stack() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager.seal_head(Some(stack));
manager.seal_head(Some(9999));
manager.seal_head(None);
assert_eq!(manager.get_stack_size(stack), 0);
}
#[test]
fn test_end_composite_frees_the_target_stack() {
let mut manager = UndoRedoManager::new();
let first = manager.create_new_stack();
let second = manager.create_new_stack();
manager.begin_composite(Some(first)).unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(first))
.unwrap();
manager.end_composite();
manager.begin_composite(Some(second)).expect(
"a closed group must release its target, or every other stack is locked \
out of compositing for the life of the manager",
);
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(second))
.unwrap();
manager.end_composite();
assert_eq!(manager.get_stack_size(first), 1);
assert_eq!(manager.get_stack_size(second), 1);
}
#[test]
fn test_an_empty_group_also_frees_the_target_stack() {
let mut manager = UndoRedoManager::new();
let first = manager.create_new_stack();
let second = manager.create_new_stack();
manager.begin_composite(Some(first)).unwrap();
manager.end_composite();
assert!(
manager.begin_composite(Some(second)).is_ok(),
"a group that recorded nothing still has to let go of its stack"
);
manager.end_composite();
}
#[test]
fn test_a_command_folded_into_an_open_group_reports_no_sequence() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
let earlier = manager.last_pushed_seq().unwrap();
manager.begin_composite(Some(stack)).unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
assert_eq!(
manager.last_pushed_seq(),
None,
"the group is still open, so nothing has been pushed yet"
);
assert_eq!(
manager.undo_if_head(Some(stack), earlier).unwrap(),
UndoStatus::Undone,
"the earlier entry is genuinely still the head — which is why reporting \
its sequence as the fold's would undo the wrong command"
);
}
#[test]
fn test_a_group_that_recorded_nothing_reports_no_sequence() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
assert!(manager.last_pushed_seq().is_some());
manager.begin_composite(Some(stack)).unwrap();
manager.end_composite();
assert_eq!(
manager.last_pushed_seq(),
None,
"an empty group is not an entry, so it has no sequence to report"
);
assert_eq!(manager.get_stack_size(stack), 1);
}
#[test]
fn test_a_cancelled_group_reports_no_sequence() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
assert!(manager.last_pushed_seq().is_some());
manager.begin_composite(Some(stack)).unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager.cancel_composite();
assert_eq!(
manager.last_pushed_seq(),
None,
"a cancelled group is not history"
);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "does not exist")]
fn test_closing_a_group_onto_a_deleted_stack_is_loud_in_debug() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager.begin_composite(Some(stack)).unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager.delete_stack(stack).unwrap();
manager.end_composite();
}
fn drain(hub: &Arc<EventHub>) -> Vec<(UndoRedoEvent, Option<String>)> {
let rx = hub.subscribe_receiver();
let mut out = Vec::new();
while let Ok(ev) = rx.try_recv() {
if let Origin::UndoRedo(kind) = ev.origin {
out.push((kind, ev.data));
}
}
out
}
#[test]
fn test_history_growth_is_announced_and_names_its_stack() {
let hub = Arc::new(EventHub::new());
let mut manager = UndoRedoManager::new();
manager.set_event_hub(&hub);
let stack = manager.create_new_stack();
let counter = Arc::new(Mutex::new(0));
drain(&hub);
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 1)),
Some(stack),
)
.unwrap();
assert_eq!(
drain(&hub),
vec![(UndoRedoEvent::StackChanged, Some(stack.to_string()))],
"a push announces itself, and says whose history moved"
);
manager
.add_command_to_stack(
Box::new(MergeableCommand::new(counter.clone(), 2)),
Some(stack),
)
.unwrap();
assert_eq!(manager.get_stack_size(stack), 1);
assert_eq!(
drain(&hub),
vec![(UndoRedoEvent::StackChanged, Some(stack.to_string()))],
"a merge changed the head, so it is a change"
);
manager.undo(Some(stack)).unwrap();
assert_eq!(
drain(&hub),
vec![(UndoRedoEvent::Undone, Some(stack.to_string()))]
);
manager.redo(Some(stack)).unwrap();
assert_eq!(
drain(&hub),
vec![(UndoRedoEvent::Redone, Some(stack.to_string()))]
);
manager.clear_stack(stack);
assert_eq!(
drain(&hub),
vec![(UndoRedoEvent::StackChanged, Some(stack.to_string()))],
"history can shrink to nothing as well as grow"
);
}
#[test]
fn test_an_untracked_write_announces_nothing() {
let hub = Arc::new(EventHub::new());
let mut manager = UndoRedoManager::new();
manager.set_event_hub(&hub);
let stack = manager.create_new_stack();
drain(&hub);
manager
.add_command_to_stack(
Box::new(TestCommand::new(Arc::new(Mutex::new(0)), 1)),
Some(UNTRACKED_STACK_ID),
)
.unwrap();
assert!(
drain(&hub).is_empty(),
"an untracked write is not history, so there is no history change to \
announce — a subscriber that repainted here would be repainting for a \
timer"
);
assert_eq!(manager.get_stack_size(stack), 0);
}
#[test]
fn test_a_nested_group_never_renames_a_named_outer_one() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager
.begin_composite_labeled(Some(stack), Some(UndoLabel::act("import_tags")))
.unwrap();
manager
.begin_composite_labeled(Some(stack), Some(UndoLabel::act("create_tag")))
.unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager.end_composite();
manager.end_composite();
assert_eq!(
manager.undo_label(Some(stack)),
Some(UndoLabel::act("import_tags")),
"the outermost caller describes what the user asked for; an inner step \
describes only one of its parts"
);
}
#[test]
fn test_a_nested_group_may_name_an_anonymous_outer_one() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
manager.begin_composite(Some(stack)).unwrap();
manager
.begin_composite_labeled(Some(stack), Some(UndoLabel::act("create_tag")))
.unwrap();
manager
.add_command_to_stack(Box::new(NamedCommand { value: 0 }), Some(stack))
.unwrap();
manager.end_composite();
manager.end_composite();
assert_eq!(
manager.undo_label(Some(stack)),
Some(UndoLabel::act("create_tag")),
"an anonymous group takes the first name offered — better than falling \
back to the least interesting command inside it"
);
}
#[test]
fn test_redo_stack_size_follows_undo_and_redo() {
let mut manager = UndoRedoManager::new();
let stack = manager.create_new_stack();
let counter = Arc::new(Mutex::new(0));
manager
.add_command_to_stack(Box::new(TestCommand::new(counter.clone(), 1)), Some(stack))
.unwrap();
assert_eq!(manager.get_redo_stack_size(stack), 0);
manager.undo(Some(stack)).unwrap();
assert_eq!(manager.get_redo_stack_size(stack), 1);
assert_eq!(manager.get_stack_size(stack), 0);
manager.redo(Some(stack)).unwrap();
assert_eq!(manager.get_redo_stack_size(stack), 0);
assert_eq!(manager.get_stack_size(stack), 1);
manager.undo(Some(stack)).unwrap();
assert_eq!(manager.get_redo_stack_size(stack), 1);
manager
.add_command_to_stack(Box::new(TestCommand::new(counter.clone(), 1)), Some(stack))
.unwrap();
assert_eq!(manager.get_redo_stack_size(stack), 0);
}