diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-11 21:50:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-11 21:50:15 +0300 |
| commit | b315ebbcd92e58249c6ed8f04217ef7adcdde5d5 (patch) | |
| tree | d264b595dd2215523ecd1f50540342a3697b612b | |
| parent | 8dbe047feaae419d9a5bdc34dfe9153e6704fd7f (diff) | |
more on this
| -rw-r--r-- | internal/repl/commands.go | 31 | ||||
| -rw-r--r-- | internal/repl/completer.go | 2 | ||||
| -rw-r--r-- | internal/repl/completer_test.go | 2 | ||||
| -rw-r--r-- | internal/repl/concurrent_test.go | 53 | ||||
| -rw-r--r-- | internal/repl/repl.go | 145 | ||||
| -rw-r--r-- | internal/repl/repl_completer_test.go | 2 | ||||
| -rw-r--r-- | internal/repl/repl_test.go | 644 | ||||
| -rw-r--r-- | internal/rpn/number.go | 41 | ||||
| -rw-r--r-- | internal/rpn/operations_test.go | 170 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 6 |
10 files changed, 522 insertions, 574 deletions
diff --git a/internal/repl/commands.go b/internal/repl/commands.go index 841265b..3bdcde5 100644 --- a/internal/repl/commands.go +++ b/internal/repl/commands.go @@ -13,20 +13,12 @@ import ( // Commands: help, clear, quit, exit, rpn, calc, rat var builtinCommandsList = []string{"help", "clear", "quit", "exit", "rpn", "calc", "rat"} -// builtinCommands returns the list of built-in commands. -// This is a package-level wrapper for backward compatibility. -// -// Returns a slice of built-in command names -func builtinCommands() []string { - return builtinCommandsList -} - // Commands returns the list of built-in command names supported by the REPL. // This is a public function that exposes the built-in command list. // // Returns a slice of built-in command names (e.g., "help", "clear", "quit") func Commands() []string { - return builtinCommands() + return builtinCommandsList } // ExecuteCommand runs a built-in command and returns its output or error. @@ -144,3 +136,24 @@ func cmdQuit() error { fmt.Println("Goodbye!") return nil } + +// isBuiltinCommand checks if input starts with a built-in command. +// It performs case-insensitive matching against known built-in commands. +// +// input: the command string to check +// Returns the input string and true if it starts with a built-in command, +// or empty string and false otherwise +func isBuiltinCommand(input string) (string, bool) { + args := strings.Fields(input) + if len(args) == 0 { + return "", false + } + + cmd := strings.ToLower(args[0]) + for _, builtin := range builtinCommandsList { + if cmd == builtin { + return input, true + } + } + return "", false +} diff --git a/internal/repl/completer.go b/internal/repl/completer.go index d66a5ef..0eb31e0 100644 --- a/internal/repl/completer.go +++ b/internal/repl/completer.go @@ -43,7 +43,7 @@ func completer(d prompt.Document) []prompt.Suggest { } var suggestions []prompt.Suggest - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, diff --git a/internal/repl/completer_test.go b/internal/repl/completer_test.go index e64a000..4166843 100644 --- a/internal/repl/completer_test.go +++ b/internal/repl/completer_test.go @@ -140,7 +140,7 @@ func TestCompleter(t *testing.T) { // Only generate suggestions if text is not empty // (empty string is a prefix of all strings, so we need to handle it specially) if tt.text != "" { - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(tt.text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, diff --git a/internal/repl/concurrent_test.go b/internal/repl/concurrent_test.go index d07b9ce..501b363 100644 --- a/internal/repl/concurrent_test.go +++ b/internal/repl/concurrent_test.go @@ -3,61 +3,92 @@ package repl import ( "sync" "testing" + + "codeberg.org/snonux/gt/internal/rpn" ) +// TestConcurrentExecutor tests concurrent calls to defaultExecutor with fresh state func TestConcurrentExecutor(t *testing.T) { - // Test concurrent calls to executor() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - executor("20% of 150") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "20% of 150") }(i) } wg.Wait() } +// TestConcurrentRPN tests concurrent inline RPN evaluation func TestConcurrentRPN(t *testing.T) { - // Test concurrent calls to runRPN() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - // Ignore error return as the expression "3 4 +" should always succeed - _, _ = runRPN("3 4 +") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + _, _ = rpnCalc.ParseAndEvaluate("3 4 +") }(i) } wg.Wait() } +// TestConcurrentRatModeToggle tests concurrent rat mode toggles with fresh state func TestConcurrentRatModeToggle(t *testing.T) { - // Test concurrent calls to executor() that change mode var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - executor("rat toggle") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat toggle") }(i) } wg.Wait() } +// TestConcurrentExecutorAndRPN tests concurrent executor and RPN calls with fresh state func TestConcurrentExecutorAndRPN(t *testing.T) { - // Test concurrent calls to executor() and runRPN() var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(2) go func(id int) { defer wg.Done() - executor("20% of 150") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "20% of 150") }(i) go func(id int) { defer wg.Done() - // Ignore error return as the expression "3 4 +" should always succeed - _, _ = runRPN("3 4 +") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + _, _ = rpnCalc.ParseAndEvaluate("3 4 +") }(i) } wg.Wait() diff --git a/internal/repl/repl.go b/internal/repl/repl.go index f452351..e70be01 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -6,7 +6,6 @@ package repl import ( "fmt" "strings" - "sync" "codeberg.org/snonux/gt/internal/rpn" @@ -22,26 +21,6 @@ type RPNState struct { rpnCalc *rpn.RPN } -// executorREPL holds the REPL instance created by the executor function. -// This is used for backward compatibility with tests that need to access RPN state -// after calling executor(). It's not part of the main REPL architecture. -// Thread safety: Use executorREPLOnce for lazy initialization and executorREPLMu for access. -var executorREPL *REPL -var executorREPLOnce sync.Once -var executorREPLMu sync.Mutex - -// ResetExecutorREPL resets the executorREPL for clean test isolation. -// This should be called between tests that use executor() and getRPNState() -// to ensure each test starts with a fresh RPN state. -// -// Note: This function is intended for test use only and should not be used -// in production code. For production use, create new REPL instances with NewREPL(). -func ResetExecutorREPL() { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - executorREPL = nil -} - // REPL manages the interactive command-line interface for the percentage calculator. // It provides an interactive prompt with history, tab-completion, signal handling, // and command processing through a chain of responsibility pattern. @@ -190,7 +169,7 @@ func defaultCompleter(r *REPL, d prompt.Document) []prompt.Suggest { } var suggestions []prompt.Suggest - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, @@ -237,125 +216,3 @@ func RunREPL() error { repl := NewREPL(nil, nil) return repl.Run() } - -// executor runs a calculation command and returns the result. -// This is a package-level wrapper for backward compatibility and testing. -// It creates a minimal REPL instance without building a prompt, allowing -// calculation execution in non-interactive contexts. -// -// input: the calculation or command string to execute -// The function processes the input through defaultExecutor, which handles -// commands via the chain of responsibility pattern, including percentage -// calculations, RPN expressions, and built-in commands. -func executor(input string) { - // Initialize executorREPL only once using sync.Once for thread-safe lazy initialization - executorREPLOnce.Do(func() { - vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - - // Create a minimal REPL instance without building a prompt - executorREPL = &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: NewCommandChain(), - rpnState: rpnState, - } - }) - - // Use mutex to protect access to executorREPL during execution - executorREPLMu.Lock() - repl := executorREPL - executorREPLMu.Unlock() - - defaultExecutor(repl, input) -} - -// runRPN parses and evaluates an RPN (Reverse Polish Notation) expression. -// This is a package-level wrapper for backward compatibility that delegates to -// the executor's REPL runRPN method. -// -// input: the RPN expression to evaluate -// Returns the result string and an error if the expression is invalid -func runRPN(input string) (string, error) { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - - if executorREPL != nil { - return executorREPL.rpnState.rpnCalc.ParseAndEvaluate(input) - } - return "", fmt.Errorf("no executor REPL available - call executor() first") -} - -// getRPNState returns the RPN state from the executor's REPL. -// This is a package-level helper for backward compatibility with tests that need -// to access RPN state after calling executor(). It's not part of the main REPL -// architecture. -// -// Returns the RPNState instance from the last executor() call, or nil if executor() hasn't been called -func getRPNState() *RPNState { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - - if executorREPL != nil { - return executorREPL.rpnState - } - return nil -} - - - - - -// getHistoryPath returns the absolute path to the history file. -// This is a package-level wrapper for backward compatibility. -// The history file is stored in the user's home directory. -// -// Returns the full path to the history file, or empty string on error -func getHistoryPath() string { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Path() -} - -// loadHistory loads history from the history file. -// This is a package-level wrapper for backward compatibility that uses NewHistoryManager. -// -// Returns a slice of history entries, or nil if the file doesn't exist -func loadHistory() []string { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Load() -} - -// saveHistory saves history to the history file. -// This is a package-level wrapper for backward compatibility that uses NewHistoryManager. -// -// history: the slice of history entries to save -// Returns an error if the file cannot be written -func saveHistory(history []string) error { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Save(history) -} - -// isBuiltinCommand checks if input starts with a built-in command. -// It performs case-insensitive matching against known built-in commands. -// -// input: the command string to check -// Returns the input string and true if it starts with a built-in command, -// or empty string and false otherwise -func isBuiltinCommand(input string) (string, bool) { - args := strings.Fields(input) - if len(args) == 0 { - return "", false - } - - cmd := strings.ToLower(args[0]) - for _, builtin := range builtinCommands() { - if cmd == builtin { - return input, true - } - } - return "", false -} diff --git a/internal/repl/repl_completer_test.go b/internal/repl/repl_completer_test.go index 05ef194..9eb0a37 100644 --- a/internal/repl/repl_completer_test.go +++ b/internal/repl/repl_completer_test.go @@ -48,7 +48,7 @@ func TestCompleterLogic(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Simulate the completer logic var found bool - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(tc.text)) { found = true break diff --git a/internal/repl/repl_test.go b/internal/repl/repl_test.go index 3abb664..a242956 100644 --- a/internal/repl/repl_test.go +++ b/internal/repl/repl_test.go @@ -8,50 +8,63 @@ import ( "testing" "codeberg.org/snonux/gt/internal/rpn" - - "github.com/c-bata/go-prompt" ) -func TestExecutor(t *testing.T) { - // Test that executor doesn't panic on empty input - executor("") +// Helper to create a minimal REPL for testing without prompt (no TTY required) +func createTestREPL() *REPL { + vars := rpn.NewVariables() + return &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpn.NewRPN(vars)}, + } } func TestExecutorWithHelp(t *testing.T) { - // Test executor with help command - executor("help") + repl := createTestREPL() + defaultExecutor(repl, "help") } func TestExecutorWithClear(t *testing.T) { - executor("clear") + repl := createTestREPL() + defaultExecutor(repl, "clear") } func TestExecutorWithQuit(t *testing.T) { - executor("quit") + repl := createTestREPL() + defaultExecutor(repl, "quit") } func TestExecutorWithExit(t *testing.T) { - executor("exit") + repl := createTestREPL() + defaultExecutor(repl, "exit") } func TestExecutorWithPercentage(t *testing.T) { - executor("20% of 150") + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") } func TestExecutorWithRPN(t *testing.T) { - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithInvalid(t *testing.T) { - executor("invalid input") + repl := createTestREPL() + defaultExecutor(repl, "invalid input") } func TestExecutorWithVars(t *testing.T) { - executor("rpn x 5 = vars") + repl := createTestREPL() + defaultExecutor(repl, "rpn x 5 = vars") } func TestExecutorWithClearVariables(t *testing.T) { - executor("rpn clear") + repl := createTestREPL() + defaultExecutor(repl, "rpn clear") } func TestIsBuiltinCommand(t *testing.T) { @@ -152,7 +165,7 @@ func TestIsBuiltinCommandWithMixedCase(t *testing.T) { } } -// TestRunRPN tests the runRPN helper function +// TestRunRPN tests inline RPN evaluation (like cmd/gt/main.go does) func TestRunRPN(t *testing.T) { tests := []struct { name string @@ -172,9 +185,19 @@ func TestRunRPN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := runRPN(tt.input) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + + input := strings.TrimSpace(tt.input) + if strings.HasPrefix(input, "rpn ") { + input = strings.TrimPrefix(input, "rpn ") + } else if strings.HasPrefix(input, "calc ") { + input = strings.TrimPrefix(input, "calc ") + } + + _, err := rpnCalc.ParseAndEvaluate(input) if (err != nil) != tt.wantErr { - t.Errorf("runRPN(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + t.Errorf("RPN evaluation error = %v, wantErr %v", err, tt.wantErr) } }) } @@ -212,198 +235,230 @@ func TestGetCommandDescriptionForUnknownCommand(t *testing.T) { } func TestExecutorWithSingleOperator(t *testing.T) { - executor("+") - executor("-") - executor("*") - executor("/") - executor("^") - executor("%") - executor("dup") - executor("swap") - executor("pop") - executor("show") - executor("vars") - executor("clear") + repl := createTestREPL() + for _, op := range []string{"+", "-", "*", "/", "^", "%", "dup", "swap", "pop", "show", "vars", "clear"} { + t.Run(op, func(t *testing.T) { + defaultExecutor(repl, op) + }) + } } func TestExecutorWithPercentageExpression(t *testing.T) { - executor("20% of 150") - executor("30 is what %% of 150") - executor("30 is 20%% of what") + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") + defaultExecutor(repl, "30 is what %% of 150") + defaultExecutor(repl, "30 is 20%% of what") } func TestExecutorWithInvalidPercentage(t *testing.T) { - executor("invalid percentage input") + repl := createTestREPL() + defaultExecutor(repl, "invalid percentage input") } func TestExecutorWithOperatorOnly(t *testing.T) { - executor("1 2 +") - executor("+") + repl := createTestREPL() + defaultExecutor(repl, "1 2 +") + defaultExecutor(repl, "+") } func TestExecutorWithRPNPrefix(t *testing.T) { - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithCalcPrefix(t *testing.T) { - executor("calc 5 6 +") + repl := createTestREPL() + defaultExecutor(repl, "calc 5 6 +") } func TestExecutorWithEmptyInput(t *testing.T) { - executor("") + repl := createTestREPL() + defaultExecutor(repl, "") } func TestExecutorWithWhitespaceOnly(t *testing.T) { - executor(" ") + repl := createTestREPL() + defaultExecutor(repl, " ") } func TestExecutorWithInvalidInput(t *testing.T) { tests := []string{"invalid input", "not a valid command", "xyz"} for _, input := range tests { t.Run(input, func(t *testing.T) { - executor(input) + repl := createTestREPL() + defaultExecutor(repl, input) }) } } func TestExecutorWithInvalidRPN(t *testing.T) { - executor("rpn 1 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 +") } func TestExecutorWithEmptyRPNPrefix(t *testing.T) { - executor("rpn") - executor("calc") + repl := createTestREPL() + defaultExecutor(repl, "rpn") + defaultExecutor(repl, "calc") } func TestExecutorWithAssignment(t *testing.T) { - executor("rpn x 42 =") - executor("rpn x") + repl := createTestREPL() + defaultExecutor(repl, "rpn x 42 =") + defaultExecutor(repl, "rpn x") } func TestExecutorWithPercentageAndRPNFallback(t *testing.T) { - executor("20% of 150") - executor("3 4 +") -} - -func TestGetHistoryPath(t *testing.T) { - path := getHistoryPath() - if path == "" { - t.Error("getHistoryPath() returned empty string") - } -} - -func TestLoadHistory(t *testing.T) { - history := loadHistory() - _ = history -} - -func TestSaveHistory(t *testing.T) { - err := saveHistory([]string{"test1", "test2"}) - _ = err + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") + defaultExecutor(repl, "3 4 +") } func TestExecutorWithRPNExpressionOnly(t *testing.T) { - executor("5 3 +") + repl := createTestREPL() + defaultExecutor(repl, "5 3 +") } func TestExecutorWithRPNThenOperator(t *testing.T) { - executor("1 2 +") - executor("+") + repl := createTestREPL() + defaultExecutor(repl, "1 2 +") + defaultExecutor(repl, "+") } func TestExecutorWithRPNThenRPN(t *testing.T) { - executor("rpn 1 2 +") - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithRPNShow(t *testing.T) { - executor("rpn show") + repl := createTestREPL() + defaultExecutor(repl, "rpn show") } func TestExecutorWithRPNDup(t *testing.T) { - executor("rpn dup") + repl := createTestREPL() + defaultExecutor(repl, "rpn dup") } func TestExecutorWithRPNSwap(t *testing.T) { - executor("rpn swap") + repl := createTestREPL() + defaultExecutor(repl, "rpn swap") } func TestExecutorWithRPNSingle(t *testing.T) { - executor("rpn 42") + repl := createTestREPL() + defaultExecutor(repl, "rpn 42") } func TestExecutorWithRPNMulti(t *testing.T) { - executor("rpn 1 2 3 4 5 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 3 4 5 +") } func TestExecutorWithStackOps(t *testing.T) { - executor("dup") - executor("swap") - executor("pop") - executor("show") + repl := createTestREPL() + defaultExecutor(repl, "dup") + defaultExecutor(repl, "swap") + defaultExecutor(repl, "pop") + defaultExecutor(repl, "show") } func TestExecutorWithRPNClear(t *testing.T) { - executor("rpn clear") + repl := createTestREPL() + defaultExecutor(repl, "rpn clear") } func TestExecutorWithHistoryCommands(t *testing.T) { - executor("vars") - executor("clear") + repl := createTestREPL() + defaultExecutor(repl, "vars") + defaultExecutor(repl, "clear") } func TestExecutorWithMixedInput(t *testing.T) { - executor("25% of 200") - executor("10 20 +") + repl := createTestREPL() + defaultExecutor(repl, "25% of 200") + defaultExecutor(repl, "10 20 +") } func TestExecutorWithRPNCalcMixed(t *testing.T) { - executor("rpn 1 2 +") - executor("3 4 +") - executor("calc 5 6 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") + defaultExecutor(repl, "3 4 +") + defaultExecutor(repl, "calc 5 6 +") } func TestExecutorCommandsEdgeCases(t *testing.T) { - executor(" clear ") - executor("HELP") - executor("CLEAR") + repl := createTestREPL() + defaultExecutor(repl, " clear ") + defaultExecutor(repl, "HELP") + defaultExecutor(repl, "CLEAR") } func TestExecutorWithRPMPrefix(t *testing.T) { - executor("rpn 1 2 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") } func TestExecutorWithCalcPrefixMixed(t *testing.T) { - executor("calc 1 2 +") + repl := createTestREPL() + defaultExecutor(repl, "calc 1 2 +") } +// TestExecutorWithRatModeOn tests that rat on works with fresh REPL func TestExecutorWithRatModeOn(t *testing.T) { - executor("rat on") - state := getRPNState() - if state.rpnCalc.GetMode() != rpn.RationalMode { - t.Errorf("Expected RationalMode after rat on, got %v", state.rpnCalc.GetMode()) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat on") + if rpnCalc.GetMode() != rpn.RationalMode { + t.Errorf("Expected RationalMode after rat on, got %v", rpnCalc.GetMode()) } } +// TestExecutorWithRatModeOff tests that rat off works with fresh REPL func TestExecutorWithRatModeOff(t *testing.T) { - executor("rat off") - state := getRPNState() - if state.rpnCalc.GetMode() != rpn.FloatMode { - t.Errorf("Expected FloatMode after rat off, got %v", state.rpnCalc.GetMode()) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat off") + if rpnCalc.GetMode() != rpn.FloatMode { + t.Errorf("Expected FloatMode after rat off, got %v", rpnCalc.GetMode()) } } +// TestExecutorWithRatModeToggle tests that rat toggle works with fresh REPL func TestExecutorWithRatModeToggle(t *testing.T) { - // First toggle - should enable rational mode if currently float - executor("rat toggle") - state := getRPNState() - mode1 := state.rpnCalc.GetMode() - - // Second toggle - should toggle back - executor("rat toggle") - state = getRPNState() - mode2 := state.rpnCalc.GetMode() - + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + + // First toggle + defaultExecutor(rpl, "rat toggle") + mode1 := rpnCalc.GetMode() + + // Second toggle + defaultExecutor(rpl, "rat toggle") + mode2 := rpnCalc.GetMode() + // Modes should be different after toggle if mode1 == mode2 { t.Errorf("Modes should be different after toggle: %v -> %v", mode1, mode2) @@ -411,13 +466,15 @@ func TestExecutorWithRatModeToggle(t *testing.T) { } func TestExecutorWithRatModeInvalid(t *testing.T) { + repl := createTestREPL() // Just verify it doesn't panic - executor("rat invalid") + defaultExecutor(repl, "rat invalid") } func TestExecutorWithRatModeNoArg(t *testing.T) { + repl := createTestREPL() // Just verify it doesn't panic - executor("rat") + defaultExecutor(repl, "rat") } func TestIsBuiltinCommandWithSubcommandHelp(t *testing.T) { @@ -427,222 +484,31 @@ func TestIsBuiltinCommandWithSubcommandHelp(t *testing.T) { } } -func TestRPNHandlerWithUnknownInput(t *testing.T) { - // Test that unknown input falls through to next handler - chain := NewCommandChain() - - // Create a minimal REPL - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - } - - // Test unknown input - should not be handled by RPNHandler directly - // but will be handled by Error handler after RPNHandler passes it through - output, handled, err := chain.Handle(r, "unknowncommand") - if handled { - t.Errorf("Expected unknowncommand to be handled by error handler, got handled=%v, err=%v, output=%q", handled, err, output) - } -} - -func TestRPNHandlerWithPercentageExpression(t *testing.T) { - // Test that percentage expressions are handled by PercentageHandler, not RPNHandler - chain := NewCommandChain() - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - } - - // Test percentage expression - output, handled, err := chain.Handle(r, "20% of 150") - if !handled { - t.Errorf("Expected percentage expression to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for percentage expression, got %v", err) - } -} - -func TestRPNHandlerWithRPNExpression(t *testing.T) { - // Test RPN expressions - chain := NewCommandChain() - vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - rpnState: rpnState, - } - - // Test RPN expression - output, handled, err := chain.Handle(r, "3 4 +") - if !handled { - t.Errorf("Expected RPN expression to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for RPN expression, got %v", err) - } -} - -func TestRPNHandlerWithSingleNumber(t *testing.T) { - // Test single number input (RPN - pushes number onto stack) - chain := NewCommandChain() +// TestExecutorWithAssignmentRight tests := and =: operators +func TestExecutorWithAssignmentRight(t *testing.T) { vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - r := &REPL{ + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ ttyChecker: &TTYChecker{}, historyMgr: NewHistoryManager(".gt_history"), signalHandler: NewSignalHandler(), - commandChain: chain, - rpnState: rpnState, - } - - // Test single number - output, handled, err := chain.Handle(r, "42") - if !handled { - t.Errorf("Expected single number to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for single number, got %v", err) - } -} - -// TestNewREPL tests that NewREPL creates a valid REPL instance. -// Note: This test is skipped when not running in a TTY because the prompt -// library requires TTY access. -func TestNewREPL(t *testing.T) { - // Skip this test if not running in a TTY - ttyChecker := &TTYChecker{} - if !ttyChecker.IsTTY() { - t.Skip("Skipping test - not running in a TTY") - } - - // Test that NewREPL creates a valid REPL instance without panicking - repl := NewREPL(nil, nil) - if repl == nil { - t.Fatal("Expected REPL to be created, got nil") - } - if repl.prompt == nil { - t.Error("Expected prompt to be set") - } - if repl.commandChain == nil { - t.Error("Expected commandChain to be set") - } - if repl.ttyChecker == nil { - t.Error("Expected ttyChecker to be set") - } - if repl.historyMgr == nil { - t.Error("Expected historyMgr to be set") - } - if repl.signalHandler == nil { - t.Error("Expected signalHandler to be set") - } -} - -func TestDefaultCompleter(t *testing.T) { - // Test the default completer function directly - // Note: This test has limited coverage because defaultCompleter uses - // GetWordBeforeCursor() which requires proper cursor position. - // The actual completer logic is tested in completer_test.go - - // Test with text that would match if cursor position was set correctly - repl := &REPL{} - doc := prompt.Document{Text: "h"} - suggestions := defaultCompleter(repl, doc) - - // When cursor is at position 0 (default), GetWordBeforeCursor returns empty - // But the test in completer_test.go verifies the actual behavior - _ = suggestions - - // Test with clear prefix - doc2 := prompt.Document{Text: "cl"} - suggestions2 := defaultCompleter(repl, doc2) - _ = suggestions2 -} - -func TestDefaultGetCommandDescription(t *testing.T) { - // Create a REPL and test the defaultGetCommandDescription method - repl := &REPL{} - - tests := []struct { - cmd string - wantPrefix string - }{ - {"help", "Show"}, - {"clear", "Clear"}, - {"quit", "Exit"}, - {"exit", "Exit"}, - {"rpn", "Evaluate"}, - {"calc", "Same"}, - } - - for _, tt := range tests { - t.Run(tt.cmd, func(t *testing.T) { - desc := repl.defaultGetCommandDescription(tt.cmd) - if !strings.Contains(desc, tt.wantPrefix) { - t.Errorf("defaultGetCommandDescription(%q) = %q, should contain %q", tt.cmd, desc, tt.wantPrefix) - } - }) + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, } -} - -func TestExecutorWithUnknownCommand(t *testing.T) { - // Test that unknown commands are handled by the error handler - // This should exercise the "Not handled by any handler" path - executor("completelyunknowncommand123") -} - -func TestDefaultExecutorCodePaths(t *testing.T) { - // Test all code paths in defaultExecutor - // 1. Empty input (returns early at line 110) - // 2. Handled=true with error (prints error, returns at line 124) - // 3. Handled=true with output (prints output, returns at line 124) - // 4. Handled=false with error (prints error at line 130) - // 5. Handled=false without error (does nothing) - - // Path 1: Empty input - executor("") - - // Path 2: Built-in command with error (clear should not error but let's verify) - executor("clear") - - // Path 3: Built-in command with output (help returns help text) - executor("help") - - // Path 4: Unknown command (error handler returns handled=false, err!=nil) - executor("completelyunknowncommand123") - - // Path 5: Whitespace only (trimmed to empty, returns early) - executor(" ") -} - -func TestExecutorWithAssignmentRight(t *testing.T) { - // Test := and =: operators - executor("5 x :=") - state := getRPNState() - val, exists := state.vars.GetVariable("x") + + // Test := operator + defaultExecutor(rpl, "5 x :=") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x :=") } if val != 5 { t.Errorf("Variable x = %v, want 5", val) } - - executor("y 3 =:") - state = getRPNState() - val, exists = state.vars.GetVariable("y") + + // T |
