package alias import ( "errors" "reflect" "strconv" "strings" "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 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 { 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} }