Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
// Command describes an action that can be performed, undone, and redone.
|
||||
type Command interface {
|
||||
// Do performs the action.
|
||||
// An error is returned if the action could not be performed.
|
||||
Do(modder world.Modder) error
|
||||
|
||||
// Undo reverses its action to restore the environment to the previous state.
|
||||
// An error is returned if the action could not be successfully undone. The
|
||||
// environment may not be in the state as before in an error occurred.
|
||||
Undo(modder world.Modder) error
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cmd
|
||||
|
||||
// Commander tries to execute the given command.
|
||||
type Commander interface {
|
||||
Queue(command Command)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cmd
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
// List is a sequence of commands, which are executed as one step.
|
||||
type List []Command
|
||||
|
||||
// Do performs the entries in the list in ascending order.
|
||||
// If an entry returns an error, the iteration is aborted and that error is returned.
|
||||
func (list List) Do(modder world.Modder) error {
|
||||
for _, entry := range list {
|
||||
err := entry.Do(modder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Undo performs the entries in the list in descending order.
|
||||
// If an entry returns an error, the iteration is aborted and that error is returned.
|
||||
func (list List) Undo(modder world.Modder) error {
|
||||
for i := len(list) - 1; i >= 0; i-- {
|
||||
err := list[i].Undo(modder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/world"
|
||||
|
||||
type stackEntry struct {
|
||||
link *stackEntry
|
||||
cmd Command
|
||||
}
|
||||
|
||||
// Stack describes a list of commands. The stack allows to sequentially
|
||||
// undo and redo stacked commands.
|
||||
// It essentially stores two lists: a list of commands to undo, and
|
||||
// another of commands to redo.
|
||||
// Modifying stack functions will panic if they are called while already in use.
|
||||
type Stack struct {
|
||||
lockedBy string
|
||||
undoList *stackEntry
|
||||
redoList *stackEntry
|
||||
}
|
||||
|
||||
// Perform executes the given command and puts it on the stack
|
||||
// if the command was successful.
|
||||
// This function also clears the list of commands to be redone.
|
||||
func (stack *Stack) Perform(cmd Command, modder world.Modder) error {
|
||||
stack.lock("Perform")
|
||||
defer stack.unlock()
|
||||
|
||||
err := cmd.Do(modder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack.undoList = &stackEntry{link: stack.undoList, cmd: cmd}
|
||||
stack.redoList = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanUndo returns true if there is at least one more command that can be undone.
|
||||
func (stack *Stack) CanUndo() bool {
|
||||
return stack.undoList != nil
|
||||
}
|
||||
|
||||
// Undo attempts to undo the previous command on the list.
|
||||
// If there is no further command to undo, nothing happens.
|
||||
// An error is returned if the command failed. In this case, the stack is
|
||||
// unchanged and a further attempt to undo will try the same command again.
|
||||
func (stack *Stack) Undo(modder world.Modder) error {
|
||||
stack.lock("Undo")
|
||||
defer stack.unlock()
|
||||
|
||||
if stack.undoList == nil {
|
||||
return nil
|
||||
}
|
||||
entry := stack.undoList
|
||||
err := entry.cmd.Undo(modder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack.undoList = entry.link
|
||||
entry.link = stack.redoList
|
||||
stack.redoList = entry
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanRedo returns true if there is at least one more command that can be redone.
|
||||
func (stack *Stack) CanRedo() bool {
|
||||
return stack.redoList != nil
|
||||
}
|
||||
|
||||
// Redo attempts to perform the next command on the redo list.
|
||||
// If there is no further command to redo, nothing happens.
|
||||
// An error is returned if the command failed. In this case, the stack is
|
||||
// unchanged and a further attempt to redo will try the same command again.
|
||||
func (stack *Stack) Redo(modder world.Modder) error {
|
||||
stack.lock("Redo")
|
||||
defer stack.unlock()
|
||||
|
||||
if stack.redoList == nil {
|
||||
return nil
|
||||
}
|
||||
entry := stack.redoList
|
||||
err := entry.cmd.Do(modder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stack.redoList = entry.link
|
||||
entry.link = stack.undoList
|
||||
stack.undoList = entry
|
||||
return nil
|
||||
}
|
||||
|
||||
func (stack *Stack) lock(by string) {
|
||||
if stack.lockedBy != "" {
|
||||
panic("Stack already in use by <" + stack.lockedBy + ">")
|
||||
}
|
||||
stack.lockedBy = by
|
||||
}
|
||||
|
||||
func (stack *Stack) unlock() {
|
||||
stack.lockedBy = ""
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
// Task represents a modification on given modder.
|
||||
type Task func(modder world.Modder) error
|
||||
|
||||
// TransactionError is returned from a transaction execution if a task failed.
|
||||
type TransactionError struct {
|
||||
// Name is that of the transaction, if it was set.
|
||||
Name string
|
||||
// Index identifies the list index of tasks.
|
||||
Index int
|
||||
// Nested is the error returned from the task. In case of nested commands, this may be another TransactionError.
|
||||
Nested error
|
||||
}
|
||||
|
||||
// Error returns the path of the transaction task, combined with the nested error text.
|
||||
func (err TransactionError) Error() string {
|
||||
return fmt.Sprintf("%s[%d]: %v", err.Name, err.Index, err.Nested)
|
||||
}
|
||||
|
||||
// Unwrap returns the nested error, to support diving into nested errors.
|
||||
func (err TransactionError) Unwrap() error {
|
||||
return err.Nested
|
||||
}
|
||||
|
||||
// Transaction is a named list of forward and reverse tasks.
|
||||
// Transaction provides execution of the tasks as a Command.
|
||||
type Transaction struct {
|
||||
Name string
|
||||
Forward []Task
|
||||
Reverse []Task
|
||||
}
|
||||
|
||||
// Do executes the forward tasks in series. The first task to return an error aborts the iteration.
|
||||
// A TransactionError is returned in case of aborted iteration.
|
||||
func (txn Transaction) Do(modder world.Modder) error {
|
||||
return txn.perform(txn.Forward, modder)
|
||||
}
|
||||
|
||||
// Undo executes the reverse tasks in series. The first task to return an error aborts the iteration.
|
||||
// A TransactionError is returned in case of aborted iteration.
|
||||
func (txn Transaction) Undo(modder world.Modder) error {
|
||||
return txn.perform(txn.Reverse, modder)
|
||||
}
|
||||
|
||||
func (txn Transaction) perform(tasks []Task, modder world.Modder) error {
|
||||
for i, task := range tasks {
|
||||
err := task(modder)
|
||||
if err != nil {
|
||||
return TransactionError{
|
||||
Name: txn.Name,
|
||||
Index: i,
|
||||
Nested: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransactionModifier is a function to change a transaction while it is being built.
|
||||
type TransactionModifier func(*Transaction) error
|
||||
|
||||
// Registry allows to build commands as transactions with modifier functions.
|
||||
type Registry interface {
|
||||
Commander
|
||||
Register(modifier ...TransactionModifier) error
|
||||
}
|
||||
|
||||
// TransactionBuilder provides a way to register a command at a commander with
|
||||
// possibly nested actions.
|
||||
type TransactionBuilder struct {
|
||||
Commander Commander
|
||||
active *Transaction
|
||||
}
|
||||
|
||||
// Register creates a new transaction based on given modifier.
|
||||
func (builder *TransactionBuilder) Register(modifier ...TransactionModifier) error {
|
||||
previous := builder.active
|
||||
current := &Transaction{}
|
||||
|
||||
builder.active = current
|
||||
for _, mod := range modifier {
|
||||
err := mod(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
builder.active = previous
|
||||
|
||||
// invert reverse task for easier iteration
|
||||
for left, right := 0, len(current.Reverse)-1; left < right; left, right = left+1, right-1 {
|
||||
current.Reverse[left], current.Reverse[right] = current.Reverse[right], current.Reverse[left]
|
||||
}
|
||||
|
||||
builder.Queue(current)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Queue implements the Commander interface to add given command to a currently built transaction.
|
||||
// If there is no outer transaction being built, then the command is immediately forwarded to the commander.
|
||||
// Avoid building a loop between TransactionBuilder instances, as this will result in a stack overflow.
|
||||
func (builder *TransactionBuilder) Queue(command Command) {
|
||||
if builder.active == nil {
|
||||
builder.Commander.Queue(command)
|
||||
return
|
||||
}
|
||||
|
||||
builder.active.Forward = append(builder.active.Forward, command.Do)
|
||||
builder.active.Reverse = append(builder.active.Reverse, command.Undo)
|
||||
}
|
||||
|
||||
// Named allows to name the current level of transaction.
|
||||
func Named(name string) TransactionModifier {
|
||||
return func(txn *Transaction) error {
|
||||
txn.Name = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Forward creates a modifier for a task that is called when doing a command.
|
||||
// If a nested command is added to the transaction after the forward function, then
|
||||
// the nested task is performed after the given task.
|
||||
// If a nested command is added to the transaction before the forward function, then
|
||||
// the nested task is performed before the given task.
|
||||
func Forward(task Task) TransactionModifier {
|
||||
return func(txn *Transaction) error {
|
||||
txn.Forward = append(txn.Forward, task)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse creates a modifier for a task that is called when undoing a command.
|
||||
// If a nested command is added to the transaction after the reverse function, then
|
||||
// the nested task is performed before the given task.
|
||||
// If a nested command is added to the transaction before the reverse function, then
|
||||
// the nested task is performed after the given task.
|
||||
func Reverse(task Task) TransactionModifier {
|
||||
return func(txn *Transaction) error {
|
||||
txn.Reverse = append(txn.Reverse, task)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Nested adds a further level to the currently registered transaction.
|
||||
// Depending on the order of the modifier in the register, the corresponding tasks will be called
|
||||
// in sequence. See other modifier.
|
||||
func Nested(nested func() error) TransactionModifier {
|
||||
return func(txn *Transaction) error {
|
||||
return nested()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestTransactionBuilder(t *testing.T) {
|
||||
suite.Run(t, new(TransactionBuilderSuite))
|
||||
}
|
||||
|
||||
type TransactionBuilderSuite struct {
|
||||
suite.Suite
|
||||
|
||||
builder cmd.TransactionBuilder
|
||||
command cmd.Command
|
||||
result []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) SetupTest() {
|
||||
suite.builder = cmd.TransactionBuilder{Commander: suite}
|
||||
suite.command = nil
|
||||
suite.result = nil
|
||||
suite.err = nil
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) TestTransactionCallsForwardTasksFromLeftToRight() {
|
||||
suite.givenRegisteredTransaction(
|
||||
cmd.Forward(suite.aTask("fa")),
|
||||
cmd.Forward(suite.aTask("fb")),
|
||||
cmd.Reverse(suite.aTask("ra")),
|
||||
)
|
||||
suite.whenCommandIsDone()
|
||||
suite.thenResultShouldBe([]string{"fa", "fb"})
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) TestTransactionCallsReverseTasksFromRightToLeft() {
|
||||
suite.givenRegisteredTransaction(
|
||||
cmd.Forward(suite.aTask("fa")),
|
||||
cmd.Reverse(suite.aTask("ra")),
|
||||
cmd.Reverse(suite.aTask("rb")),
|
||||
)
|
||||
suite.whenCommandIsUndone()
|
||||
suite.thenResultShouldBe([]string{"rb", "ra"})
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) TestTransactionCallsNestedInForwardDirection() {
|
||||
suite.givenRegisteredTransaction(
|
||||
cmd.Forward(suite.aTask("fa")),
|
||||
cmd.Nested(func() error {
|
||||
return suite.builder.Register(cmd.Forward(suite.aTask("na")))
|
||||
}),
|
||||
cmd.Forward(suite.aTask("fb")),
|
||||
)
|
||||
suite.whenCommandIsDone()
|
||||
suite.thenResultShouldBe([]string{"fa", "na", "fb"})
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) TestTransactionCallsNestedInReverseDirection() {
|
||||
suite.givenRegisteredTransaction(
|
||||
cmd.Reverse(suite.aTask("ra")),
|
||||
cmd.Nested(func() error {
|
||||
return suite.builder.Register(cmd.Reverse(suite.aTask("na")), cmd.Reverse(suite.aTask("nb")))
|
||||
}),
|
||||
cmd.Reverse(suite.aTask("rb")),
|
||||
)
|
||||
suite.whenCommandIsUndone()
|
||||
suite.thenResultShouldBe([]string{"rb", "nb", "na", "ra"})
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) TestTransactionReturnsErrorWithNamedPath() {
|
||||
suite.givenRegisteredTransaction(
|
||||
cmd.Named("main"),
|
||||
cmd.Forward(suite.aTask("fa")),
|
||||
cmd.Nested(func() error {
|
||||
return suite.builder.Register(cmd.Named("nested"), cmd.Forward(suite.aTaskReturning(ss1.StringError("fail"))))
|
||||
}),
|
||||
cmd.Forward(suite.aTask("fb")),
|
||||
)
|
||||
suite.whenCommandIsDone()
|
||||
suite.thenResultShouldBeError("main[1]: nested[0]: fail")
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) givenRegisteredTransaction(modifier ...cmd.TransactionModifier) {
|
||||
err := suite.builder.Register(modifier...)
|
||||
require.Nil(suite.T(), err, "no error expected registering transaction")
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) whenCommandIsDone() {
|
||||
suite.err = suite.command.Do(nil)
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) whenCommandIsUndone() {
|
||||
suite.err = suite.command.Undo(nil)
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) thenResultShouldBe(expected []string) {
|
||||
assert.Equal(suite.T(), expected, suite.result)
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) thenResultShouldBeError(expected string) {
|
||||
require.NotNil(suite.T(), suite.err, "no error returned")
|
||||
var txnErr cmd.TransactionError
|
||||
isTxnErr := errors.As(suite.err, &txnErr)
|
||||
require.True(suite.T(), isTxnErr, "error is not a transaction error: %v", suite.err)
|
||||
assert.Equal(suite.T(), expected, txnErr.Error())
|
||||
assert.NotNil(suite.T(), txnErr.Nested, "no nested error set")
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) Queue(command cmd.Command) {
|
||||
require.Nil(suite.T(), suite.command, "Not expecting a second command")
|
||||
suite.command = command
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) aTask(text string) cmd.Task {
|
||||
return func(modder world.Modder) error {
|
||||
suite.result = append(suite.result, text)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TransactionBuilderSuite) aTaskReturning(err error) cmd.Task {
|
||||
return func(modder world.Modder) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user