summaryrefslogtreecommitdiff
path: root/integrationtests/cli_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'integrationtests/cli_test.go')
-rw-r--r--integrationtests/cli_test.go546
1 files changed, 546 insertions, 0 deletions
diff --git a/integrationtests/cli_test.go b/integrationtests/cli_test.go
new file mode 100644
index 0000000..8a0a076
--- /dev/null
+++ b/integrationtests/cli_test.go
@@ -0,0 +1,546 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package integrationtests
+
+import (
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+// buildBinary builds the gt binary to a temporary location.
+func buildBinary(t *testing.T) string {
+ t.Helper()
+
+ // Get the project root directory
+ projectRoot := os.Getenv("GITHUB_WORKSPACE")
+ if projectRoot == "" {
+ projectRoot = "/home/paul/git/gt"
+ }
+
+ buildCmd := exec.Command("go", "build", "-o", "/tmp/gt-test", "./cmd/gt")
+ buildCmd.Dir = projectRoot
+ if err := buildCmd.Run(); err != nil {
+ t.Fatalf("build failed: %v", err)
+ }
+
+ t.Cleanup(func() {
+ // Explicitly ignore error return from os.Remove during cleanup
+ _ = os.Remove("/tmp/gt-test")
+ })
+
+ return "/tmp/gt-test"
+}
+
+// TestCLIVersion tests that the version command works correctly.
+func TestCLIVersion(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ cmd := exec.Command(binaryPath, "version")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("version command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ versionOutput := strings.TrimSpace(string(output))
+ if !strings.HasPrefix(versionOutput, "v") {
+ t.Errorf("version output should start with 'v', got: %s", versionOutput)
+ }
+}
+
+// TestCLIVersionOnly tests that the version command works correctly.
+func TestCLIVersionOnly(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ cmd := exec.Command(binaryPath, "version")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("version command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ versionOutput := strings.TrimSpace(string(output))
+ if !strings.HasPrefix(versionOutput, "v") {
+ t.Errorf("version output should start with 'v', got: %s", versionOutput)
+ }
+}
+
+// TestCLIPercentageCalculation tests percentage calculation commands.
+
+// TestCLIPercentageCalculation tests percentage calculation commands.
+func TestCLIPercentageCalculation(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "20% of 150",
+ args: []string{"20% of 150"},
+ expected: "30",
+ },
+ {
+ name: "what is 20% of 150",
+ args: []string{"what is 20% of 150"},
+ expected: "30",
+ },
+ {
+ name: "30 is what % of 150",
+ args: []string{"30 is what % of 150"},
+ expected: "20",
+ },
+ {
+ name: "30 is 20% of what",
+ args: []string{"30 is 20% of what"},
+ expected: "150",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIRPNCalculation tests RPN calculation commands.
+func TestCLIRPNCalculation(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "3 4 +",
+ args: []string{"3 4 +"},
+ expected: "7",
+ },
+ {
+ name: "5 6 *",
+ args: []string{"5 6 *"},
+ expected: "30",
+ },
+ {
+ name: "10 2 /",
+ args: []string{"10 2 /"},
+ expected: "5",
+ },
+ {
+ name: "2 3 ^",
+ args: []string{"2 3 ^"},
+ expected: "8",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIExitCode tests that the CLI returns proper exit codes.
+func TestCLIExitCode(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ // Test successful command returns 0
+ cmd := exec.Command(binaryPath, "version")
+ if err := cmd.Run(); err != nil {
+ t.Errorf("version command should succeed, got error: %v", err)
+ }
+
+ // Test invalid command returns non-zero
+ cmd = exec.Command(binaryPath, "invalidcommand")
+ output, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Errorf("invalid command should fail, but succeeded")
+ }
+ if len(output) > 0 && !strings.Contains(string(output), "Error:") {
+ t.Errorf("error output should contain 'Error:', got: %s", string(output))
+ }
+}
+
+// TestCLIInvalidPercentage tests invalid percentage calculation.
+func TestCLIInvalidPercentage(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ cmd := exec.Command(binaryPath, "invalid percentage")
+ output, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Errorf("invalid percentage should fail, but succeeded")
+ }
+ if len(output) > 0 && !strings.Contains(string(output), "Error:") {
+ t.Errorf("error output should contain 'Error:', got: %s", string(output))
+ }
+}
+
+// TestCLIInvalidRPN tests invalid RPN expression.
+func TestCLIInvalidRPN(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ cmd := exec.Command(binaryPath, "3 +")
+ output, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Errorf("invalid RPN should fail, but succeeded")
+ }
+ if len(output) > 0 && !strings.Contains(string(output), "Error:") {
+ t.Errorf("error output should contain 'Error:', got: %s", string(output))
+ }
+}
+
+// TestCLIStdin tests that stdin input works correctly.
+func TestCLIStdin(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ stdin string
+ expected string
+ }{
+ {
+ name: "RPN expression via stdin",
+ stdin: "3 4 +",
+ expected: "7",
+ },
+ {
+ name: "Percentage expression via stdin",
+ stdin: "20% of 150",
+ expected: "30",
+ },
+ {
+ name: "Variable assignment via stdin",
+ stdin: "x 5 = x x +",
+ expected: "10",
+ },
+ {
+ name: "Boolean comparison via stdin",
+ stdin: "5 3 ==",
+ expected: "false",
+ },
+ {
+ name: "Boolean to number coercion via stdin",
+ stdin: "true 2 *",
+ expected: "2",
+ },
+ {
+ name: "Complex expression via stdin",
+ stdin: "2 3 + 4 *",
+ expected: "20",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath)
+ cmd.Stdin = strings.NewReader(tt.stdin)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIStdinWithPiping tests stdin via pipe (simulating `echo EXP | gt`).
+func TestCLIStdinWithPiping(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "echo 3 4 +",
+ input: "3 4 +",
+ expected: "7",
+ },
+ {
+ name: "echo 20%% of 150",
+ input: "20% of 150",
+ expected: "30",
+ },
+ {
+ name: "echo x 5 = x x +",
+ input: "x 5 = x x +",
+ expected: "10",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Simulate piping: echo INPUT | gt
+ cmd := exec.Command(binaryPath)
+ cmd.Stdin = strings.NewReader(tt.input)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIStdinWithMultipleLines tests stdin with multiple lines (should use first line).
+func TestCLIStdinWithMultipleLines(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ stdin string
+ expected string
+ }{
+ {
+ name: "Multiple lines - first line used",
+ stdin: "3 4 +\n5 6 +",
+ expected: "7",
+ },
+ {
+ name: "Empty line then expression",
+ stdin: "\n3 4 +",
+ expected: "7",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath)
+ cmd.Stdin = strings.NewReader(tt.stdin)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIVariableAssignment tests all variable assignment syntaxes.
+func TestCLIVariableAssignment(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "Standard assignment x = 2",
+ args: []string{"x", "2", "=", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Right assignment x 2 := (value on stack, right)",
+ args: []string{"x", "2", ":=", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Left assignment 2 x =: (value on stack, left)",
+ args: []string{"2", "x", "=: ", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Stack variant with =: (value on stack, left)",
+ args: []string{"2", "x", "=: ", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Assignment with existing variable",
+ args: []string{"x", "5", "=", "x", "3", "+"},
+ expected: "8",
+ },
+ {
+ name: "Assignment with complex expression",
+ args: []string{"x", "2", "=", "x", "x", "*", "x", "+"},
+ expected: "6",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIVariableAssignmentSyntaxes tests each assignment syntax individually
+// and verifies the variable can be used in subsequent expressions.
+func TestCLIVariableAssignmentSyntaxes(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "Assignment: x = 2, then x 2 +",
+ args: []string{"x", "2", "=", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Right assignment: x 2 :=, then x 2 +",
+ args: []string{"x", "2", ":=", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Left assignment: 2 x =:, then x 2 +",
+ args: []string{"2", "x", "=: ", "x", "2", "+"},
+ expected: "4",
+ },
+ {
+ name: "Assignment: y = 10, then y 5 *",
+ args: []string{"y", "10", "=", "y", "5", "*"},
+ expected: "50",
+ },
+ {
+ name: "Assignment: pi = 3.14159, then pi 2 *",
+ args: []string{"pi", "3.14159", "=", "pi", "2", "*"},
+ expected: "6.28318",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIVariableAssignmentPreservation tests that variables persist within a single expression.
+func TestCLIVariableAssignmentPreservation(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "Single assignment, multiple uses",
+ args: []string{"x", "5", "=", "x", "x", "+"},
+ expected: "10", // x=5, then x+x=10
+ },
+ {
+ name: "Assignment with calculation as value (stack-variant)",
+ args: []string{"2", "3", "+", "x", "=: ", "x", "4", "*"},
+ expected: "20", // 2+3=5, x=: assigns 5 to x, x*4=20
+ },
+ {
+ name: "Stack-variant with := (value on stack)",
+ args: []string{"x", "2", "3", "+", ":=", "x", "1", "+"},
+ expected: "6", // x=2+3=5, x+1=6
+ },
+ {
+ name: "Stack-variant with =: (value first)",
+ args: []string{"2", "3", "+", "x", "=: ", "x", "1", "+"},
+ expected: "6", // 2+3=5, x=: assigns 5 to x, x+1=6
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIVariableAssignmentRepetition tests that repeated assignment works correctly.
+func TestCLIVariableAssignmentRepetition(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "Reassign same variable",
+ args: []string{"x", "5", ":=", "x", "10", ":=", "x", "2", "+"},
+ expected: "12", // x=5, x=10, x+2=12
+ },
+ {
+ name: "Stack-variant reassignment",
+ args: []string{"x", "1", ":=", "x", "2", ":=", "x", "3", "+"},
+ expected: "5", // x=1 then x=2, x+3=5
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}