mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
e0de872740
Extract ConvertChunk/FormatHexFrame into pkg/charset (zero external deps). Extract UIEvent/UIEventKind/UIPanelKind types into internal/event. Update all references across main package to use qualified imports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
497 lines
13 KiB
Go
497 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
|
)
|
|
|
|
func setupTestPipes() {
|
|
var cr *io.PipeReader
|
|
cr, stdinPipe = io.Pipe()
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := cr.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func newTestAppForCommand() *App {
|
|
a := &App{
|
|
cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"},
|
|
plugins: NewPluginManager(),
|
|
uiEvents: make(chan event.UIEvent, 32),
|
|
done: make(chan struct{}),
|
|
}
|
|
a.SetUIEnabled(true)
|
|
a.forward = NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
|
|
a.dispatcher = NewCommandDispatcher(a)
|
|
return a
|
|
}
|
|
|
|
func TestCommandCompleteRoot(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
line, cands := a.dispatcher.Complete(".")
|
|
if line != "." {
|
|
t.Fatalf("expected line unchanged for ambiguous completion, got %q", line)
|
|
}
|
|
if len(cands) == 0 {
|
|
t.Fatalf("expected root command candidates")
|
|
}
|
|
for _, c := range cands {
|
|
if c == ".ctrl" {
|
|
t.Fatalf(".ctrl should be removed from command set")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommandCompleteForwardSubcommands(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
_, cands := a.dispatcher.Complete(".forward ")
|
|
joined := strings.Join(cands, ",")
|
|
for _, name := range []string{"list", "add", "remove", "enable", "disable", "update", "stats"} {
|
|
if !strings.Contains(joined, name) {
|
|
t.Fatalf("missing forward candidate %q in %v", name, cands)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteUnknown(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".unknown")
|
|
if !handled {
|
|
t.Fatalf("unknown command should be marked handled")
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("expected unknown command error")
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteHelpShowsModal(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".help")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".help execute failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
ev := mustReadEvent(t, a.uiEvents)
|
|
if ev.Kind != event.UIEventModal || ev.Title == "" {
|
|
t.Fatalf("expected help modal event, got %+v", ev)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteForwardListShowsPanel(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".forward list")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".forward list execute failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
ev := mustReadEvent(t, a.uiEvents)
|
|
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelForward {
|
|
t.Fatalf("expected forward panel event, got %+v", ev)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecutePluginListShowsPanel(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
if _, err := a.plugins.Load("plugins/demo.lua"); err == nil {
|
|
_ = a.plugins.Disable("demo")
|
|
}
|
|
handled, err := a.dispatcher.Execute(".plugin list")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".plugin list execute failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
ev := mustReadEvent(t, a.uiEvents)
|
|
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelPlugin {
|
|
t.Fatalf("expected plugin panel event, got %+v", ev)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecutePluginWithoutSubcommandShowsPanel(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".plugin")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".plugin execute failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
ev := mustReadEvent(t, a.uiEvents)
|
|
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelPlugin {
|
|
t.Fatalf("expected plugin panel event for bare command, got %+v", ev)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteModeShowsPanel(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".mode show")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode execute failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
ev := mustReadEvent(t, a.uiEvents)
|
|
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelMode {
|
|
t.Fatalf("expected mode panel event, got %+v", ev)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteModeSet(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".mode set end \\r\\n")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set end failed handled=%v err=%v", handled, err)
|
|
}
|
|
if a.cfg.endStr != "\\r\\n" {
|
|
t.Fatalf("mode set end not applied, got=%q", a.cfg.endStr)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".mode set timestamp on")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set timestamp failed handled=%v err=%v", handled, err)
|
|
}
|
|
if !a.cfg.timesTamp {
|
|
t.Fatalf("mode set timestamp should enable timesTamp")
|
|
}
|
|
}
|
|
|
|
func TestParseOnOff(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
val bool
|
|
valid bool
|
|
}{
|
|
{in: "on", val: true, valid: true},
|
|
{in: "true", val: true, valid: true},
|
|
{in: "1", val: true, valid: true},
|
|
{in: "yes", val: true, valid: true},
|
|
{in: "off", val: false, valid: true},
|
|
{in: "false", val: false, valid: true},
|
|
{in: "0", val: false, valid: true},
|
|
{in: "no", val: false, valid: true},
|
|
{in: "", val: false, valid: false},
|
|
{in: "maybe", val: false, valid: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got, ok := parseOnOff(tt.in)
|
|
if ok != tt.valid || got != tt.val {
|
|
t.Fatalf("parseOnOff(%q) got=(%v,%v) want=(%v,%v)", tt.in, got, ok, tt.val, tt.valid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompleteForward(t *testing.T) {
|
|
tests := []struct {
|
|
args []string
|
|
want []string
|
|
}{
|
|
{args: []string{".forward"}, want: []string{"list", "add", "remove", "enable", "disable", "update", "stats"}},
|
|
{args: []string{".forward", ""}, want: []string{"list", "add", "remove", "enable", "disable", "update", "stats"}},
|
|
{args: []string{".forward", "add", ""}, want: []string{"tcp", "udp"}},
|
|
{args: []string{".forward", "update", "1", ""}, want: []string{"tcp", "udp"}},
|
|
{args: []string{".forward", "list", "1"}, want: nil},
|
|
}
|
|
for _, tt := range tests {
|
|
got := completeForward(tt.args)
|
|
if !stringSlicesEqual(got, tt.want) {
|
|
t.Fatalf("completeForward(%v) got=%v want=%v", tt.args, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompletePlugin(t *testing.T) {
|
|
tests := []struct {
|
|
args []string
|
|
want []string
|
|
}{
|
|
{args: []string{".plugin"}, want: []string{"list", "load", "unload", "enable", "disable", "reload"}},
|
|
{args: []string{".plugin", "load", ""}, want: nil},
|
|
{args: []string{".plugin", "unload", "demo"}, want: nil},
|
|
}
|
|
for _, tt := range tests {
|
|
got := completePlugin(tt.args)
|
|
if !stringSlicesEqual(got, tt.want) {
|
|
t.Fatalf("completePlugin(%v) got=%v want=%v", tt.args, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompleteMode(t *testing.T) {
|
|
tests := []struct {
|
|
args []string
|
|
want []string
|
|
}{
|
|
{args: []string{".mode"}, want: []string{"show", "set"}},
|
|
{args: []string{".mode", "set", ""}, want: []string{"in", "out", "end", "frame", "timestamp", "timefmt"}},
|
|
{args: []string{".mode", "set", "timestamp", ""}, want: []string{"on", "off"}},
|
|
{args: []string{".mode", "set", "in", ""}, want: nil},
|
|
}
|
|
for _, tt := range tests {
|
|
got := completeMode(tt.args)
|
|
if !stringSlicesEqual(got, tt.want) {
|
|
t.Fatalf("completeMode(%v) got=%v want=%v", tt.args, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stringSlicesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestHelpText(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
text := a.dispatcher.HelpText()
|
|
for _, cmd := range []string{".help", ".exit", ".hex", ".forward", ".plugin", ".mode"} {
|
|
if !strings.Contains(text, cmd) {
|
|
t.Fatalf("HelpText missing command %q", cmd)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteHex(t *testing.T) {
|
|
setupTestPipes()
|
|
a := newTestAppForCommand()
|
|
handled, err := a.dispatcher.Execute(".hex 41 42 43")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".hex valid failed handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".hex")
|
|
if !handled || err == nil {
|
|
t.Fatalf(".hex no args should error, handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".hex xyz")
|
|
if !handled || err == nil {
|
|
t.Fatalf(".hex invalid hex should error, handled=%v err=%v", handled, err)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteExit(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
a.Close()
|
|
if !a.isClosed() {
|
|
t.Fatalf("expected app closed after Close()")
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteModeSetAll(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
|
|
handled, err := a.dispatcher.Execute(".mode set frame 32")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set frame failed: handled=%v err=%v", handled, err)
|
|
}
|
|
if a.cfg.frameSize != 32 {
|
|
t.Fatalf("frameSize not set, got=%d", a.cfg.frameSize)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".mode set timefmt 2006")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set timefmt failed: handled=%v err=%v", handled, err)
|
|
}
|
|
if a.cfg.timesFmt != "2006" {
|
|
t.Fatalf("timesFmt not set, got=%q", a.cfg.timesFmt)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".mode set out GBK")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set out failed: handled=%v err=%v", handled, err)
|
|
}
|
|
if a.cfg.outputCode != "GBK" {
|
|
t.Fatalf("outputCode not set, got=%q", a.cfg.outputCode)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".mode set in GBK")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode set in failed: handled=%v err=%v", handled, err)
|
|
}
|
|
if a.cfg.inputCode != "GBK" {
|
|
t.Fatalf("inputCode not set, got=%q", a.cfg.inputCode)
|
|
}
|
|
}
|
|
|
|
func TestCommandExecuteModeErrors(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
|
|
handled, err := a.dispatcher.Execute(".mode")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".mode with no subcommand in UI mode shows panel, handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".mode set")
|
|
if err == nil {
|
|
t.Fatalf(".mode set with no args should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".mode set frame abc")
|
|
if err == nil {
|
|
t.Fatalf(".mode set frame with non-int should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".mode set timestamp maybe")
|
|
if err == nil {
|
|
t.Fatalf(".mode set timestamp with invalid value should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".mode set invalid_field value")
|
|
if err == nil {
|
|
t.Fatalf(".mode set unknown field should error")
|
|
}
|
|
}
|
|
|
|
func TestHandleForwardCommandErrors(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
|
|
_, err := a.dispatcher.Execute(".forward add")
|
|
if err == nil {
|
|
t.Fatalf(".forward add with no args should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward add badmode 127.0.0.1:1")
|
|
if err == nil {
|
|
t.Fatalf(".forward add with invalid mode should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward remove abc")
|
|
if err == nil {
|
|
t.Fatalf(".forward remove with non-int ID should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward remove 999")
|
|
if err == nil {
|
|
t.Fatalf(".forward remove non-existing should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward enable abc")
|
|
if err == nil {
|
|
t.Fatalf(".forward enable with non-int ID should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward disable abc")
|
|
if err == nil {
|
|
t.Fatalf(".forward disable with non-int ID should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward update")
|
|
if err == nil {
|
|
t.Fatalf(".forward update with no args should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward update 1")
|
|
if err == nil {
|
|
t.Fatalf(".forward update with missing addr should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward update 1 badmode 127.0.0.1:1")
|
|
if err == nil {
|
|
t.Fatalf(".forward update with invalid mode should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".forward unknown_sub")
|
|
if err == nil {
|
|
t.Fatalf(".forward unknown subcommand should error")
|
|
}
|
|
}
|
|
|
|
func TestHandleForwardCommandNoUI(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
a.SetUIEnabled(false)
|
|
|
|
handled, err := a.dispatcher.Execute(".forward")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".forward in non-UI should default to list, handled=%v err=%v", handled, err)
|
|
}
|
|
|
|
handled, err = a.dispatcher.Execute(".forward list")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".forward list in non-UI failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandlePluginCommandErrors(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
|
|
_, err := a.dispatcher.Execute(".plugin load")
|
|
if err == nil {
|
|
t.Fatalf(".plugin load with no path should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".plugin unload")
|
|
if err == nil {
|
|
t.Fatalf(".plugin unload with no name should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".plugin enable")
|
|
if err == nil {
|
|
t.Fatalf(".plugin enable with no name should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".plugin disable")
|
|
if err == nil {
|
|
t.Fatalf(".plugin disable with no name should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".plugin reload")
|
|
if err == nil {
|
|
t.Fatalf(".plugin reload with no name should error")
|
|
}
|
|
|
|
_, err = a.dispatcher.Execute(".plugin unknown_sub")
|
|
if err == nil {
|
|
t.Fatalf(".plugin unknown subcommand should error")
|
|
}
|
|
}
|
|
|
|
func TestHandlePluginCommandNoUI(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
a.SetUIEnabled(false)
|
|
|
|
handled, err := a.dispatcher.Execute(".plugin")
|
|
if err != nil || !handled {
|
|
t.Fatalf(".plugin in non-UI should default to list, handled=%v err=%v", handled, err)
|
|
}
|
|
}
|
|
|
|
func TestCompleteFirstTokenEdgeCases(t *testing.T) {
|
|
a := newTestAppForCommand()
|
|
line, cands := a.dispatcher.Complete(".he")
|
|
if line != ".he" {
|
|
t.Fatalf("ambiguous completion should not change line, got=%q", line)
|
|
}
|
|
found := false
|
|
for _, c := range cands {
|
|
if c == ".help" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected .help in completion candidates, got %v", cands)
|
|
}
|
|
|
|
line, cands = a.dispatcher.Complete(".exi")
|
|
if line != ".exit " || len(cands) != 1 || cands[0] != ".exit" {
|
|
t.Fatalf("exact completion of .exi failed: line=%q cands=%v", line, cands)
|
|
}
|
|
|
|
line, _ = a.dispatcher.Complete("")
|
|
if line != "" {
|
|
t.Fatalf("empty completion should be noop, got=%q", line)
|
|
}
|
|
}
|