Files
commandkit/internal/alias/resolver_test.go

186 lines
6.1 KiB
Go

package alias
import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
)
func TestResolverUsesLongestTokenPrefixAndPreservesSuffix(t *testing.T) {
resolver, err := NewResolver([]Mapping{
{Source: commandpath.Path{"list"}, Target: commandpath.Path{"promptkit", "list"}},
{Source: commandpath.Path{"list", "collections"}, Target: commandpath.Path{"app", "collections"}},
})
if err != nil {
t.Fatal(err)
}
got := resolver.Resolve(commandpath.Path{"list", "collections", "extra"})
want := commandpath.Path{"app", "collections", "extra"}
if !got.Equal(want) {
t.Fatalf("Resolve() = %#v, want %#v", got, want)
}
got = resolver.Resolve(commandpath.Path{"list", "prompts"})
want = commandpath.Path{"promptkit", "list", "prompts"}
if !got.Equal(want) {
t.Fatalf("Resolve() = %#v, want %#v", got, want)
}
reversed, err := NewResolver([]Mapping{
{Source: commandpath.Path{"list", "collections"}, Target: commandpath.Path{"app", "collections"}},
{Source: commandpath.Path{"list"}, Target: commandpath.Path{"promptkit", "list"}},
})
if err != nil {
t.Fatal(err)
}
if got := reversed.Resolve(commandpath.Path{"list", "collections", "extra"}); !got.Equal(commandpath.Path{"app", "collections", "extra"}) {
t.Fatalf("reverse-order Resolve() = %#v", got)
}
}
func TestResolverFallsBackAfterLongerTokenMatchFails(t *testing.T) {
resolver, err := NewResolver([]Mapping{
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"short"}},
{Source: commandpath.Path{"a", "b", "c"}, Target: commandpath.Path{"long"}},
})
if err != nil {
t.Fatal(err)
}
got := resolver.Resolve(commandpath.Path{"a", "b", "x"})
want := commandpath.Path{"short", "b", "x"}
if !got.Equal(want) {
t.Fatalf("Resolve() = %#v, want %#v", got, want)
}
}
func TestResolverDoesNotMatchUnknownTokens(t *testing.T) {
resolver, err := NewResolver([]Mapping{{Source: commandpath.Path{"list"}, Target: commandpath.Path{"promptkit", "list"}}})
if err != nil {
t.Fatal(err)
}
input := commandpath.Path{"listing", "collections"}
got := resolver.Resolve(input)
if !got.Equal(input) {
t.Fatalf("Resolve() = %#v, want unchanged %#v", got, input)
}
}
func TestCompiledMachineMatchesDirectResolver(t *testing.T) {
mappings := []Mapping{
{Source: commandpath.Path{"list"}, Target: commandpath.Path{"promptkit", "list"}},
{Source: commandpath.Path{"list", "collections"}, Target: commandpath.Path{"promptkit", "list", "collections"}},
{Source: commandpath.Path{"pk"}, Target: commandpath.Path{"promptkit"}},
{Source: commandpath.Path{"promptkit", "list", "bundles"}, Target: commandpath.Path{"promptkit", "list", "collections"}},
{Source: commandpath.Path{"short"}, Target: commandpath.Path{"promptkit", "list"}},
{Source: commandpath.Path{"short", "b", "c"}, Target: commandpath.Path{"long"}},
}
resolver, machine, err := Compile(mappings)
if err != nil {
t.Fatal(err)
}
for key, operation := range machine.transitions {
if len(operation.push) > 2 {
t.Fatalf("transition %+v pushes %d symbols", key, len(operation.push))
}
}
cases := []commandpath.Path{
{"list"},
{"list", "prompts"},
{"list", "collections"},
{"list", "collections", "extra"},
{"pk", "list", "bundles", "tail"},
{"short", "b", "x"},
{"unknown", "operand"},
{},
}
for _, marker := range []symbolKind{endSymbol, boundarySymbol} {
for _, input := range cases {
t.Run(input.String(), func(t *testing.T) {
got, err := runMachineForTest(machine, input, marker)
if err != nil {
t.Fatal(err)
}
want := expectedSymbols(machine, resolver.Resolve(input), marker)
if !reflect.DeepEqual(got, want) {
t.Fatalf("machine result = %#v, want direct result %#v", got, want)
}
})
}
}
}
func TestResolverTreatsIdenticalMappingsAsIdempotent(t *testing.T) {
mapping := Mapping{Source: commandpath.Path{"list"}, Target: commandpath.Path{"promptkit", "list"}}
resolver, err := NewResolver([]Mapping{mapping, mapping})
if err != nil {
t.Fatal(err)
}
got := resolver.Resolve(commandpath.Path{"list", "prompts"})
if want := (commandpath.Path{"promptkit", "list", "prompts"}); !got.Equal(want) {
t.Fatalf("Resolve() = %#v, want %#v", got, want)
}
}
func TestResolverRejectsInvalidAndConflictingMappings(t *testing.T) {
for name, mappings := range map[string][]Mapping{
"empty source": {{Target: commandpath.Path{"target"}}},
"empty target": {{Source: commandpath.Path{"source"}}},
"conflicting target": {
{Source: commandpath.Path{"source"}, Target: commandpath.Path{"first"}},
{Source: commandpath.Path{"source"}, Target: commandpath.Path{"second"}},
},
} {
t.Run(name, func(t *testing.T) {
if _, err := NewResolver(mappings); err == nil {
t.Fatal("NewResolver() succeeded")
}
})
}
}
func expectedSymbols(machine *Machine, path commandpath.Path, marker symbolKind) []symbol {
result := make([]symbol, 0, len(path)+1)
for _, token := range path {
result = append(result, machine.classify(token))
}
result = append(result, symbol{kind: marker})
return result
}
func runMachineForTest(machine *Machine, input commandpath.Path, marker symbolKind) ([]symbol, error) {
stack := expectedSymbols(machine, input, marker)
control := machine.start
for steps := 0; steps < 10000; steps++ {
if control == machine.halt {
return stack, nil
}
if len(stack) == 0 {
return nil, errMachineStackEmpty
}
operation, ok := machine.transitions[transitionKey{control: control, read: stack[0]}]
if !ok {
return nil, errMachineTransitionMissing
}
stack = append(append([]symbol(nil), operation.push...), stack[1:]...)
control = operation.next
}
return nil, errMachineTestDidNotHalt
}
var (
errMachineStackEmpty = testError("compiled machine emptied its stack")
errMachineTransitionMissing = testError("compiled machine has no transition")
errMachineTestDidNotHalt = testError("compiled machine did not halt in the test interpreter")
)
type testError string
func (err testError) Error() string { return string(err) }
func (machine *Machine) classify(token string) symbol {
if _, exists := machine.literals[token]; exists {
return symbol{kind: literalSymbol, value: token}
}
return symbol{kind: otherSymbol}
}