use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CommandId(u64);
impl CommandId {
pub fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GroupId(u64);
impl GroupId {
pub fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CheckpointId(u64);
impl CheckpointId {
pub fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandResult {
Success,
Failed(String),
Cancelled,
NeedsConfirmation(String),
}
impl CommandResult {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn is_failed(&self) -> bool {
matches!(self, Self::Failed(_))
}
}
pub trait Command: Send + Sync {
fn execute(&mut self) -> CommandResult;
fn undo(&mut self) -> CommandResult;
fn redo(&mut self) -> CommandResult {
self.execute()
}
fn description(&self) -> &str;
fn can_merge(&self, _other: &dyn Command) -> bool {
false
}
fn merge(&mut self, _other: Box<dyn Command>) -> Option<Box<dyn Command>> {
None
}
fn memory_size(&self) -> usize {
64
}
fn metadata(&self) -> Option<&dyn Any> {
None
}
}
struct HistoryEntry {
#[allow(dead_code)]
id: CommandId,
command: Box<dyn Command>,
group_id: Option<GroupId>,
timestamp: u64,
#[allow(dead_code)]
executed: bool,
}
#[derive(Debug, Clone)]
pub struct HistoryConfig {
pub max_commands: usize,
pub max_memory: usize,
pub enable_merging: bool,
pub auto_group_interval_ms: u64,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
max_commands: 1000,
max_memory: 10 * 1024 * 1024, enable_merging: true,
auto_group_interval_ms: 500,
}
}
}
#[derive(Debug, Clone)]
pub struct Checkpoint {
pub id: CheckpointId,
pub name: String,
pub position: usize,
pub timestamp: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HistoryEvent {
Executed(CommandId),
Undone(CommandId),
Redone(CommandId),
Cleared,
CheckpointCreated(CheckpointId),
CheckpointRestored(CheckpointId),
Trimmed(usize),
}
pub type HistoryCallback = Arc<dyn Fn(HistoryEvent) + Send + Sync>;
pub struct CommandHistory {
config: HistoryConfig,
next_command_id: u64,
next_group_id: u64,
next_checkpoint_id: u64,
undo_stack: Vec<HistoryEntry>,
redo_stack: Vec<HistoryEntry>,
current_group: Option<GroupId>,
checkpoints: HashMap<CheckpointId, Checkpoint>,
timestamp: u64,
last_command_time: u64,
listeners: Vec<HistoryCallback>,
current_memory: usize,
recording: bool,
}
impl Default for CommandHistory {
fn default() -> Self {
Self::new(HistoryConfig::default())
}
}
impl CommandHistory {
pub fn new(config: HistoryConfig) -> Self {
Self {
config,
next_command_id: 1,
next_group_id: 1,
next_checkpoint_id: 1,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
current_group: None,
checkpoints: HashMap::new(),
timestamp: 0,
last_command_time: 0,
listeners: Vec::new(),
current_memory: 0,
recording: true,
}
}
pub fn execute(&mut self, mut command: Box<dyn Command>) -> CommandResult {
if !self.recording {
return command.execute();
}
let result = command.execute();
if !result.is_success() {
return result;
}
self.redo_stack.clear();
let id = CommandId(self.next_command_id);
self.next_command_id += 1;
let group_id = if self.current_group.is_some() {
self.current_group
} else if self.config.auto_group_interval_ms > 0
&& self.timestamp.saturating_sub(self.last_command_time)
< self.config.auto_group_interval_ms
&& !self.undo_stack.is_empty()
{
self.undo_stack.last().and_then(|e| e.group_id)
} else {
None
};
if self.config.enable_merging && !self.undo_stack.is_empty() {
let can_merge = self
.undo_stack
.last()
.is_some_and(|last| last.command.can_merge(command.as_ref()));
if can_merge {
if let Some(last_entry) = self.undo_stack.last_mut() {
if let Some(merged) = last_entry.command.merge(command) {
self.current_memory -= last_entry.command.memory_size();
self.current_memory += merged.memory_size();
last_entry.command = merged;
self.emit(&HistoryEvent::Executed(id));
return result;
}
}
return result;
}
}
let memory = command.memory_size();
self.current_memory += memory;
let entry = HistoryEntry {
id,
command,
group_id,
timestamp: self.timestamp,
executed: true,
};
self.undo_stack.push(entry);
self.last_command_time = self.timestamp;
self.trim_if_needed();
self.emit(&HistoryEvent::Executed(id));
result
}
pub fn undo(&mut self) -> Option<CommandResult> {
let entry = self.undo_stack.pop()?;
let id = entry.id;
let group_id = entry.group_id;
let mut command = entry.command;
let result = command.undo();
if result.is_success() {
self.current_memory -= command.memory_size();
let redo_entry = HistoryEntry {
id,
command,
group_id,
timestamp: entry.timestamp,
executed: false,
};
self.current_memory += redo_entry.command.memory_size();
self.redo_stack.push(redo_entry);
self.emit(&HistoryEvent::Undone(id));
if let Some(gid) = group_id {
while let Some(last) = self.undo_stack.last() {
if last.group_id == Some(gid) {
self.undo();
} else {
break;
}
}
}
}
Some(result)
}
pub fn redo(&mut self) -> Option<CommandResult> {
let entry = self.redo_stack.pop()?;
let id = entry.id;
let group_id = entry.group_id;
let mut command = entry.command;
let result = command.redo();
if result.is_success() {
self.current_memory -= command.memory_size();
let undo_entry = HistoryEntry {
id,
command,
group_id,
timestamp: entry.timestamp,
executed: true,
};
self.current_memory += undo_entry.command.memory_size();
self.undo_stack.push(undo_entry);
self.emit(&HistoryEvent::Redone(id));
if let Some(gid) = group_id {
while let Some(last) = self.redo_stack.last() {
if last.group_id == Some(gid) {
self.redo();
} else {
break;
}
}
}
}
Some(result)
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn undo_description(&self) -> Option<&str> {
self.undo_stack.last().map(|e| e.command.description())
}
pub fn redo_description(&self) -> Option<&str> {
self.redo_stack.last().map(|e| e.command.description())
}
pub fn begin_group(&mut self) -> GroupId {
let id = GroupId(self.next_group_id);
self.next_group_id += 1;
self.current_group = Some(id);
id
}
pub fn end_group(&mut self) {
self.current_group = None;
}
pub fn execute_group<I>(&mut self, commands: I) -> Vec<CommandResult>
where
I: IntoIterator<Item = Box<dyn Command>>,
{
let _group = self.begin_group();
let results: Vec<_> = commands.into_iter().map(|cmd| self.execute(cmd)).collect();
self.end_group();
results
}
pub fn create_checkpoint(&mut self, name: impl Into<String>) -> CheckpointId {
let id = CheckpointId(self.next_checkpoint_id);
self.next_checkpoint_id += 1;
let checkpoint = Checkpoint {
id,
name: name.into(),
position: self.undo_stack.len(),
timestamp: self.timestamp,
};
self.checkpoints.insert(id, checkpoint);
self.emit(&HistoryEvent::CheckpointCreated(id));
id
}
pub fn restore_checkpoint(&mut self, id: CheckpointId) -> bool {
let checkpoint = match self.checkpoints.get(&id) {
Some(c) => c.clone(),
None => return false,
};
while self.undo_stack.len() > checkpoint.position {
if self.undo().is_none() {
break;
}
}
self.emit(&HistoryEvent::CheckpointRestored(id));
true
}
pub fn get_checkpoint(&self, id: CheckpointId) -> Option<&Checkpoint> {
self.checkpoints.get(&id)
}
pub fn checkpoints(&self) -> impl Iterator<Item = &Checkpoint> {
self.checkpoints.values()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
self.checkpoints.clear();
self.current_memory = 0;
self.emit(&HistoryEvent::Cleared);
}
pub fn memory_usage(&self) -> usize {
self.current_memory
}
pub fn tick(&mut self, delta_ms: u64) {
self.timestamp += delta_ms;
}
pub fn pause(&mut self) {
self.recording = false;
}
pub fn resume(&mut self) {
self.recording = true;
}
pub fn is_recording(&self) -> bool {
self.recording
}
pub fn on_event(&mut self, callback: HistoryCallback) {
self.listeners.push(callback);
}
fn emit(&self, event: &HistoryEvent) {
for listener in &self.listeners {
listener(event.clone());
}
}
fn trim_if_needed(&mut self) {
let mut trimmed = 0;
while self.undo_stack.len() > self.config.max_commands {
if let Some(entry) = self.undo_stack.first() {
self.current_memory -= entry.command.memory_size();
}
self.undo_stack.remove(0);
trimmed += 1;
}
while self.current_memory > self.config.max_memory && !self.undo_stack.is_empty() {
if let Some(entry) = self.undo_stack.first() {
self.current_memory -= entry.command.memory_size();
}
self.undo_stack.remove(0);
trimmed += 1;
}
if trimmed > 0 {
for checkpoint in self.checkpoints.values_mut() {
checkpoint.position = checkpoint.position.saturating_sub(trimmed);
}
self.emit(&HistoryEvent::Trimmed(trimmed));
}
}
}
pub struct CompositeCommand {
description: String,
commands: Vec<Box<dyn Command>>,
executed: Vec<bool>,
}
impl CompositeCommand {
pub fn new(description: impl Into<String>) -> Self {
Self {
description: description.into(),
commands: Vec::new(),
executed: Vec::new(),
}
}
pub fn with_command(mut self, command: Box<dyn Command>) -> Self {
self.commands.push(command);
self
}
pub fn build(self) -> Box<dyn Command> {
Box::new(CompositeCommandImpl {
description: self.description,
commands: self.commands,
executed: self.executed,
})
}
}
struct CompositeCommandImpl {
description: String,
commands: Vec<Box<dyn Command>>,
executed: Vec<bool>,
}
impl Command for CompositeCommandImpl {
fn execute(&mut self) -> CommandResult {
self.executed.clear();
for cmd in &mut self.commands {
let result = cmd.execute();
if !result.is_success() {
for (i, was_executed) in self.executed.iter().enumerate().rev() {
if *was_executed {
self.commands[i].undo();
}
}
return result;
}
self.executed.push(true);
}
CommandResult::Success
}
fn undo(&mut self) -> CommandResult {
for (i, cmd) in self.commands.iter_mut().enumerate().rev() {
if i < self.executed.len() && self.executed[i] {
let result = cmd.undo();
if !result.is_success() {
return result;
}
}
}
self.executed.clear();
CommandResult::Success
}
fn description(&self) -> &str {
&self.description
}
fn memory_size(&self) -> usize {
std::mem::size_of::<Self>() + self.commands.iter().map(|c| c.memory_size()).sum::<usize>()
}
}
pub struct SetValueCommand<T: Clone + Send + Sync + 'static> {
description: String,
value: Arc<std::sync::RwLock<T>>,
old_value: Option<T>,
new_value: T,
}
impl<T: Clone + Send + Sync + 'static> SetValueCommand<T> {
pub fn new(
description: impl Into<String>,
value: Arc<std::sync::RwLock<T>>,
new_value: T,
) -> Self {
Self {
description: description.into(),
value,
old_value: None,
new_value,
}
}
}
impl<T: Clone + Send + Sync + 'static> Command for SetValueCommand<T> {
fn execute(&mut self) -> CommandResult {
let Ok(mut guard) = self.value.write() else {
return CommandResult::Failed("Lock poisoned".into());
};
self.old_value = Some(guard.clone());
*guard = self.new_value.clone();
CommandResult::Success
}
fn undo(&mut self) -> CommandResult {
let Some(old) = self.old_value.clone() else {
return CommandResult::Failed("No old value".into());
};
let Ok(mut guard) = self.value.write() else {
return CommandResult::Failed("Lock poisoned".into());
};
*guard = old;
CommandResult::Success
}
fn description(&self) -> &str {
&self.description
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicI32, Ordering};
struct IncrementCommand {
counter: Arc<AtomicI32>,
amount: i32,
}
impl Command for IncrementCommand {
fn execute(&mut self) -> CommandResult {
self.counter.fetch_add(self.amount, Ordering::SeqCst);
CommandResult::Success
}
fn undo(&mut self) -> CommandResult {
self.counter.fetch_sub(self.amount, Ordering::SeqCst);
CommandResult::Success
}
fn description(&self) -> &str {
"Increment counter"
}
}
struct FailingCommand {
should_fail: bool,
}
impl Command for FailingCommand {
fn execute(&mut self) -> CommandResult {
if self.should_fail {
CommandResult::Failed("Intentional failure".into())
} else {
CommandResult::Success
}
}
fn undo(&mut self) -> CommandResult {
CommandResult::Success
}
fn description(&self) -> &str {
"Failing command"
}
}
#[test]
fn test_basic_execute() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let cmd = Box::new(IncrementCommand {
counter: counter.clone(),
amount: 5,
});
let result = history.execute(cmd);
assert!(result.is_success());
assert_eq!(counter.load(Ordering::SeqCst), 5);
assert_eq!(history.undo_count(), 1);
}
#[test]
fn test_undo() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 10,
}));
assert_eq!(counter.load(Ordering::SeqCst), 10);
let result = history.undo();
assert!(result.is_some());
assert!(result.unwrap().is_success());
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
#[test]
fn test_redo() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 7,
}));
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 0);
let result = history.redo();
assert!(result.is_some());
assert!(result.unwrap().is_success());
assert_eq!(counter.load(Ordering::SeqCst), 7);
}
#[test]
fn test_multiple_undo_redo() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 3,
}));
assert_eq!(counter.load(Ordering::SeqCst), 6);
assert_eq!(history.undo_count(), 3);
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 3);
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 1);
history.redo();
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_can_undo_redo() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
assert!(!history.can_undo());
assert!(!history.can_redo());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
assert!(history.can_undo());
assert!(!history.can_redo());
history.undo();
assert!(!history.can_undo());
assert!(history.can_redo());
}
#[test]
fn test_redo_cleared_on_new_execute() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.undo();
assert!(history.can_redo());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 5,
}));
assert!(!history.can_redo());
}
#[test]
fn test_failed_command_not_added() {
let mut history = CommandHistory::default();
let result = history.execute(Box::new(FailingCommand { should_fail: true }));
assert!(result.is_failed());
assert_eq!(history.undo_count(), 0);
}
#[test]
fn test_command_descriptions() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
assert!(history.undo_description().is_none());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
assert_eq!(history.undo_description(), Some("Increment counter"));
}
#[test]
fn test_command_groups() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let _group = history.begin_group();
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 3,
}));
history.end_group();
assert_eq!(counter.load(Ordering::SeqCst), 6);
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
#[test]
fn test_execute_group() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let commands: Vec<Box<dyn Command>> = vec![
Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}),
Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}),
];
let results = history.execute_group(commands);
assert!(results.iter().all(|r| r.is_success()));
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_checkpoints() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 5,
}));
let checkpoint = history.create_checkpoint("Initial state");
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 10,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 15,
}));
assert_eq!(counter.load(Ordering::SeqCst), 30);
let restored = history.restore_checkpoint(checkpoint);
assert!(restored);
assert_eq!(counter.load(Ordering::SeqCst), 5);
}
#[test]
fn test_get_checkpoint() {
let mut history = CommandHistory::default();
let id = history.create_checkpoint("Test checkpoint");
let checkpoint = history.get_checkpoint(id);
assert!(checkpoint.is_some());
assert_eq!(checkpoint.unwrap().name, "Test checkpoint");
}
#[test]
fn test_list_checkpoints() {
let mut history = CommandHistory::default();
history.create_checkpoint("First");
history.create_checkpoint("Second");
history.create_checkpoint("Third");
let checkpoints: Vec<_> = history.checkpoints().collect();
assert_eq!(checkpoints.len(), 3);
}
#[test]
fn test_clear() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.create_checkpoint("Test");
history.clear();
assert!(!history.can_undo());
assert!(!history.can_redo());
assert_eq!(history.checkpoints().count(), 0);
}
#[test]
fn test_max_commands_limit() {
let config = HistoryConfig {
max_commands: 3,
..Default::default()
};
let mut history = CommandHistory::new(config);
let counter = Arc::new(AtomicI32::new(0));
for i in 0..5 {
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: i + 1,
}));
}
assert_eq!(history.undo_count(), 3);
}
#[test]
fn test_pause_resume_recording() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.pause();
assert!(!history.is_recording());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
assert_eq!(counter.load(Ordering::SeqCst), 3);
assert_eq!(history.undo_count(), 1);
history.resume();
assert!(history.is_recording());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 3,
}));
assert_eq!(history.undo_count(), 2);
}
#[test]
fn test_event_callbacks() {
use std::sync::atomic::AtomicUsize;
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let event_count = Arc::new(AtomicUsize::new(0));
let ec = event_count.clone();
history.on_event(Arc::new(move |_event| {
ec.fetch_add(1, Ordering::SeqCst);
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.undo();
history.redo();
assert_eq!(event_count.load(Ordering::SeqCst), 3);
}
#[test]
fn test_memory_tracking() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
assert_eq!(history.memory_usage(), 0);
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
assert!(history.memory_usage() > 0);
}
#[test]
fn test_tick() {
let mut history = CommandHistory::default();
history.tick(100);
history.tick(50);
}
#[test]
fn test_command_result_helpers() {
let success = CommandResult::Success;
let failed = CommandResult::Failed("error".into());
let cancelled = CommandResult::Cancelled;
assert!(success.is_success());
assert!(!success.is_failed());
assert!(!failed.is_success());
assert!(failed.is_failed());
assert!(!cancelled.is_success());
assert!(!cancelled.is_failed());
}
#[test]
fn test_command_id() {
let id = CommandId(42);
assert_eq!(id.as_u64(), 42);
}
#[test]
fn test_group_id() {
let id = GroupId(123);
assert_eq!(id.as_u64(), 123);
}
#[test]
fn test_checkpoint_id() {
let id = CheckpointId(456);
assert_eq!(id.as_u64(), 456);
}
#[test]
fn test_composite_command() {
let counter = Arc::new(AtomicI32::new(0));
let composite = CompositeCommand::new("Add 6")
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}))
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}))
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 3,
}))
.build();
let mut history = CommandHistory::default();
history.execute(composite);
assert_eq!(counter.load(Ordering::SeqCst), 6);
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 0);
history.redo();
assert_eq!(counter.load(Ordering::SeqCst), 6);
}
#[test]
fn test_composite_command_rollback_on_failure() {
let counter = Arc::new(AtomicI32::new(0));
let composite = CompositeCommand::new("Should fail")
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 5,
}))
.with_command(Box::new(FailingCommand { should_fail: true }))
.build();
let mut history = CommandHistory::default();
let result = history.execute(composite);
assert!(result.is_failed());
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
#[test]
fn test_set_value_command() {
let value = Arc::new(std::sync::RwLock::new(10));
let cmd = Box::new(SetValueCommand::new("Set to 42", value.clone(), 42));
let mut history = CommandHistory::default();
history.execute(cmd);
assert_eq!(*value.read().unwrap(), 42);
history.undo();
assert_eq!(*value.read().unwrap(), 10);
history.redo();
assert_eq!(*value.read().unwrap(), 42);
}
#[test]
fn test_default_config() {
let config = HistoryConfig::default();
assert_eq!(config.max_commands, 1000);
assert_eq!(config.max_memory, 10 * 1024 * 1024);
assert!(config.enable_merging);
assert_eq!(config.auto_group_interval_ms, 500);
}
#[test]
fn test_undo_on_empty_returns_none() {
let mut history = CommandHistory::default();
assert!(history.undo().is_none());
}
#[test]
fn test_redo_on_empty_returns_none() {
let mut history = CommandHistory::default();
assert!(history.redo().is_none());
}
#[test]
fn test_restore_invalid_checkpoint() {
let mut history = CommandHistory::default();
let invalid_id = CheckpointId(999);
assert!(!history.restore_checkpoint(invalid_id));
}
#[test]
fn test_history_event_variants() {
let events = vec![
HistoryEvent::Executed(CommandId(1)),
HistoryEvent::Undone(CommandId(2)),
HistoryEvent::Redone(CommandId(3)),
HistoryEvent::Cleared,
HistoryEvent::CheckpointCreated(CheckpointId(1)),
HistoryEvent::CheckpointRestored(CheckpointId(1)),
HistoryEvent::Trimmed(5),
];
for event in &events {
assert_eq!(event, event);
}
}
#[test]
fn test_redo_description() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
assert!(history.redo_description().is_none());
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.undo();
assert_eq!(history.redo_description(), Some("Increment counter"));
}
#[test]
fn test_redo_count() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
assert_eq!(history.redo_count(), 0);
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.undo();
assert_eq!(history.redo_count(), 1);
history.undo();
assert_eq!(history.redo_count(), 2);
}
#[test]
fn test_composite_command_description() {
let counter = Arc::new(AtomicI32::new(0));
let composite = CompositeCommand::new("My Composite")
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}))
.build();
assert_eq!(composite.description(), "My Composite");
}
#[test]
fn test_command_id_debug() {
let id = CommandId(42);
let debug = format!("{id:?}");
assert!(debug.contains("CommandId"));
assert!(debug.contains("42"));
}
#[test]
fn test_command_id_eq() {
let id1 = CommandId(100);
let id2 = CommandId(100);
let id3 = CommandId(200);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_command_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(CommandId(1));
set.insert(CommandId(2));
set.insert(CommandId(1)); assert_eq!(set.len(), 2);
}
#[test]
fn test_group_id_debug() {
let id = GroupId(99);
let debug = format!("{id:?}");
assert!(debug.contains("GroupId"));
}
#[test]
fn test_group_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(GroupId(1));
set.insert(GroupId(2));
assert_eq!(set.len(), 2);
}
#[test]
fn test_checkpoint_id_debug() {
let id = CheckpointId(77);
let debug = format!("{id:?}");
assert!(debug.contains("CheckpointId"));
}
#[test]
fn test_checkpoint_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(CheckpointId(1));
set.insert(CheckpointId(2));
assert_eq!(set.len(), 2);
}
#[test]
fn test_command_result_debug() {
let result = CommandResult::Success;
let debug = format!("{result:?}");
assert!(debug.contains("Success"));
let failed = CommandResult::Failed("error".to_string());
let debug = format!("{failed:?}");
assert!(debug.contains("Failed"));
}
#[test]
fn test_command_result_clone() {
let result = CommandResult::NeedsConfirmation("confirm?".to_string());
let cloned = result.clone();
assert_eq!(result, cloned);
}
#[test]
fn test_command_result_all_variants() {
let variants = vec![
CommandResult::Success,
CommandResult::Failed("error".to_string()),
CommandResult::Cancelled,
CommandResult::NeedsConfirmation("confirm".to_string()),
];
for variant in &variants {
let _ = format!("{variant:?}");
let cloned = variant.clone();
assert_eq!(variant, &cloned);
}
}
#[test]
fn test_history_config_debug() {
let config = HistoryConfig::default();
let debug = format!("{config:?}");
assert!(debug.contains("HistoryConfig"));
}
#[test]
fn test_history_config_clone() {
let config = HistoryConfig {
max_commands: 500,
max_memory: 5 * 1024 * 1024,
enable_merging: false,
auto_group_interval_ms: 1000,
};
let cloned = config.clone();
assert_eq!(cloned.max_commands, 500);
assert!(!cloned.enable_merging);
}
#[test]
fn test_checkpoint_debug() {
let checkpoint = Checkpoint {
id: CheckpointId(1),
name: "Test".to_string(),
position: 5,
timestamp: 1000,
};
let debug = format!("{checkpoint:?}");
assert!(debug.contains("Checkpoint"));
}
#[test]
fn test_checkpoint_clone() {
let checkpoint = Checkpoint {
id: CheckpointId(1),
name: "Test".to_string(),
position: 5,
timestamp: 1000,
};
let cloned = checkpoint.clone();
assert_eq!(cloned.name, "Test");
assert_eq!(cloned.position, 5);
}
#[test]
fn test_history_event_debug() {
let event = HistoryEvent::Cleared;
let debug = format!("{event:?}");
assert!(debug.contains("Cleared"));
}
#[test]
fn test_history_event_clone() {
let event = HistoryEvent::Trimmed(10);
let cloned = event.clone();
assert_eq!(event, cloned);
}
#[test]
fn test_memory_limit_trimming() {
let config = HistoryConfig {
max_commands: 1000,
max_memory: 200, ..Default::default()
};
let mut history = CommandHistory::new(config);
let counter = Arc::new(AtomicI32::new(0));
for i in 0..10 {
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: i,
}));
}
assert!(history.undo_count() < 10);
}
#[test]
fn test_default_command_methods() {
let counter = Arc::new(AtomicI32::new(0));
let cmd = IncrementCommand {
counter: counter.clone(),
amount: 1,
};
assert!(!cmd.can_merge(&cmd));
assert_eq!(cmd.memory_size(), 64);
assert!(cmd.metadata().is_none());
}
#[test]
fn test_composite_command_memory_size() {
let counter = Arc::new(AtomicI32::new(0));
let composite = CompositeCommand::new("Multi")
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}))
.with_command(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}))
.build();
let memory = composite.memory_size();
assert!(memory > 64); }
#[test]
fn test_group_redo() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let _group = history.begin_group();
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
history.end_group();
assert_eq!(counter.load(Ordering::SeqCst), 3);
history.undo();
assert_eq!(counter.load(Ordering::SeqCst), 0);
history.redo();
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_checkpoint_position_updates_on_trim() {
let config = HistoryConfig {
max_commands: 3,
..Default::default()
};
let mut history = CommandHistory::new(config);
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
let checkpoint_id = history.create_checkpoint("Early");
for i in 2..=5 {
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: i,
}));
}
let checkpoint = history.get_checkpoint(checkpoint_id);
assert!(checkpoint.is_some());
}
#[test]
fn test_multiple_event_listeners() {
use std::sync::atomic::AtomicUsize;
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
let event_count1 = Arc::new(AtomicUsize::new(0));
let event_count2 = Arc::new(AtomicUsize::new(0));
let ec1 = event_count1.clone();
history.on_event(Arc::new(move |_| {
ec1.fetch_add(1, Ordering::SeqCst);
}));
let ec2 = event_count2.clone();
history.on_event(Arc::new(move |_| {
ec2.fetch_add(1, Ordering::SeqCst);
}));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
assert_eq!(event_count1.load(Ordering::SeqCst), 1);
assert_eq!(event_count2.load(Ordering::SeqCst), 1);
}
#[test]
fn test_set_value_command_description() {
let value = Arc::new(std::sync::RwLock::new(0));
let cmd = SetValueCommand::new("Set value", value, 100);
assert_eq!(cmd.description(), "Set value");
}
#[test]
fn test_undo_without_old_value() {
let value = Arc::new(std::sync::RwLock::new(10));
let mut cmd = SetValueCommand::new("Set", value.clone(), 42);
let result = cmd.undo();
assert!(result.is_failed());
}
#[test]
fn test_empty_composite_command() {
let composite = CompositeCommand::new("Empty").build();
let mut history = CommandHistory::default();
let result = history.execute(composite);
assert!(result.is_success());
}
#[test]
fn test_get_nonexistent_checkpoint() {
let history = CommandHistory::default();
let result = history.get_checkpoint(CheckpointId(999));
assert!(result.is_none());
}
#[test]
fn test_history_clear_resets_memory() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
assert!(history.memory_usage() > 0);
history.clear();
assert_eq!(history.memory_usage(), 0);
}
struct MergeableIncrement {
counter: Arc<AtomicI32>,
total_amount: i32,
}
impl Command for MergeableIncrement {
fn execute(&mut self) -> CommandResult {
self.counter.fetch_add(self.total_amount, Ordering::SeqCst);
CommandResult::Success
}
fn undo(&mut self) -> CommandResult {
self.counter.fetch_sub(self.total_amount, Ordering::SeqCst);
CommandResult::Success
}
fn description(&self) -> &str {
"Mergeable increment"
}
fn can_merge(&self, _other: &dyn Command) -> bool {
true
}
fn merge(&mut self, _other: Box<dyn Command>) -> Option<Box<dyn Command>> {
self.total_amount += 10;
None
}
}
#[test]
fn test_command_merging() {
let config = HistoryConfig {
enable_merging: true,
auto_group_interval_ms: 0, ..Default::default()
};
let mut history = CommandHistory::new(config);
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(MergeableIncrement {
counter: counter.clone(),
total_amount: 5,
}));
history.execute(Box::new(MergeableIncrement {
counter: counter.clone(),
total_amount: 3,
}));
assert_eq!(history.undo_count(), 1);
}
#[test]
fn test_redo_clears_on_new_execute() {
let mut history = CommandHistory::default();
let counter = Arc::new(AtomicI32::new(0));
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 1,
}));
history.undo();
assert_eq!(history.redo_count(), 1);
history.execute(Box::new(IncrementCommand {
counter: counter.clone(),
amount: 2,
}));
assert_eq!(history.redo_count(), 0);
}
}