Validate command alias termination
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package alias
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
||||
@@ -138,6 +141,134 @@ func TestResolverRejectsInvalidAndConflictingMappings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminationRejectsCyclesAndGrowingPaths(t *testing.T) {
|
||||
for name, mappings := range map[string][]Mapping{
|
||||
"direct cycle": {
|
||||
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"b"}},
|
||||
{Source: commandpath.Path{"b"}, Target: commandpath.Path{"a"}},
|
||||
},
|
||||
"growing path": {
|
||||
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"b", "extra"}},
|
||||
{Source: commandpath.Path{"b"}, Target: commandpath.Path{"a"}},
|
||||
},
|
||||
"suffix-dependent cycle": {
|
||||
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"b"}},
|
||||
{Source: commandpath.Path{"b"}, Target: commandpath.Path{"group"}},
|
||||
{Source: commandpath.Path{"b", "x"}, Target: commandpath.Path{"a", "x"}},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := ValidateTermination(mappings)
|
||||
var loop *NonTerminationError
|
||||
if !errors.As(err, &loop) {
|
||||
t.Fatalf("ValidateTermination() error = %v, want NonTerminationError", err)
|
||||
}
|
||||
if len(loop.Mappings) == 0 {
|
||||
t.Fatal("NonTerminationError has no participating mappings")
|
||||
}
|
||||
for _, mapping := range loop.Mappings {
|
||||
if !strings.Contains(err.Error(), mapping.Source.String()) {
|
||||
t.Fatalf("error %q omits participating source %q", err, mapping.Source.String())
|
||||
}
|
||||
}
|
||||
if name == "suffix-dependent cycle" {
|
||||
resolver, err := NewResolver(mappings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := resolver.Resolve(commandpath.Path{"a"}); !got.Equal(commandpath.Path{"group"}) {
|
||||
t.Fatalf("bare route resolves to %#v, want [group]", got)
|
||||
}
|
||||
first, _, matched := resolver.ResolveOnce(commandpath.Path{"a", "x"})
|
||||
second, _, matchedAgain := resolver.ResolveOnce(first)
|
||||
if !matched || !matchedAgain || !second.Equal(commandpath.Path{"a", "x"}) {
|
||||
t.Fatalf("suffix cycle is not demonstrated: first %#v, second %#v", first, second)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminationAcceptsConsumingRepetition(t *testing.T) {
|
||||
mappings := []Mapping{
|
||||
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"group"}},
|
||||
{Source: commandpath.Path{"group", "again"}, Target: commandpath.Path{"a"}},
|
||||
}
|
||||
if err := ValidateTermination(mappings); err != nil {
|
||||
t.Fatalf("ValidateTermination() = %v", err)
|
||||
}
|
||||
resolver, err := NewResolver(mappings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := resolver.Resolve(commandpath.Path{"a", "again", "again"})
|
||||
if want := (commandpath.Path{"group"}); !got.Equal(want) {
|
||||
t.Fatalf("Resolve() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminationUsesLongestMatchBeforeFollowingShorterRule(t *testing.T) {
|
||||
mappings := []Mapping{
|
||||
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"b", "x"}},
|
||||
{Source: commandpath.Path{"b"}, Target: commandpath.Path{"a"}},
|
||||
{Source: commandpath.Path{"b", "x"}, Target: commandpath.Path{"group"}},
|
||||
}
|
||||
if err := ValidateTermination(mappings); err != nil {
|
||||
t.Fatalf("ValidateTermination() = %v", err)
|
||||
}
|
||||
resolver, err := NewResolver(mappings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := resolver.Resolve(commandpath.Path{"a"}); !got.Equal(commandpath.Path{"group"}) {
|
||||
t.Fatalf("Resolve() = %#v, want [group]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminationAcceptsLongChainsInAnyRegistrationOrder(t *testing.T) {
|
||||
const count = 80
|
||||
mappings := make([]Mapping, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
source := commandpath.Path{fmtAliasName(index)}
|
||||
target := commandpath.Path{fmtAliasName(index + 1)}
|
||||
if index == count-1 {
|
||||
target = commandpath.Path{"canonical"}
|
||||
}
|
||||
mappings = append(mappings, Mapping{Source: source, Target: target})
|
||||
}
|
||||
if err := ValidateTermination(mappings); err != nil {
|
||||
t.Fatalf("forward registration order: %v", err)
|
||||
}
|
||||
for left, right := 0, len(mappings)-1; left < right; left, right = left+1, right-1 {
|
||||
mappings[left], mappings[right] = mappings[right], mappings[left]
|
||||
}
|
||||
if err := ValidateTermination(mappings); err != nil {
|
||||
t.Fatalf("reverse registration order: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalMarkersCannotBePopped(t *testing.T) {
|
||||
_, machine, err := Compile([]Mapping{{Source: commandpath.Path{"a"}, Target: commandpath.Path{"b"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pop := saturatePopSummaries(machine)
|
||||
for markerName, marker := range map[string]symbolKind{"end": endSymbol, "boundary": boundarySymbol} {
|
||||
for from := range pop {
|
||||
if from.symbol.kind == marker {
|
||||
t.Fatalf("%s marker can be popped from control %d", markerName, from.control)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ValidateTermination(nil); err != nil {
|
||||
t.Fatalf("empty mapping set: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fmtAliasName(index int) string {
|
||||
return "alias" + strconv.Itoa(index)
|
||||
}
|
||||
|
||||
func expectedSymbols(machine *Machine, path commandpath.Path, marker symbolKind) []symbol {
|
||||
result := make([]symbol, 0, len(path)+1)
|
||||
for _, token := range path {
|
||||
|
||||
Reference in New Issue
Block a user