349 lines
9.3 KiB
Go
349 lines
9.3 KiB
Go
package cmd_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/inkyblackness/hacked/ss1"
|
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
|
"github.com/inkyblackness/hacked/ss1/world"
|
|
)
|
|
|
|
type TestCommand struct {
|
|
name string
|
|
|
|
executed int
|
|
reverted int
|
|
|
|
pendingError error
|
|
task func()
|
|
}
|
|
|
|
func (cmd *TestCommand) Do(modder world.Modder) error {
|
|
cmd.executed++
|
|
return cmd.run()
|
|
}
|
|
|
|
func (cmd *TestCommand) Undo(modder world.Modder) error {
|
|
cmd.reverted++
|
|
return cmd.run()
|
|
}
|
|
|
|
func (cmd *TestCommand) run() (err error) {
|
|
if cmd.task != nil {
|
|
cmd.task()
|
|
}
|
|
err = cmd.pendingError
|
|
cmd.pendingError = nil
|
|
return
|
|
}
|
|
|
|
type StackSuite struct {
|
|
suite.Suite
|
|
|
|
stack cmd.Stack
|
|
commands map[string]*TestCommand
|
|
}
|
|
|
|
func TestStackSuite(t *testing.T) {
|
|
suite.Run(t, new(StackSuite))
|
|
}
|
|
|
|
func (suite *StackSuite) SetupTest() {
|
|
suite.stack = cmd.Stack{}
|
|
suite.commands = make(map[string]*TestCommand)
|
|
}
|
|
|
|
func (suite *StackSuite) TestNewStackCantDoAnything() {
|
|
suite.thenStackShouldNotSupportRedo()
|
|
suite.thenStackShouldNotSupportUndo()
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformExecutesCommand() {
|
|
suite.whenPerforming(suite.aCommand("cmd1"))
|
|
suite.thenCommandShouldHaveBeenExecuted("cmd1")
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformAllowsUndoIfSuccessful() {
|
|
suite.whenPerforming(suite.aCommand("cmd1"))
|
|
suite.thenStackShouldSupportUndo()
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformIgnoresCommandIfItFails() {
|
|
suite.whenPerforming(suite.aCommandReturningError())
|
|
suite.thenStackShouldNotSupportUndo()
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformReturnsErrorOfCommand() {
|
|
err := ss1.StringError("fail first time")
|
|
suite.thenPerformShouldReturnError(suite.aCommandReturning(err), err)
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoRevertsCommand() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenReverted("cmd1")
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoRevertsCommandOnlyOnce() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenRevertedTimes("cmd1", 1)
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoRevertsCommandsInSequence() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenCommandWasPerformed("cmd2")
|
|
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenReverted("cmd2")
|
|
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenReverted("cmd1")
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoLeavesStackUnchangedIfCommandFails() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenCommandWasPerformed("cmd2")
|
|
suite.givenCommandWillFail("cmd2")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenRevertedTimes("cmd2", 2)
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoEnablesRedo() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.whenUndoing()
|
|
suite.thenStackShouldSupportRedo()
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoExecutesCommandAgain() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd1", 2)
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoExecutesCommandOnlyOnce() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.givenRedoWasCalledTimes(1)
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd1", 2)
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoExecutesCommandsInSequence() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenCommandWasPerformed("cmd2")
|
|
suite.givenUndoWasCalledTimes(2)
|
|
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd1", 2)
|
|
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd2", 2)
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoLeavesStackUnchangedIfCommandFails() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.givenCommandWillFail("cmd1")
|
|
suite.givenRedoWasCalledTimes(1)
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd1", 3)
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoMakesCommandsUndoableAgain() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.givenRedoWasCalledTimes(1)
|
|
suite.whenUndoing()
|
|
suite.thenCommandShouldHaveBeenRevertedTimes("cmd1", 2)
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformDropsPendingRedoStack() {
|
|
suite.givenCommandWasPerformed("cmd1")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
|
|
suite.whenPerforming(suite.aCommand("cmd2"))
|
|
suite.thenStackShouldNotSupportRedo()
|
|
|
|
suite.whenRedoing()
|
|
suite.thenCommandShouldHaveBeenExecutedTimes("cmd1", 1)
|
|
}
|
|
|
|
func (suite *StackSuite) TestPerformPanicsIfStackIsInUse() {
|
|
callPerform := func(name string) func() {
|
|
var times int
|
|
return func() {
|
|
require.Equal(suite.T(), 0, times, "Re-entrant call detected of <"+name+">. Should actually panic.")
|
|
times++
|
|
|
|
assert.Panics(suite.T(), func() {
|
|
_ = suite.stack.Perform(suite.aCommand(name+"-nested"), nil)
|
|
}, "Perform during function <"+name+"> should panic")
|
|
}
|
|
}
|
|
|
|
suite.assertPanics(callPerform)
|
|
}
|
|
|
|
func (suite *StackSuite) TestUndoPanicsIfStackIsInUse() {
|
|
callUndo := func(name string) func() {
|
|
var times int
|
|
return func() {
|
|
require.Equal(suite.T(), 0, times, "Re-entrant call detected of <"+name+">. Should actually panic.")
|
|
times++
|
|
|
|
assert.Panics(suite.T(), func() {
|
|
_ = suite.stack.Undo(nil)
|
|
}, "Undo during function <"+name+"> should panic")
|
|
}
|
|
}
|
|
|
|
suite.assertPanics(callUndo)
|
|
}
|
|
|
|
func (suite *StackSuite) TestRedoPanicsIfStackIsInUse() {
|
|
callRedo := func(name string) func() {
|
|
var times int
|
|
return func() {
|
|
require.Equal(suite.T(), 0, times, "Re-entrant call detected of <"+name+">. Should actually panic.")
|
|
times++
|
|
|
|
assert.Panics(suite.T(), func() {
|
|
_ = suite.stack.Undo(nil)
|
|
}, "Redo during function <"+name+"> should panic")
|
|
}
|
|
}
|
|
|
|
suite.assertPanics(callRedo)
|
|
}
|
|
|
|
func (suite *StackSuite) assertPanics(taskFor func(string) func()) {
|
|
cmd1 := suite.aCommandExecuting("cmd1", taskFor("Perform"))
|
|
suite.whenPerforming(cmd1)
|
|
|
|
suite.givenCommandWasPerformed("cmd2")
|
|
suite.givenCommandExecutes("cmd2", taskFor("Undo"))
|
|
suite.whenUndoing()
|
|
|
|
suite.givenCommandWasPerformed("cmd3")
|
|
suite.givenUndoWasCalledTimes(1)
|
|
suite.givenCommandExecutes("cmd3", taskFor("Redo"))
|
|
suite.whenRedoing()
|
|
}
|
|
|
|
func (suite *StackSuite) givenCommandWasPerformed(name string) {
|
|
suite.whenPerforming(suite.aCommand(name))
|
|
}
|
|
|
|
func (suite *StackSuite) givenUndoWasCalledTimes(times int) {
|
|
for i := 0; i < times; i++ {
|
|
_ = suite.stack.Undo(nil)
|
|
}
|
|
}
|
|
|
|
func (suite *StackSuite) givenRedoWasCalledTimes(times int) {
|
|
for i := 0; i < times; i++ {
|
|
_ = suite.stack.Redo(nil)
|
|
}
|
|
}
|
|
|
|
func (suite *StackSuite) givenCommandWillFail(name string) {
|
|
suite.pastCommand(name).pendingError = ss1.StringError("failing")
|
|
}
|
|
|
|
func (suite *StackSuite) givenCommandExecutes(name string, task func()) {
|
|
cmd := suite.pastCommand(name)
|
|
cmd.task = task
|
|
}
|
|
|
|
func (suite *StackSuite) whenUndoing() {
|
|
_ = suite.stack.Undo(nil)
|
|
}
|
|
|
|
func (suite *StackSuite) whenRedoing() {
|
|
_ = suite.stack.Redo(nil)
|
|
}
|
|
|
|
func (suite *StackSuite) whenPerforming(command cmd.Command) {
|
|
_ = suite.stack.Perform(command, nil)
|
|
}
|
|
|
|
func (suite *StackSuite) thenStackShouldSupportRedo() {
|
|
assert.True(suite.T(), suite.stack.CanRedo(), "Stack should be able to redo")
|
|
}
|
|
|
|
func (suite *StackSuite) thenStackShouldNotSupportRedo() {
|
|
assert.False(suite.T(), suite.stack.CanRedo(), "Stack should not be able to redo")
|
|
}
|
|
|
|
func (suite *StackSuite) thenStackShouldSupportUndo() {
|
|
assert.True(suite.T(), suite.stack.CanUndo(), "Stack should be able to undo")
|
|
}
|
|
|
|
func (suite *StackSuite) thenStackShouldNotSupportUndo() {
|
|
assert.False(suite.T(), suite.stack.CanUndo(), "Stack should not be able to undo")
|
|
}
|
|
|
|
func (suite *StackSuite) thenCommandShouldHaveBeenExecuted(name string) {
|
|
cmd := suite.pastCommand(name)
|
|
assert.True(suite.T(), cmd.executed > 0, "Command <"+name+"> should have been executed at least once")
|
|
}
|
|
|
|
func (suite *StackSuite) thenCommandShouldHaveBeenExecutedTimes(name string, expected int) {
|
|
cmd := suite.pastCommand(name)
|
|
assert.Equal(suite.T(), expected, cmd.executed)
|
|
}
|
|
|
|
func (suite *StackSuite) thenCommandShouldHaveBeenReverted(name string) {
|
|
cmd := suite.pastCommand(name)
|
|
assert.True(suite.T(), cmd.reverted > 0, "Command <"+name+"> should have been reverted at least once")
|
|
}
|
|
|
|
func (suite *StackSuite) thenCommandShouldHaveBeenRevertedTimes(name string, expected int) {
|
|
cmd := suite.pastCommand(name)
|
|
assert.Equal(suite.T(), expected, cmd.reverted)
|
|
}
|
|
|
|
func (suite *StackSuite) thenPerformShouldReturnError(cmd cmd.Command, expected error) {
|
|
result := suite.stack.Perform(cmd, nil)
|
|
assert.Equal(suite.T(), expected, result)
|
|
}
|
|
|
|
func (suite *StackSuite) aCommand(name string) cmd.Command {
|
|
cmd := &TestCommand{name: name}
|
|
suite.commands[name] = cmd
|
|
return cmd
|
|
}
|
|
|
|
func (suite *StackSuite) aCommandExecuting(name string, task func()) cmd.Command {
|
|
cmd := &TestCommand{name: name, task: task}
|
|
suite.commands[name] = cmd
|
|
return cmd
|
|
}
|
|
|
|
func (suite *StackSuite) aCommandReturningError() cmd.Command {
|
|
return suite.aCommandReturning(ss1.StringError("fail"))
|
|
}
|
|
|
|
func (suite *StackSuite) aCommandReturning(err error) cmd.Command {
|
|
name := "unnamed"
|
|
cmd := &TestCommand{name: name, pendingError: err}
|
|
suite.commands[name] = cmd
|
|
return cmd
|
|
}
|
|
|
|
func (suite *StackSuite) pastCommand(name string) *TestCommand {
|
|
cmd, found := suite.commands[name]
|
|
if !found {
|
|
require.True(suite.T(), found, "Command not found <"+name+"> - test is wrong")
|
|
}
|
|
return cmd
|
|
}
|