From 4636c649d74b5a620d70a32438869b2450daddb7 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 20 Mar 2026 22:32:55 +0200 Subject: internal/rpn: fix race condition in concurrent test - Each goroutine in TestOperationsConcurrent now uses its own stack - This prevents data races when accessing Stack from multiple goroutines --- internal/rpn/operations_test.go | 3 ++- internal/rpn/rpn.go | 10 +++---- internal/rpn/rpn_test.go | 58 ++++++++++++++++++++--------------------- internal/rpn/variables.go | 6 ++--- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go index 0083196..7aa6a32 100644 --- a/internal/rpn/operations_test.go +++ b/internal/rpn/operations_test.go @@ -525,13 +525,14 @@ func TestOperationsClearVariables(t *testing.T) { func TestOperationsConcurrent(t *testing.T) { v := NewVariables().(*Variables) o := NewOperations(v) - s := NewStack() // Test concurrent variable access + // Each goroutine uses its own stack to avoid race conditions done := make(chan bool, 10) for i := 0; i < 5; i++ { go func(id int) { name := fmt.Sprintf("concurrent%d", id) + s := NewStack() s.Push(float64(id)) o.AssignVariable(s, name) done <- true diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index 5fd1d39..debc2c8 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -8,9 +8,9 @@ import ( // RPN represents the RPN parser and evaluator. type RPN struct { - vars VariableStore - ops Operator - maxStack int + vars VariableStore + ops Operator + maxStack int currentStack *Stack } @@ -85,8 +85,8 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { // Handle assignment with expression: "name value = expression..." pos := strings.Index(input, " = ") if pos >= 0 { - before := input[:pos] // "name value" - after := input[pos+3:] // "expr..." + before := input[:pos] // "name value" + after := input[pos+3:] // "expr..." beforeFields := strings.Fields(before) if len(beforeFields) == 2 { diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index 5cd0af4..d68d43a 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -462,23 +462,23 @@ func TestParseAndEvaluateAssignmentErrors(t *testing.T) { expectedError string }{ { - name: "invalid value for assignment (non-numeric)", - input: "x abc =", + name: "invalid value for assignment (non-numeric)", + input: "x abc =", expectedError: "unknown token 'x'", }, { - name: "assignment with variable name containing space", - input: "my var 5 =", + name: "assignment with variable name containing space", + input: "my var 5 =", expectedError: "unknown token 'my'", }, { - name: "assignment with value containing space", - input: "x 5 6 =", + name: "assignment with value containing space", + input: "x 5 6 =", expectedError: "unknown token 'x'", }, { - name: "empty assignment", - input: " = ", + name: "empty assignment", + input: " = ", expectedError: "invalid assignment syntax", }, } @@ -507,23 +507,23 @@ func TestParseAndEvaluateEvaluateErrors(t *testing.T) { expectedError string }{ { - name: "invalid assignment syntax (standalone =)", - input: "=", + name: "invalid assignment syntax (standalone =)", + input: "=", expectedError: "invalid assignment syntax", }, { - name: "'d' command not supported", - input: "d", + name: "'d' command not supported", + input: "d", expectedError: "'d' command not supported as standalone token", }, { - name: "empty result after evaluation", - input: "1 2 + pop", // 1 2 + => 3, then pop => empty stack + name: "empty result after evaluation", + input: "1 2 + pop", // 1 2 + => 3, then pop => empty stack expectedError: "empty result: expression evaluated to nothing", }, { - name: "stack overflow (simulate many numbers)", - input: "", // placeholder + name: "stack overflow (simulate many numbers)", + input: "", // placeholder expectedError: "stack overflow", }, } @@ -560,28 +560,28 @@ func TestResultStackErrors(t *testing.T) { expectedError string }{ { - name: "division by zero", - input: []string{"5", "0", "/"}, + name: "division by zero", + input: []string{"5", "0", "/"}, expectedError: "division by zero", }, { - name: "unknown token", - input: []string{"1", "2", "+", "unknown"}, + name: "unknown token", + input: []string{"1", "2", "+", "unknown"}, expectedError: "unknown token", }, { - name: "insufficient operands for +", - input: []string{"5", "+"}, + name: "insufficient operands for +", + input: []string{"5", "+"}, expectedError: "insufficient operands", }, { - name: "insufficient operands for -", - input: []string{"5", "-"}, + name: "insufficient operands for -", + input: []string{"5", "-"}, expectedError: "insufficient operands", }, { - name: "invalid assignment syntax in ResultStack", - input: []string{"="}, + name: "invalid assignment syntax in ResultStack", + input: []string{"="}, expectedError: "unknown token '='", }, } @@ -613,7 +613,7 @@ func TestResultStackMultipleValues(t *testing.T) { { name: "two values on stack", input: []string{"1", "2", "3", "+"}, // 1 2 3 + => 1 (5) => two values: 1, 5 - expected: "1 5", // Show should show all values + expected: "1 5", // Show should show all values }, { name: "three values on stack", @@ -623,7 +623,7 @@ func TestResultStackMultipleValues(t *testing.T) { { name: "multiple values with variables", input: []string{"x", "y", "z"}, // after setting variables - expected: "10 20 30", // variables x, y, z have values 10, 20, 30 + expected: "10 20 30", // variables x, y, z have values 10, 20, 30 }, } @@ -913,7 +913,7 @@ func TestHyperModuloByZero(t *testing.T) { func TestHyperOperatorEdgeCases(t *testing.T) { // Test with single value should error for all hyper operators testCases := []struct { - input string + input string operands int }{ {"100 [%]", 1}, diff --git a/internal/rpn/variables.go b/internal/rpn/variables.go index 46efa2c..40f0844 100644 --- a/internal/rpn/variables.go +++ b/internal/rpn/variables.go @@ -9,8 +9,8 @@ import ( // Error variables for external error checking. var ( - ErrVariableNotFound = fmt.Errorf("variable not found") - ErrInvalidVariableName = fmt.Errorf("invalid variable name") + ErrVariableNotFound = fmt.Errorf("variable not found") + ErrInvalidVariableName = fmt.Errorf("invalid variable name") ) // Stack represents a simple float64 stack for RPN calculations. @@ -71,7 +71,7 @@ func (s *Stack) Clear() { // Variables stores variable name-value pairs for RPN calculations. // It provides thread-safe access to variable storage. type Variables struct { - mu sync.RWMutex + mu sync.RWMutex variables map[string]float64 } -- cgit v1.2.3