summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/repl/commands.go31
-rw-r--r--internal/repl/completer.go2
-rw-r--r--internal/repl/completer_test.go2
-rw-r--r--internal/repl/concurrent_test.go53
-rw-r--r--internal/repl/repl.go145
-rw-r--r--internal/repl/repl_completer_test.go2
-rw-r--r--internal/repl/repl_test.go644
-rw-r--r--internal/rpn/number.go41
-rw-r--r--internal/rpn/operations_test.go170
-rw-r--r--internal/rpn/rpn_parse.go6
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")
+
+ // Test =: operator
+ defaultExecutor(rpl, "y 3 =:")
+ val, exists = vars.GetVariable("y")
if !exists {
t.Errorf("Variable y should exist after y =:")
}
@@ -651,14 +517,21 @@ func TestExecutorWithAssignmentRight(t *testing.T) {
}
}
-
+// TestExecutorWithAssignmentAfterCalculation tests assignment after a calculation
func TestExecutorWithAssignmentAfterCalculation(t *testing.T) {
+ vars := rpn.NewVariables()
+ rpnCalc := rpn.NewRPN(vars)
+ rpl := &REPL{
+ ttyChecker: &TTYChecker{},
+ historyMgr: NewHistoryManager(".gt_history"),
+ signalHandler: NewSignalHandler(),
+ commandChain: NewCommandChain(),
+ rpnState: &RPNState{vars: vars, rpnCalc: r