diff --git a/benchmarks/results/benchmark_2025-04-10_14-26-23.json b/benchmarks/results/benchmark_2025-04-10_14-26-23.json
new file mode 100644
index 0000000..cf65a46
--- /dev/null
+++ b/benchmarks/results/benchmark_2025-04-10_14-26-23.json
@@ -0,0 +1,45 @@
+{"Time":"2025-04-10T14:26:34.196527-04:00","Action":"start","Package":"github.com/robbyt/go-polyscript/engine"}
+{"Time":"2025-04-10T14:26:34.212069-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"goos: darwin\n"}
+{"Time":"2025-04-10T14:26:34.212168-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"goarch: amd64\n"}
+{"Time":"2025-04-10T14:26:34.212178-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"pkg: github.com/robbyt/go-polyscript/engine\n"}
+{"Time":"2025-04-10T14:26:34.212189-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"cpu: Intel(R) Xeon(R) W-3275M CPU @ 2.50GHz\n"}
+{"Time":"2025-04-10T14:26:34.2122-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns"}
+{"Time":"2025-04-10T14:26:34.21221-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns","Output":"=== RUN BenchmarkEvaluationPatterns\n"}
+{"Time":"2025-04-10T14:26:34.212225-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns","Output":"BenchmarkEvaluationPatterns\n"}
+{"Time":"2025-04-10T14:26:34.212525-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/SingleExecution"}
+{"Time":"2025-04-10T14:26:34.212555-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"=== RUN BenchmarkEvaluationPatterns/SingleExecution\n"}
+{"Time":"2025-04-10T14:26:34.212572-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"BenchmarkEvaluationPatterns/SingleExecution\n"}
+{"Time":"2025-04-10T14:26:35.417933-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"BenchmarkEvaluationPatterns/SingleExecution-56 \t 4477\t 262703 ns/op\t 460987 B/op\t 1173 allocs/op\n"}
+{"Time":"2025-04-10T14:26:35.418013-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany"}
+{"Time":"2025-04-10T14:26:35.41803-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"=== RUN BenchmarkEvaluationPatterns/CompileOnceRunMany\n"}
+{"Time":"2025-04-10T14:26:35.418048-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"BenchmarkEvaluationPatterns/CompileOnceRunMany\n"}
+{"Time":"2025-04-10T14:26:36.661812-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"BenchmarkEvaluationPatterns/CompileOnceRunMany-56 \t 7039\t 173882 ns/op\t 373306 B/op\t 436 allocs/op\n"}
+{"Time":"2025-04-10T14:26:36.673916-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders"}
+{"Time":"2025-04-10T14:26:36.673943-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders","Output":"=== RUN BenchmarkDataProviders\n"}
+{"Time":"2025-04-10T14:26:36.673957-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders","Output":"BenchmarkDataProviders\n"}
+{"Time":"2025-04-10T14:26:36.673967-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/StaticProvider"}
+{"Time":"2025-04-10T14:26:36.673987-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/StaticProvider","Output":"=== RUN BenchmarkDataProviders/StaticProvider\n"}
+{"Time":"2025-04-10T14:26:36.674002-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/StaticProvider","Output":"BenchmarkDataProviders/StaticProvider\n"}
+{"Time":"2025-04-10T14:26:37.930088-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/StaticProvider","Output":"BenchmarkDataProviders/StaticProvider-56 \t 6999\t 178169 ns/op\t 373314 B/op\t 436 allocs/op\n"}
+{"Time":"2025-04-10T14:26:37.93016-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/ContextProvider"}
+{"Time":"2025-04-10T14:26:37.930176-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/ContextProvider","Output":"=== RUN BenchmarkDataProviders/ContextProvider\n"}
+{"Time":"2025-04-10T14:26:37.930183-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/ContextProvider","Output":"BenchmarkDataProviders/ContextProvider\n"}
+{"Time":"2025-04-10T14:26:39.124363-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/ContextProvider","Output":"BenchmarkDataProviders/ContextProvider-56 \t 6654\t 176240 ns/op\t 372992 B/op\t 435 allocs/op\n"}
+{"Time":"2025-04-10T14:26:39.124432-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/CompositeProvider"}
+{"Time":"2025-04-10T14:26:39.124441-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/CompositeProvider","Output":"=== RUN BenchmarkDataProviders/CompositeProvider\n"}
+{"Time":"2025-04-10T14:26:39.12445-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/CompositeProvider","Output":"BenchmarkDataProviders/CompositeProvider\n"}
+{"Time":"2025-04-10T14:26:40.470563-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkDataProviders/CompositeProvider","Output":"BenchmarkDataProviders/CompositeProvider-56 \t 7592\t 174850 ns/op\t 374157 B/op\t 445 allocs/op\n"}
+{"Time":"2025-04-10T14:26:40.470657-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison"}
+{"Time":"2025-04-10T14:26:40.470669-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison","Output":"=== RUN BenchmarkVMComparison\n"}
+{"Time":"2025-04-10T14:26:40.470676-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison","Output":"BenchmarkVMComparison\n"}
+{"Time":"2025-04-10T14:26:40.471566-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/RisorVM"}
+{"Time":"2025-04-10T14:26:40.471582-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/RisorVM","Output":"=== RUN BenchmarkVMComparison/RisorVM\n"}
+{"Time":"2025-04-10T14:26:40.471589-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/RisorVM","Output":"BenchmarkVMComparison/RisorVM\n"}
+{"Time":"2025-04-10T14:26:41.745863-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/RisorVM","Output":"BenchmarkVMComparison/RisorVM-56 \t 7122\t 176101 ns/op\t 373331 B/op\t 436 allocs/op\n"}
+{"Time":"2025-04-10T14:26:41.74593-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/StarlarkVM"}
+{"Time":"2025-04-10T14:26:41.745944-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/StarlarkVM","Output":"=== RUN BenchmarkVMComparison/StarlarkVM\n"}
+{"Time":"2025-04-10T14:26:41.74596-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/StarlarkVM","Output":"BenchmarkVMComparison/StarlarkVM\n"}
+{"Time":"2025-04-10T14:26:43.21085-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Test":"BenchmarkVMComparison/StarlarkVM","Output":"BenchmarkVMComparison/StarlarkVM-56 \t 114759\t 11738 ns/op\t 7049 B/op\t 65 allocs/op\n"}
+{"Time":"2025-04-10T14:26:43.210933-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"PASS\n"}
+{"Time":"2025-04-10T14:26:43.238187-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engine","Output":"ok \tgithub.com/robbyt/go-polyscript/engine\t9.042s\n"}
+{"Time":"2025-04-10T14:26:43.238259-04:00","Action":"pass","Package":"github.com/robbyt/go-polyscript/engine","Elapsed":9.042}
diff --git a/benchmarks/results/benchmark_2025-04-10_14-26-23.txt b/benchmarks/results/benchmark_2025-04-10_14-26-23.txt
new file mode 100644
index 0000000..e597a61
--- /dev/null
+++ b/benchmarks/results/benchmark_2025-04-10_14-26-23.txt
@@ -0,0 +1,13 @@
+goos: darwin
+goarch: amd64
+pkg: github.com/robbyt/go-polyscript/engine
+cpu: Intel(R) Xeon(R) W-3275M CPU @ 2.50GHz
+BenchmarkEvaluationPatterns/SingleExecution-56 4154 285805 ns/op 461073 B/op 1173 allocs/op
+BenchmarkEvaluationPatterns/CompileOnceRunMany-56 6836 177584 ns/op 373308 B/op 436 allocs/op
+BenchmarkDataProviders/StaticProvider-56 6912 179514 ns/op 373329 B/op 436 allocs/op
+BenchmarkDataProviders/ContextProvider-56 7078 180442 ns/op 372985 B/op 435 allocs/op
+BenchmarkDataProviders/CompositeProvider-56 6172 178387 ns/op 374177 B/op 445 allocs/op
+BenchmarkVMComparison/RisorVM-56 6769 175350 ns/op 373351 B/op 436 allocs/op
+BenchmarkVMComparison/StarlarkVM-56 121674 11599 ns/op 7046 B/op 65 allocs/op
+PASS
+ok github.com/robbyt/go-polyscript/engine 8.915s
diff --git a/benchmarks/results/comparison.txt b/benchmarks/results/comparison.txt
index 2f61b68..55ec233 100644
--- a/benchmarks/results/comparison.txt
+++ b/benchmarks/results/comparison.txt
@@ -4,39 +4,39 @@ pkg: github.com/robbyt/go-polyscript/engine
cpu: Intel(R) Xeon(R) W-3275M CPU @ 2.50GHz
│ previous │ current │
│ sec/op │ sec/op vs base │
-EvaluationPatterns/SingleExecution-56 306.8µ ± ∞ ¹ 315.2µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-EvaluationPatterns/CompileOnceRunMany-56 183.6µ ± ∞ ¹ 189.5µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/StaticProvider-56 185.8µ ± ∞ ¹ 201.0µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/ContextProvider-56 186.4µ ± ∞ ¹ 193.0µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/CompositeProvider-56 185.6µ ± ∞ ¹ 191.7µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/RisorVM-56 183.8µ ± ∞ ¹ 187.7µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/StarlarkVM-56 11.72µ ± ∞ ¹ 12.52µ ± ∞ ¹ ~ (p=1.000 n=1) ²
-geomean 134.1µ 139.8µ +4.26%
+EvaluationPatterns/SingleExecution-56 315.2µ ± ∞ ¹ 285.8µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+EvaluationPatterns/CompileOnceRunMany-56 189.5µ ± ∞ ¹ 177.6µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/StaticProvider-56 201.0µ ± ∞ ¹ 179.5µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/ContextProvider-56 193.0µ ± ∞ ¹ 180.4µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/CompositeProvider-56 191.7µ ± ∞ ¹ 178.4µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/RisorVM-56 187.7µ ± ∞ ¹ 175.4µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/StarlarkVM-56 12.52µ ± ∞ ¹ 11.60µ ± ∞ ¹ ~ (p=1.000 n=1) ²
+geomean 139.8µ 129.1µ -7.69%
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05
│ previous │ current │
│ B/op │ B/op vs base │
-EvaluationPatterns/SingleExecution-56 450.0Ki ± ∞ ¹ 450.1Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-EvaluationPatterns/CompileOnceRunMany-56 364.3Ki ± ∞ ¹ 364.4Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/StaticProvider-56 364.4Ki ± ∞ ¹ 364.4Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/ContextProvider-56 364.1Ki ± ∞ ¹ 364.1Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/CompositeProvider-56 364.8Ki ± ∞ ¹ 364.9Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/RisorVM-56 364.4Ki ± ∞ ¹ 364.5Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/StarlarkVM-56 6.875Ki ± ∞ ¹ 6.666Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
-geomean 213.0Ki 212.1Ki -0.43%
+EvaluationPatterns/SingleExecution-56 450.1Ki ± ∞ ¹ 450.3Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+EvaluationPatterns/CompileOnceRunMany-56 364.4Ki ± ∞ ¹ 364.6Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/StaticProvider-56 364.4Ki ± ∞ ¹ 364.6Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/ContextProvider-56 364.1Ki ± ∞ ¹ 364.2Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/CompositeProvider-56 364.9Ki ± ∞ ¹ 365.4Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/RisorVM-56 364.5Ki ± ∞ ¹ 364.6Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/StarlarkVM-56 6.666Ki ± ∞ ¹ 6.881Ki ± ∞ ¹ ~ (p=1.000 n=1) ²
+geomean 212.1Ki 213.1Ki +0.50%
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05
│ previous │ current │
│ allocs/op │ allocs/op vs base │
-EvaluationPatterns/SingleExecution-56 1.166k ± ∞ ¹ 1.168k ± ∞ ¹ ~ (p=1.000 n=1) ²
-EvaluationPatterns/CompileOnceRunMany-56 432.0 ± ∞ ¹ 434.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/StaticProvider-56 432.0 ± ∞ ¹ 434.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/ContextProvider-56 431.0 ± ∞ ¹ 433.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
-DataProviders/CompositeProvider-56 438.0 ± ∞ ¹ 440.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/RisorVM-56 432.0 ± ∞ ¹ 434.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
-VMComparison/StarlarkVM-56 65.00 ± ∞ ¹ 60.00 ± ∞ ¹ ~ (p=1.000 n=1) ²
-geomean 380.4 377.4 -0.79%
+EvaluationPatterns/SingleExecution-56 1.168k ± ∞ ¹ 1.173k ± ∞ ¹ ~ (p=1.000 n=1) ²
+EvaluationPatterns/CompileOnceRunMany-56 434.0 ± ∞ ¹ 436.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/StaticProvider-56 434.0 ± ∞ ¹ 436.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/ContextProvider-56 433.0 ± ∞ ¹ 435.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
+DataProviders/CompositeProvider-56 440.0 ± ∞ ¹ 445.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/RisorVM-56 434.0 ± ∞ ¹ 436.0 ± ∞ ¹ ~ (p=1.000 n=1) ²
+VMComparison/StarlarkVM-56 60.00 ± ∞ ¹ 65.00 ± ∞ ¹ ~ (p=1.000 n=1) ²
+geomean 377.4 383.6 +1.64%
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05
diff --git a/benchmarks/results/latest.txt b/benchmarks/results/latest.txt
index 3e76087..6dbdf6f 120000
--- a/benchmarks/results/latest.txt
+++ b/benchmarks/results/latest.txt
@@ -1 +1 @@
-benchmark_2025-03-27_19-39-40.txt
\ No newline at end of file
+benchmark_2025-04-10_14-26-23.txt
\ No newline at end of file
diff --git a/engine/evaluatorResponse_test.go b/engine/evaluatorResponse_test.go
new file mode 100644
index 0000000..11dfa7d
--- /dev/null
+++ b/engine/evaluatorResponse_test.go
@@ -0,0 +1,146 @@
+package engine_test
+
+import (
+ "testing"
+
+ "github.com/robbyt/go-polyscript/execution/data"
+ "github.com/robbyt/go-polyscript/machines/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestEvaluatorResponseInterface tests all methods of the EvaluatorResponse interface
+func TestEvaluatorResponseInterface(t *testing.T) {
+ t.Parallel()
+ mockResponse := new(mocks.EvaluatorResponse)
+
+ // Test Type method with various return types
+ t.Run("Type method", func(t *testing.T) {
+ typeTests := []struct {
+ name string
+ dataType data.Types
+ }{
+ {"String type", data.STRING},
+ {"Integer type", data.INT},
+ {"Float type", data.FLOAT},
+ {"Boolean type", data.BOOL},
+ {"Map type", data.MAP},
+ {"Function type", data.FUNCTION},
+ {"List type", data.LIST},
+ {"Set type", data.SET},
+ {"Tuple type", data.TUPLE},
+ {"Error type", data.ERROR},
+ {"None type", data.NONE},
+ }
+
+ for _, tt := range typeTests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockResponse.On("Type").Return(tt.dataType).Once()
+ result := mockResponse.Type()
+ assert.Equal(t, tt.dataType, result, "Type() should return expected type")
+ })
+ }
+ })
+
+ t.Run("Inspect method", func(t *testing.T) {
+ inspectTests := []struct {
+ name string
+ inspectResult string
+ }{
+ {"Empty string", ""},
+ {"Simple string", "test string"},
+ {"JSON representation", `{"key":"value"}`},
+ {"Integer representation", "42"},
+ {"Boolean representation", "true"},
+ }
+
+ for _, tt := range inspectTests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockResponse.On("Inspect").Return(tt.inspectResult).Once()
+ result := mockResponse.Inspect()
+ assert.Equal(
+ t,
+ tt.inspectResult,
+ result,
+ "Inspect() should return expected string representation",
+ )
+ })
+ }
+ })
+
+ // Test Interface method with different return types
+ t.Run("Interface method", func(t *testing.T) {
+ interfaceTests := []struct {
+ name string
+ value any
+ }{
+ {"String value", "test string"},
+ {"Integer value", 42},
+ {"Float value", 3.14},
+ {"Boolean value", true},
+ {"Map value", map[string]any{"key": "value"}},
+ {"Slice value", []any{1, 2, 3}},
+ {"Nil value", nil},
+ }
+
+ for _, tt := range interfaceTests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockResponse.On("Interface").Return(tt.value).Once()
+ result := mockResponse.Interface()
+ assert.Equal(t, tt.value, result, "Interface() should return expected value")
+ })
+ }
+ })
+
+ // Test script ID and execution time methods
+ t.Run("Script metadata methods", func(t *testing.T) {
+ mockResponse.On("GetScriptExeID").Return("script-123").Once()
+ scriptID := mockResponse.GetScriptExeID()
+ assert.Equal(t, "script-123", scriptID, "GetScriptExeID() should return expected ID")
+
+ mockResponse.On("GetExecTime").Return("42ms").Once()
+ execTime := mockResponse.GetExecTime()
+ assert.Equal(t, "42ms", execTime, "GetExecTime() should return expected time")
+ })
+
+ // Verify all expected assertions
+ mockResponse.AssertExpectations(t)
+}
+
+// TestEvaluatorResponseUsage tests how EvaluatorResponse is typically used in real code
+func TestEvaluatorResponseUsage(t *testing.T) {
+ t.Parallel()
+ mockResponse := new(mocks.EvaluatorResponse)
+
+ // Test a typical usage pattern where a string value is returned
+ mockResponse.On("Interface").Return("Hello World").Once()
+ mockResponse.On("Type").Return(data.STRING).Once()
+
+ // Type checking pattern
+ result := mockResponse.Interface()
+ require.Equal(t, mockResponse.Type(), data.STRING)
+
+ strResult, ok := result.(string)
+ assert.True(t, ok, "Should convert to string")
+ assert.Equal(t, "Hello World", strResult, "String value should match")
+
+ // Test map pattern
+ mapValue := map[string]any{
+ "name": "John",
+ "age": 42,
+ }
+ mockResponse.On("Interface").Return(mapValue).Once()
+ mockResponse.On("Type").Return(data.MAP).Once()
+
+ // Type checking for map
+ result = mockResponse.Interface()
+ require.Equal(t, mockResponse.Type(), data.MAP)
+
+ mapResult, ok := result.(map[string]any)
+ assert.True(t, ok, "Should convert to map")
+ assert.Equal(t, mapValue, mapResult, "Map value should match")
+ assert.Equal(t, "John", mapResult["name"], "Can access map values")
+
+ // Verify all expected assertions
+ mockResponse.AssertExpectations(t)
+}
diff --git a/engine/evaluator_test.go b/engine/evaluator_test.go
index 36b6751..d301532 100644
--- a/engine/evaluator_test.go
+++ b/engine/evaluator_test.go
@@ -2,6 +2,7 @@ package engine_test
import (
"context"
+ "errors"
"fmt"
"log/slog"
"net/http"
@@ -9,15 +10,107 @@ import (
"testing"
"github.com/robbyt/go-polyscript"
+ "github.com/robbyt/go-polyscript/engine"
"github.com/robbyt/go-polyscript/engine/options"
"github.com/robbyt/go-polyscript/execution/constants"
"github.com/robbyt/go-polyscript/execution/data"
+ "github.com/robbyt/go-polyscript/machines/mocks"
risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
+// mockDataPreparer is a mock implementation of engine.EvalDataPreparer
+type mockDataPreparer struct {
+ mock.Mock
+}
+
+func (m *mockDataPreparer) PrepareContext(
+ ctx context.Context,
+ data ...any,
+) (context.Context, error) {
+ args := m.Called(ctx, data)
+ return args.Get(0).(context.Context), args.Error(1)
+}
+
+// mockEvaluatorWithPreparer creates an evaluator implementation that satisfies both interfaces
+type mockEvaluatorWithPreparer struct {
+ mock.Mock
+}
+
+func (m *mockEvaluatorWithPreparer) Eval(ctx context.Context) (engine.EvaluatorResponse, error) {
+ args := m.Called(ctx)
+ if args.Get(0) == nil {
+ return nil, args.Error(1)
+ }
+ return args.Get(0).(engine.EvaluatorResponse), args.Error(1)
+}
+
+func (m *mockEvaluatorWithPreparer) PrepareContext(
+ ctx context.Context,
+ data ...any,
+) (context.Context, error) {
+ args := m.Called(ctx, data)
+ return args.Get(0).(context.Context), args.Error(1)
+}
+
+func TestEvaluatorInterface(t *testing.T) {
+ t.Parallel()
+ // Create a mock evaluator response
+ mockResponse := new(mocks.EvaluatorResponse)
+ mockResponse.On("Interface").Return("test result")
+ mockResponse.On("GetScriptExeID").Return("test-script-id")
+ mockResponse.On("GetExecTime").Return("10µs")
+ mockResponse.On("Type").Return(data.STRING)
+ mockResponse.On("Inspect").Return("test result")
+
+ // use a custom type for the context key lookup, to avoid lint warnings
+ type contextKey string
+ testKey := contextKey("test-key")
+
+ // Create a context with a test key
+ ctx := context.WithValue(context.Background(), testKey, "test-value")
+
+ // Create a mock evaluator with success case
+ evaluator := new(mocks.Evaluator)
+ evaluator.On("Eval", mock.MatchedBy(func(c context.Context) bool {
+ // Verify that context is passed correctly
+ _, hasKey := c.Value(testKey).(string)
+ return hasKey
+ })).Return(mockResponse, nil)
+
+ // Test the Eval method with the context
+ response, err := evaluator.Eval(ctx)
+
+ require.NoError(t, err, "Eval should not return an error")
+ require.NotNil(t, response, "Response should not be nil")
+
+ // Verify response methods
+ assert.Equal(t, "test result", response.Interface(), "Interface() should return expected value")
+ assert.Equal(
+ t,
+ "test-script-id",
+ response.GetScriptExeID(),
+ "GetScriptExeID() should return expected value",
+ )
+ assert.Equal(t, "10µs", response.GetExecTime(), "GetExecTime() should return expected value")
+ assert.Equal(t, data.STRING, response.Type(), "Type() should return expected value")
+ assert.Equal(t, "test result", response.Inspect(), "Inspect() should return expected value")
+
+ // Test error case
+ errorEvaluator := new(mocks.Evaluator)
+ errorEvaluator.On("Eval", mock.Anything).
+ Return((*mocks.EvaluatorResponse)(nil), errors.New("evaluation error"))
+
+ response, err = errorEvaluator.Eval(context.Background())
+ assert.Error(t, err, "Eval should return an error")
+ assert.Nil(t, response, "Response should be nil when there's an error")
+ assert.Contains(t, err.Error(), "evaluation error", "Error message should be preserved")
+}
+
func TestEvalDataPreparerInterface(t *testing.T) {
+ t.Parallel()
// Create a logger for testing
handler := slog.NewTextHandler(os.Stdout, nil)
@@ -39,7 +132,7 @@ method + " " + greeting
// Create context and test data
ctx := context.Background()
- req, err := http.NewRequest("GET", "https://example.com", nil)
+ req, err := http.NewRequest("GET", "http://localhost/test", nil)
require.NoError(t, err)
scriptData := map[string]any{"greeting": "Hello, World!"}
@@ -77,7 +170,120 @@ method + " " + greeting
)
}
+func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) {
+ t.Parallel()
+ dataPreparer := &mockDataPreparer{}
+
+ // Test with various data types
+ ctx := context.Background()
+ data1 := "string data"
+ data2 := map[string]any{"key": "value"}
+ data3 := 123
+
+ // Create enriched context with the test data
+ enrichedCtx := ctx
+ type dataKey string
+ for i, item := range []any{data1, data2, data3} {
+ key := dataKey(fmt.Sprintf("data-%d", i))
+ enrichedCtx = context.WithValue(enrichedCtx, key, item)
+ require.NotNil(t, enrichedCtx)
+ }
+
+ // Set up the mock behavior
+ dataPreparer.On("PrepareContext", ctx, []any{data1, data2, data3}).Return(enrichedCtx, nil)
+
+ // Call PrepareContext
+ resultCtx, err := dataPreparer.PrepareContext(ctx, data1, data2, data3)
+ require.NoError(t, err, "PrepareContext should not return an error")
+ require.NotNil(t, resultCtx, "Enriched context should not be nil")
+
+ // Verify data was stored correctly
+ for i, item := range []any{data1, data2, data3} {
+ key := dataKey(fmt.Sprintf("data-%d", i))
+ storedItem := resultCtx.Value(key)
+ require.NotNil(t, storedItem, "Stored item should not be nil")
+ assert.Equal(t, item, storedItem, "Stored item should match original data")
+ }
+
+ // Test error case
+ errorPreparer := &mockDataPreparer{}
+ errorPreparer.On("PrepareContext", ctx, []any{"test"}).
+ Return(ctx, errors.New("preparation error"))
+
+ ogCtx, err := errorPreparer.PrepareContext(ctx, "test")
+ assert.Error(t, err, "Should return an error")
+ assert.ErrorContains(t, err, "preparation error", "Error message should be preserved")
+ assert.Equal(t, ctx, ogCtx, "Original context should be returned on error")
+}
+
+func TestEvaluatorWithPrepInterface(t *testing.T) {
+ t.Parallel()
+ // Create a mock evaluator response
+ mockResponse := new(mocks.EvaluatorResponse)
+ mockResponse.On("Interface").Return("combined result")
+ mockResponse.On("GetScriptExeID").Return("test-script-id")
+ mockResponse.On("GetExecTime").Return("10µs")
+ mockResponse.On("Type").Return(data.STRING)
+ mockResponse.On("Inspect").Return("combined result")
+
+ // use a custom type for the context key lookup, to avoid lint warnings
+ type prepKey string
+ prepDataKey := prepKey("prepared-data")
+
+ // Create a mock combined implementation
+ combinedEvaluator := &mockEvaluatorWithPreparer{}
+
+ // Define context and test data
+ ctx := context.Background()
+ enrichedCtx := context.WithValue(ctx, prepDataKey, "test-value")
+
+ // Set up mock behaviors
+ combinedEvaluator.On("PrepareContext", ctx, []any{"test data"}).Return(enrichedCtx, nil)
+ combinedEvaluator.On("Eval", mock.MatchedBy(func(c context.Context) bool {
+ val, ok := c.Value(prepDataKey).(string)
+ return ok && val == "test-value"
+ })).Return(mockResponse, nil)
+
+ // Test the full workflow: prepare context then evaluate
+ resultCtx, err := combinedEvaluator.PrepareContext(ctx, "test data")
+ require.NoError(t, err, "PrepareContext should not return an error")
+ require.NotNil(t, resultCtx, "Enriched context should not be nil")
+
+ // Then evaluate with the enriched context
+ response, err := combinedEvaluator.Eval(resultCtx)
+ require.NoError(t, err, "Eval should not return an error when context is prepared")
+ require.NotNil(t, response, "Response should not be nil")
+
+ // Verify the response
+ assert.Equal(
+ t,
+ "combined result",
+ response.Interface(),
+ "Interface() should return expected value",
+ )
+
+ // Test error in preparation
+ prepErrorEvaluator := &mockEvaluatorWithPreparer{}
+ prepErrorEvaluator.On("PrepareContext", ctx, []any{"test data"}).
+ Return(ctx, errors.New("preparation error"))
+
+ _, err = prepErrorEvaluator.PrepareContext(ctx, "test data")
+ assert.Error(t, err, "Should return an error when preparation fails")
+
+ // Test error in evaluation
+ evalErrorEvaluator := &mockEvaluatorWithPreparer{}
+ evalErrorEvaluator.On("PrepareContext", ctx, []any{"test data"}).Return(enrichedCtx, nil)
+ evalErrorEvaluator.On("Eval", mock.Anything).
+ Return((*mocks.EvaluatorResponse)(nil), errors.New("evaluation error"))
+
+ evalCtx, prepErr := evalErrorEvaluator.PrepareContext(ctx, "test data")
+ require.NoError(t, prepErr, "PrepareContext should not return an error")
+ _, err = evalErrorEvaluator.Eval(evalCtx)
+ assert.Error(t, err, "Should return an error when evaluation fails")
+}
+
func TestEvaluatorWithPrepErrors(t *testing.T) {
+ t.Parallel()
// Create a logger for testing
handler := slog.NewTextHandler(os.Stdout, nil)
diff --git a/engine/executionPackage_test.go b/engine/executionPackage_test.go
new file mode 100644
index 0000000..a50cd9a
--- /dev/null
+++ b/engine/executionPackage_test.go
@@ -0,0 +1,127 @@
+package engine_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/robbyt/go-polyscript/engine"
+ "github.com/robbyt/go-polyscript/execution/script"
+ "github.com/robbyt/go-polyscript/machines/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewExecutionPackage(t *testing.T) {
+ t.Parallel()
+ // Setup test mocks
+ mockEvaluator := new(mocks.Evaluator)
+ mockUnit := &script.ExecutableUnit{}
+ timeout := 5 * time.Second
+
+ // Create execution package
+ execPkg := engine.NewExecutionPackage(mockEvaluator, mockUnit, timeout)
+
+ // Assert execution package was created correctly
+ assert.NotNil(t, execPkg, "Execution package should not be nil")
+ assert.Equal(t, mockEvaluator, execPkg.GetEvaluator(), "Should return the provided evaluator")
+ assert.Equal(
+ t,
+ mockUnit,
+ execPkg.GetExecutableUnit(),
+ "Should return the provided executable unit",
+ )
+ assert.Equal(t, timeout, execPkg.GetEvalTimeout(), "Should return the provided timeout")
+}
+
+func TestExecutionPackage_String(t *testing.T) {
+ t.Parallel()
+ // Setup test mocks
+ mockEvaluator := new(mocks.Evaluator)
+ mockUnit := &script.ExecutableUnit{}
+ timeout := 5 * time.Second
+
+ // Create execution package
+ execPkg := engine.NewExecutionPackage(mockEvaluator, mockUnit, timeout)
+
+ // Test String method
+ stringRep := execPkg.String()
+ assert.Contains(
+ t,
+ stringRep,
+ "engine.ExecutionPackage",
+ "String representation should contain type information",
+ )
+ assert.Contains(t, stringRep, "Evaluator", "String representation should mention evaluator")
+ assert.Contains(
+ t,
+ stringRep,
+ "ExecutableUnit",
+ "String representation should mention executable unit",
+ )
+}
+
+func TestExecutionPackage_Getters(t *testing.T) {
+ t.Parallel()
+ // Setup test cases
+ testCases := []struct {
+ name string
+ evaluator engine.Evaluator
+ unit *script.ExecutableUnit
+ evalTimeout time.Duration
+ }{
+ {
+ name: "Standard values",
+ evaluator: new(mocks.Evaluator),
+ unit: &script.ExecutableUnit{},
+ evalTimeout: 5 * time.Second,
+ },
+ {
+ name: "Zero timeout",
+ evaluator: new(mocks.Evaluator),
+ unit: &script.ExecutableUnit{},
+ evalTimeout: 0,
+ },
+ {
+ name: "Negative timeout (should still work, though not recommended)",
+ evaluator: new(mocks.Evaluator),
+ unit: &script.ExecutableUnit{},
+ evalTimeout: -1 * time.Second,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Create execution package
+ execPkg := engine.NewExecutionPackage(tc.evaluator, tc.unit, tc.evalTimeout)
+
+ // Test getter methods
+ assert.Equal(t, tc.evaluator, execPkg.GetEvaluator(),
+ "GetEvaluator should return the provided evaluator")
+ assert.Equal(t, tc.unit, execPkg.GetExecutableUnit(),
+ "GetExecutableUnit should return the provided executable unit")
+ assert.Equal(t, tc.evalTimeout, execPkg.GetEvalTimeout(),
+ "GetEvalTimeout should return the provided timeout")
+ })
+ }
+}
+
+func TestExecutionPackage_WithNilValues(t *testing.T) {
+ t.Parallel()
+ // Test with nil evaluator (not recommended but should still create the package)
+ execPkgNilEval := engine.NewExecutionPackage(nil, &script.ExecutableUnit{}, 5*time.Second)
+ assert.NotNil(t, execPkgNilEval, "Should create package even with nil evaluator")
+ assert.Nil(t, execPkgNilEval.GetEvaluator(), "GetEvaluator should return nil when provided nil")
+
+ // Test with nil unit (not recommended but should still create the package)
+ execPkgNilUnit := engine.NewExecutionPackage(new(mocks.Evaluator), nil, 5*time.Second)
+ assert.NotNil(t, execPkgNilUnit, "Should create package even with nil unit")
+ assert.Nil(
+ t,
+ execPkgNilUnit.GetExecutableUnit(),
+ "GetExecutableUnit should return nil when provided nil",
+ )
+
+ // Test String method with nil values
+ stringRep := execPkgNilEval.String()
+ require.NotEmpty(t, stringRep, "String method should handle nil values without panicking")
+}
diff --git a/engine/options/options_test.go b/engine/options/options_test.go
index 68d2a70..451b3d9 100644
--- a/engine/options/options_test.go
+++ b/engine/options/options_test.go
@@ -47,6 +47,7 @@ func NewMockLoader() *MockLoader {
}
func TestWithOptions(t *testing.T) {
+ t.Parallel()
// Create test config
cfg := &Config{
machineType: types.Starlark,
@@ -77,6 +78,7 @@ func TestWithOptions(t *testing.T) {
}
func TestConfigValidation(t *testing.T) {
+ t.Parallel()
// Test with missing loader
cfg1 := &Config{
machineType: types.Starlark,
@@ -105,6 +107,7 @@ func TestConfigValidation(t *testing.T) {
}
func TestConfigGetters(t *testing.T) {
+ t.Parallel()
testHandler := slog.NewTextHandler(os.Stdout, nil)
testDataProvider := data.NewStaticProvider(map[string]any{"test": "value"})
testLoader := NewMockLoader()
diff --git a/examples/data-prep/risor/main.go b/examples/data-prep/risor/main.go
index 556d576..f8642bb 100644
--- a/examples/data-prep/risor/main.go
+++ b/examples/data-prep/risor/main.go
@@ -71,9 +71,9 @@ func prepareRuntimeData(
requestData := map[string]any{
"Method": "GET",
"URL_Path": "/api/users",
- "URL_Host": "example.com",
- "Host": "example.com",
- "RemoteAddr": "192.168.1.1:12345",
+ "URL_Host": "localhost:8080",
+ "Host": "localhost:8080",
+ "RemoteAddr": "127.0.0.1:8080",
}
// General request metadata
diff --git a/examples/data-prep/starlark/main.go b/examples/data-prep/starlark/main.go
index efd14ba..dcca9a2 100644
--- a/examples/data-prep/starlark/main.go
+++ b/examples/data-prep/starlark/main.go
@@ -62,8 +62,8 @@ func prepareRuntimeData(
) (context.Context, error) {
logger.Info("Preparing runtime data")
- // Create an HTTP request object
- reqURL, err := url.Parse("https://example.com/api/users?limit=10&offset=0")
+ // Create an HTTP request object (will not make a real request!)
+ reqURL, err := url.Parse("http://localhost:8080/api/users?limit=10&offset=0")
if err != nil {
logger.Error("Failed to parse URL", "error", err)
return nil, err
@@ -76,8 +76,8 @@ func prepareRuntimeData(
"Content-Type": []string{"application/json"},
"User-Agent": []string{"Example Client/1.0"},
},
- Host: "example.com",
- RemoteAddr: "192.168.1.1:12345",
+ Host: "localhost",
+ RemoteAddr: "127.0.1:8080",
}
// Create user data
diff --git a/execution/data/README.md b/execution/data/README.md
index e64546b..699581e 100644
--- a/execution/data/README.md
+++ b/execution/data/README.md
@@ -29,15 +29,15 @@ Both types of data are made available to scripts as part of the top-level `ctx`
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Static Data │ │ Dynamic Data │ │ Provider │
│ │ │ │ │ │
-│ │ │ │ │ GetData() │
-│ - Config values │ │ - Request params │ │ AddDataToContext()│
+│ │ │ │ │ GetData() │
+│ - Config values │ │ - Request params │ │ AddDataToContext()│
│ - Constants │ │ - User inputs │ │ │
└─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
-│ Context │
+│ Context │
│ │
│ Data stored under constants.EvalData key with structure: │
│ { │
@@ -55,7 +55,7 @@ Both types of data are made available to scripts as part of the top-level `ctx`
│
▼
┌─────────────────────────────────────────────────────────────────────┐
-│ VM Execution │
+│ VM Execution │
│ │
│ - VM implementations access data through the Provider interface │
│ - Each VM makes the data available as a global `ctx` variable │
diff --git a/execution/data/compositeProvider_test.go b/execution/data/compositeProvider_test.go
index 5527189..459229e 100644
--- a/execution/data/compositeProvider_test.go
+++ b/execution/data/compositeProvider_test.go
@@ -49,10 +49,7 @@ func TestCompositeProvider_Creation(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
composite := NewCompositeProvider(tt.providers...)
require.NotNil(t, composite, "CompositeProvider should never be nil")
assert.Len(
@@ -118,8 +115,11 @@ func TestCompositeProvider_GetData(t *testing.T) {
)
},
setupContext: func() context.Context {
- contextData := map[string]any{"runtime_key": "runtime_value"}
- return context.WithValue(context.Background(), constants.EvalData, contextData)
+ return context.WithValue(
+ context.Background(),
+ constants.EvalData,
+ map[string]any{"runtime_key": "runtime_value"},
+ )
},
expectedData: map[string]any{
"static_key": "static_value",
@@ -139,11 +139,13 @@ func TestCompositeProvider_GetData(t *testing.T) {
)
},
setupContext: func() context.Context {
- contextData := map[string]any{
- "shared_key": "runtime_value",
- "runtime_key": "runtime_value",
- }
- return context.WithValue(context.Background(), constants.EvalData, contextData)
+ return context.WithValue(
+ context.Background(),
+ constants.EvalData,
+ map[string]any{
+ "shared_key": "runtime_value",
+ "runtime_key": "runtime_value",
+ })
},
expectedData: map[string]any{
"shared_key": "runtime_value", // Context provider overrides static provider
@@ -166,23 +168,23 @@ func TestCompositeProvider_GetData(t *testing.T) {
)
},
setupContext: func() context.Context {
- contextData := map[string]any{
+ data := map[string]any{
"input": "API User",
"request": map[string]any{
"id": "123",
},
"config": map[string]any{
- "host": "example.com", // New key in existing map
- "retries": 5, // Override existing key
+ "host": "localhost:8080", // New key in existing map
+ "retries": 5, // Override existing key
},
}
- return context.WithValue(context.Background(), constants.EvalData, contextData)
+ return context.WithValue(context.Background(), constants.EvalData, data)
},
expectedData: map[string]any{
"config": map[string]any{
"timeout": 30,
- "retries": 5, // Overridden
- "host": "example.com", // Added
+ "retries": 5, // Overridden
+ "host": "localhost:8080", // Added
},
"input": "API User",
"request": map[string]any{
@@ -226,10 +228,7 @@ func TestCompositeProvider_GetData(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
provider := tt.setupProvider()
require.NotNil(t, provider, "Provider should never be nil")
@@ -242,14 +241,10 @@ func TestCompositeProvider_GetData(t *testing.T) {
}
assert.NoError(t, err, "Should not return error for valid providers")
- assert.Equal(t, tt.expectedData, result, "Result should match expected data")
+ assertMapContainsExpectedHelper(t, tt.expectedData, result)
- // Get a new result to verify data consistency
- if result != nil {
- newResult, err := provider.GetData(ctx)
- assert.NoError(t, err)
- assert.Equal(t, result, newResult, "Result should be consistent across calls")
- }
+ // Verify data consistency across calls
+ getDataCheckHelper(t, provider, ctx)
})
}
}
@@ -259,36 +254,32 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) {
t.Parallel()
t.Run("empty providers list", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider()
- require.NotNil(t, composite)
+ provider := NewCompositeProvider()
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.NoError(t, err, "Should not return error with empty provider list")
assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
t.Run("single context provider succeeds", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(NewContextProvider(constants.EvalData))
- require.NotNil(t, composite)
+ provider := NewCompositeProvider(NewContextProvider(constants.EvalData))
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.NoError(t, err, "Should not return error for context provider")
assert.NotEqual(t, ctx, newCtx, "Context should be modified")
// Verify data was added correctly
- data, err := composite.GetData(newCtx)
+ data, err := provider.GetData(newCtx)
assert.NoError(t, err)
assert.Contains(t, data, constants.InputData)
@@ -298,45 +289,41 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) {
})
t.Run("single static provider always errors", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(NewStaticProvider(simpleData))
- require.NotNil(t, composite)
+ provider := NewCompositeProvider(NewStaticProvider(simpleData))
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.Error(t, err, "Should return error for static provider")
assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
+ assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates))
// Verify static data is still available
- data, err := composite.GetData(ctx)
- assert.NoError(t, err)
+ data, getErr := provider.GetData(ctx)
+ assert.NoError(t, getErr)
assert.Equal(t, simpleData, data, "Static data should still be available")
})
t.Run("mixed providers (static fails, context succeeds)", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(
+ provider := NewCompositeProvider(
NewStaticProvider(simpleData),
NewContextProvider(constants.EvalData),
)
- require.NotNil(t, composite)
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
- // StaticProvider errors are ignored when ContextProvider succeeds
assert.NoError(t, err, "Should not return error when at least one provider succeeds")
assert.NotEqual(t, ctx, newCtx, "Context should be modified")
// Verify both static and context data are available
- data, err := composite.GetData(newCtx)
+ data, err := provider.GetData(newCtx)
assert.NoError(t, err)
// Static data should be present
@@ -350,93 +337,59 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) {
})
t.Run("all providers fail", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(
+ provider := NewCompositeProvider(
NewStaticProvider(simpleData),
newMockErrorProvider(),
)
- require.NotNil(t, composite)
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.Error(t, err, "Should return error when all non-static providers fail")
assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
- t.Run("multiple successful context providers", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(
- NewContextProvider(constants.ContextKey("key1")),
- NewContextProvider(constants.ContextKey("key2")),
- )
- require.NotNil(t, composite)
-
- ctx := context.Background()
- inputData := map[string]any{"data": "value"}
-
- newCtx, err := composite.AddDataToContext(ctx, inputData)
-
- assert.NoError(t, err, "Should not return error with multiple context providers")
- assert.NotEqual(t, ctx, newCtx, "Context should be modified")
-
- // Verify both context keys were updated
- value1 := newCtx.Value(constants.ContextKey("key1"))
- assert.NotNil(t, value1)
-
- value2 := newCtx.Value(constants.ContextKey("key2"))
- assert.NotNil(t, value2)
- })
-
t.Run("nil providers are skipped", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(
+ provider := NewCompositeProvider(
nil,
NewContextProvider(constants.EvalData),
nil,
)
- require.NotNil(t, composite)
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.NoError(t, err, "Should not return error when skipping nil providers")
assert.NotEqual(t, ctx, newCtx, "Context should be modified")
// Verify context data was added
- data, err := composite.GetData(newCtx)
+ data, err := provider.GetData(newCtx)
assert.NoError(t, err)
assert.Contains(t, data, constants.InputData)
})
t.Run("composite with only static providers", func(t *testing.T) {
- t.Parallel()
-
- composite := NewCompositeProvider(
+ provider := NewCompositeProvider(
NewStaticProvider(map[string]any{"key1": "value1"}),
NewStaticProvider(map[string]any{"key2": "value2"}),
)
- require.NotNil(t, composite)
+ require.NotNil(t, provider)
ctx := context.Background()
inputData := map[string]any{"key": "value"}
- newCtx, err := composite.AddDataToContext(ctx, inputData)
+ newCtx, err := provider.AddDataToContext(ctx, inputData)
assert.Error(t, err, "Should return error when all providers are static")
- assert.True(
- t,
- errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
- "Error should be StaticProviderNoRuntimeUpdates",
- )
assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
+ assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
+ "Error should be StaticProviderNoRuntimeUpdates")
})
}
@@ -447,6 +400,7 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
tests := []struct {
name string
setupProviders func() *CompositeProvider
+ setupContext func() context.Context
expectedResult map[string]any
}{
{
@@ -470,6 +424,9 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
})
return NewCompositeProvider(innerComposite, outerStatic)
},
+ setupContext: func() context.Context {
+ return context.Background()
+ },
expectedResult: map[string]any{
"inner1_key": "inner1_value",
"inner2_key": "inner2_value",
@@ -503,6 +460,9 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
})
return NewCompositeProvider(level2Composite, level1Static)
},
+ setupContext: func() context.Context {
+ return context.Background()
+ },
expectedResult: map[string]any{
"level": 1, // Should be overridden to 1
"level1_key": "level1_value",
@@ -511,98 +471,6 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
"override_key": "level1_value", // Verifies proper override hierarchy
},
},
- {
- name: "nested composites with complex nested data structures",
- setupProviders: func() *CompositeProvider {
- // Inner provider with nested map
- innerProvider := NewStaticProvider(map[string]any{
- "config": map[string]any{
- "database": map[string]any{
- "host": "localhost",
- "port": 5432,
- "username": "user1",
- "timeout": 30,
- },
- "cache": map[string]any{
- "enabled": true,
- "ttl": 60,
- },
- },
- "metrics": map[string]any{
- "enabled": false,
- },
- })
-
- // Outer provider that overrides some nested values
- outerProvider := NewStaticProvider(map[string]any{
- "config": map[string]any{
- "database": map[string]any{
- "username": "admin", // Should override the inner value
- "password": "secret", // New field
- },
- "logging": map[string]any{ // New nested section
- "level": "debug",
- },
- },
- "metrics": map[string]any{
- "enabled": true, // Override the inner value
- "interval": 15, // New field
- },
- })
-
- return NewCompositeProvider(innerProvider, outerProvider)
- },
- expectedResult: map[string]any{
- "config": map[string]any{
- "database": map[string]any{
- "username": "admin", // Overridden
- "password": "secret", // Added
- "host": "localhost", // Preserved
- "port": 5432, // Preserved
- "timeout": 30, // Preserved
- },
- "cache": map[string]any{
- "enabled": true,
- "ttl": 60,
- },
- "logging": map[string]any{
- "level": "debug",
- },
- },
- "metrics": map[string]any{
- "enabled": true, // Overridden
- "interval": 15, // Added
- },
- },
- },
- {
- name: "array and non-map types are fully replaced",
- setupProviders: func() *CompositeProvider {
- // First provider with various data types
- provider1 := NewStaticProvider(map[string]any{
- "array": []any{1, 2, 3},
- "string": "original",
- "number": 42,
- "bool": true,
- })
-
- // Second provider that overrides with different types
- provider2 := NewStaticProvider(map[string]any{
- "array": []any{4, 5, 6}, // Should completely replace the array
- "string": "replaced", // Should replace the string
- "number": 99, // Should replace the number
- "bool": false, // Should replace the boolean
- })
-
- return NewCompositeProvider(provider1, provider2)
- },
- expectedResult: map[string]any{
- "array": []any{4, 5, 6}, // Completely replaced
- "string": "replaced", // Replaced
- "number": 99, // Replaced
- "bool": false, // Replaced
- },
- },
{
name: "mixed provider types in nested composites",
setupProviders: func() *CompositeProvider {
@@ -626,6 +494,15 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
return NewCompositeProvider(innerComposite, outerStatic)
},
+ setupContext: func() context.Context {
+ data := map[string]any{
+ "context_key": "context_value",
+ constants.InputData: map[string]any{
+ "nested_key": "nested_value",
+ },
+ }
+ return context.WithValue(context.Background(), constants.EvalData, data)
+ },
expectedResult: map[string]any{
"static_key": "static_value",
"outer_key": "outer_value",
@@ -639,64 +516,21 @@ func TestCompositeProvider_NestedStructures(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
composite := tt.setupProviders()
-
- // Set up context with data if needed for the mixed provider test
- ctx := context.Background()
- if tt.name == "mixed provider types in nested composites" {
- contextData := map[string]any{
- "context_key": "context_value",
- constants.InputData: map[string]any{
- "nested_key": "nested_value",
- },
- }
- ctx = context.WithValue(ctx, constants.EvalData, contextData)
- }
+ ctx := tt.setupContext()
// Get the combined data
result, err := composite.GetData(ctx)
require.NoError(t, err, "GetData should not error with valid providers")
// Verify all expected values are present
- for key, expected := range tt.expectedResult {
- assert.Contains(t, result, key, "Result should contain key: %s", key)
-
- // For maps, we need to check deeply
- expectedMap, expectedIsMap := expected.(map[string]any)
- actualMap, actualIsMap := result[key].(map[string]any)
-
- if expectedIsMap && actualIsMap {
- // Deep compare for maps
- for nestedKey, nestedValue := range expectedMap {
- assert.Contains(
- t,
- actualMap,
- nestedKey,
- "Nested map should contain key: %s",
- nestedKey,
- )
- assert.Equal(
- t,
- nestedValue,
- actualMap[nestedKey],
- "Nested value should match for key: %s",
- nestedKey,
- )
- }
- } else {
- // Direct compare for non-maps
- assert.Equal(t, expected, result[key], "Value should match for key: %s", key)
- }
- }
+ assertMapContainsExpectedHelper(t, tt.expectedResult, result)
})
}
}
-// TestCompositeProvider_DeepMerge tests specific edge cases of deep merging behavior
+// TestCompositeProvider_DeepMerge tests the deep merge functionality
func TestCompositeProvider_DeepMerge(t *testing.T) {
t.Parallel()
@@ -811,10 +645,7 @@ func TestCompositeProvider_DeepMerge(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
result := deepMerge(tt.src, tt.dst)
assert.Equal(t, tt.expected, result, tt.description)
diff --git a/execution/data/contextProvider_test.go b/execution/data/contextProvider_test.go
index 335b158..795a025 100644
--- a/execution/data/contextProvider_test.go
+++ b/execution/data/contextProvider_test.go
@@ -6,6 +6,7 @@ import (
"github.com/robbyt/go-polyscript/execution/constants"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
// TestContextProvider_Creation tests the creation and initialization of ContextProvider
@@ -13,56 +14,34 @@ func TestContextProvider_Creation(t *testing.T) {
t.Parallel()
t.Run("standard context key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
assert.Equal(t, constants.EvalData, provider.contextKey,
"Context key should be set correctly")
-
assert.Equal(t, constants.InputData, provider.storageKey,
"Storage key should be initialized")
-
assert.Equal(t, constants.Request, provider.requestKey,
"Request key should be initialized")
-
assert.Equal(t, constants.Response, provider.responseKey,
"Response key should be initialized")
})
t.Run("custom context key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("custom_key")
- assert.Equal(
- t,
- constants.ContextKey("custom_key"),
- provider.contextKey,
- "Context key should be set correctly",
- )
- assert.Equal(
- t,
- constants.InputData,
- provider.storageKey,
- "Storage key should be initialized",
- )
+ assert.Equal(t, constants.ContextKey("custom_key"), provider.contextKey,
+ "Context key should be set correctly")
+ assert.Equal(t, constants.InputData, provider.storageKey,
+ "Storage key should be initialized")
})
t.Run("empty context key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("")
- assert.Equal(
- t,
- constants.ContextKey(""),
- provider.contextKey,
- "Context key should be set correctly",
- )
- assert.Equal(
- t,
- constants.InputData,
- provider.storageKey,
- "Storage key should be initialized",
- )
+ assert.Equal(t, constants.ContextKey(""), provider.contextKey,
+ "Context key should be set correctly")
+ assert.Equal(t, constants.InputData, provider.storageKey,
+ "Storage key should be initialized")
})
}
@@ -71,7 +50,6 @@ func TestContextProvider_GetData(t *testing.T) {
t.Parallel()
t.Run("empty context key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("")
ctx := context.Background()
@@ -82,7 +60,6 @@ func TestContextProvider_GetData(t *testing.T) {
})
t.Run("nil context value", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
@@ -91,10 +68,12 @@ func TestContextProvider_GetData(t *testing.T) {
assert.NoError(t, err, "Should not return error for nil context value")
assert.NotNil(t, result, "Result should be an empty map, not nil")
assert.Empty(t, result, "Result map should be empty")
+
+ // Verify data consistency
+ getDataCheckHelper(t, provider, ctx)
})
t.Run("valid simple data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, simpleData)
@@ -102,10 +81,12 @@ func TestContextProvider_GetData(t *testing.T) {
assert.NoError(t, err, "Should not return error for valid context")
assert.Equal(t, simpleData, result, "Result should match expected data")
+
+ // Verify data consistency
+ getDataCheckHelper(t, provider, ctx)
})
t.Run("valid complex data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, complexData)
@@ -113,10 +94,12 @@ func TestContextProvider_GetData(t *testing.T) {
assert.NoError(t, err, "Should not return error for valid context")
assert.Equal(t, complexData, result, "Result should match expected data")
+
+ // Verify data consistency
+ getDataCheckHelper(t, provider, ctx)
})
t.Run("invalid data type (string)", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, "not a map")
@@ -127,7 +110,6 @@ func TestContextProvider_GetData(t *testing.T) {
})
t.Run("invalid data type (int)", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, 42)
@@ -143,21 +125,22 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
t.Parallel()
t.Run("empty context key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("")
ctx := context.Background()
- _, err := provider.AddDataToContext(ctx, map[string]any{"key": "value"})
+ newCtx, err := provider.AddDataToContext(ctx, map[string]any{"key": "value"})
+
assert.Error(t, err, "Should return error for empty context key")
+ assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
t.Run("nil input data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
newCtx, err := provider.AddDataToContext(ctx, nil)
- assert.NoError(t, err)
+
+ assert.NoError(t, err, "Should not return error with nil data")
assert.NotEqual(t, ctx, newCtx, "Context should be modified even with nil data")
data, err := provider.GetData(newCtx)
@@ -166,13 +149,12 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
})
t.Run("simple map data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
- inputMap := map[string]any{"key1": "value1", "key2": 123}
- newCtx, err := provider.AddDataToContext(ctx, inputMap)
- assert.NoError(t, err)
+ newCtx, err := provider.AddDataToContext(ctx, map[string]any{"key1": "value1", "key2": 123})
+
+ assert.NoError(t, err, "Should not return error with valid map data")
assert.NotEqual(t, ctx, newCtx, "Context should be modified")
data, err := provider.GetData(newCtx)
@@ -186,14 +168,15 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
})
t.Run("multiple map data items", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
newCtx, err := provider.AddDataToContext(ctx,
map[string]any{"key1": "value1"},
map[string]any{"key2": "value2"})
- assert.NoError(t, err)
+
+ assert.NoError(t, err, "Should not return error with multiple map items")
+ assert.NotEqual(t, ctx, newCtx, "Context should be modified")
data, err := provider.GetData(newCtx)
assert.NoError(t, err)
@@ -206,32 +189,13 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
})
t.Run("HTTP request data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
- req := createTestRequest()
-
- newCtx, err := provider.AddDataToContext(ctx, req)
- assert.NoError(t, err)
-
- data, err := provider.GetData(newCtx)
- assert.NoError(t, err)
- assert.Contains(t, data, constants.Request, "Should contain request key")
-
- requestData, ok := data[constants.Request].(map[string]any)
- assert.True(t, ok, "request should be a map")
- assert.Equal(t, "GET", requestData["Method"], "Should contain HTTP method")
- assert.Equal(t, "/test", requestData["URL_Path"], "Should contain request path")
- })
- t.Run("HTTP request by value", func(t *testing.T) {
- t.Parallel()
- provider := NewContextProvider(constants.EvalData)
- ctx := context.Background()
- req := *createTestRequest() // Pass by value
+ newCtx, err := provider.AddDataToContext(ctx, createTestRequestHelper())
- newCtx, err := provider.AddDataToContext(ctx, req)
- assert.NoError(t, err)
+ assert.NoError(t, err, "Should not return error with HTTP request")
+ assert.NotEqual(t, ctx, newCtx, "Context should be modified")
data, err := provider.GetData(newCtx)
assert.NoError(t, err)
@@ -243,44 +207,16 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
assert.Equal(t, "/test", requestData["URL_Path"], "Should contain request path")
})
- t.Run("mixed data types", func(t *testing.T) {
- t.Parallel()
- provider := NewContextProvider(constants.EvalData)
- ctx := context.Background()
- req := createTestRequest()
-
- newCtx, err := provider.AddDataToContext(ctx,
- map[string]any{"key1": "value1"},
- req)
- assert.NoError(t, err)
-
- data, err := provider.GetData(newCtx)
- assert.NoError(t, err)
-
- // Verify input_data
- assert.Contains(t, data, constants.InputData, "Should contain input_data key")
- inputData, ok := data[constants.InputData].(map[string]any)
- assert.True(t, ok, "input_data should be a map")
- assert.Equal(t, "value1", inputData["key1"], "Should contain key1")
-
- // Verify request data
- assert.Contains(t, data, constants.Request, "Should contain request key")
- requestData, ok := data[constants.Request].(map[string]any)
- assert.True(t, ok, "request should be a map")
- assert.Equal(t, "GET", requestData["Method"], "Should contain HTTP method")
- })
-
t.Run("unsupported data type", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
newCtx, err := provider.AddDataToContext(ctx, 42) // Integer is not supported
- assert.Error(t, err, "Should error with unsupported data type")
- // Context should still be modified
+ assert.Error(t, err, "Should error with unsupported data type")
assert.NotEqual(t, ctx, newCtx, "Context should be modified despite error")
+ // Context should be modified but empty
data, getErr := provider.GetData(newCtx)
assert.NoError(t, getErr, "GetData should work after AddDataToContext")
assert.NotNil(t, data, "Data should not be nil despite error")
@@ -288,15 +224,17 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
})
t.Run("mixed supported and unsupported", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
+ // Only the map is supported
newCtx, err := provider.AddDataToContext(ctx,
map[string]any{"key": "value"},
- 42, // Will cause error
- "string") // Also unsupported
+ 42,
+ "string")
+
assert.Error(t, err, "Should error with unsupported data types")
+ assert.NotEqual(t, ctx, newCtx, "Context should be modified despite error")
data, getErr := provider.GetData(newCtx)
assert.NoError(t, getErr, "GetData should work after AddDataToContext")
@@ -306,24 +244,6 @@ func TestContextProvider_AddDataToContext(t *testing.T) {
assert.True(t, ok, "input_data should be a map")
assert.Equal(t, "value", inputData["key"], "Should contain supported data")
})
-
- t.Run("duplicate HTTP requests", func(t *testing.T) {
- t.Parallel()
- provider := NewContextProvider(constants.EvalData)
- ctx := context.Background()
- req := createTestRequest()
-
- newCtx, err := provider.AddDataToContext(ctx, req, req)
- assert.Error(t, err, "Should error on duplicate request")
-
- data, getErr := provider.GetData(newCtx)
- assert.NoError(t, getErr, "GetData should work after AddDataToContext")
- assert.Contains(t, data, constants.Request, "Should contain request key")
-
- requestData, ok := data[constants.Request].(map[string]any)
- assert.True(t, ok, "request should be a map")
- assert.Equal(t, "GET", requestData["Method"], "Should contain HTTP method")
- })
}
// TestContextProvider_DataIntegration tests more complex data scenarios
@@ -331,15 +251,12 @@ func TestContextProvider_DataIntegration(t *testing.T) {
t.Parallel()
t.Run("single map data item", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
-
- // Start with empty context
ctx := context.Background()
// Add some data
newCtx, err := provider.AddDataToContext(ctx, map[string]any{"key": "value"})
- assert.NoError(t, err)
+ require.NoError(t, err)
// Verify data
data, err := provider.GetData(newCtx)
@@ -354,94 +271,27 @@ func TestContextProvider_DataIntegration(t *testing.T) {
assert.Equal(t, "value", inputData["key"], "Should contain the correct value")
})
- t.Run("HTTP request only", func(t *testing.T) {
- t.Parallel()
- provider := NewContextProvider(constants.EvalData)
-
- // Start with empty context
- ctx := context.Background()
-
- // Add request data
- req := createTestRequest()
- newCtx, err := provider.AddDataToContext(ctx, req)
- assert.NoError(t, err)
-
- // Verify data
- data, err := provider.GetData(newCtx)
- assert.NoError(t, err)
-
- // Verify request data exists and has expected content
- assert.Contains(t, data, constants.Request, "Should contain request key")
- requestData, ok := data[constants.Request].(map[string]any)
- assert.True(t, ok, "request should be a map")
- assert.Equal(t, "GET", requestData["Method"], "Should contain HTTP method")
- assert.Equal(t, "/test", requestData["URL_Path"], "Should contain request path")
- })
-
- t.Run("both map and request data", func(t *testing.T) {
- t.Parallel()
- provider := NewContextProvider(constants.EvalData)
- ctx := context.Background()
-
- // Create request
- req := createTestRequest()
-
- // Add both map data and request in a single call
- newCtx, err := provider.AddDataToContext(ctx,
- map[string]any{"key1": "value1", "key2": 123},
- req)
- assert.NoError(t, err)
-
- // Verify data
- data, err := provider.GetData(newCtx)
- assert.NoError(t, err)
-
- // Check input_data
- assert.Contains(t, data, constants.InputData, "Should contain input_data key")
- inputData, ok := data[constants.InputData].(map[string]any)
- assert.True(t, ok, "input_data should be a map")
- assert.Equal(t, "value1", inputData["key1"], "Should contain string value")
- assert.Equal(t, 123, inputData["key2"], "Should contain numeric value")
-
- // Check request
- assert.Contains(t, data, constants.Request, "Should contain request key")
- requestData, ok := data[constants.Request].(map[string]any)
- assert.True(t, ok, "request should be a map")
- assert.Equal(t, "GET", requestData["Method"], "Should contain HTTP method")
- assert.Equal(t, "/test", requestData["URL_Path"], "Should contain request path")
- })
-
t.Run("should preserve context data across calls", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
- // Create a context directly with data already in it (bypassing AddDataToContext)
+ // Create a context directly with data already in it
existingData := map[string]any{
constants.InputData: map[string]any{"existing": "value"},
}
ctx := context.WithValue(context.Background(), constants.EvalData, existingData)
- // Check that the data is there
- data1, err := provider.GetData(ctx)
- assert.NoError(t, err)
- assert.Contains(t, data1, constants.InputData, "Should contain input_data key")
-
- inputData1, ok := data1[constants.InputData].(map[string]any)
- assert.True(t, ok, "input_data should be a map")
- assert.Equal(t, "value", inputData1["existing"], "Should contain existing value")
-
- // Now add more data
+ // Add more data
newCtx, err := provider.AddDataToContext(ctx, map[string]any{"new": "value"})
- assert.NoError(t, err)
+ require.NoError(t, err)
// Verify both pieces of data exist
- data2, err := provider.GetData(newCtx)
+ data, err := provider.GetData(newCtx)
assert.NoError(t, err)
- assert.Contains(t, data2, constants.InputData, "Should contain input_data key")
+ assert.Contains(t, data, constants.InputData, "Should contain input_data key")
- inputData2, ok := data2[constants.InputData].(map[string]any)
+ inputData, ok := data[constants.InputData].(map[string]any)
assert.True(t, ok, "input_data should be a map")
- assert.Equal(t, "value", inputData2["existing"], "Should preserve existing value")
- assert.Equal(t, "value", inputData2["new"], "Should add new value")
+ assert.Equal(t, "value", inputData["existing"], "Should preserve existing value")
+ assert.Equal(t, "value", inputData["new"], "Should add new value")
})
}
diff --git a/execution/data/data_helpers_test.go b/execution/data/data_helpers_test.go
new file mode 100644
index 0000000..da1bf08
--- /dev/null
+++ b/execution/data/data_helpers_test.go
@@ -0,0 +1,102 @@
+package data
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+// Standard test data sets used across all provider tests
+var (
+ // Simple data for testing basic functionality
+ simpleData = map[string]any{
+ "string": "value",
+ "int": 42,
+ "bool": true,
+ }
+
+ // Complex data for testing nested structures
+ complexData = map[string]any{
+ "string": "value",
+ "int": 42,
+ "bool": true,
+ "nested": map[string]any{
+ "key": "nested value",
+ "inner": map[string]any{"deep": "very deep"},
+ },
+ "array": []string{"one", "two", "three"},
+ }
+)
+
+// createTestRequestHelper creates a standard HTTP request for testing
+func createTestRequestHelper() *http.Request {
+ return &http.Request{
+ Method: "GET",
+ URL: &url.URL{Path: "/test", RawQuery: "param=value"},
+ Header: http.Header{"Content-Type": []string{"application/json"}},
+ }
+}
+
+// MockProvider is a testify mock implementation of Provider
+type MockProvider struct {
+ mock.Mock
+}
+
+func (m *MockProvider) GetData(ctx context.Context) (map[string]any, error) {
+ args := m.Called(ctx)
+ data, _ := args.Get(0).(map[string]any)
+ return data, args.Error(1)
+}
+
+func (m *MockProvider) AddDataToContext(ctx context.Context, data ...any) (context.Context, error) {
+ args := m.Called(append([]any{ctx}, data...))
+ newCtx, _ := args.Get(0).(context.Context)
+ return newCtx, args.Error(1)
+}
+
+// newMockErrorProvider creates a mock provider that returns errors
+func newMockErrorProvider() *MockProvider {
+ provider := new(MockProvider)
+ provider.On("GetData", mock.Anything).Return(nil, assert.AnError)
+ provider.On("AddDataToContext", mock.Anything, mock.Anything).
+ Return(mock.Anything, assert.AnError)
+ return provider
+}
+
+// assertMapContainsExpectedHelper recursively asserts that a map contains all expected key/value pairs
+func assertMapContainsExpectedHelper(t *testing.T, expected, actual map[string]any) {
+ t.Helper()
+ for key, expectedValue := range expected {
+ assert.Contains(t, actual, key, "Result should contain key: %s", key)
+
+ // Handle nested maps recursively
+ expectedMap, expectedIsMap := expectedValue.(map[string]any)
+ actualValue, exists := actual[key]
+ require.True(t, exists, "Key should exist: %s", key)
+
+ actualMap, actualIsMap := actualValue.(map[string]any)
+
+ if expectedIsMap && actualIsMap {
+ assertMapContainsExpectedHelper(t, expectedMap, actualMap)
+ } else {
+ assert.Equal(t, expectedValue, actualValue, "Value should match for key: %s", key)
+ }
+ }
+}
+
+// getDataCheckHelper checks if multiple calls to GetData return consistent results
+func getDataCheckHelper(t *testing.T, provider Provider, ctx context.Context) {
+ t.Helper()
+ result1, err1 := provider.GetData(ctx)
+ require.NoError(t, err1)
+
+ result2, err2 := provider.GetData(ctx)
+ require.NoError(t, err2)
+
+ assert.Equal(t, result1, result2, "Multiple GetData calls should return consistent results")
+}
diff --git a/execution/data/prepareContext_test.go b/execution/data/prepareContext_test.go
index e019d79..2493538 100644
--- a/execution/data/prepareContext_test.go
+++ b/execution/data/prepareContext_test.go
@@ -18,8 +18,6 @@ func TestPrepareContextHelper(t *testing.T) {
logger := slog.Default()
t.Run("nil provider returns error", func(t *testing.T) {
- t.Parallel()
-
baseCtx := context.Background()
enrichedCtx, err := PrepareContextHelper(
baseCtx,
@@ -33,8 +31,6 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("static provider always returns error", func(t *testing.T) {
- t.Parallel()
-
provider := NewStaticProvider(simpleData)
baseCtx := context.Background()
@@ -51,8 +47,6 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("context provider with valid data", func(t *testing.T) {
- t.Parallel()
-
provider := NewContextProvider(constants.EvalData)
baseCtx := context.Background()
@@ -81,11 +75,9 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("context provider with HTTP request", func(t *testing.T) {
- t.Parallel()
-
provider := NewContextProvider(constants.EvalData)
baseCtx := context.Background()
- req := createTestRequest()
+ req := createTestRequestHelper()
enrichedCtx, err := PrepareContextHelper(baseCtx, logger, provider, req)
@@ -108,11 +100,9 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("context provider with mixed data", func(t *testing.T) {
- t.Parallel()
-
provider := NewContextProvider(constants.EvalData)
baseCtx := context.Background()
- req := createTestRequest()
+ req := createTestRequestHelper()
enrichedCtx, err := PrepareContextHelper(baseCtx, logger, provider,
map[string]any{"key": "value"}, req)
@@ -142,8 +132,6 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("context provider with unsupported data", func(t *testing.T) {
- t.Parallel()
-
provider := NewContextProvider(constants.EvalData)
baseCtx := context.Background()
@@ -163,8 +151,6 @@ func TestPrepareContextHelper(t *testing.T) {
})
t.Run("composite provider with mixed success", func(t *testing.T) {
- t.Parallel()
-
provider := NewCompositeProvider(
NewStaticProvider(simpleData),
NewContextProvider(constants.EvalData),
@@ -197,8 +183,6 @@ func TestPrepareContextWithErrorHandling(t *testing.T) {
logger := slog.Default()
t.Run("provider returns error and keeps original context", func(t *testing.T) {
- t.Parallel()
-
// Create a context provider
provider := NewContextProvider(constants.EvalData)
baseCtx := context.Background()
diff --git a/execution/data/provider_test.go b/execution/data/provider_test.go
index 6b2b395..4ae50de 100644
--- a/execution/data/provider_test.go
+++ b/execution/data/provider_test.go
@@ -3,72 +3,12 @@ package data
import (
"context"
"errors"
- "net/http"
- "net/url"
"testing"
"github.com/robbyt/go-polyscript/execution/constants"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
)
-// Standard test data sets used across all provider tests
-var (
- // Simple data for testing basic functionality
- simpleData = map[string]any{
- "string": "value",
- "int": 42,
- "bool": true,
- }
-
- // Complex data for testing nested structures
- complexData = map[string]any{
- "string": "value",
- "int": 42,
- "bool": true,
- "nested": map[string]any{
- "key": "nested value",
- "inner": map[string]any{"deep": "very deep"},
- },
- "array": []string{"one", "two", "three"},
- }
-)
-
-// createTestRequest creates a standard HTTP request for testing
-func createTestRequest() *http.Request {
- return &http.Request{
- Method: "GET",
- URL: &url.URL{Path: "/test", RawQuery: "param=value"},
- Header: http.Header{"Content-Type": []string{"application/json"}},
- }
-}
-
-// MockProvider is a testify mock implementation of Provider
-type MockProvider struct {
- mock.Mock
-}
-
-func (m *MockProvider) GetData(ctx context.Context) (map[string]any, error) {
- args := m.Called(ctx)
- data, _ := args.Get(0).(map[string]any)
- return data, args.Error(1)
-}
-
-func (m *MockProvider) AddDataToContext(ctx context.Context, data ...any) (context.Context, error) {
- args := m.Called(append([]any{ctx}, data...))
- newCtx, _ := args.Get(0).(context.Context)
- return newCtx, args.Error(1)
-}
-
-// newMockErrorProvider creates a mock provider that returns errors
-func newMockErrorProvider() *MockProvider {
- provider := new(MockProvider)
- provider.On("GetData", mock.Anything).Return(nil, assert.AnError)
- provider.On("AddDataToContext", mock.Anything, mock.Anything).
- Return(mock.Anything, assert.AnError)
- return provider
-}
-
// TestProvider_Interface ensures that all provider implementations comply with the Provider interface
func TestProvider_Interface(t *testing.T) {
t.Parallel()
@@ -96,7 +36,6 @@ func TestProvider_GetData(t *testing.T) {
// Test static provider
t.Run("static provider with simple data", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
@@ -111,7 +50,6 @@ func TestProvider_GetData(t *testing.T) {
})
t.Run("static provider with empty data", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(nil)
ctx := context.Background()
@@ -122,7 +60,6 @@ func TestProvider_GetData(t *testing.T) {
// Test context provider
t.Run("context provider with valid data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, simpleData)
@@ -137,7 +74,6 @@ func TestProvider_GetData(t *testing.T) {
})
t.Run("context provider with empty key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("")
ctx := context.Background()
@@ -147,7 +83,6 @@ func TestProvider_GetData(t *testing.T) {
})
t.Run("context provider with invalid value type", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.WithValue(context.Background(), constants.EvalData, "not a map")
@@ -158,7 +93,6 @@ func TestProvider_GetData(t *testing.T) {
// Test composite provider
t.Run("composite provider with multiple sources", func(t *testing.T) {
- t.Parallel()
provider := NewCompositeProvider(
NewStaticProvider(map[string]any{"static": "value", "shared": "static"}),
NewContextProvider(constants.EvalData),
@@ -190,7 +124,6 @@ func TestProvider_GetData(t *testing.T) {
})
t.Run("empty composite provider", func(t *testing.T) {
- t.Parallel()
provider := NewCompositeProvider()
ctx := context.Background()
@@ -200,7 +133,6 @@ func TestProvider_GetData(t *testing.T) {
})
t.Run("composite provider with error", func(t *testing.T) {
- t.Parallel()
provider := NewCompositeProvider(
NewStaticProvider(simpleData),
newMockErrorProvider(),
@@ -219,7 +151,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
// Test with static provider
t.Run("static provider should reject all data", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
@@ -237,7 +168,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
// Test with context provider
t.Run("context provider with valid map data", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
@@ -257,10 +187,9 @@ func TestProvider_AddDataToContext(t *testing.T) {
})
t.Run("context provider with HTTP request", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
- req := createTestRequest()
+ req := createTestRequestHelper()
newCtx, err := provider.AddDataToContext(ctx, req)
@@ -278,7 +207,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
})
t.Run("context provider with empty key", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider("")
ctx := context.Background()
@@ -290,7 +218,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
// Test with composite provider
t.Run("composite provider with mixed providers", func(t *testing.T) {
- t.Parallel()
provider := NewCompositeProvider(
NewStaticProvider(simpleData),
NewContextProvider(constants.EvalData),
@@ -320,7 +247,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
})
t.Run("composite provider with all failures", func(t *testing.T) {
- t.Parallel()
provider := NewCompositeProvider(
NewStaticProvider(simpleData),
newMockErrorProvider(),
@@ -335,7 +261,6 @@ func TestProvider_AddDataToContext(t *testing.T) {
// Test with multiple data items
t.Run("context provider with multiple data items", func(t *testing.T) {
- t.Parallel()
provider := NewContextProvider(constants.EvalData)
ctx := context.Background()
@@ -362,7 +287,6 @@ func TestProvider_DeepMerge(t *testing.T) {
t.Parallel()
t.Run("simple merge with no overlaps", func(t *testing.T) {
- t.Parallel()
src := map[string]any{"src_key": "src_value"}
dst := map[string]any{"dst_key": "dst_value"}
expected := map[string]any{
@@ -379,7 +303,6 @@ func TestProvider_DeepMerge(t *testing.T) {
})
t.Run("overlapping keys (dst wins)", func(t *testing.T) {
- t.Parallel()
src := map[string]any{
"shared_key": "src_value",
"src_key": "src_value",
@@ -399,7 +322,6 @@ func TestProvider_DeepMerge(t *testing.T) {
})
t.Run("nested maps are merged properly", func(t *testing.T) {
- t.Parallel()
src := map[string]any{
"nested": map[string]any{
"key1": "src_value1",
@@ -425,7 +347,6 @@ func TestProvider_DeepMerge(t *testing.T) {
})
t.Run("arrays are replaced not merged", func(t *testing.T) {
- t.Parallel()
src := map[string]any{"array": []string{"one", "two", "three"}}
dst := map[string]any{"array": []string{"four", "five"}}
expected := map[string]any{"array": []string{"four", "five"}}
@@ -435,7 +356,6 @@ func TestProvider_DeepMerge(t *testing.T) {
})
t.Run("empty maps", func(t *testing.T) {
- t.Parallel()
result1 := deepMerge(map[string]any{}, map[string]any{"key": "value"})
assert.Equal(t, map[string]any{"key": "value"}, result1)
@@ -444,7 +364,6 @@ func TestProvider_DeepMerge(t *testing.T) {
})
t.Run("original maps should not be modified", func(t *testing.T) {
- t.Parallel()
src := map[string]any{
"key": "value",
"nested": map[string]any{
diff --git a/execution/data/staticProvider_test.go b/execution/data/staticProvider_test.go
index 7f67aeb..2e964cf 100644
--- a/execution/data/staticProvider_test.go
+++ b/execution/data/staticProvider_test.go
@@ -6,116 +6,124 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
// TestStaticProvider_Creation tests the creation of StaticProvider instances
func TestStaticProvider_Creation(t *testing.T) {
t.Parallel()
- t.Run("nil data creates empty map", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(nil)
-
- ctx := context.Background()
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Empty(t, result, "Result map should be empty")
- })
-
- t.Run("empty data creates empty map", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(map[string]any{})
-
- ctx := context.Background()
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Empty(t, result, "Result map should be empty")
- })
-
- t.Run("populated data is stored", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(simpleData)
-
- ctx := context.Background()
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Equal(t, simpleData, result, "Result should match input data")
- })
-
- t.Run("complex data is stored", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(complexData)
-
- ctx := context.Background()
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Equal(t, complexData, result, "Result should match input data")
- })
+ tests := []struct {
+ name string
+ inputData map[string]any
+ expectEmpty bool
+ }{
+ {
+ name: "nil data creates empty map",
+ inputData: nil,
+ expectEmpty: true,
+ },
+ {
+ name: "empty data creates empty map",
+ inputData: map[string]any{},
+ expectEmpty: true,
+ },
+ {
+ name: "populated data is stored",
+ inputData: simpleData,
+ expectEmpty: false,
+ },
+ {
+ name: "complex data is stored",
+ inputData: complexData,
+ expectEmpty: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ provider := NewStaticProvider(tt.inputData)
+ require.NotNil(t, provider, "Provider should never be nil")
+
+ ctx := context.Background()
+ result, err := provider.GetData(ctx)
+
+ assert.NoError(t, err, "GetData should never return an error")
+
+ if tt.expectEmpty {
+ assert.Empty(t, result, "Result map should be empty")
+ } else {
+ assert.Equal(t, tt.inputData, result, "Result should match input data")
+ }
+ })
+ }
}
// TestStaticProvider_GetData tests the data retrieval functionality of StaticProvider
func TestStaticProvider_GetData(t *testing.T) {
t.Parallel()
- t.Run("empty provider returns empty map", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(map[string]any{})
- ctx := context.Background()
-
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Empty(t, result, "Result map should be empty")
- })
-
- t.Run("simple data", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(simpleData)
- ctx := context.Background()
-
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Equal(t, simpleData, result, "Result should match input data")
-
- // Test that we get a copy, not the original map
- result["newTestKey"] = "newTestValue"
-
- newResult, err := provider.GetData(ctx)
- assert.NoError(t, err, "GetData should never return an error")
- assert.NotContains(
- t,
- newResult,
- "newTestKey",
- "Modifications to result should not affect provider",
- )
- })
-
- t.Run("complex nested data", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(complexData)
- ctx := context.Background()
-
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Equal(t, complexData, result, "Result should match input data")
- })
-
- t.Run("nil provider data", func(t *testing.T) {
- t.Parallel()
- provider := NewStaticProvider(nil)
- ctx := context.Background()
-
- result, err := provider.GetData(ctx)
-
- assert.NoError(t, err, "GetData should never return an error")
- assert.Empty(t, result, "Result map should be empty")
- })
+ tests := []struct {
+ name string
+ inputData map[string]any
+ modifyResult bool // Flag to check if modifying result affects provider's data
+ }{
+ {
+ name: "empty provider returns empty map",
+ inputData: map[string]any{},
+ modifyResult: false,
+ },
+ {
+ name: "simple data",
+ inputData: simpleData,
+ modifyResult: true,
+ },
+ {
+ name: "complex nested data",
+ inputData: complexData,
+ modifyResult: true,
+ },
+ {
+ name: "nil provider data",
+ inputData: nil,
+ modifyResult: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ provider := NewStaticProvider(tt.inputData)
+ ctx := context.Background()
+
+ result, err := provider.GetData(ctx)
+
+ assert.NoError(t, err, "GetData should never return an error")
+
+ if tt.inputData == nil {
+ assert.Empty(t, result, "Result map should be empty for nil input")
+ } else {
+ assert.Equal(t, tt.inputData, result, "Result should match input data")
+ }
+
+ // Verify data consistency and immutability
+ if tt.modifyResult {
+ // Test that we get a copy, not the original map
+ result["newTestKey"] = "newTestValue"
+
+ newResult, err := provider.GetData(ctx)
+ assert.NoError(t, err, "GetData should never return an error")
+ assert.NotContains(
+ t,
+ newResult,
+ "newTestKey",
+ "Modifications to result should not affect provider",
+ )
+ }
+
+ // Verify data consistency
+ getDataCheckHelper(t, provider, ctx)
+ })
+ }
}
// TestStaticProvider_AddDataToContext tests that StaticProvider properly rejects all context updates
@@ -123,16 +131,15 @@ func TestStaticProvider_AddDataToContext(t *testing.T) {
t.Parallel()
t.Run("nil context arg returns error", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
newCtx, err := provider.AddDataToContext(ctx, nil)
assert.Error(t, err, "StaticProvider should reject all attempts to add data")
+ assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
"Error should be ErrStaticProviderNoRuntimeUpdates")
- assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
// Verify data is still available
data, getErr := provider.GetData(ctx)
@@ -141,44 +148,39 @@ func TestStaticProvider_AddDataToContext(t *testing.T) {
})
t.Run("map context arg returns error", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
newCtx, err := provider.AddDataToContext(ctx, map[string]any{"new": "data"})
assert.Error(t, err, "StaticProvider should reject all attempts to add data")
+ assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
"Error should be ErrStaticProviderNoRuntimeUpdates")
- assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
t.Run("HTTP request context arg returns error", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
- req := createTestRequest()
- newCtx, err := provider.AddDataToContext(ctx, req)
+ newCtx, err := provider.AddDataToContext(ctx, createTestRequestHelper())
assert.Error(t, err, "StaticProvider should reject all attempts to add data")
+ assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
"Error should be ErrStaticProviderNoRuntimeUpdates")
- assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
t.Run("multiple args returns error", func(t *testing.T) {
- t.Parallel()
provider := NewStaticProvider(simpleData)
ctx := context.Background()
- newCtx, err := provider.AddDataToContext(ctx,
- map[string]any{"key": "value"}, "string", 42)
+ newCtx, err := provider.AddDataToContext(ctx, map[string]any{"key": "value"}, "string", 42)
assert.Error(t, err, "StaticProvider should reject all attempts to add data")
+ assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
assert.True(t, errors.Is(err, ErrStaticProviderNoRuntimeUpdates),
"Error should be ErrStaticProviderNoRuntimeUpdates")
- assert.Equal(t, ctx, newCtx, "Context should remain unchanged")
})
}
diff --git a/execution/script/compiler_test.go b/execution/script/compiler_test.go
index 590f6cf..273b284 100644
--- a/execution/script/compiler_test.go
+++ b/execution/script/compiler_test.go
@@ -72,10 +72,7 @@ func TestCompiler(t *testing.T) {
}
for _, tt := range tests {
- tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
// Create mock compiler and reader
mockCompiler := new(MockCompiler)
reader := newMockScriptReaderCloser(tt.content)
diff --git a/execution/script/loader/fromDisk_test.go b/execution/script/loader/fromDisk_test.go
index 94b5f62..4ecc1c1 100644
--- a/execution/script/loader/fromDisk_test.go
+++ b/execution/script/loader/fromDisk_test.go
@@ -1,7 +1,6 @@
package loader
import (
- "io"
"os"
"path/filepath"
"runtime"
@@ -18,7 +17,7 @@ func TestNewFromDisk(t *testing.T) {
tempDir := t.TempDir()
absPath := filepath.Join(tempDir, "test.js")
- cases := []struct {
+ tests := []struct {
name string
path string
wantPath string
@@ -35,33 +34,38 @@ func TestNewFromDisk(t *testing.T) {
},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromDisk(tc.path)
require.NoError(t, err)
require.NotNil(t, loader)
require.Equal(t, tc.wantPath, loader.path)
require.Equal(t, "file", loader.sourceURL.Scheme)
+
+ // Use helper for further validation
+ verifyLoader(t, loader, tc.wantPath)
})
}
})
t.Run("invalid schemes", func(t *testing.T) {
- cases := []struct {
+ tests := []struct {
name string
path string
}{
{
name: "http scheme",
- path: "http://example.com/script.js",
+ path: "http://localhost:8080/script.js",
},
{
name: "https scheme",
- path: "https://example.com/script.js",
+ path: "https://localhost:8080/script.js",
},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromDisk(tc.path)
require.Error(t, err)
@@ -72,7 +76,7 @@ func TestNewFromDisk(t *testing.T) {
})
t.Run("relative paths", func(t *testing.T) {
- cases := []struct {
+ tests := []struct {
name string
path string
}{
@@ -81,7 +85,8 @@ func TestNewFromDisk(t *testing.T) {
{name: "parent dir", path: "../test.js"},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromDisk(tc.path)
require.Error(t, err)
@@ -92,7 +97,7 @@ func TestNewFromDisk(t *testing.T) {
})
t.Run("empty or invalid paths", func(t *testing.T) {
- cases := []struct {
+ tests := []struct {
name string
path string
}{
@@ -103,7 +108,8 @@ func TestNewFromDisk(t *testing.T) {
{name: "parent dir", path: "../"},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
if tc.path == "\\" && runtime.GOOS != "windows" {
t.Skip("Skipping Windows-specific test on non-Windows platform")
@@ -153,23 +159,13 @@ func TestFromDisk_GetReader(t *testing.T) {
reader, err := loader.GetReader()
require.NoError(t, err, "Failed to get reader")
- // Ensure reader is closed after test
- t.Cleanup(func() {
- if reader != nil {
- require.NoError(t, reader.Close(), "Failed to close reader")
- }
- })
-
- // Read content
- content, err := io.ReadAll(reader)
- require.NoError(t, err, "Failed to read content")
- require.Equal(t, testContent, string(content), "Content mismatch")
+ verifyReaderContent(t, reader, testContent)
})
t.Run("multiple reads from same loader", func(t *testing.T) {
// Setup test file
tempDir := t.TempDir()
- testContent := "function calculate() { return 42; }"
+ testContent := FunctionContent
testFile := filepath.Join(tempDir, "test.js")
err := os.WriteFile(testFile, []byte(testContent), 0o644)
@@ -179,25 +175,7 @@ func TestFromDisk_GetReader(t *testing.T) {
loader, err := NewFromDisk(testFile)
require.NoError(t, err, "Failed to create loader")
- // First read
- reader1, err := loader.GetReader()
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reader1.Close(), "Failed to close first reader")
- })
- got1, err := io.ReadAll(reader1)
- require.NoError(t, err)
- require.Equal(t, testContent, string(got1))
-
- // Second read should return a new reader with the same content
- reader2, err := loader.GetReader()
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reader2.Close(), "Failed to close second reader")
- })
- got2, err := io.ReadAll(reader2)
- require.NoError(t, err)
- require.Equal(t, testContent, string(got2))
+ verifyMultipleReads(t, loader, testContent)
})
t.Run("file not found", func(t *testing.T) {
diff --git a/execution/script/loader/fromHTTP.go b/execution/script/loader/fromHTTP.go
index 0f02cf2..3924af0 100644
--- a/execution/script/loader/fromHTTP.go
+++ b/execution/script/loader/fromHTTP.go
@@ -123,7 +123,7 @@ type FromHTTP struct {
//
// Example:
//
-// loader, err := loader.NewFromHTTP("https://example.com/script.js")
+// loader, err := loader.NewFromHTTP("https://localhost:8080/script.js")
// if err != nil {
// return err
// }
@@ -140,15 +140,15 @@ func NewFromHTTP(rawURL string) (*FromHTTP, error) {
//
// // With basic auth
// options := loader.DefaultHTTPOptions().WithBasicAuth("user", "pass")
-// loader, err := loader.NewFromHTTPWithOptions("https://example.com/script.js", options)
+// loader, err := loader.NewFromHTTPWithOptions("https://localhost:8080/script.js", options)
//
// // With bearer token
// options := loader.DefaultHTTPOptions().WithBearerAuth("token123")
-// loader, err := loader.NewFromHTTPWithOptions("https://example.com/script.js", options)
+// loader, err := loader.NewFromHTTPWithOptions("https://localhost:8080/script.js", options)
//
// // With custom timeout
// options := loader.DefaultHTTPOptions().WithTimeout(10 * time.Second)
-// loader, err := loader.NewFromHTTPWithOptions("https://example.com/script.js", options)
+// loader, err := loader.NewFromHTTPWithOptions("https://localhost:8080/script.js", options)
func NewFromHTTPWithOptions(rawURL string, options *HTTPOptions) (*FromHTTP, error) {
sourceURL, err := url.Parse(rawURL)
if err != nil {
diff --git a/execution/script/loader/fromHTTP_test.go b/execution/script/loader/fromHTTP_test.go
index c4f3a70..0f46570 100644
--- a/execution/script/loader/fromHTTP_test.go
+++ b/execution/script/loader/fromHTTP_test.go
@@ -1,13 +1,11 @@
package loader
import (
- "bytes"
"context"
"crypto/tls"
"errors"
- "io"
"net/http"
- "net/url"
+ "net/http/httptest"
"testing"
"time"
@@ -15,100 +13,86 @@ import (
"github.com/stretchr/testify/require"
)
-// mockHTTPClient implements the httpRequester interface for testing
-type mockHTTPClient struct {
- doFunc func(req *http.Request) (*http.Response, error)
-}
+func TestNewFromHTTP(t *testing.T) {
+ t.Parallel()
-func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
- if m.doFunc != nil {
- return m.doFunc(req)
- }
- return nil, errors.New("doFunc not implemented")
-}
+ t.Run("Valid HTTPS URL", func(t *testing.T) {
+ // Set up TLS server
+ tlsServer := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer tlsServer.Close()
-// mockResponseBody implements io.ReadCloser for testing
-type mockResponseBody struct {
- io.Reader
- closeFunc func() error
- closed bool
-}
+ testURL := tlsServer.URL + "/script.js"
-func (m *mockResponseBody) Close() error {
- if m.closed {
- return nil
- }
- m.closed = true
+ // Set InsecureSkipVerify to make test work with self-signed cert
+ options := DefaultHTTPOptions()
+ options.InsecureSkipVerify = true
+ loader, err := NewFromHTTPWithOptions(testURL, options)
+ require.NoError(t, err)
+ require.NotNil(t, loader)
- if m.closeFunc != nil {
- return m.closeFunc()
- }
- return nil
-}
+ // Verify loader properties
+ require.Equal(t, testURL, loader.url)
+ require.NotNil(t, loader.sourceURL)
+ require.Equal(t, testURL, loader.sourceURL.String())
+ require.NotNil(t, loader.client)
+ require.NotNil(t, loader.options)
-// newMockResponse creates a new mock HTTP response
-func newMockResponse(statusCode int, body string) *http.Response {
- return &http.Response{
- StatusCode: statusCode,
- Body: &mockResponseBody{Reader: bytes.NewBufferString(body)},
- Status: http.StatusText(statusCode),
- Header: make(http.Header),
- }
-}
+ // Use TLS server's client to accept its certificate
+ loader.client = tlsServer.Client()
-func TestNewFromHTTP(t *testing.T) {
- t.Parallel()
+ // Additional verification
+ verifyLoader(t, loader, testURL)
+ })
- tests := []struct {
- name string
- url string
- expectError bool
- errorContains string
- }{
- {
- name: "Valid HTTPS URL",
- url: "https://example.com/script.js",
- expectError: false,
- },
- {
- name: "Valid HTTP URL",
- url: "http://example.com/script.js",
- expectError: false,
- },
- {
- name: "Invalid URL scheme",
- url: "file:///path/to/script.js",
- expectError: true,
- errorContains: "unsupported scheme",
- },
- {
- name: "Invalid URL format",
- url: "://invalid-url",
- expectError: true,
- errorContains: "unable to parse URL",
- },
- }
+ t.Run("Valid HTTP URL", func(t *testing.T) {
+ // Set up HTTP server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- loader, err := NewFromHTTP(tt.url)
- if tt.expectError {
- require.Error(t, err)
- if tt.errorContains != "" {
- require.Contains(t, err.Error(), tt.errorContains)
- }
- return
- }
+ testURL := server.URL + "/script.js"
- require.NoError(t, err)
- require.NotNil(t, loader)
- require.Equal(t, tt.url, loader.url)
- require.NotNil(t, loader.sourceURL)
- require.Equal(t, tt.url, loader.sourceURL.String())
- require.NotNil(t, loader.client)
- require.NotNil(t, loader.options)
- })
- }
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
+ require.NotNil(t, loader)
+
+ // Verify loader properties
+ require.Equal(t, testURL, loader.url)
+ require.NotNil(t, loader.sourceURL)
+ require.Equal(t, testURL, loader.sourceURL.String())
+ require.NotNil(t, loader.client)
+ require.NotNil(t, loader.options)
+
+ // Additional verification
+ verifyLoader(t, loader, testURL)
+ })
+
+ t.Run("Invalid URL scheme", func(t *testing.T) {
+ testURL := "file:///path/to/script.js"
+
+ loader, err := NewFromHTTP(testURL)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unsupported scheme")
+ require.Nil(t, loader)
+ })
+
+ t.Run("Invalid URL format", func(t *testing.T) {
+ testURL := "://invalid-url"
+
+ loader, err := NewFromHTTP(testURL)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unable to parse URL")
+ require.Nil(t, loader)
+ })
}
func TestNewFromHTTPWithOptions(t *testing.T) {
@@ -116,7 +100,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
tests := []struct {
name string
- url string
optionsModifier func(options *HTTPOptions) *HTTPOptions
validateOption func(t *testing.T, loader *FromHTTP)
expectError bool
@@ -124,7 +107,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
}{
{
name: "Custom timeout",
- url: "https://example.com/script.js",
optionsModifier: func(options *HTTPOptions) *HTTPOptions {
return options.WithTimeout(60 * time.Second)
},
@@ -135,7 +117,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
},
{
name: "Basic auth",
- url: "https://example.com/script.js",
optionsModifier: func(options *HTTPOptions) *HTTPOptions {
return options.WithBasicAuth("user", "pass")
},
@@ -149,7 +130,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
},
{
name: "Bearer auth",
- url: "https://example.com/script.js",
optionsModifier: func(options *HTTPOptions) *HTTPOptions {
return options.WithBearerAuth("token123")
},
@@ -162,7 +142,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
},
{
name: "Custom headers",
- url: "https://example.com/script.js",
optionsModifier: func(options *HTTPOptions) *HTTPOptions {
options.Headers["X-Custom"] = "TestValue"
options.Headers["User-Agent"] = "Test-Agent"
@@ -176,34 +155,50 @@ func TestNewFromHTTPWithOptions(t *testing.T) {
},
}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
+ t.Run(tc.name, func(t *testing.T) {
+ // Create test server for this test case
+ server := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
// Start with default options and apply modifier if provided
options := DefaultHTTPOptions()
- if tt.optionsModifier != nil {
- options = tt.optionsModifier(options)
+ if tc.optionsModifier != nil {
+ options = tc.optionsModifier(options)
}
- loader, err := NewFromHTTPWithOptions(tt.url, options)
- if tt.expectError {
+ loader, err := NewFromHTTPWithOptions(testURL, options)
+ if tc.expectError {
require.Error(t, err)
- if tt.errorContains != "" {
- require.Contains(t, err.Error(), tt.errorContains)
+ if tc.errorContains != "" {
+ require.Contains(t, err.Error(), tc.errorContains)
}
return
}
require.NoError(t, err)
require.NotNil(t, loader)
- require.Equal(t, tt.url, loader.url)
+ require.Equal(t, testURL, loader.url)
require.NotNil(t, loader.sourceURL)
- require.Equal(t, tt.url, loader.sourceURL.String())
+ require.Equal(t, testURL, loader.sourceURL.String())
require.NotNil(t, loader.client)
require.NotNil(t, loader.options)
- if tt.validateOption != nil {
- tt.validateOption(t, loader)
+ if tc.validateOption != nil {
+ tc.validateOption(t, loader)
}
+
+ // Use helper for further validation
+ verifyLoader(t, loader, testURL)
})
}
}
@@ -212,10 +207,22 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
t.Parallel()
t.Run("with insecure skip verify", func(t *testing.T) {
+ // Create test server for this test
+ server := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
options := DefaultHTTPOptions()
options.InsecureSkipVerify = true
- loader, err := NewFromHTTPWithOptions("https://example.com/script.js", options)
+ loader, err := NewFromHTTPWithOptions(testURL, options)
require.NoError(t, err)
require.NotNil(t, loader)
@@ -228,15 +235,29 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
})
t.Run("with custom TLS config", func(t *testing.T) {
+ // Create test server for this test
+ server := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
options := DefaultHTTPOptions()
customTLS := &tls.Config{
MinVersion: tls.VersionTLS12,
// Add custom ciphers
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384},
+ // Use InsecureSkipVerify for the test server
+ InsecureSkipVerify: true,
}
options.TLSConfig = customTLS
- loader, err := NewFromHTTPWithOptions("https://example.com/script.js", options)
+ loader, err := NewFromHTTPWithOptions(testURL, options)
require.NoError(t, err)
require.NotNil(t, loader)
@@ -250,6 +271,18 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
})
t.Run("TLSConfig takes precedence over InsecureSkipVerify", func(t *testing.T) {
+ // Create test server for this test
+ server := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
options := DefaultHTTPOptions()
options.InsecureSkipVerify = true
customTLS := &tls.Config{
@@ -258,7 +291,7 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
}
options.TLSConfig = customTLS
- loader, err := NewFromHTTPWithOptions("https://example.com/script.js", options)
+ loader, err := NewFromHTTPWithOptions(testURL, options)
require.NoError(t, err)
require.NotNil(t, loader)
@@ -271,12 +304,27 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
require.False(t, transport.TLSClientConfig.InsecureSkipVerify,
"TLSConfig should override InsecureSkipVerify")
require.Equal(t, uint16(tls.VersionTLS13), transport.TLSClientConfig.MinVersion)
+
+ // For testing purposes, replace the client with one that accepts the test server's certificate
+ loader.client = server.Client()
})
t.Run("no TLS modifications when neither option is set", func(t *testing.T) {
+ // Create test server for this test
+ server := httptest.NewTLSServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }),
+ )
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
options := DefaultHTTPOptions()
- loader, err := NewFromHTTPWithOptions("https://example.com/script.js", options)
+ loader, err := NewFromHTTPWithOptions(testURL, options)
require.NoError(t, err)
require.NotNil(t, loader)
@@ -287,265 +335,210 @@ func TestFromHTTP_TLSConfig(t *testing.T) {
// The http.Client only initializes Transport when needed, so it might be nil at this point
// We should check that it's nil when neither TLS option is set
require.Nil(t, client.Transport, "Expected Transport to be nil when no TLS options are set")
+
+ // For testing purposes, replace the client with one that accepts the test server's certificate
+ loader.client = server.Client()
})
}
func TestFromHTTP_GetReader(t *testing.T) {
t.Parallel()
- const testScript = `function test() { return "Hello, World!"; }`
+ const testScript = FunctionContent
- tests := []struct {
- name string
- url string
- optionsModifier func(options *HTTPOptions) *HTTPOptions
- customResp func() *http.Response
- mockError error
- requestValidator func(t *testing.T, req *http.Request)
- expectError bool
- errorContains string
- validateBody bool
- }{
- {
- name: "Success - Default",
- url: "https://example.com/script.js",
- customResp: func() *http.Response {
- return newMockResponse(http.StatusOK, testScript)
- },
- requestValidator: func(t *testing.T, req *http.Request) {
- t.Helper()
- require.Equal(t, "https://example.com/script.js", req.URL.String())
- require.Equal(t, http.MethodGet, req.Method)
- require.Equal(t, "go-polyscript/http-loader", req.Header.Get("User-Agent"))
- },
- validateBody: true,
- },
- {
- name: "Success - Basic Auth",
- url: "https://example.com/auth",
- optionsModifier: func(options *HTTPOptions) *HTTPOptions {
- return options.WithBasicAuth("user", "pass").WithTimeout(5 * time.Second)
- },
- customResp: func() *http.Response {
- return newMockResponse(http.StatusOK, testScript)
- },
- requestValidator: func(t *testing.T, req *http.Request) {
- t.Helper()
- require.Equal(t, "https://example.com/auth", req.URL.String())
- username, password, ok := req.BasicAuth()
- require.True(t, ok, "Expected Basic Auth to be set")
- require.Equal(t, "user", username)
- require.Equal(t, "pass", password)
- },
- validateBody: true,
- },
- {
- name: "Success - Bearer Auth",
- url: "https://example.com/header-auth",
- optionsModifier: func(options *HTTPOptions) *HTTPOptions {
- return options.WithBearerAuth("test-token").WithTimeout(5 * time.Second)
- },
- customResp: func() *http.Response {
- return newMockResponse(http.StatusOK, testScript)
- },
- requestValidator: func(t *testing.T, req *http.Request) {
- t.Helper()
- require.Equal(t, "https://example.com/header-auth", req.URL.String())
- require.Equal(t, "Bearer test-token", req.Header.Get("Authorization"))
- },
- validateBody: true,
- },
- {
- name: "Success - Custom Headers",
- url: "https://example.com/header-auth",
- optionsModifier: func(options *HTTPOptions) *HTTPOptions {
- options.Headers["User-Agent"] = "Custom-Agent"
- options.Headers["X-Custom"] = "value"
- return options
- },
- customResp: func() *http.Response {
- return newMockResponse(http.StatusOK, testScript)
- },
- requestValidator: func(t *testing.T, req *http.Request) {
- t.Helper()
- require.Equal(t, "Custom-Agent", req.Header.Get("User-Agent"))
- require.Equal(t, "value", req.Header.Get("X-Custom"))
- },
- validateBody: true,
- },
- {
- name: "Failure - Unauthorized",
- url: "https://example.com/auth",
- customResp: func() *http.Response {
- return newMockResponse(http.StatusUnauthorized, "Unauthorized")
- },
- expectError: true,
- errorContains: "HTTP 401",
- },
- {
- name: "Failure - Not Found",
- url: "https://example.com/error",
- customResp: func() *http.Response {
- return newMockResponse(http.StatusNotFound, "Not Found")
- },
- expectError: true,
- errorContains: "HTTP 404",
- },
- {
- name: "Failure - Network Error",
- url: "https://invalid-domain.example",
- mockError: errors.New("network error"),
- expectError: true,
- errorContains: "failed to execute HTTP request",
- },
- }
+ // Test with simple basic mocks instead of complex HTTP validation
+ t.Run("successful read", func(t *testing.T) {
+ // Create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(testScript))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Create a mock client for this test
- mockClient := &mockHTTPClient{
- doFunc: func(req *http.Request) (*http.Response, error) {
- if tt.requestValidator != nil {
- tt.requestValidator(t, req)
- }
-
- if tt.mockError != nil {
- return nil, tt.mockError
- }
-
- resp := tt.customResp()
- return resp, nil
- },
- }
+ testURL := server.URL + "/script.js"
- var loader *FromHTTP
- var err error
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
- // Start with default options and apply modifier if provided
- options := DefaultHTTPOptions()
- if tt.optionsModifier != nil {
- options = tt.optionsModifier(options)
- }
+ // Use real server with helper
+ reader, err := loader.GetReader()
+ require.NoError(t, err)
+ verifyReaderContent(t, reader, testScript)
+ })
- loader, err = NewFromHTTPWithOptions(tt.url, options)
- require.NoError(t, err, "Failed to create HTTP loader")
+ t.Run("unauthorized error", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ _, err := w.Write([]byte("Unauthorized"))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
- // Replace the client with our mock
- loader.client = mockClient
+ testURL := server.URL + "/auth"
- reader, err := loader.GetReader()
- if tt.expectError {
- require.Error(t, err)
- if tt.errorContains != "" {
- require.Contains(t, err.Error(), tt.errorContains)
- }
- return
- }
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
+
+ reader, err := loader.GetReader()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "HTTP 401")
+ require.Nil(t, reader)
+ })
+ t.Run("not found error", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, err := w.Write([]byte("Not Found"))
require.NoError(t, err)
- require.NotNil(t, reader)
- defer func() { require.NoError(t, reader.Close(), "Failed to close reader") }()
+ }))
+ defer server.Close()
- if tt.validateBody {
- content, err := io.ReadAll(reader)
- require.NoError(t, err)
- require.Equal(t, testScript, string(content))
- }
- })
- }
+ testURL := server.URL + "/not-found"
+
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
+
+ reader, err := loader.GetReader()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "HTTP 404")
+ require.Nil(t, reader)
+ })
+
+ t.Run("network error", func(t *testing.T) {
+ // Use any URL since we'll replace the client with a mock
+ testURL := "https://localhost:8080/script.js"
+
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
+
+ // Replace with client that returns an error
+ mockClient := &mockHTTPClient{
+ doFunc: func(req *http.Request) (*http.Response, error) {
+ return nil, errors.New("network error")
+ },
+ }
+ loader.client = mockClient
+
+ reader, err := loader.GetReader()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "failed to execute HTTP request")
+ require.Nil(t, reader)
+ })
}
func TestFromHTTP_GetReaderWithContext(t *testing.T) {
t.Parallel()
+ const testScript = FunctionContent
- const testScript = `function test() { return "Hello, World!"; }`
+ t.Run("Success - Background Context", func(t *testing.T) {
+ // Create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(testScript))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
- tests := []struct {
- name string
- url string
- ctx context.Context
- cancelFunc func()
- expectError bool
- errorContains string
- }{
- {
- name: "Success - Background Context",
- url: "https://example.com/script.js",
- ctx: context.Background(),
- },
- {
- name: "Failure - Cancelled Context",
- url: "https://example.com/script.js",
- ctx: func() context.Context { ctx, cancel := context.WithCancel(context.Background()); cancel(); return ctx }(),
- expectError: true,
- errorContains: "context canceled",
- },
- {
- name: "Failure - Timeout Context",
- url: "https://example.com/script.js",
- ctx: func() context.Context {
- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
- defer cancel()
- time.Sleep(5 * time.Millisecond)
- return ctx
- }(),
- expectError: true,
- errorContains: "context deadline exceeded",
- },
- }
+ testURL := server.URL + "/script.js"
+
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- loader, err := NewFromHTTP(tt.url)
+ ctx := context.Background()
+ reader, err := loader.GetReaderWithContext(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, reader)
+ verifyReaderContent(t, reader, testScript)
+ })
+
+ t.Run("Failure - Cancelled Context", func(t *testing.T) {
+ // Create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(testScript))
require.NoError(t, err)
+ }))
+ defer server.Close()
- mockClient := &mockHTTPClient{
- doFunc: func(req *http.Request) (*http.Response, error) {
- // Check if context error happens before we'd even make the request
- if err := req.Context().Err(); err != nil {
- return nil, err
- }
- // Create a new response each time to ensure it can be properly closed
- resp := newMockResponse(http.StatusOK, testScript)
- return resp, nil
- },
- }
- loader.client = mockClient
+ testURL := server.URL + "/script.js"
- reader, err := loader.GetReaderWithContext(tt.ctx)
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
- if tt.expectError {
- require.Error(t, err)
- if tt.errorContains != "" {
- require.Contains(t, err.Error(), tt.errorContains)
- }
- return
- }
+ // Create cancelled context
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel() // Cancel immediately
+
+ // Use mock client to ensure we're testing context cancellation
+ mockClient := &mockHTTPClient{
+ doFunc: func(req *http.Request) (*http.Response, error) {
+ // Should fail with context error
+ return nil, req.Context().Err()
+ },
+ }
+ loader.client = mockClient
+ reader, err := loader.GetReaderWithContext(ctx)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "context canceled")
+ require.Nil(t, reader)
+ })
+
+ t.Run("Failure - Timeout Context", func(t *testing.T) {
+ // Create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Delay to ensure timeout happens
+ time.Sleep(100 * time.Millisecond)
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(testScript))
require.NoError(t, err)
- require.NotNil(t, reader)
- defer func() { require.NoError(t, reader.Close(), "Failed to close reader") }()
- })
- }
-}
+ }))
+ defer server.Close()
-func TestFromHTTP_String(t *testing.T) {
- t.Parallel()
+ testURL := server.URL + "/script.js"
- t.Run("successful string representation", func(t *testing.T) {
- // Test successful String() result with mock client
- testURL := "https://example.com/script.js"
loader, err := NewFromHTTP(testURL)
require.NoError(t, err)
- // Mock client that returns content for SHA256 calculation
+ // Create context with very short timeout
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
+ defer cancel()
+
+ // Use mock client to ensure we're testing context timeout
mockClient := &mockHTTPClient{
doFunc: func(req *http.Request) (*http.Response, error) {
- return newMockResponse(http.StatusOK, "test script content"), nil
+ // Small sleep to ensure context times out
+ time.Sleep(1 * time.Millisecond)
+ return nil, req.Context().Err()
},
}
loader.client = mockClient
+ reader, err := loader.GetReaderWithContext(ctx)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "context deadline exceeded")
+ require.Nil(t, reader)
+ })
+}
+
+func TestFromHTTP_String(t *testing.T) {
+ t.Parallel()
+
+ t.Run("successful string representation", func(t *testing.T) {
+ // Create test server that returns content
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte("test script content"))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
+ loader, err := NewFromHTTP(testURL)
+ require.NoError(t, err)
+
str := loader.String()
require.Contains(t, str, "loader.FromHTTP{URL:")
require.Contains(t, str, testURL)
@@ -553,18 +546,12 @@ func TestFromHTTP_String(t *testing.T) {
})
t.Run("string representation with network error", func(t *testing.T) {
- testURL := "https://example.com/script.js"
+ // Create server that deliberately fails connections (invalid port)
+ testURL := "http://localhost:1" // This port is unlikely to be listening
+
loader, err := NewFromHTTP(testURL)
require.NoError(t, err)
- // Mock client that simulates an error
- failingMockClient := &mockHTTPClient{
- doFunc: func(req *http.Request) (*http.Response, error) {
- return nil, errors.New("network error")
- },
- }
- loader.client = failingMockClient
-
str := loader.String()
require.Contains(t, str, "loader.FromHTTP{URL:")
require.Contains(t, str, testURL)
@@ -572,18 +559,19 @@ func TestFromHTTP_String(t *testing.T) {
})
t.Run("string representation with HTTP error", func(t *testing.T) {
- testURL := "https://example.com/script.js"
+ // Create test server that returns an error status
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, err := w.Write([]byte("Not Found"))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
loader, err := NewFromHTTP(testURL)
require.NoError(t, err)
- // Mock client that returns an error status code
- errorMockClient := &mockHTTPClient{
- doFunc: func(req *http.Request) (*http.Response, error) {
- return newMockResponse(http.StatusNotFound, "Not Found"), nil
- },
- }
- loader.client = errorMockClient
-
str := loader.String()
require.Contains(t, str, "loader.FromHTTP{URL:")
require.Contains(t, str, testURL)
@@ -653,17 +641,21 @@ func TestFromHTTP_GetSourceURL(t *testing.T) {
t.Parallel()
t.Run("source URL", func(t *testing.T) {
- testURL := "https://example.com/script.js"
+ // Create test server for this test
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(FunctionContent))
+ require.NoError(t, err)
+ }))
+ defer server.Close()
+
+ testURL := server.URL + "/script.js"
+
loader, err := NewFromHTTP(testURL)
require.NoError(t, err)
sourceURL := loader.GetSourceURL()
require.NotNil(t, sourceURL)
require.Equal(t, testURL, sourceURL.String())
-
- // Test that the returned URL is a copy that can't modify the internal state
- parsedURL, err := url.Parse(testURL)
- require.NoError(t, err)
- require.Equal(t, parsedURL, sourceURL)
})
}
diff --git a/execution/script/loader/fromString_test.go b/execution/script/loader/fromString_test.go
index 36c8d02..c1e45f3 100644
--- a/execution/script/loader/fromString_test.go
+++ b/execution/script/loader/fromString_test.go
@@ -12,15 +12,15 @@ func TestNewFromString(t *testing.T) {
t.Parallel()
t.Run("valid content", func(t *testing.T) {
- cases := []struct {
+ tests := []struct {
name string
content string
want string
}{
{
name: "simple content",
- content: "test content",
- want: "test content",
+ content: SimpleContent,
+ want: SimpleContent,
},
{
name: "trim whitespace",
@@ -29,8 +29,8 @@ func TestNewFromString(t *testing.T) {
},
{
name: "multiline content",
- content: "line1\nline2\nline3",
- want: "line1\nline2\nline3",
+ content: MultilineContent,
+ want: MultilineContent,
},
{
name: "mixed line endings",
@@ -44,7 +44,8 @@ func TestNewFromString(t *testing.T) {
},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromString(tc.content)
require.NoError(t, err)
@@ -54,12 +55,15 @@ func TestNewFromString(t *testing.T) {
// Verify the URL includes the hash of the content
expectedHash := helpers.SHA256(tc.want)[:8]
require.Contains(t, loader.GetSourceURL().String(), expectedHash)
+
+ // Use helper for further validation
+ verifyLoader(t, loader, "string://inline/"+expectedHash)
})
}
})
t.Run("invalid content", func(t *testing.T) {
- cases := []struct {
+ tests := []struct {
name string
content string
}{
@@ -73,7 +77,8 @@ func TestNewFromString(t *testing.T) {
},
}
- for _, tc := range cases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromString(tc.content)
require.Error(t, err)
@@ -82,15 +87,6 @@ func TestNewFromString(t *testing.T) {
})
}
})
-
- t.Run("URL parsing error simulation", func(t *testing.T) {
- // For this test we'll just verify normal operation
- // since mocking url.Parse is complicated
- content := "valid content"
- loader, err := NewFromString(content)
- require.NoError(t, err)
- require.NotNil(t, loader)
- })
}
func TestFromString_GetReader(t *testing.T) {
@@ -104,39 +100,15 @@ func TestFromString_GetReader(t *testing.T) {
reader, err := loader.GetReader()
require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reader.Close(), "Failed to close reader")
- })
-
- got, err := io.ReadAll(reader)
- require.NoError(t, err)
- require.Equal(t, content, string(got))
+ verifyReaderContent(t, reader, content)
})
t.Run("multiple reads from same loader", func(t *testing.T) {
- content := "function calculate(x) { return x * 2; }"
+ content := FunctionContent
loader, err := NewFromString(content)
require.NoError(t, err)
- // First read
- reader1, err := loader.GetReader()
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reader1.Close(), "Failed to close first reader")
- })
- got1, err := io.ReadAll(reader1)
- require.NoError(t, err)
- require.Equal(t, content, string(got1))
-
- // Second read should return a new reader with the same content
- reader2, err := loader.GetReader()
- require.NoError(t, err)
- t.Cleanup(func() {
- require.NoError(t, reader2.Close(), "Failed to close second reader")
- })
- got2, err := io.ReadAll(reader2)
- require.NoError(t, err)
- require.Equal(t, content, string(got2))
+ verifyMultipleReads(t, loader, content)
})
t.Run("partial reads", func(t *testing.T) {
@@ -168,7 +140,7 @@ func TestFromString_GetSourceURL(t *testing.T) {
t.Parallel()
t.Run("source url", func(t *testing.T) {
- content := "test content"
+ content := SimpleContent
loader, err := NewFromString(content)
require.NoError(t, err)
@@ -200,7 +172,7 @@ func TestFromString_String(t *testing.T) {
t.Run("string representation", func(t *testing.T) {
// Test with different content lengths
- testCases := []struct {
+ tests := []struct {
name string
content string
shouldMatch string
@@ -217,7 +189,8 @@ func TestFromString_String(t *testing.T) {
},
}
- for _, tc := range testCases {
+ for _, tc := range tests {
+ tc := tc // Capture range variable
t.Run(tc.name, func(t *testing.T) {
loader, err := NewFromString(tc.content)
require.NoError(t, err)
diff --git a/execution/script/loader/httpauth/basic_test.go b/execution/script/loader/httpauth/basic_test.go
index 612fb92..f0cc140 100644
--- a/execution/script/loader/httpauth/basic_test.go
+++ b/execution/script/loader/httpauth/basic_test.go
@@ -13,11 +13,10 @@ func TestBasicAuth(t *testing.T) {
t.Parallel()
t.Run("Valid credentials", func(t *testing.T) {
- t.Parallel()
username := "testuser"
password := "testpass"
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
auth := NewBasicAuth(username, password)
@@ -33,11 +32,10 @@ func TestBasicAuth(t *testing.T) {
})
t.Run("Empty username (no auth applied)", func(t *testing.T) {
- t.Parallel()
username := ""
password := "testpass"
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
auth := NewBasicAuth(username, password)
@@ -52,11 +50,10 @@ func TestBasicAuth(t *testing.T) {
})
t.Run("With context", func(t *testing.T) {
- t.Parallel()
username := "testuser"
password := "testpass"
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx := context.Background()
@@ -73,11 +70,10 @@ func TestBasicAuth(t *testing.T) {
})
t.Run("With cancelled context", func(t *testing.T) {
- t.Parallel()
username := "testuser"
password := "testpass"
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -92,11 +88,10 @@ func TestBasicAuth(t *testing.T) {
})
t.Run("With timeout context", func(t *testing.T) {
- t.Parallel()
username := "testuser"
password := "testpass"
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
diff --git a/execution/script/loader/httpauth/header_test.go b/execution/script/loader/httpauth/header_test.go
index 6152403..a7933cf 100644
--- a/execution/script/loader/httpauth/header_test.go
+++ b/execution/script/loader/httpauth/header_test.go
@@ -13,8 +13,6 @@ func TestHeaderAuth(t *testing.T) {
t.Parallel()
t.Run("Multiple custom headers", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(map[string]string{
"Authorization": "Bearer token123",
"X-API-Key": "secret-key",
@@ -22,7 +20,7 @@ func TestHeaderAuth(t *testing.T) {
})
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
err = auth.Authenticate(req)
@@ -34,12 +32,10 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("Empty headers map", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(map[string]string{})
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
err = auth.Authenticate(req)
@@ -49,12 +45,10 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("Nil headers map", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(nil)
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
err = auth.Authenticate(req)
@@ -64,12 +58,10 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("Bearer token helper", func(t *testing.T) {
- t.Parallel()
-
auth := NewBearerAuth("my-test-token")
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
err = auth.Authenticate(req)
@@ -79,14 +71,12 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("With context", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(map[string]string{
"Authorization": "Bearer token123",
})
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx := context.Background()
@@ -97,14 +87,12 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("With cancelled context", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(map[string]string{
"Authorization": "Bearer token123",
})
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -116,14 +104,12 @@ func TestHeaderAuth(t *testing.T) {
})
t.Run("With timeout context", func(t *testing.T) {
- t.Parallel()
-
auth := NewHeaderAuth(map[string]string{
"Authorization": "Bearer token123",
})
require.Equal(t, "Header", auth.Name())
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
@@ -152,7 +138,7 @@ func TestHeaderAuthCloning(t *testing.T) {
originalHeaders["X-New"] = "added"
// Create a request
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
// Apply auth
diff --git a/execution/script/loader/httpauth/noauth_test.go b/execution/script/loader/httpauth/noauth_test.go
index 27a29de..f6ea355 100644
--- a/execution/script/loader/httpauth/noauth_test.go
+++ b/execution/script/loader/httpauth/noauth_test.go
@@ -19,9 +19,7 @@ func TestNoAuth(t *testing.T) {
require.Equal(t, "None", auth.Name())
t.Run("Basic authentication", func(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
err = auth.Authenticate(req)
@@ -32,9 +30,7 @@ func TestNoAuth(t *testing.T) {
})
t.Run("With context authentication", func(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx := context.Background()
@@ -45,9 +41,7 @@ func TestNoAuth(t *testing.T) {
})
t.Run("With cancelled context", func(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -59,9 +53,7 @@ func TestNoAuth(t *testing.T) {
})
t.Run("With timeout context", func(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil)
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
diff --git a/execution/script/loader/loader_test.go b/execution/script/loader/loader_test.go
new file mode 100644
index 0000000..3a08b8e
--- /dev/null
+++ b/execution/script/loader/loader_test.go
@@ -0,0 +1,101 @@
+package loader
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// Standard test content strings used across loader tests
+const (
+ SimpleContent = "test content"
+ MultilineContent = "line1\nline2\nline3"
+ FunctionContent = "function test(x) { return x * 2; }"
+)
+
+// mockHTTPClient implements the httpRequester interface for testing
+type mockHTTPClient struct {
+ doFunc func(req *http.Request) (*http.Response, error)
+}
+
+func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
+ if m.doFunc != nil {
+ return m.doFunc(req)
+ }
+ return nil, errors.New("doFunc not implemented")
+}
+
+// verifyLoader performs common verification steps for all loader implementations
+func verifyLoader(t *testing.T, loader Loader, expectedURLString string) {
+ t.Helper()
+
+ // Verify loader is properly instantiated
+ require.NotNil(t, loader)
+
+ // Verify source URL
+ sourceURL := loader.GetSourceURL()
+ require.NotNil(t, sourceURL)
+
+ if expectedURLString != "" {
+ parsedURL, err := url.Parse(expectedURLString)
+ require.NoError(t, err)
+ require.Equal(t, parsedURL.Scheme, sourceURL.Scheme)
+ }
+
+ // Test getting a reader
+ reader, err := loader.GetReader()
+ if err == nil {
+ // If no error, verify reader works and cleanup
+ require.NotNil(t, reader)
+ t.Cleanup(func() {
+ require.NoError(t, reader.Close(), "Failed to close reader")
+ })
+ }
+}
+
+// verifyReaderContent verifies the content returned by a reader
+func verifyReaderContent(t *testing.T, reader io.ReadCloser, expectedContent string) {
+ t.Helper()
+
+ // Add cleanup to ensure reader is closed
+ t.Cleanup(func() {
+ require.NoError(t, reader.Close(), "Failed to close reader")
+ })
+
+ // Read content
+ content, err := io.ReadAll(reader)
+ require.NoError(t, err)
+ require.Equal(t, expectedContent, string(content))
+}
+
+// verifyMultipleReads tests that a loader can provide multiple readers
+// with the same content
+func verifyMultipleReads(t *testing.T, loader Loader, expectedContent string) {
+ t.Helper()
+
+ // First read
+ reader1, err := loader.GetReader()
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, reader1.Close(), "Failed to close first reader")
+ })
+
+ content1, err := io.ReadAll(reader1)
+ require.NoError(t, err)
+ require.Equal(t, expectedContent, string(content1))
+
+ // Second read
+ reader2, err := loader.GetReader()
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, reader2.Close(), "Failed to close second reader")
+ })
+
+ content2, err := io.ReadAll(reader2)
+ require.NoError(t, err)
+ require.Equal(t, expectedContent, string(content2))
+}
diff --git a/machines/TESTING.md b/machines/TESTING.md
new file mode 100644
index 0000000..e29dda0
--- /dev/null
+++ b/machines/TESTING.md
@@ -0,0 +1,192 @@
+# Testing Guidelines for go-polyscript
+
+This document outlines the standardized testing patterns for go-polyscript. Following these guidelines ensures consistency, maintainability, and comprehensive test coverage across the codebase.
+
+## Core Testing Principles
+
+1. **Consistency**: Use standardized test patterns across all packages and VM implementations
+2. **Clarity**: Write clear, self-documenting tests with logical organization
+3. **Comprehensiveness**: Test both success paths and error conditions thoroughly
+4. **Efficiency**: Avoid duplication through table-driven tests and helper functions
+
+## Test Structure and Organization
+
+### Test Function Naming
+
+Use these consistent naming patterns:
+
+| Component | Test Function Name |
+|-----------|-------------------|
+| Compiler creation | `TestNewCompiler` |
+| Compilation | `TestCompiler_Compile` |
+| Options | `TestCompilerOptions` or `TestCompilerOptionsDetailed` |
+| Executables | `TestExecutable` |
+| Evaluator execution | `TestBytecodeEvaluator_Evaluate` |
+| Context preparation | `TestBytecodeEvaluator_PrepareContext` |
+| Response handling | `TestResponseMethods` |
+| Type conversion | `TestToGoType`/`TestToMachineType` |
+
+### Subtest Organization
+
+- Use the Go subtests pattern with `t.Run()` to group related test cases
+- Organize subtests into logical categories:
+ ```go
+ t.Run("success cases", func(t *testing.T) {
+ // Tests for normal operation
+ })
+ t.Run("error cases", func(t *testing.T) {
+ // Tests for error handling
+ })
+ ```
+- Keep verification code separate from setup code
+- Use descriptive subtest names instead of relying on comments
+
+### Test Parallelization
+
+- Use `t.Parallel()` ONLY in parent test functions, not in subtests
+ ```go
+ func TestResponseMethods(t *testing.T) {
+ t.Parallel() // Only in parent test function
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) { ... })
+ }
+ }
+ ```
+
+## Testify Usage Standards
+
+Always use the testify library consistently:
+
+### Assertion Types
+
+- **require**: For conditions that must pass for the test to continue
+- **assert**: For conditions that shouldn't halt the test if they fail
+- **mock**: For creating and verifying mock behaviors
+
+### Preferred Assertion Methods
+
+| Purpose | Preferred Method |
+|---------|------------------|
+| Value equality | `assert.Equal(t, expected, actual)` |
+| Negative comparison | `assert.NotEqual(t, unexpected, actual)` |
+| Boolean checks | `assert.True(t, condition)` / `assert.False(t, condition)` |
+| Nil/NotNil checks | `assert.Nil(t, value)` / `assert.NotNil(t, value)` |
+| Collections | `assert.Contains(t, container, element)` |
+| Order-independent slice equality | `assert.ElementsMatch(t, expected, actual)` |
+| No error | `require.NoError(t, err)` |
+| Error occurred | `require.Error(t, err)` |
+| Specific error | `require.ErrorIs(t, err, expectedErr)` (preferred over string comparison) |
+| Error message check | `require.Contains(t, err.Error(), "expected message")` |
+| Mock setup | `mock.On("MethodName", mock.Anything).Return(returnValue)` |
+
+Include meaningful messages with assertions to aid debugging:
+```go
+assert.Equal(t, expected, actual, "User ID should match after conversion")
+```
+
+## Component-Specific Guidelines
+
+### Compiler Tests
+
+| Component | Key Testing Focus |
+|-----------|------------------|
+| Compiler Creation | • Creation with default settings
• Creation with various options
• Error handling for invalid options |
+| Compilation | • Successful compilation of valid scripts
• Error handling for nil content, empty content, invalid syntax
• VM-specific compiler features |
+| Options | • Group related options under logical sections
• Test both valid and invalid option values
• Test default values and option combinations |
+| Evaluator | • Success paths with various input data types
• Context cancellation handling
• Nil executable/bytecode testing
• Metadata verification (execution time, script ID) |
+| Response | • All methods: Type, Interface, Inspect, String, GetScriptExeID, GetExecTime
• All data types: primitives, collections, complex nested structures
• Error handling for invalid types |
+| Type Conversion | • Bidirectional conversions: Go → VM and VM → Go
• Organized by type (primitives, collections, complex, errors)
• VM-specific type handling |
+
+## Best Practices
+
+### Test Helper Functions
+
+- Always mark test helpers with `t.Helper()` to improve error reporting:
+ ```go
+ func assertMapContainsExpectedHelper(t *testing.T, expected, actual map[string]any) {
+ t.Helper() // Marks this as a helper function
+ // Verification logic
+ }
+ ```
+- Keep helper functions focused on a single verification task
+- Extract common verification logic for consistency and readability
+
+### Mock Usage
+
+- Use the standard mocks from `machines/mocks` package
+- Set specific expectations for each test case:
+ ```go
+ mockObj.On("MethodName", mock.MatchedBy(func(arg string) bool {
+ return strings.Contains(arg, "expected")
+ })).Return("result", nil)
+ ```
+- Verify all expectations with `mockObj.AssertExpectations(t)` at the end of tests
+- Use typed nil values when needed for interface parameters
+
+## Example Test Pattern
+
+Here's the recommended pattern for consistent table-driven tests:
+
+```go
+func TestResponseMethods(t *testing.T) {
+ t.Parallel() // Only in parent test function
+
+ t.Run("type detection", func(t *testing.T) {
+ tests := []struct {
+ name string
+ input any
+ expected data.Types
+ }{
+ {"string value", "test", data.STRING},
+ {"integer value", 42, data.INT},
+ {"bool value", true, data.BOOL},
+ }
+
+ for _, tc := range tests {
+ tc := tc // Capture range variable
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tc.input, 0, "test-id")
+
+ // Verify - separate from setup
+ assert.Equal(t, tc.expected, result.Type(), "Type detection should match expected type")
+ })
+ }
+ })
+}
+```
+
+## Test Quality Checklist
+
+✅ Use table-driven tests for similar test cases
+✅ Apply proper parallelization with `t.Parallel()` only in parent tests
+✅ Capture range variables in loops to prevent race conditions
+✅ Separate setup code from verification code
+✅ Test both success paths and error conditions
+✅ Use `require.ErrorIs()` instead of string comparison for errors
+✅ Provide descriptive assertion messages
+✅ Mark helper functions with `t.Helper()`
+✅ Verify all mock expectations after tests
+✅ Check error returns from all functions that return errors
+✅ Use local test servers instead of external dependencies
+✅ Organize imports consistently (stdlib first, then external, then local)
+✅ Group related tests under logical parent tests
+
+## Test Coverage Improvements
+
+The codebase underwent significant test improvements with metrics tracked:
+
+| Package | Original Coverage | Final Coverage | Notes |
+|---------|------------------|----------------|-------|
+| engine | 0.0% | 100% | Added comprehensive tests |
+| machines/extism/evaluator | 62.5% | 90.4% | Added tests for edge cases |
+| machines/risor/evaluator | 82.6% | 88.4% | Improved structure and coverage |
+| machines/starlark/evaluator | 64.3% | 76.5% | Harmonized test structure |
+
+Key improvements included:
+- Reduction of test file size while maintaining or improving coverage
+- Extracting common test patterns into helper functions
+- Standardizing test structure across different VM implementations
+- Improving test reliability by removing external dependencies
+- Enhancing error case testing and edge case coverage
diff --git a/machines/extism/compiler/compiler_test.go b/machines/extism/compiler/compiler_test.go
index d098d56..0b21eb0 100644
--- a/machines/extism/compiler/compiler_test.go
+++ b/machines/extism/compiler/compiler_test.go
@@ -4,7 +4,6 @@ import (
"context"
_ "embed"
"encoding/json"
- "errors"
"io"
"log/slog"
"os"
@@ -30,7 +29,7 @@ func createTestCompiler(t *testing.T, entryPoint string) *Compiler {
comp, err := NewCompiler(
WithEntryPoint(entryPoint),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
)
require.NoError(t, err)
require.NotNil(t, comp)
@@ -65,248 +64,238 @@ func (m *mockScriptReaderCloser) Close() error {
return args.Error(0)
}
-func TestCompiler(t *testing.T) {
+func TestNewCompiler(t *testing.T) {
t.Parallel()
- t.Run("valid wasm binary with existing function", func(t *testing.T) {
- t.Parallel()
- wasmBytes := readTestWasm(t)
- entryPoint := "greet"
-
- // Create compiler using functional options
- comp := createTestCompiler(t, entryPoint)
-
- // Create mock reader with content
- reader := newMockScriptReaderCloser(wasmBytes)
- reader.On("Close").Return(nil)
-
- // Compile
- execContent, err := comp.Compile(reader)
- require.NoError(t, err)
- require.NotNil(t, execContent)
-
- // Type assertion
- executable, ok := execContent.(*Executable)
- require.True(t, ok, "Expected *Executable type")
-
- // Validate source matches
- assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
-
- // Validate the executable
- assert.NotNil(t, executable.GetExtismByteCode())
- plugin := executable.GetExtismByteCode()
- require.NotNil(t, plugin)
-
- // Create instance to check function existence
- instance, err := plugin.Instance(
- context.Background(),
- extismSDK.PluginInstanceConfig{},
- )
- require.NoError(t, err)
- defer func() { require.NoError(t, instance.Close(context.Background()), "Failed to close instance") }()
-
- assert.True(t, instance.FunctionExists("greet"), "Function 'greet' should exist")
-
- // Test function execution
- exit, output, err := instance.Call("greet", []byte(`{"input":"Test"}`))
- require.NoError(t, err)
- assert.Equal(t, uint32(0), exit)
-
- var result struct {
- Greeting string `json:"greeting"`
- }
- require.NoError(t, json.Unmarshal(output, &result))
- assert.Equal(t, "Hello, Test!", result.Greeting)
-
- // Test Close functionality
- ctx := context.Background()
- require.NoError(t, executable.Close(ctx))
-
- // Verify mock expectations
- reader.AssertExpectations(t)
- })
-
- t.Run("custom entry point function exists", func(t *testing.T) {
- t.Parallel()
- wasmBytes := readTestWasm(t)
- entryPoint := "process_complex"
-
- // Create compiler using functional options
- comp := createTestCompiler(t, entryPoint)
-
- // Create mock reader with content
- reader := newMockScriptReaderCloser(wasmBytes)
- reader.On("Close").Return(nil)
-
- // Compile
- execContent, err := comp.Compile(reader)
- require.NoError(t, err)
- require.NotNil(t, execContent)
-
- // Type assertion
- executable, ok := execContent.(*Executable)
- require.True(t, ok, "Expected *Executable type")
-
- // Validate source matches
- assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
-
- // Validate entry point
- assert.Equal(t, "process_complex", executable.GetEntryPoint())
- plugin := executable.GetExtismByteCode()
- require.NotNil(t, plugin)
-
- // Create instance to check function existence
- instance, err := plugin.Instance(
- context.Background(),
- extismSDK.PluginInstanceConfig{},
- )
- require.NoError(t, err)
- defer func() { require.NoError(t, instance.Close(context.Background()), "Failed to close instance") }()
-
- assert.True(t, instance.FunctionExists("process_complex"),
- "Function 'process_complex' should exist")
-
- // Test Close functionality
- ctx := context.Background()
- require.NoError(t, executable.Close(ctx))
-
- // Verify mock expectations
- reader.AssertExpectations(t)
- })
-
- t.Run("custom compilation options", func(t *testing.T) {
- t.Parallel()
- wasmBytes := readTestWasm(t)
- entryPoint := "greet"
-
- // Create compiler with custom runtime config
+ t.Run("basic creation", func(t *testing.T) {
comp, err := NewCompiler(
- WithEntryPoint(entryPoint),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
- WithRuntimeConfig(wazero.NewRuntimeConfig()),
+ WithEntryPoint("main"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
)
require.NoError(t, err)
require.NotNil(t, comp)
- // Create mock reader with content
- reader := newMockScriptReaderCloser(wasmBytes)
- reader.On("Close").Return(nil)
-
- // Compile
- execContent, err := comp.Compile(reader)
- require.NoError(t, err)
- require.NotNil(t, execContent)
-
- // Type assertion
- executable, ok := execContent.(*Executable)
- require.True(t, ok, "Expected *Executable type")
-
- // Validate source matches
- assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
-
- // Test Close functionality
- ctx := context.Background()
- require.NoError(t, executable.Close(ctx))
-
- // Verify mock expectations
- reader.AssertExpectations(t)
+ // Test String method
+ result := comp.String()
+ require.NotEmpty(t, result)
+ require.Contains(t, result, "Compiler")
})
- t.Run("nil content", func(t *testing.T) {
- t.Parallel()
-
- // Create compiler using functional options
+ t.Run("with entry point", func(t *testing.T) {
comp, err := NewCompiler(
- WithEntryPoint("main"),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithEntryPoint("custom_function"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
)
require.NoError(t, err)
require.NotNil(t, comp)
-
- // Compile with nil reader
- execContent, err := comp.Compile(nil)
- require.Error(t, err)
- require.Nil(t, execContent)
- require.True(t, errors.Is(err, ErrContentNil),
- "Expected error %v, got %v", ErrContentNil, err)
})
- t.Run("empty content", func(t *testing.T) {
- t.Parallel()
-
- // Create compiler using functional options
+ t.Run("with custom runtime config", func(t *testing.T) {
comp, err := NewCompiler(
WithEntryPoint("main"),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ WithRuntimeConfig(wazero.NewRuntimeConfig()),
)
require.NoError(t, err)
require.NotNil(t, comp)
-
- // Create empty reader
- reader := newMockScriptReaderCloser([]byte{})
- reader.On("Close").Return(nil)
-
- // Compile
- execContent, err := comp.Compile(reader)
- require.Error(t, err)
- require.Nil(t, execContent)
- require.True(t, errors.Is(err, ErrContentNil),
- "Expected error %v, got %v", ErrContentNil, err)
-
- // Verify mock expectations
- reader.AssertExpectations(t)
})
- t.Run("invalid wasm binary", func(t *testing.T) {
- t.Parallel()
-
- // Create compiler using functional options
+ t.Run("with custom logger", func(t *testing.T) {
+ handler := slog.NewTextHandler(io.Discard, nil)
+ logger := slog.New(handler)
comp, err := NewCompiler(
WithEntryPoint("main"),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithLogger(logger),
)
require.NoError(t, err)
require.NotNil(t, comp)
-
- // Create reader with invalid content
- reader := newMockScriptReaderCloser([]byte("not-wasm"))
- reader.On("Close").Return(nil)
-
- // Compile
- execContent, err := comp.Compile(reader)
- require.Error(t, err)
- require.Nil(t, execContent)
- require.True(t, errors.Is(err, ErrValidationFailed),
- "Expected error %v, got %v", ErrValidationFailed, err)
-
- // Verify mock expectations
- reader.AssertExpectations(t)
})
+}
- t.Run("missing function", func(t *testing.T) {
- t.Parallel()
- wasmBytes := readTestWasm(t)
-
- // Create compiler with non-existent function
- comp, err := NewCompiler(
- WithEntryPoint("nonexistent_function"),
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
- )
- require.NoError(t, err)
- require.NotNil(t, comp)
-
- // Create mock reader with content
- reader := newMockScriptReaderCloser(wasmBytes)
- reader.On("Close").Return(nil)
+func TestCompiler_Compile(t *testing.T) {
+ t.Parallel()
- // Compile
- execContent, err := comp.Compile(reader)
- require.Error(t, err)
- require.Nil(t, execContent)
- require.True(t, errors.Is(err, ErrValidationFailed),
- "Expected error %v, got %v", ErrValidationFailed, err)
+ t.Run("success cases", func(t *testing.T) {
+ t.Run("valid wasm binary with existing function", func(t *testing.T) {
+ wasmBytes := readTestWasm(t)
+ entryPoint := "greet"
+ comp := createTestCompiler(t, entryPoint)
+ reader := newMockScriptReaderCloser(wasmBytes)
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.NoError(t, err)
+ require.NotNil(t, execContent)
+
+ executable, ok := execContent.(*Executable)
+ require.True(t, ok, "Expected *Executable type")
+ assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
+
+ plugin := executable.GetExtismByteCode()
+ require.NotNil(t, plugin)
+
+ instance, err := plugin.Instance(
+ context.Background(),
+ extismSDK.PluginInstanceConfig{},
+ )
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, instance.Close(context.Background()), "Failed to close instance")
+ }()
+
+ assert.True(t, instance.FunctionExists("greet"), "Function 'greet' should exist")
+
+ exit, output, err := instance.Call("greet", []byte(`{"input":"Test"}`))
+ require.NoError(t, err)
+ assert.Equal(t, uint32(0), exit)
+
+ var result struct {
+ Greeting string `json:"greeting"`
+ }
+ require.NoError(t, json.Unmarshal(output, &result))
+ assert.Equal(t, "Hello, Test!", result.Greeting)
+
+ ctx := context.Background()
+ require.NoError(t, executable.Close(ctx))
+ reader.AssertExpectations(t)
+ })
+
+ t.Run("custom entry point function exists", func(t *testing.T) {
+ wasmBytes := readTestWasm(t)
+ entryPoint := "process_complex"
+ comp := createTestCompiler(t, entryPoint)
+ reader := newMockScriptReaderCloser(wasmBytes)
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.NoError(t, err)
+ require.NotNil(t, execContent)
+
+ executable, ok := execContent.(*Executable)
+ require.True(t, ok, "Expected *Executable type")
+ assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
+ assert.Equal(t, "process_complex", executable.GetEntryPoint())
+
+ plugin := executable.GetExtismByteCode()
+ require.NotNil(t, plugin)
+
+ instance, err := plugin.Instance(
+ context.Background(),
+ extismSDK.PluginInstanceConfig{},
+ )
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, instance.Close(context.Background()), "Failed to close instance")
+ }()
+
+ assert.True(t, instance.FunctionExists("process_complex"),
+ "Function 'process_complex' should exist")
+
+ ctx := context.Background()
+ require.NoError(t, executable.Close(ctx))
+ reader.AssertExpectations(t)
+ })
+
+ t.Run("custom compilation options", func(t *testing.T) {
+ wasmBytes := readTestWasm(t)
+ entryPoint := "greet"
+
+ comp, err := NewCompiler(
+ WithEntryPoint(entryPoint),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ WithRuntimeConfig(wazero.NewRuntimeConfig()),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+
+ reader := newMockScriptReaderCloser(wasmBytes)
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.NoError(t, err)
+ require.NotNil(t, execContent)
+
+ executable, ok := execContent.(*Executable)
+ require.True(t, ok, "Expected *Executable type")
+ assert.Equal(t, wasmBytes, []byte(executable.GetSource()))
+
+ ctx := context.Background()
+ require.NoError(t, executable.Close(ctx))
+ reader.AssertExpectations(t)
+ })
+ })
- // Verify mock expectations
- reader.AssertExpectations(t)
+ t.Run("error cases", func(t *testing.T) {
+ t.Run("nil content", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithEntryPoint("main"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+
+ execContent, err := comp.Compile(nil)
+ require.Error(t, err)
+ require.Nil(t, execContent)
+ require.ErrorIs(t, err, ErrContentNil)
+ })
+
+ t.Run("empty content", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithEntryPoint("main"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+
+ reader := newMockScriptReaderCloser([]byte{})
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err)
+ require.Nil(t, execContent)
+ require.ErrorIs(t, err, ErrContentNil)
+
+ reader.AssertExpectations(t)
+ })
+
+ t.Run("invalid wasm binary", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithEntryPoint("main"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+
+ reader := newMockScriptReaderCloser([]byte("not-wasm"))
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err)
+ require.Nil(t, execContent)
+ require.ErrorIs(t, err, ErrValidationFailed)
+
+ reader.AssertExpectations(t)
+ })
+
+ t.Run("missing function", func(t *testing.T) {
+ wasmBytes := readTestWasm(t)
+ comp, err := NewCompiler(
+ WithEntryPoint("nonexistent_function"),
+ WithLogHandler(slog.NewTextHandler(io.Discard, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+
+ reader := newMockScriptReaderCloser(wasmBytes)
+ reader.On("Close").Return(nil)
+
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err)
+ require.Nil(t, execContent)
+ require.ErrorIs(t, err, ErrValidationFailed)
+
+ reader.AssertExpectations(t)
+ })
})
}
diff --git a/machines/extism/compiler/executable_test.go b/machines/extism/compiler/executable_test.go
index 0d5c647..6af80ea 100644
--- a/machines/extism/compiler/executable_test.go
+++ b/machines/extism/compiler/executable_test.go
@@ -62,79 +62,107 @@ func (m *MockPluginInstance) Close(ctx context.Context) error {
return args.Error(0)
}
-func TestNewExecutable(t *testing.T) {
+// TestExecutable tests the functionality of Executable
+func TestExecutable(t *testing.T) {
t.Parallel()
- // Test data
- wasmBytes := []byte("mock wasm bytes")
- entryPoint := "run"
-
- // Create a mock plugin
- mockPlugin := new(MockCompiledPlugin)
-
- // Empty entry point test
- t.Run("empty entry point", func(t *testing.T) {
- exe := NewExecutable(wasmBytes, mockPlugin, "")
- assert.Nil(t, exe)
- })
-
- // Empty script bytes test
- t.Run("empty script bytes", func(t *testing.T) {
- exe := NewExecutable(nil, mockPlugin, entryPoint)
- assert.Nil(t, exe)
+ // Test creation scenarios
+ t.Run("Creation", func(t *testing.T) {
+ // Test data
+ wasmBytes := []byte("mock wasm bytes")
+ entryPoint := "run"
+
+ // Create a mock plugin
+ mockPlugin := new(MockCompiledPlugin)
+
+ t.Run("valid creation", func(t *testing.T) {
+ exe := NewExecutable(wasmBytes, mockPlugin, entryPoint)
+ require.NotNil(t, exe)
+
+ // Verify properties
+ assert.Equal(t, string(wasmBytes), exe.GetSource())
+ assert.Equal(t, mockPlugin, exe.GetByteCode())
+ assert.Equal(t, mockPlugin, exe.GetExtismByteCode())
+ assert.Equal(t, machineTypes.Extism, exe.GetMachineType())
+ assert.Equal(t, entryPoint, exe.GetEntryPoint())
+ assert.False(t, exe.closed.Load())
+ })
+
+ t.Run("empty entry point", func(t *testing.T) {
+ exe := NewExecutable(wasmBytes, mockPlugin, "")
+ assert.Nil(t, exe)
+ })
+
+ t.Run("empty script bytes", func(t *testing.T) {
+ exe := NewExecutable(nil, mockPlugin, entryPoint)
+ assert.Nil(t, exe)
+ })
+
+ t.Run("nil plugin", func(t *testing.T) {
+ exe := NewExecutable(wasmBytes, nil, entryPoint)
+ assert.Nil(t, exe)
+ })
})
- // Nil plugin test
- t.Run("nil plugin", func(t *testing.T) {
- exe := NewExecutable(wasmBytes, nil, entryPoint)
- assert.Nil(t, exe)
- })
+ // Test getters
+ t.Run("Getters", func(t *testing.T) {
+ wasmBytes := []byte("mock wasm bytes")
+ entryPoint := "run"
+ mockPlugin := new(MockCompiledPlugin)
- // Valid creation test
- t.Run("valid creation", func(t *testing.T) {
exe := NewExecutable(wasmBytes, mockPlugin, entryPoint)
require.NotNil(t, exe)
- // Verify properties
- assert.Equal(t, string(wasmBytes), exe.GetSource())
- assert.Equal(t, mockPlugin, exe.GetByteCode())
- assert.Equal(t, mockPlugin, exe.GetExtismByteCode())
- assert.Equal(t, machineTypes.Extism, exe.GetMachineType())
- assert.Equal(t, entryPoint, exe.GetEntryPoint())
- assert.False(t, exe.closed.Load())
+ t.Run("GetSource", func(t *testing.T) {
+ source := exe.GetSource()
+ assert.Equal(t, string(wasmBytes), source)
+ })
+
+ t.Run("GetByteCode", func(t *testing.T) {
+ bytecode := exe.GetByteCode()
+ assert.Equal(t, mockPlugin, bytecode)
+ })
+
+ t.Run("GetExtismByteCode", func(t *testing.T) {
+ bytecode := exe.GetExtismByteCode()
+ assert.Equal(t, mockPlugin, bytecode)
+ })
+
+ t.Run("GetMachineType", func(t *testing.T) {
+ machineType := exe.GetMachineType()
+ assert.Equal(t, machineTypes.Extism, machineType)
+ })
+
+ t.Run("GetEntryPoint", func(t *testing.T) {
+ ep := exe.GetEntryPoint()
+ assert.Equal(t, entryPoint, ep)
+ })
})
-}
-func TestExecutable_Close(t *testing.T) {
- t.Parallel()
+ // Test Close functionality (specific to Extism)
+ t.Run("Close", func(t *testing.T) {
+ ctx := context.Background()
+ wasmBytes := []byte("mock wasm bytes")
+ entryPoint := "run"
- // Test data
- wasmBytes := []byte("mock wasm bytes")
- entryPoint := "run"
- ctx := context.Background()
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", ctx).Return(nil)
- // Create and setup mock plugin
- mockPlugin := new(MockCompiledPlugin)
- mockPlugin.On("Close", ctx).Return(nil)
-
- // Create executable
- exe := NewExecutable(wasmBytes, mockPlugin, entryPoint)
- require.NotNil(t, exe)
-
- // Verify initial state
- assert.False(t, exe.closed.Load())
-
- // Close executable
- err := exe.Close(ctx)
- require.NoError(t, err)
+ exe := NewExecutable(wasmBytes, mockPlugin, entryPoint)
+ require.NotNil(t, exe)
+ assert.False(t, exe.closed.Load())
- // Verify closed state
- assert.True(t, exe.closed.Load())
+ t.Run("first close", func(t *testing.T) {
+ err := exe.Close(ctx)
+ require.NoError(t, err)
+ assert.True(t, exe.closed.Load())
+ })
- // Verify idempotent close - should not call plugin Close again
- err = exe.Close(ctx)
- assert.NoError(t, err)
+ t.Run("second close (no-op)", func(t *testing.T) {
+ err := exe.Close(ctx)
+ assert.NoError(t, err)
+ })
- // Verify expectations
- mockPlugin.AssertExpectations(t)
+ mockPlugin.AssertExpectations(t)
+ })
}
diff --git a/machines/extism/compiler/internal/compile/compile_test.go b/machines/extism/compiler/internal/compile/compile_test.go
index ba4e220..c39bc3f 100644
--- a/machines/extism/compiler/internal/compile/compile_test.go
+++ b/machines/extism/compiler/internal/compile/compile_test.go
@@ -38,7 +38,6 @@ func TestCompileSuccess(t *testing.T) {
ctx := context.Background()
t.Run("default options", func(t *testing.T) {
- t.Parallel()
plugin, err := CompileBytes(ctx, wasmBytes, nil)
require.NoError(t, err)
require.NotNil(t, plugin)
@@ -55,7 +54,6 @@ func TestCompileSuccess(t *testing.T) {
})
t.Run("custom options", func(t *testing.T) {
- t.Parallel()
opts := &Settings{
EnableWASI: true,
RuntimeConfig: wazero.NewRuntimeConfig().
@@ -78,7 +76,6 @@ func TestCompileSuccess(t *testing.T) {
})
t.Run("base64 input default options", func(t *testing.T) {
- t.Parallel()
wasmBase64 := base64.StdEncoding.EncodeToString(wasmBytes)
plugin, err := CompileBase64(ctx, wasmBase64, nil)
require.NoError(t, err)
@@ -96,7 +93,6 @@ func TestCompileSuccess(t *testing.T) {
})
t.Run("base64 input custom options", func(t *testing.T) {
- t.Parallel()
opts := &Settings{
EnableWASI: true,
RuntimeConfig: wazero.NewRuntimeConfig(),
@@ -121,90 +117,111 @@ func TestCompileSuccess(t *testing.T) {
func testFunctions(t *testing.T, instance adapters.PluginInstance) {
t.Helper()
- t.Run("greet function", func(t *testing.T) {
- input := []byte(`{"input":"World"}`)
- exit, output, err := instance.Call("greet", input)
- require.NoError(t, err)
- assert.Equal(t, uint32(0), exit, "Function should execute successfully")
-
- var result struct {
- Greeting string `json:"greeting"`
- }
- require.NoError(t, json.Unmarshal(output, &result))
- assert.Equal(t, "Hello, World!", result.Greeting)
- })
-
- t.Run("reverse_string function", func(t *testing.T) {
- input := []byte(`{"input":"Hello"}`)
- exit, output, err := instance.Call("reverse_string", input)
- require.NoError(t, err)
- assert.Equal(t, uint32(0), exit, "Function should execute successfully")
-
- var result struct {
- Reversed string `json:"reversed"`
- }
- require.NoError(t, json.Unmarshal(output, &result))
- assert.Equal(t, "olleH", result.Reversed)
- })
-
- t.Run("count_vowels function", func(t *testing.T) {
- input := []byte(`{"input":"Hello World"}`)
- exit, output, err := instance.Call("count_vowels", input)
- require.NoError(t, err)
- assert.Equal(t, uint32(0), exit, "Function should execute successfully")
- var result struct {
- Count int `json:"count"`
- Vowels string `json:"vowels"`
- Input string `json:"input"`
- }
- require.NoError(t, json.Unmarshal(output, &result))
- assert.Equal(t, 3, result.Count) // "e", "o", "o" in "Hello World"
- assert.Equal(t, "Hello World", result.Input)
- })
-
- t.Run("process_complex function", func(t *testing.T) {
- req := TestRequest{
- ID: "test-123",
- Timestamp: time.Now().Unix(),
- Data: map[string]any{
- "key1": "value1",
- "key2": 42,
+ // Test different Wasm functions
+ tests := []struct {
+ name string
+ funcName string
+ input any
+ assertFunc func(t *testing.T, output []byte)
+ }{
+ {
+ name: "greet function",
+ funcName: "greet",
+ input: map[string]string{"input": "World"},
+ assertFunc: func(t *testing.T, output []byte) {
+ t.Helper()
+ var result struct {
+ Greeting string `json:"greeting"`
+ }
+ require.NoError(t, json.Unmarshal(output, &result))
+ assert.Equal(t, "Hello, World!", result.Greeting)
},
- Tags: []string{"test", "example"},
- Metadata: map[string]string{
- "source": "unit-test",
- "version": "1.0",
+ },
+ {
+ name: "reverse_string function",
+ funcName: "reverse_string",
+ input: map[string]string{"input": "Hello"},
+ assertFunc: func(t *testing.T, output []byte) {
+ t.Helper()
+ var result struct {
+ Reversed string `json:"reversed"`
+ }
+ require.NoError(t, json.Unmarshal(output, &result))
+ assert.Equal(t, "olleH", result.Reversed)
},
- Count: 42,
- Active: true,
- }
- input, err := json.Marshal(req)
- require.NoError(t, err)
+ },
+ {
+ name: "count_vowels function",
+ funcName: "count_vowels",
+ input: map[string]string{"input": "Hello World"},
+ assertFunc: func(t *testing.T, output []byte) {
+ t.Helper()
+ var result struct {
+ Count int `json:"count"`
+ Vowels string `json:"vowels"`
+ Input string `json:"input"`
+ }
+ require.NoError(t, json.Unmarshal(output, &result))
+ assert.Equal(t, 3, result.Count) // "e", "o", "o" in "Hello World"
+ assert.Equal(t, "Hello World", result.Input)
+ },
+ },
+ {
+ name: "process_complex function",
+ funcName: "process_complex",
+ input: TestRequest{
+ ID: "test-123",
+ Timestamp: time.Now().Unix(),
+ Data: map[string]any{
+ "key1": "value1",
+ "key2": 42,
+ },
+ Tags: []string{"test", "example"},
+ Metadata: map[string]string{
+ "source": "unit-test",
+ "version": "1.0",
+ },
+ Count: 42,
+ Active: true,
+ },
+ assertFunc: func(t *testing.T, output []byte) {
+ t.Helper()
+ var result struct {
+ RequestID string `json:"request_id"`
+ ProcessedAt string `json:"processed_at"`
+ Results map[string]any `json:"results"`
+ TagCount int `json:"tag_count"`
+ MetaCount int `json:"meta_count"`
+ IsActive bool `json:"is_active"`
+ Summary string `json:"summary"`
+ }
+ require.NoError(t, json.Unmarshal(output, &result))
+ assert.Equal(t, "test-123", result.RequestID)
+ assert.Equal(t, 2, result.TagCount)
+ assert.Equal(t, 2, result.MetaCount)
+ assert.True(t, result.IsActive)
+ assert.Contains(t, result.Summary, "test-123")
+ },
+ },
+ }
- exit, output, err := instance.Call("process_complex", input)
- require.NoError(t, err)
- assert.Equal(t, uint32(0), exit, "Function should execute successfully")
-
- var result struct {
- RequestID string `json:"request_id"`
- ProcessedAt string `json:"processed_at"`
- Results map[string]any `json:"results"`
- TagCount int `json:"tag_count"`
- MetaCount int `json:"meta_count"`
- IsActive bool `json:"is_active"`
- Summary string `json:"summary"`
- }
- require.NoError(t, json.Unmarshal(output, &result))
- assert.Equal(t, "test-123", result.RequestID)
- assert.Equal(t, 2, result.TagCount)
- assert.Equal(t, 2, result.MetaCount)
- assert.True(t, result.IsActive)
- assert.Contains(t, result.Summary, "test-123")
- })
+ for _, tt := range tests {
+ t.Run(tt.funcName, func(t *testing.T) {
+ inputJSON, err := json.Marshal(tt.input)
+ require.NoError(t, err)
+
+ exit, output, err := instance.Call(tt.funcName, inputJSON)
+ require.NoError(t, err)
+ assert.Equal(t, uint32(0), exit, "Function should execute successfully")
+
+ tt.assertFunc(t, output)
+ })
+ }
}
func TestCompileErrors(t *testing.T) {
+ t.Parallel()
ctx := context.Background()
tests := []struct {
@@ -239,6 +256,11 @@ func TestCompileErrors(t *testing.T) {
[]byte("corrupted")...),
wantErr: ErrCompileFailed,
},
+ {
+ name: "empty bytes",
+ input: []byte{},
+ wantErr: ErrContentNil,
+ },
}
for _, tt := range tests {
diff --git a/machines/extism/compiler/options_test.go b/machines/extism/compiler/options_test.go
index 307efd9..6ed2762 100644
--- a/machines/extism/compiler/options_test.go
+++ b/machines/extism/compiler/options_test.go
@@ -12,378 +12,443 @@ import (
"github.com/tetratelabs/wazero"
)
-func TestWithEntryPoint(t *testing.T) {
- // Test that WithEntryPoint properly sets the entry point
- entryPoint := "custom_entrypoint"
-
- c := &Compiler{
- entryPointName: "",
- }
- c.applyDefaults()
- opt := WithEntryPoint(entryPoint)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, entryPoint, c.GetEntryPointName())
-
- // Test with empty entry point
- emptyOpt := WithEntryPoint("")
- err = emptyOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "entry point cannot be empty")
-}
-
-func TestLoggerConfiguration(t *testing.T) {
- t.Run("default initialization", func(t *testing.T) {
- // Create a compiler with default settings
- c, err := NewCompiler()
- require.NoError(t, err)
-
- // Verify that both logHandler and logger are set
- require.NotNil(t, c.logHandler, "logHandler should be initialized")
- require.NotNil(t, c.logger, "logger should be initialized")
- })
-
- t.Run("with explicit log handler", func(t *testing.T) {
- // Create a custom handler
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
-
- // Create compiler with the handler
- c, err := NewCompiler(WithLogHandler(customHandler))
- require.NoError(t, err)
-
- // Verify handler was set and used to create logger
- require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
- require.NotNil(t, c.logger, "logger should be created from handler")
-
- // Test logging works with the custom handler
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with explicit logger", func(t *testing.T) {
- // Create a custom logger
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
- customLogger := slog.New(customHandler)
-
- // Create compiler with the logger
- c, err := NewCompiler(WithLogger(customLogger))
- require.NoError(t, err)
-
- // Verify logger was set
- require.Equal(t, customLogger, c.logger, "custom logger should be set")
- require.NotNil(t, c.logHandler, "handler should be extracted from logger")
-
- // Test logging works with the custom logger
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with both logger options, last one wins", func(t *testing.T) {
- // Create two buffers to verify which one receives logs
- var handlerBuf, loggerBuf bytes.Buffer
- customHandler := slog.NewTextHandler(&handlerBuf, nil)
- customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
-
- // Case 1: Handler then Logger
- c1, err := NewCompiler(
- WithLogHandler(customHandler),
- WithLogger(customLogger),
- )
- require.NoError(t, err)
- require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
- c1.logger.Info("test message")
- require.Contains(t, loggerBuf.String(), "test message", "logger buffer should receive logs")
- require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
-
- // Clear buffers
- handlerBuf.Reset()
- loggerBuf.Reset()
-
- // Case 2: Logger then Handler
- c2, err := NewCompiler(
- WithLogger(customLogger),
- WithLogHandler(customHandler),
- )
- require.NoError(t, err)
- require.Equal(t, customHandler, c2.logHandler, "handler option should take precedence")
- c2.logger.Info("test message")
- require.Contains(
- t,
- handlerBuf.String(),
- "test message",
- "handler buffer should receive logs",
- )
- require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
- })
-}
-
-func TestWithLogHandler(t *testing.T) {
- // Test that WithLogHandler properly sets the handler field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogHandler(handler)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, handler, c.logHandler)
- require.Nil(t, c.logger) // Should clear Logger field
-
- // Test with nil handler
- nilOpt := WithLogHandler(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "log handler cannot be nil")
-}
-
-func TestWithLogger(t *testing.T) {
- // Test that WithLogger properly sets the logger field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
- logger := slog.New(handler)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogger(logger)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, logger, c.logger)
- require.Nil(t, c.logHandler) // Should clear LogHandler field
-
- // Test with nil logger
- nilOpt := WithLogger(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "logger cannot be nil")
-}
-
-func TestWithWASIEnabled(t *testing.T) {
- // Test that WithWASIEnabled properly sets the EnableWASI field
- c := &Compiler{
- options: &compile.Settings{},
- }
- c.applyDefaults()
-
- // Test enabling WASI
- enableOpt := WithWASIEnabled(true)
- err := enableOpt(c)
-
- require.NoError(t, err)
- require.True(t, c.options.EnableWASI)
-
- // Test disabling WASI
- disableOpt := WithWASIEnabled(false)
- err = disableOpt(c)
-
- require.NoError(t, err)
- require.False(t, c.options.EnableWASI)
-}
-
-func TestWithRuntimeConfig(t *testing.T) {
- // Test that WithRuntimeConfig properly sets the RuntimeConfig field
- runtimeConfig := wazero.NewRuntimeConfig()
-
- c := &Compiler{
- options: &compile.Settings{},
- }
- c.applyDefaults()
- opt := WithRuntimeConfig(runtimeConfig)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, runtimeConfig, c.options.RuntimeConfig)
-
- // Test with nil runtime config
- nilOpt := WithRuntimeConfig(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "runtime config cannot be nil")
-}
-
-func TestWithHostFunctions(t *testing.T) {
- // Test that WithHostFunctions properly sets the HostFunctions field
- testHostFn := extismSDK.NewHostFunctionWithStack(
- "test_function",
- func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {
- // No-op function for testing
- },
- nil, nil,
- )
- testHostFn.SetNamespace("test")
-
- hostFuncs := []extismSDK.HostFunction{testHostFn}
-
- c := &Compiler{
- options: &compile.Settings{},
- }
- c.applyDefaults()
- opt := WithHostFunctions(hostFuncs)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, hostFuncs, c.options.HostFunctions)
-
- // Test with empty host functions
- emptyOpt := WithHostFunctions([]extismSDK.HostFunction{})
- err = emptyOpt(c)
-
- require.NoError(t, err)
- require.Empty(t, c.options.HostFunctions)
-}
-
-func TestApplyDefaults(t *testing.T) {
- t.Run("empty compiler", func(t *testing.T) {
- // Test that defaults are properly applied to an empty compiler
- c := &Compiler{}
- c.applyDefaults()
-
- require.NotNil(t, c.logHandler)
- require.Nil(t, c.logger)
- require.Equal(t, defaultEntryPoint, c.GetEntryPointName())
- require.NotNil(t, c.options)
- require.True(t, c.options.EnableWASI)
- require.NotNil(t, c.options.RuntimeConfig)
- require.NotNil(t, c.options.HostFunctions)
- require.Empty(t, c.options.HostFunctions)
- require.NotNil(t, c.ctx)
- })
-
- t.Run("empty string entrypoint", func(t *testing.T) {
- // Test with an empty string entrypoint
- c := &Compiler{
- entryPointName: "",
- options: &compile.Settings{},
- ctx: context.Background(),
- }
- c.applyDefaults()
-
- // Check if the defaultEntryPoint was correctly applied
- require.Equal(t, defaultEntryPoint, c.entryPointName)
- })
-
- t.Run("reset empty entrypoint", func(t *testing.T) {
- // Test that emptying the entry point and reapplying defaults sets it back
- c := &Compiler{
- entryPointName: "initialValue",
- options: &compile.Settings{},
- ctx: context.Background(),
- }
-
- // First verify the initial value
- require.Equal(t, "initialValue", c.entryPointName)
-
- // Now set to empty string and apply defaults again
- c.entryPointName = ""
- c.applyDefaults()
-
- // Should be reset to defaultEntryPoint
- require.Equal(t, defaultEntryPoint, c.entryPointName)
+// TestCompilerOptions tests all compiler options functionality
+func TestCompilerOptions(t *testing.T) {
+ t.Parallel()
+
+ t.Run("EntryPoint", func(t *testing.T) {
+ t.Run("valid entry point", func(t *testing.T) {
+ entryPoint := "custom_entrypoint"
+
+ c := &Compiler{
+ entryPointName: "",
+ }
+ c.applyDefaults()
+ opt := WithEntryPoint(entryPoint)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, entryPoint, c.GetEntryPointName())
+ })
+
+ t.Run("empty entry point", func(t *testing.T) {
+ c := &Compiler{
+ entryPointName: "existing",
+ }
+ c.applyDefaults()
+ emptyOpt := WithEntryPoint("")
+ err := emptyOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "entry point cannot be empty")
+ })
+
+ t.Run("GetEntryPointName", func(t *testing.T) {
+ // Test with a normal value
+ c1 := &Compiler{
+ entryPointName: "test_function",
+ }
+ require.Equal(t, "test_function", c1.GetEntryPointName())
+
+ // Test with empty string
+ c2 := &Compiler{
+ entryPointName: "",
+ }
+ require.Equal(t, "", c2.GetEntryPointName())
+ })
})
- t.Run("non-default value preserved", func(t *testing.T) {
- // Test that a non-default value is preserved through applyDefaults
- customEntryPoint := "custom_function"
- c := &Compiler{
- entryPointName: customEntryPoint,
- options: &compile.Settings{},
- ctx: context.Background(),
- }
-
- // Apply defaults, which should not change the entry point
- c.applyDefaults()
-
- // The custom value should be preserved
- require.Equal(t, customEntryPoint, c.entryPointName)
+ t.Run("Logger", func(t *testing.T) {
+ t.Run("default initialization", func(t *testing.T) {
+ c, err := NewCompiler()
+ require.NoError(t, err)
+ require.NotNil(t, c.logHandler, "logHandler should be initialized")
+ require.NotNil(t, c.logger, "logger should be initialized")
+ })
+
+ t.Run("with explicit log handler", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+
+ c, err := NewCompiler(WithLogHandler(customHandler))
+ require.NoError(t, err)
+
+ require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
+ require.NotNil(t, c.logger, "logger should be created from handler")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("with explicit logger", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+ customLogger := slog.New(customHandler)
+
+ c, err := NewCompiler(WithLogger(customLogger))
+ require.NoError(t, err)
+
+ require.Equal(t, customLogger, c.logger, "custom logger should be set")
+ require.NotNil(t, c.logHandler, "handler should be extracted from logger")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("option precedence", func(t *testing.T) {
+ var handlerBuf, loggerBuf bytes.Buffer
+ customHandler := slog.NewTextHandler(&handlerBuf, nil)
+ customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
+
+ t.Run("handler then logger", func(t *testing.T) {
+ c1, err := NewCompiler(
+ WithLogHandler(customHandler),
+ WithLogger(customLogger),
+ )
+ require.NoError(t, err)
+ require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
+ c1.logger.Info("test message")
+ require.Contains(
+ t,
+ loggerBuf.String(),
+ "test message",
+ "logger buffer should receive logs",
+ )
+ require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
+ })
+
+ // Clear buffers
+ handlerBuf.Reset()
+ loggerBuf.Reset()
+
+ t.Run("logger then handler", func(t *testing.T) {
+ c2, err := NewCompiler(
+ WithLogger(customLogger),
+ WithLogHandler(customHandler),
+ )
+ require.NoError(t, err)
+ require.Equal(
+ t,
+ customHandler,
+ c2.logHandler,
+ "handler option should take precedence",
+ )
+ c2.logger.Info("test message")
+ require.Contains(
+ t,
+ handlerBuf.String(),
+ "test message",
+ "handler buffer should receive logs",
+ )
+ require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
+ })
+ })
+
+ t.Run("WithLogHandler option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+
+ t.Run("valid handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogHandler(handler)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, handler, c.logHandler)
+ require.Nil(t, c.logger) // Should clear Logger field
+ })
+
+ t.Run("nil handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogHandler(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "log handler cannot be nil")
+ })
+ })
+
+ t.Run("WithLogger option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+ logger := slog.New(handler)
+
+ t.Run("valid logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogger(logger)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, logger, c.logger)
+ require.Nil(t, c.logHandler) // Should clear LogHandler field
+ })
+
+ t.Run("nil logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogger(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "logger cannot be nil")
+ })
+ })
})
-}
-
-func TestValidate(t *testing.T) {
- // Test validation with proper defaults
- c := &Compiler{}
- c.applyDefaults()
-
- err := c.validate()
- require.NoError(t, err)
-
- // Test validation with manually cleared logger and handler
- c = &Compiler{}
- c.applyDefaults()
- c.logHandler = nil
- c.logger = nil
-
- err = c.validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), "either log handler or logger must be specified")
-
- // Test validation with empty entry point
- c = &Compiler{}
- c.applyDefaults()
- c.entryPointName = ""
-
- err = c.validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), "entry point must be specified")
-
- // Test validation with nil runtime config
- c = &Compiler{}
- c.applyDefaults()
- c.options.RuntimeConfig = nil
-
- err = c.validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), "runtime config cannot be nil")
-}
-func TestWithContext(t *testing.T) {
- // Test that WithContext properly sets the Context field
- ctx := context.Background()
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithContext(ctx)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, ctx, c.ctx)
-
- // We need to test our validation of nil contexts but without passing nil directly
- // to satisfy the linter. Use a type conversion trick to create a nil context.
- var nilContext context.Context
- nilOpt := WithContext(nilContext)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "context cannot be nil")
-}
-
-func TestGetEntryPointName(t *testing.T) {
- t.Run("normal value", func(t *testing.T) {
- // Test with a normal value
- c := &Compiler{
- entryPointName: "test_function",
- }
-
- // Should return the stored value
- require.Equal(t, "test_function", c.GetEntryPointName())
+ t.Run("Runtime", func(t *testing.T) {
+ t.Run("WASI options", func(t *testing.T) {
+ t.Run("enable/disable WASI", func(t *testing.T) {
+ c := &Compiler{
+ options: &compile.Settings{},
+ }
+ c.applyDefaults()
+
+ enableOpt := WithWASIEnabled(true)
+ err := enableOpt(c)
+ require.NoError(t, err)
+ require.True(t, c.options.EnableWASI)
+
+ disableOpt := WithWASIEnabled(false)
+ err = disableOpt(c)
+ require.NoError(t, err)
+ require.False(t, c.options.EnableWASI)
+ })
+
+ t.Run("with nil options", func(t *testing.T) {
+ c := &Compiler{
+ options: nil,
+ }
+ c.options = &compile.Settings{}
+
+ opt := WithWASIEnabled(true)
+ err := opt(c)
+ require.NoError(t, err)
+ require.True(t, c.options.EnableWASI)
+ })
+ })
+
+ t.Run("runtime config", func(t *testing.T) {
+ t.Run("normal runtime config", func(t *testing.T) {
+ runtimeConfig := wazero.NewRuntimeConfig()
+ c := &Compiler{
+ options: &compile.Settings{},
+ }
+ c.applyDefaults()
+
+ opt := WithRuntimeConfig(runtimeConfig)
+ err := opt(c)
+ require.NoError(t, err)
+ require.Equal(t, runtimeConfig, c.options.RuntimeConfig)
+ })
+
+ t.Run("nil runtime config", func(t *testing.T) {
+ c := &Compiler{
+ options: &compile.Settings{},
+ }
+ c.applyDefaults()
+
+ nilOpt := WithRuntimeConfig(nil)
+ err := nilOpt(c)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "runtime config cannot be nil")
+ })
+
+ t.Run("with nil options", func(t *testing.T) {
+ c := &Compiler{
+ options: nil,
+ }
+ c.options = &compile.Settings{}
+ runtimeConfig := wazero.NewRuntimeConfig()
+
+ opt := WithRuntimeConfig(runtimeConfig)
+ err := opt(c)
+ require.NoError(t, err)
+ require.Equal(t, runtimeConfig, c.options.RuntimeConfig)
+ })
+ })
+
+ t.Run("host functions", func(t *testing.T) {
+ t.Run("valid host functions", func(t *testing.T) {
+ testHostFn := extismSDK.NewHostFunctionWithStack(
+ "test_function",
+ func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {
+ // No-op function for testing
+ },
+ nil, nil,
+ )
+ testHostFn.SetNamespace("test")
+ hostFuncs := []extismSDK.HostFunction{testHostFn}
+
+ c := &Compiler{
+ options: &compile.Settings{},
+ }
+ c.applyDefaults()
+
+ opt := WithHostFunctions(hostFuncs)
+ err := opt(c)
+ require.NoError(t, err)
+ require.Equal(t, hostFuncs, c.options.HostFunctions)
+ })
+
+ t.Run("empty host functions", func(t *testing.T) {
+ c := &Compiler{
+ options: &compile.Settings{},
+ }
+ c.applyDefaults()
+
+ emptyOpt := WithHostFunctions([]extismSDK.HostFunction{})
+ err := emptyOpt(c)
+ require.NoError(t, err)
+ require.Empty(t, c.options.HostFunctions)
+ })
+
+ t.Run("with nil options", func(t *testing.T) {
+ c := &Compiler{
+ options: nil,
+ }
+ c.options = &compile.Settings{}
+
+ testHostFn := extismSDK.NewHostFunctionWithStack(
+ "test_function",
+ func(ctx context.Context, p *extismSDK.CurrentPlugin, stack []uint64) {},
+ nil, nil,
+ )
+
+ hostFuncs := []extismSDK.HostFunction{testHostFn}
+ opt := WithHostFunctions(hostFuncs)
+ err := opt(c)
+ require.NoError(t, err)
+ require.Equal(t, hostFuncs, c.options.HostFunctions)
+ })
+ })
+
+ t.Run("WithContext option", func(t *testing.T) {
+ ctx := context.Background()
+
+ t.Run("valid context", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithContext(ctx)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, ctx, c.ctx)
+ })
+
+ t.Run("nil context", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ // We need to test our validation of nil contexts but without passing nil directly
+ // to satisfy the linter. Use a type conversion trick to create a nil context.
+ var nilContext context.Context
+ nilOpt := WithContext(nilContext)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "context cannot be nil")
+ })
+ })
})
- t.Run("empty string value", func(t *testing.T) {
- // Test with empty string
- c := &Compiler{
- entryPointName: "",
- }
-
- // Should return empty string
- require.Equal(t, "", c.GetEntryPointName())
+ t.Run("Defaults and Validation", func(t *testing.T) {
+ t.Run("defaults - empty compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ require.NotNil(t, c.logHandler)
+ require.Nil(t, c.logger)
+ require.Equal(t, defaultEntryPoint, c.GetEntryPointName())
+ require.NotNil(t, c.options)
+ require.True(t, c.options.EnableWASI)
+ require.NotNil(t, c.options.RuntimeConfig)
+ require.NotNil(t, c.options.HostFunctions)
+ require.Empty(t, c.options.HostFunctions)
+ require.NotNil(t, c.ctx)
+ })
+
+ t.Run("defaults - entry point handling", func(t *testing.T) {
+ t.Run("empty string entry point", func(t *testing.T) {
+ c := &Compiler{
+ entryPointName: "",
+ options: &compile.Settings{},
+ ctx: context.Background(),
+ }
+ c.applyDefaults()
+
+ require.Equal(t, defaultEntryPoint, c.entryPointName)
+ })
+
+ t.Run("reset empty entry point", func(t *testing.T) {
+ c := &Compiler{
+ entryPointName: "initialValue",
+ options: &compile.Settings{},
+ ctx: context.Background(),
+ }
+
+ require.Equal(t, "initialValue", c.entryPointName)
+
+ c.entryPointName = ""
+ c.applyDefaults()
+
+ require.Equal(t, defaultEntryPoint, c.entryPointName)
+ })
+
+ t.Run("preserve non-default value", func(t *testing.T) {
+ customEntryPoint := "custom_function"
+ c := &Compiler{
+ entryPointName: customEntryPoint,
+ options: &compile.Settings{},
+ ctx: context.Background(),
+ }
+
+ c.applyDefaults()
+
+ require.Equal(t, customEntryPoint, c.entryPointName)
+ })
+ })
+
+ t.Run("validation", func(t *testing.T) {
+ t.Run("valid compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+
+ t.Run("missing logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ c.logHandler = nil
+ c.logger = nil
+
+ err := c.validate()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "either log handler or logger must be specified")
+ })
+
+ t.Run("empty entry point", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ c.entryPointName = ""
+
+ err := c.validate()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "entry point must be specified")
+ })
+
+ t.Run("nil runtime config", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ c.options.RuntimeConfig = nil
+
+ err := c.validate()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "runtime config cannot be nil")
+ })
+ })
})
}
diff --git a/machines/extism/evaluator/bytecodeEvaluator_test.go b/machines/extism/evaluator/bytecodeEvaluator_test.go
index 07bb916..dffe475 100644
--- a/machines/extism/evaluator/bytecodeEvaluator_test.go
+++ b/machines/extism/evaluator/bytecodeEvaluator_test.go
@@ -7,240 +7,608 @@ import (
"os"
"testing"
+ extismSDK "github.com/extism/go-sdk"
"github.com/robbyt/go-polyscript/execution/constants"
"github.com/robbyt/go-polyscript/execution/data"
"github.com/robbyt/go-polyscript/execution/script"
+ "github.com/robbyt/go-polyscript/machines/extism/adapters"
+ "github.com/robbyt/go-polyscript/machines/extism/compiler"
"github.com/robbyt/go-polyscript/machines/extism/internal"
machineTypes "github.com/robbyt/go-polyscript/machines/types"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
-func TestLoadInputData(t *testing.T) {
- t.Parallel()
+// MockCompiledPlugin is a mock implementation of adapters.CompiledPlugin
+type MockCompiledPlugin struct {
+ mock.Mock
+}
- tests := []struct {
- name string
- ctxData any
- expectedEmpty bool
- }{
- {
- name: "empty context",
- ctxData: nil,
- expectedEmpty: true,
- },
- {
- name: "valid data",
- ctxData: map[string]any{
- "foo": "bar",
- "nested": map[string]any{
- "a": 1,
- "b": 2,
- },
- },
- expectedEmpty: false,
- },
- {
- name: "empty data",
- ctxData: map[string]any{},
- expectedEmpty: true,
- },
+func (m *MockCompiledPlugin) Instance(
+ ctx context.Context,
+ cfg extismSDK.PluginInstanceConfig,
+) (adapters.PluginInstance, error) {
+ args := m.Called(ctx, cfg)
+ return args.Get(0).(adapters.PluginInstance), args.Error(1)
+}
+
+func (m *MockCompiledPlugin) Close(ctx context.Context) error {
+ args := m.Called(ctx)
+ return args.Error(0)
+}
+
+// createMockExecutable creates a real compiler.Executable with our mock plugin
+func createMockExecutable(
+ mockPlugin adapters.CompiledPlugin,
+ entryPoint string,
+) *compiler.Executable {
+ // Create some mock WASM bytes
+ wasmBytes := []byte{0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00}
+
+ // Use the real Executable type with our mock plugin
+ return compiler.NewExecutable(wasmBytes, mockPlugin, entryPoint)
+}
+
+// mockErrProvider implements the data.Provider interface and always returns an error
+type mockErrProvider struct {
+ err error
+}
+
+func (m *mockErrProvider) GetData(ctx context.Context) (map[string]any, error) {
+ return nil, m.err
+}
+
+func (m *mockErrProvider) AddDataToContext(
+ ctx context.Context,
+ data ...any,
+) (context.Context, error) {
+ return ctx, m.err
+}
+
+// mockPluginInstance is a mock implementation of the adapters.PluginInstance interface
+type mockPluginInstance struct {
+ exitCode uint32
+ output []byte
+ callErr error
+ closeErr error
+ wasCalled bool
+ wasClosed bool
+ cancelFunc func()
+}
+
+func (m *mockPluginInstance) CallWithContext(
+ ctx context.Context,
+ functionName string,
+ input []byte,
+) (uint32, []byte, error) {
+ m.wasCalled = true
+ // Execute the cancel function if provided (to simulate context cancellation)
+ if m.cancelFunc != nil {
+ m.cancelFunc()
}
+ // Check if the context was canceled
+ if ctx.Err() != nil {
+ return 0, nil, ctx.Err()
+ }
+ return m.exitCode, m.output, m.callErr
+}
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
+func (m *mockPluginInstance) Call(name string, data []byte) (uint32, []byte, error) {
+ m.wasCalled = true
+ return m.exitCode, m.output, m.callErr
+}
+
+func (m *mockPluginInstance) FunctionExists(name string) bool {
+ return true
+}
+
+func (m *mockPluginInstance) Close(ctx context.Context) error {
+ m.wasClosed = true
+ return m.closeErr
+}
+
+type mockExecutableContent struct {
+ machineType machineTypes.Type
+ source string
+ bytecode any
+}
+
+func (m *mockExecutableContent) GetMachineType() machineTypes.Type {
+ return m.machineType
+}
+
+func (m *mockExecutableContent) GetSource() string {
+ return m.source
+}
+
+func (m *mockExecutableContent) GetByteCode() any {
+ return m.bytecode
+}
+
+// TestBytecodeEvaluator_Evaluate tests evaluating WASM scripts with Extism
+func TestBytecodeEvaluator_Evaluate(t *testing.T) {
+ t.Parallel()
+
+ t.Run("success cases", func(t *testing.T) {
+ // Test successful JSON response
+ t.Run("successful execution with JSON output", func(t *testing.T) {
+ // Skip this test in CI environments that may not support WASM
+ if os.Getenv("CI") != "" {
+ t.Skip("Skipping WASM test in CI environment")
+ }
handler := slog.NewTextHandler(os.Stdout, nil)
- // Create a context provider
+ // Create context provider
ctxProvider := data.NewContextProvider(constants.EvalData)
- // Create a dummy executableUnit
- dummyExe := &script.ExecutableUnit{
+ // Create mock plugin
+ mockPlugin := new(MockCompiledPlugin)
+ mockInstance := &mockPluginInstance{
+ exitCode: 0, // Success
+ output: []byte(`{"result":"success", "value": 42}`),
+ }
+ mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+
+ // Create a real compiler.Executable with our mock plugin
+ content := createMockExecutable(mockPlugin, "main")
+
+ // Create a mock executable
+ exe := &script.ExecutableUnit{
+ ID: "test-json-success",
DataProvider: ctxProvider,
+ Content: content,
}
- evaluator := NewBytecodeEvaluator(handler, dummyExe)
+ evaluator := NewBytecodeEvaluator(handler, exe)
+
ctx := context.Background()
+ evalData := map[string]any{"test": "data"}
+ ctx = context.WithValue(ctx, constants.EvalData, evalData)
+
+ response, err := evaluator.Eval(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, response)
+
+ // Verify the response
+ resultMap, ok := response.Interface().(map[string]any)
+ require.True(t, ok, "Expected map response")
+ require.Contains(t, resultMap, "result")
+ require.Equal(t, "success", resultMap["result"])
+ require.Contains(t, resultMap, "value")
+ require.Equal(t, float64(42), resultMap["value"])
+ })
- if tt.ctxData != nil {
- // Temporarily ignoring the "string as context key" warning until type system is fixed
- ctx = context.WithValue(ctx, constants.EvalData, tt.ctxData)
+ // Test successful string response
+ t.Run("successful execution with string output", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
+
+ mockPlugin := new(MockCompiledPlugin)
+ mockInstance := &mockPluginInstance{
+ exitCode: 0,
+ output: []byte(`Hello, World!`), // Plain text
+ }
+ mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+
+ content := createMockExecutable(mockPlugin, "main")
+ exe := &script.ExecutableUnit{
+ ID: "test-string-success",
+ DataProvider: ctxProvider,
+ Content: content,
}
- // Test the loadInputData method
- result, err := evaluator.loadInputData(ctx)
+ evaluator := NewBytecodeEvaluator(handler, exe)
+ ctx := context.Background()
+ evalData := map[string]any{"test": "data"}
+ ctx = context.WithValue(ctx, constants.EvalData, evalData)
+
+ response, err := evaluator.Eval(ctx)
require.NoError(t, err)
+ require.NotNil(t, response)
- if tt.expectedEmpty {
- assert.Empty(t, result)
- } else {
- assert.NotEmpty(t, result)
- if validMap, ok := tt.ctxData.(map[string]any); ok {
- assert.Equal(t, validMap, result)
- }
+ // Verify the string response
+ require.Equal(t, "Hello, World!", response.Interface())
+ })
+
+ // Test load input data with various context values
+ t.Run("load input data", func(t *testing.T) {
+ tests := []struct {
+ name string
+ ctxData any
+ expectedEmpty bool
+ }{
+ {
+ name: "empty context",
+ ctxData: nil,
+ expectedEmpty: true,
+ },
+ {
+ name: "valid data",
+ ctxData: map[string]any{
+ "foo": "bar",
+ "nested": map[string]any{
+ "a": 1,
+ "b": 2,
+ },
+ },
+ expectedEmpty: false,
+ },
+ {
+ name: "empty data",
+ ctxData: map[string]any{},
+ expectedEmpty: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
+ dummyExe := &script.ExecutableUnit{
+ DataProvider: ctxProvider,
+ }
+
+ evaluator := NewBytecodeEvaluator(handler, dummyExe)
+ ctx := context.Background()
+
+ if tt.ctxData != nil {
+ ctx = context.WithValue(ctx, constants.EvalData, tt.ctxData)
+ }
+
+ // Test the loadInputData method
+ result, err := evaluator.loadInputData(ctx)
+ require.NoError(t, err)
+
+ if tt.expectedEmpty {
+ assert.Empty(t, result)
+ } else {
+ assert.NotEmpty(t, result)
+ if validMap, ok := tt.ctxData.(map[string]any); ok {
+ assert.Equal(t, validMap, result)
+ }
+ }
+ })
}
})
- }
-}
-func TestBytecodeEvaluatorInvalidInputs(t *testing.T) {
- t.Parallel()
+ // Test how input data is formatted for Extism
+ t.Run("input data formatting", func(t *testing.T) {
+ // Create a test map that simulates data from our providers
+ inputData := map[string]any{
+ "initial": "top-level-value", // Static data at top level
+ "input_data": map[string]any{ // Dynamic data nested under input_data
+ "input": "API User",
+ "request": map[string]any{}, // HTTP request data nested under input_data
+ },
+ }
- // Common test setup helper
- setupTest := func(content *mockExecutableContent) (slog.Handler, *script.ExecutableUnit) {
- handler := slog.NewTextHandler(os.Stdout, nil)
- ctxProvider := data.NewContextProvider(constants.EvalData)
+ // Convert the input data for Extism
+ jsonBytes, err := internal.ConvertToExtismFormat(inputData)
+ require.NoError(t, err)
+ require.NotNil(t, jsonBytes)
- exe := &script.ExecutableUnit{
- ID: "test-case",
- Content: content,
- DataProvider: ctxProvider,
- }
+ // Verify current behavior
+ expected := `{"initial":"top-level-value","input_data":{"input":"API User","request":{}}}`
+ assert.JSONEq(t, expected, string(jsonBytes))
+ })
+ })
- return handler, exe
- }
+ t.Run("error cases", func(t *testing.T) {
+ // Test nil executable unit
+ t.Run("nil executable unit", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ evaluator := NewBytecodeEvaluator(handler, nil)
- // Test case: nil bytecode
- t.Run("nil bytecode", func(t *testing.T) {
- t.Parallel()
+ ctx := context.Background()
+ _, err := evaluator.Eval(ctx)
- mockContent := &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "invalid wasm",
- bytecode: nil, // Nil bytecode will cause error
- }
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "executable unit is nil")
+ })
- handler, exe := setupTest(mockContent)
- evaluator := NewBytecodeEvaluator(handler, exe)
+ // Test nil bytecode
+ t.Run("nil bytecode", func(t *testing.T) {
+ mockContent := &mockExecutableContent{
+ machineType: machineTypes.Extism,
+ source: "invalid wasm",
+ bytecode: nil, // Nil bytecode will cause error
+ }
- ctx := context.Background()
- _, err := evaluator.Eval(ctx)
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "bytecode is nil")
- })
+ exe := &script.ExecutableUnit{
+ ID: "test-case",
+ Content: mockContent,
+ DataProvider: ctxProvider,
+ }
- // Test case: invalid content type
- t.Run("invalid content type", func(t *testing.T) {
- t.Parallel()
+ evaluator := NewBytecodeEvaluator(handler, exe)
- mockContent := &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "invalid wasm",
- bytecode: []byte{0x00}, // Not a valid WASM module
- }
+ ctx := context.Background()
+ _, err := evaluator.Eval(ctx)
- handler, exe := setupTest(mockContent)
- evaluator := NewBytecodeEvaluator(handler, exe)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "bytecode is nil")
+ })
- ctx := context.Background()
- _, err := evaluator.Eval(ctx)
+ // Test invalid content type
+ t.Run("invalid content type", func(t *testing.T) {
+ mockContent := &mockExecutableContent{
+ machineType: machineTypes.Extism,
+ source: "invalid wasm",
+ bytecode: []byte{0x00}, // Not a valid WASM plugin
+ }
- require.Error(t, err)
- assert.Contains(t, err.Error(), "invalid executable type")
- })
-}
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
-func TestNilHandlerFallback(t *testing.T) {
- // Test that the evaluator handles nil handlers by creating a default
- exe := &script.ExecutableUnit{
- ID: "test-nil-handler",
- DataProvider: data.NewContextProvider(constants.EvalData),
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D},
- },
- }
+ exe := &script.ExecutableUnit{
+ ID: "test-case",
+ Content: mockContent,
+ DataProvider: ctxProvider,
+ }
- // Create with nil handler
- evaluator := NewBytecodeEvaluator(nil, exe)
+ evaluator := NewBytecodeEvaluator(handler, exe)
- // Shouldn't panic
- require.NotNil(t, evaluator)
- require.NotNil(t, evaluator.logger)
- require.NotNil(t, evaluator.logHandler)
-}
+ ctx := context.Background()
+ _, err := evaluator.Eval(ctx)
-func TestEvaluatorString(t *testing.T) {
- handler := slog.NewTextHandler(os.Stdout, nil)
- evaluator := NewBytecodeEvaluator(handler, nil)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "invalid executable type")
+ })
- // Test the string representation
- strRep := evaluator.String()
- require.Equal(t, "extism.BytecodeEvaluator", strRep)
-}
+ // Test context cancellation
+ t.Run("context cancellation", func(t *testing.T) {
+ // Create a cancel context
+ ctx, cancel := context.WithCancel(context.Background())
+
+ // Create mock plugin that will check for cancellation
+ mockPlugin := new(MockCompiledPlugin)
+ mockInstance := &mockPluginInstance{
+ cancelFunc: func() {
+ // This will be called during execution to cancel the context
+ cancel()
+ },
+ callErr: context.Canceled,
+ }
+ mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
-func TestEvalWithNilExecutableUnit(t *testing.T) {
- handler := slog.NewTextHandler(os.Stdout, nil)
- evaluator := NewBytecodeEvaluator(handler, nil)
+ // Create a real compiler.Executable with our mock plugin
+ content := createMockExecutable(mockPlugin, "main")
- // Attempt to evaluate with nil executable unit
- ctx := context.Background()
- _, err := evaluator.Eval(ctx)
+ // Create executor unit
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ execUnit := &script.ExecutableUnit{
+ ID: "test-cancel",
+ Content: content,
+ DataProvider: data.NewContextProvider(constants.EvalData),
+ }
- // Should get an error
- require.Error(t, err)
- require.Contains(t, err.Error(), "executable unit is nil")
-}
+ evaluator := NewBytecodeEvaluator(handler, execUnit)
-type mockExecutableContent struct {
- machineType machineTypes.Type
- source string
- bytecode any
-}
+ // Add test data to context
+ ctx = context.WithValue(ctx, constants.EvalData, map[string]any{"test": "data"})
-func (m *mockExecutableContent) GetMachineType() machineTypes.Type {
- return m.machineType
-}
+ // Call Eval, which should be cancelled during execution
+ result, err := evaluator.Eval(ctx)
-func (m *mockExecutableContent) GetSource() string {
- return m.source
-}
+ // Should get a cancellation error
+ assert.Error(t, err)
+ assert.Nil(t, result)
+ assert.Contains(t, err.Error(), "execution")
-func (m *mockExecutableContent) GetByteCode() any {
- return m.bytecode
-}
+ // Instance should have been called
+ mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything)
-// TestBasicExecution is a simplified test that mocks the execution
-func TestBasicExecution(t *testing.T) {
- // Skip this test in CI environments that may not support WASM
- if os.Getenv("CI") != "" {
- t.Skip("Skipping WASM test in CI environment")
- }
+ // Instance should have been closed
+ assert.True(t, mockInstance.wasClosed)
+ })
- handler := slog.NewTextHandler(os.Stdout, nil)
+ // Test execution with non-zero exit code
+ t.Run("non-zero exit code", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
- // Create context provider
- ctxProvider := data.NewContextProvider(constants.EvalData)
+ mockPlugin := new(MockCompiledPlugin)
+ mockInstance := &mockPluginInstance{
+ exitCode: 1, // Error exit code
+ output: []byte(`{"error":"something went wrong"}`),
+ }
+ mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
- // Create a mock executable
- exe := &script.ExecutableUnit{
- ID: "test-basic",
- DataProvider: ctxProvider,
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D}, // WASM magic bytes only
- },
- }
+ content := createMockExecutable(mockPlugin, "main")
+ exe := &script.ExecutableUnit{
+ ID: "test-error-exit",
+ DataProvider: ctxProvider,
+ Content: content,
+ }
+
+ evaluator := NewBytecodeEvaluator(handler, exe)
+ ctx := context.Background()
+ evalData := map[string]any{"test": "data"}
+ ctx = context.WithValue(ctx, constants.EvalData, evalData)
+
+ _, err := evaluator.Eval(ctx)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "non-zero exit code")
+ })
- evaluator := NewBytecodeEvaluator(handler, exe)
+ // Test error creating plugin instance
+ t.Run("error creating plugin instance", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ mockPlugin := new(MockCompiledPlugin)
+ mockInstance := &mockPluginInstance{}
+ mockPlugin.On("Instance", mock.Anything, mock.Anything).
+ Return(mockInstance, errors.New("instance creation error"))
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+
+ content := createMockExecutable(mockPlugin, "main")
+ exe := &script.ExecutableUnit{
+ ID: "test-instance-error",
+ DataProvider: data.NewContextProvider(constants.EvalData),
+ Content: content,
+ }
+
+ evaluator := NewBytecodeEvaluator(handler, exe)
+ ctx := context.Background()
- // This will fail during execution but should handle the error gracefully
- ctx := context.Background()
- evalData := map[string]any{"test": "data"}
- ctx = context.WithValue(ctx, constants.EvalData, evalData)
+ _, err := evaluator.Eval(ctx)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to create plugin instance")
+ })
+ })
+
+ t.Run("metadata tests", func(t *testing.T) {
+ // Test nil handler fallback
+ t.Run("nil handler fallback", func(t *testing.T) {
+ // Create mock plugin
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+
+ // Create a real compiler.Executable with our mock plugin
+ content := createMockExecutable(mockPlugin, "main")
+
+ exe := &script.ExecutableUnit{
+ ID: "test-nil-handler",
+ DataProvider: data.NewContextProvider(constants.EvalData),
+ Content: content,
+ }
+
+ // Create with nil handler
+ evaluator := NewBytecodeEvaluator(nil, exe)
+
+ // Shouldn't panic
+ require.NotNil(t, evaluator)
+ require.NotNil(t, evaluator.logger)
+ require.NotNil(t, evaluator.logHandler)
+ })
+
+ // Test String method
+ t.Run("String method", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ evaluator := NewBytecodeEvaluator(handler, nil)
+
+ // Test the string representation
+ strRep := evaluator.String()
+ require.Equal(t, "extism.BytecodeEvaluator", strRep)
+ })
+
+ // Test the exec helper function
+ t.Run("exec helper", func(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func() (*mockPluginInstance, context.Context, context.CancelFunc)
+ entryPoint string
+ input []byte
+ wantErr bool
+ errContains string
+ }{
+ {
+ name: "successful execution",
+ setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(context.Background())
+ return &mockPluginInstance{
+ exitCode: 0,
+ output: []byte(`{"result": "success", "count": 42}`),
+ }, ctx, cancel
+ },
+ entryPoint: "main",
+ input: []byte(`{"key":"value"}`),
+ wantErr: false,
+ },
+ {
+ name: "non-zero exit code",
+ setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(context.Background())
+ return &mockPluginInstance{
+ exitCode: 1,
+ output: []byte(`{"error": "something went wrong"}`),
+ }, ctx, cancel
+ },
+ entryPoint: "main",
+ input: []byte(`{"key":"value"}`),
+ wantErr: true,
+ errContains: "non-zero exit code",
+ },
+ {
+ name: "execution error",
+ setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(context.Background())
+ return &mockPluginInstance{
+ callErr: errors.New("execution failed"),
+ }, ctx, cancel
+ },
+ entryPoint: "main",
+ input: []byte(`{"key":"value"}`),
+ wantErr: true,
+ errContains: "execution failed",
+ },
+ {
+ name: "context cancellation",
+ setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(context.Background())
+ mock := &mockPluginInstance{
+ cancelFunc: cancel, // This will cancel the context during execution
+ callErr: context.Canceled,
+ }
+ return mock, ctx, cancel
+ },
+ entryPoint: "main",
+ input: []byte(`{"key":"value"}`),
+ wantErr: true,
+ errContains: "cancelled",
+ },
+ }
- _, err := evaluator.Eval(ctx)
- // We expect an error since our mock WASM isn't valid
- assert.Error(t, err)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockInstance, ctx, cancel := tt.setup()
+ defer cancel()
+
+ logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
+ result, execTime, err := execHelper(
+ ctx,
+ logger,
+ mockInstance,
+ tt.entryPoint,
+ tt.input,
+ )
+
+ // Verify the mock was called
+ assert.True(
+ t,
+ mockInstance.wasCalled,
+ "Expected the mock instance to be called",
+ )
+
+ // Check for expected errors
+ if tt.wantErr {
+ assert.Error(t, err)
+ if tt.errContains != "" {
+ assert.Contains(t, err.Error(), tt.errContains)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.NotNil(t, result)
+ }
+
+ // Execution time should always be measured
+ assert.Greater(t, execTime.Nanoseconds(), int64(0))
+ })
+ }
+ })
+ })
}
-func TestPrepareContext(t *testing.T) {
+// TestBytecodeEvaluator_PrepareContext tests the PrepareContext method with various scenarios
+func TestBytecodeEvaluator_PrepareContext(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -254,14 +622,15 @@ func TestPrepareContext(t *testing.T) {
name: "nil data provider",
setupExe: func(t *testing.T) *script.ExecutableUnit {
t.Helper()
+
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+ content := createMockExecutable(mockPlugin, "main")
+
return &script.ExecutableUnit{
ID: "test-nil-provider",
DataProvider: nil,
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D},
- },
+ Content: content,
}
},
inputs: []any{map[string]any{"test": "data"}},
@@ -272,14 +641,15 @@ func TestPrepareContext(t *testing.T) {
name: "valid simple data",
setupExe: func(t *testing.T) *script.ExecutableUnit {
t.Helper()
+
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+ content := createMockExecutable(mockPlugin, "main")
+
return &script.ExecutableUnit{
ID: "test-valid-data",
DataProvider: data.NewContextProvider(constants.EvalData),
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D},
- },
+ Content: content,
}
},
inputs: []any{map[string]any{"test": "data"}},
@@ -289,14 +659,15 @@ func TestPrepareContext(t *testing.T) {
name: "empty input",
setupExe: func(t *testing.T) *script.ExecutableUnit {
t.Helper()
+
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+ content := createMockExecutable(mockPlugin, "main")
+
return &script.ExecutableUnit{
ID: "test-empty-input",
DataProvider: data.NewContextProvider(constants.EvalData),
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D},
- },
+ Content: content,
}
},
inputs: []any{},
@@ -316,17 +687,18 @@ func TestPrepareContext(t *testing.T) {
name: "with error throwing provider",
setupExe: func(t *testing.T) *script.ExecutableUnit {
t.Helper()
+
+ mockPlugin := new(MockCompiledPlugin)
+ mockPlugin.On("Close", mock.Anything).Return(nil)
+ content := createMockExecutable(mockPlugin, "main")
+
mockProvider := &mockErrProvider{
err: errors.New("provider error"),
}
return &script.ExecutableUnit{
ID: "test-err-provider",
DataProvider: mockProvider,
- Content: &mockExecutableContent{
- machineType: machineTypes.Extism,
- source: "test wasm",
- bytecode: []byte{0x00, 0x61, 0x73, 0x6D},
- },
+ Content: content,
}
},
inputs: []any{map[string]any{"test": "data"}},
@@ -336,10 +708,7 @@ func TestPrepareContext(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
handler := slog.NewTextHandler(os.Stdout, nil)
exe := tt.setupExe(t)
evaluator := NewBytecodeEvaluator(handler, exe)
@@ -362,322 +731,3 @@ func TestPrepareContext(t *testing.T) {
})
}
}
-
-// mockErrProvider implements the data.Provider interface and always returns an error
-type mockErrProvider struct {
- err error
-}
-
-func (m *mockErrProvider) GetData(ctx context.Context) (map[string]any, error) {
- return nil, m.err
-}
-
-func (m *mockErrProvider) AddDataToContext(
- ctx context.Context,
- data ...any,
-) (context.Context, error) {
- return ctx, m.err
-}
-
-// mockPluginInstance is a mock implementation of the testPluginInstance interface
-type mockPluginInstance struct {
- exitCode uint32
- output []byte
- callErr error
- closeErr error
- wasCalled bool
- wasClosed bool
- cancelFunc func()
-}
-
-func (m *mockPluginInstance) CallWithContext(
- ctx context.Context,
- functionName string,
- input []byte,
-) (uint32, []byte, error) {
- m.wasCalled = true
- // Execute the cancel function if provided (to simulate context cancellation)
- if m.cancelFunc != nil {
- m.cancelFunc()
- }
- // Check if the context was canceled
- if ctx.Err() != nil {
- return 0, nil, ctx.Err()
- }
- return m.exitCode, m.output, m.callErr
-}
-
-func (m *mockPluginInstance) Close(ctx context.Context) error {
- m.wasClosed = true
- return m.closeErr
-}
-
-func TestExecHelper(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- setup func() (*mockPluginInstance, context.Context, context.CancelFunc)
- entryPoint string
- input []byte
- wantErr bool
- errContains string
- }{
- {
- name: "successful execution with json output",
- setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- return &mockPluginInstance{
- exitCode: 0,
- output: []byte(`{"result": "success", "count": 42}`),
- }, ctx, cancel
- },
- entryPoint: "main",
- input: []byte(`{"key":"value"}`),
- wantErr: false,
- },
- {
- name: "successful execution with string output",
- setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- return &mockPluginInstance{
- exitCode: 0,
- output: []byte(`plain text output`),
- }, ctx, cancel
- },
- entryPoint: "main",
- input: []byte(`{"key":"value"}`),
- wantErr: false,
- },
- {
- name: "non-zero exit code",
- setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- return &mockPluginInstance{
- exitCode: 1,
- output: []byte(`{"error": "something went wrong"}`),
- }, ctx, cancel
- },
- entryPoint: "main",
- input: []byte(`{"key":"value"}`),
- wantErr: true,
- errContains: "non-zero exit code",
- },
- {
- name: "execution error",
- setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- return &mockPluginInstance{
- callErr: errors.New("execution failed"),
- }, ctx, cancel
- },
- entryPoint: "main",
- input: []byte(`{"key":"value"}`),
- wantErr: true,
- errContains: "execution failed",
- },
- {
- name: "context cancellation",
- setup: func() (*mockPluginInstance, context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- mock := &mockPluginInstance{
- cancelFunc: cancel, // This will cancel the context during execution
- callErr: context.Canceled,
- }
- return mock, ctx, cancel
- },
- entryPoint: "main",
- input: []byte(`{"key":"value"}`),
- wantErr: true,
- errContains: "cancelled",
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- mockInstance, ctx, cancel := tt.setup()
- defer cancel()
-
- logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
- result, execTime, err := execHelper(ctx, logger, mockInstance, tt.entryPoint, tt.input)
-
- // Verify the mock was called
- assert.True(t, mockInstance.wasCalled, "Expected the mock instance to be called")
-
- // Check for expected errors
- if tt.wantErr {
- assert.Error(t, err)
- if tt.errContains != "" {
- assert.Contains(t, err.Error(), tt.errContains)
- }
- } else {
- assert.NoError(t, err)
- assert.NotNil(t, result)
- }
-
- // Execution time should always be measured
- assert.Greater(t, execTime.Nanoseconds(), int64(0))
- })
- }
-}
-
-/*
-func TestEvalWithCancelledContext(t *testing.T) {
- // Load the test WASM file
- wasmContent, err := os.ReadFile(testWasmPath)
- require.NoError(t, err, "Failed to read WASM test file")
-
- // Create a temporary directory using the testing package
- tmpDir := t.TempDir()
-
- // Write the test WASM bytes to a file
- wasmFile := filepath.Join(tmpDir, "test.wasm")
- err = os.WriteFile(wasmFile, wasmContent, 0o644)
- require.NoError(t, err, "Failed to write test WASM file")
-
- // Create a mock compiled plugin
- ctx := context.Background()
- compileOpts := compile.WithDefaultCompileSettings()
- compiledPlugin, err := compile.CompileBytes(ctx, wasmContent, compileOpts)
- require.NoError(t, err, "Failed to compile plugin")
-
- // Create our executable
- exec := compile.NewExecutable(wasmContent, compiledPlugin, "greet")
-
- // Create a context provider
- ctxProvider := data.NewContextProvider(constants.EvalData)
-
- // Create a context that we can cancel
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- // Create the executable unit
- execUnit := &script.ExecutableUnit{
- ID: "test-cancellation",
- DataProvider: ctxProvider,
- Content: exec,
- }
-
- // Create handler and evaluator
- handler := slog.NewTextHandler(os.Stdout, nil)
- evaluator := NewBytecodeEvaluator(handler, execUnit)
-
- // Set up context data
- evalData := map[string]any{"name": "TestUser"}
- ctx = context.WithValue(ctx, constants.EvalData, evalData)
-
- // Cancel the context before evaluation
- cancel()
-
- // Try to evaluate with the cancelled context
- _, err = evaluator.Eval(ctx)
-
- // Should get an error (either cancellation or plugin error)
- require.Error(t, err)
-}
-*/
-
-/*
-// TestStaticAndDynamicDataCombination tests how static data and dynamic data are combined
-// with the CompositeProvider
-func TestStaticAndDynamicDataCombination(t *testing.T) {
- t.Skip("Need to confirm behavior of the input_data in ctx")
- // Load the test WASM file
- wasmContent, err := os.ReadFile(testWasmPath)
- require.NoError(t, err, "Failed to read WASM test file")
-
- // Create a mock compiled plugin
- ctx := context.Background()
- compileOpts := compile.WithDefaultCompileSettings()
- compiledPlugin, err := compile.CompileBytes(ctx, wasmContent, compileOpts)
- require.NoError(t, err, "Failed to compile plugin")
-
- // Create our executable
- exec := compiler.NewExecutable(wasmContent, compiledPlugin, "greet")
-
- // Create a context provider for runtime data
- ctxProvider := data.NewContextProvider(constants.EvalData)
-
- // Create static data for compile-time configuration
- staticData := map[string]any{"initial": "value"}
-
- // Create a static provider
- staticProvider := data.NewStaticProvider(staticData)
-
- // Create a composite provider that combines static and context data
- compositeProvider := data.NewCompositeProvider(staticProvider, ctxProvider)
-
- // Create the executable unit with the composite provider
- execUnit := &script.ExecutableUnit{
- ID: "test-data-provider",
- DataProvider: compositeProvider,
- Content: exec,
- }
-
- // Create handler and evaluator
- handler := slog.NewTextHandler(os.Stdout, nil)
- evaluator := NewBytecodeEvaluator(handler, execUnit)
-
- // Create a context
- ctx = context.Background()
-
- // First test: load data with empty context
- result1, err := evaluator.loadInputData(ctx)
- require.NoError(t, err)
- assert.Contains(t, result1, "initial")
- assert.Equal(t, "value", result1["initial"])
-
- // Second test: add data to context and verify it's merged with static data
- inputData := map[string]any{"input": "test input"}
- enrichedCtx, err := evaluator.PrepareContext(ctx, inputData)
- require.NoError(t, err)
-
- result2, err := evaluator.loadInputData(enrichedCtx)
- require.NoError(t, err)
-
- // Static data should still be there at top level
- assert.Contains(t, result2, "initial")
- assert.Equal(t, "value", result2["initial"])
-
- // Runtime data from the ContextProvider is stored under the 'input_data' key
- assert.Contains(t, result2, constants.InputData)
-
- // Extract the input_data map and verify it's the correct type
- dynamicData, ok := result2[constants.InputData].(map[string]any)
- require.True(t, ok, "input_data should be a map")
-
- // Verify our input data was correctly stored in the input_data map
- assert.Contains(t, dynamicData, "input")
- assert.Equal(t, "test input", dynamicData["input"])
-}
-*/
-
-// TestExtismDirectInputFormat tests how input data is formatted for Extism
-func TestExtismDirectInputFormat(t *testing.T) {
- // Create a test map that simulates data from our providers
- inputData := map[string]any{
- "initial": "top-level-value", // Static data at top level
- "input_data": map[string]any{ // Dynamic data nested under input_data
- "input": "API User",
- "request": map[string]any{}, // HTTP request data nested under input_data
- },
- }
-
- // First, log the structure to understand what we're dealing with
- t.Logf("Input data structure: %#v", inputData)
-
- // Convert the input data for Extism
- jsonBytes, err := internal.ConvertToExtismFormat(inputData)
- require.NoError(t, err)
- require.NotNil(t, jsonBytes)
-
- // Log the JSON output
- t.Logf("JSON for Extism: %s", string(jsonBytes))
-
- // Verify current behavior
- expected := `{"initial":"top-level-value","input_data":{"input":"API User","request":{}}}`
- assert.JSONEq(t, expected, string(jsonBytes))
-}
diff --git a/machines/extism/evaluator/response_test.go b/machines/extism/evaluator/response_test.go
index f62c52b..3ab8e85 100644
--- a/machines/extism/evaluator/response_test.go
+++ b/machines/extism/evaluator/response_test.go
@@ -12,179 +12,378 @@ import (
"github.com/stretchr/testify/require"
)
-func TestNewEvalResult(t *testing.T) {
+// TestResponseMethods tests all methods of the EvaluatorResponse interface
+func TestResponseMethods(t *testing.T) {
t.Parallel()
- tests := []struct {
- name string
- value any
- execTime time.Duration
- versionID string
- expectValue any
- }{
- {
- name: "string value",
- value: "hello",
- execTime: 100 * time.Millisecond,
- versionID: "test-1",
- expectValue: "hello",
- },
- {
- name: "int value",
- value: 42,
- execTime: 200 * time.Millisecond,
- versionID: "test-2",
- expectValue: 42,
- },
- {
- name: "bool value",
- value: true,
- execTime: 50 * time.Millisecond,
- versionID: "test-3",
- expectValue: true,
- },
- {
- name: "nil value",
- value: nil,
- execTime: 75 * time.Millisecond,
- versionID: "test-4",
- expectValue: nil,
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
+ t.Run("Creation", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ execTime time.Duration
+ versionID string
+ expectValue any
+ }{
+ {
+ name: "string value",
+ value: "hello",
+ execTime: 100 * time.Millisecond,
+ versionID: "test-1",
+ expectValue: "hello",
+ },
+ {
+ name: "int value",
+ value: 42,
+ execTime: 200 * time.Millisecond,
+ versionID: "test-2",
+ expectValue: 42,
+ },
+ {
+ name: "bool value",
+ value: true,
+ execTime: 50 * time.Millisecond,
+ versionID: "test-3",
+ expectValue: true,
+ },
+ {
+ name: "nil value",
+ value: nil,
+ execTime: 75 * time.Millisecond,
+ versionID: "test-4",
+ expectValue: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)
+ require.NotNil(t, result)
+ assert.Equal(t, tt.expectValue, result.value)
+ assert.Equal(t, tt.execTime, result.execTime)
+ assert.Equal(t, tt.versionID, result.scriptExeID)
+ require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
+ })
+ }
+ })
+
+ t.Run("Type", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ expected data.Types
+ }{
+ {"nil value", nil, data.NONE},
+ {"bool value", true, data.BOOL},
+ {"int32 value", int32(42), data.INT},
+ {"int64 value", int64(42), data.INT},
+ {"uint32 value", uint32(42), data.INT},
+ {"uint64 value", uint64(42), data.INT},
+ {"float32 value", float32(3.14), data.FLOAT},
+ {"float64 value", float64(3.14), data.FLOAT},
+ {"string value", "hello", data.STRING},
+ {"empty list", []any{}, data.LIST},
+ {"list value", []any{1, 2, 3}, data.LIST},
+ {"empty dict", map[string]any{}, data.MAP},
+ {"dict value", map[string]any{"key": "value"}, data.MAP},
+ {"complex dict", map[string]any{
+ "str": "value",
+ "num": 42,
+ "bool": true,
+ "list": []any{1, 2, 3},
+ "inner": map[string]any{"key": "value"},
+ }, data.MAP},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, time.Second, "test-1")
+ assert.Equal(t, tt.expected, result.Type())
+ })
+ }
+
+ t.Run("unknown type", func(t *testing.T) {
+ // Create a custom type
+ type CustomType struct {
+ Field string
+ }
+
+ // Create result with unknown type
+ customValue := CustomType{Field: "test"}
handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)
- require.NotNil(t, result)
- assert.Equal(t, tt.expectValue, result.value)
- assert.Equal(t, tt.execTime, result.execTime)
- assert.Equal(t, tt.versionID, result.scriptExeID)
- require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
+ result := newEvalResult(handler, customValue, time.Second, "test-id")
+
+ // Should return ERROR for unknown types
+ assert.Equal(t, data.ERROR, result.Type())
})
- }
-}
+ })
-func TestExecResult_Type(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- value any
- expected data.Types
- }{
- {"nil value", nil, data.NONE},
- {"bool value", true, data.BOOL},
- {"int32 value", int32(42), data.INT},
- {"int64 value", int64(42), data.INT},
- {"uint32 value", uint32(42), data.INT},
- {"uint64 value", uint64(42), data.INT},
- {"float32 value", float32(3.14), data.FLOAT},
- {"float64 value", float64(3.14), data.FLOAT},
- {"string value", "hello", data.STRING},
- {"empty list", []any{}, data.LIST},
- {"list value", []any{1, 2, 3}, data.LIST},
- {"empty dict", map[string]any{}, data.MAP},
- {"dict value", map[string]any{"key": "value"}, data.MAP},
- {"complex dict", map[string]any{
- "str": "value",
- "num": 42,
- "bool": true,
- "list": []any{1, 2, 3},
- "inner": map[string]any{"key": "value"},
- }, data.MAP},
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, tt.value, time.Second, "test-1")
- assert.Equal(t, tt.expected, result.Type())
+ t.Run("String", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ execTime time.Duration
+ versionID string
+ expected string
+ }{
+ {
+ name: "string value",
+ value: "hello",
+ execTime: 100 * time.Millisecond,
+ versionID: "v1.0.0",
+ expected: "execResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
+ },
+ {
+ name: "int32 value",
+ value: int32(42),
+ execTime: 200 * time.Millisecond,
+ versionID: "v2.0.0",
+ expected: "execResult{Type: int, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
+ },
+ {
+ name: "float64 value",
+ value: float64(3.14),
+ execTime: 300 * time.Millisecond,
+ versionID: "v3.0.0",
+ expected: "execResult{Type: float, Value: 3.14, ExecTime: 300ms, ScriptExeID: v3.0.0}",
+ },
+ {
+ name: "nil value",
+ value: nil,
+ execTime: 50 * time.Millisecond,
+ versionID: "v4.0.0",
+ expected: "execResult{Type: none, Value: , ExecTime: 50ms, ScriptExeID: v4.0.0}",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)
+ assert.Equal(t, tt.expected, result.String())
+ })
+ }
+
+ t.Run("string representation coverage", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ valueTypeString string
+ }{
+ {"nil value", nil, "none"},
+ {"string value", "test", "string"},
+ {"int value", int32(42), "int"}, // Use int32 instead of int to match implementation
+ {"float value", 3.14, "float"},
+ {"bool value", true, "bool"},
+ {"map value", map[string]any{"key": "value"}, "map"},
+ {"list value", []any{1, 2, 3}, "list"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, 100*time.Millisecond, "test-123")
+
+ // Check string method
+ strResult := result.String()
+
+ // Should contain all essential information
+ assert.Contains(t, strResult, "execResult")
+ assert.Contains(t, strResult, tt.valueTypeString)
+ assert.Contains(t, strResult, "100ms")
+ assert.Contains(t, strResult, "test-123")
+ })
+ }
})
- }
-}
+ })
+
+ t.Run("Inspect", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ expected string
+ }{
+ {"string value", "hello", "hello"},
+ {"int value", 42, "42"},
+ {"bool value", true, "true"},
+ {"nil value", nil, ""},
+ {"float value", 3.14159, "3.14159"},
+ {"list value", []any{1, 2, 3}, "[1 2 3]"},
+ {"dict value", map[string]any{"key": "value"}, "{\"key\":\"value\"}"},
+ {"complex dict", map[string]any{
+ "num": 42,
+ "str": "test",
+ "bool": true,
+ }, "{\"bool\":true,\"num\":42,\"str\":\"test\"}"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, time.Second, "test-1")
+ assert.Equal(t, tt.expected, result.Inspect())
+ })
+ }
+
+ t.Run("with invalid JSON", func(t *testing.T) {
+ // Create a map with a value that can't be marshaled to JSON
+ badMap := map[string]any{
+ "fn": func() {}, // Functions can't be marshaled to JSON
+ }
-func TestExecResult_String(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- value any
- execTime time.Duration
- versionID string
- expected string
- }{
- {
- name: "string value",
- value: "hello",
- execTime: 100 * time.Millisecond,
- versionID: "v1.0.0",
- expected: "execResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
- },
- {
- name: "int32 value",
- value: int32(42),
- execTime: 200 * time.Millisecond,
- versionID: "v2.0.0",
- expected: "execResult{Type: int, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
- },
- {
- name: "float64 value",
- value: float64(3.14),
- execTime: 300 * time.Millisecond,
- versionID: "v3.0.0",
- expected: "execResult{Type: float, Value: 3.14, ExecTime: 300ms, ScriptExeID: v3.0.0}",
- },
- {
- name: "nil value",
- value: nil,
- execTime: 50 * time.Millisecond,
- versionID: "v4.0.0",
- expected: "execResult{Type: none, Value: , ExecTime: 50ms, ScriptExeID: v4.0.0}",
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)
- assert.Equal(t, tt.expected, result.String())
+ result := newEvalResult(handler, badMap, time.Second, "test-id")
+
+ // Should fall back to default string representation
+ inspectResult := result.Inspect()
+ assert.Contains(t, inspectResult, "map[")
})
- }
-}
-func TestExecResult_Inspect(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- value any
- expected string
- }{
- {"string value", "hello", "hello"},
- {"int value", 42, "42"},
- {"bool value", true, "true"},
- {"nil value", nil, ""},
- {"float value", 3.14159, "3.14159"},
- {"list value", []any{1, 2, 3}, "[1 2 3]"},
- {"dict value", map[string]any{"key": "value"}, "{\"key\":\"value\"}"},
- {"complex dict", map[string]any{
- "num": 42,
- "str": "test",
- "bool": true,
- }, "{\"bool\":true,\"num\":42,\"str\":\"test\"}"},
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
+ t.Run("nested complex values", func(t *testing.T) {
+ // Create nested complex data structure
+ complexValue := map[string]any{
+ "string": "text",
+ "number": 42,
+ "boolean": true,
+ "null": nil,
+ "array": []any{1, "two", true},
+ "map": map[string]any{"nested": "value"},
+ }
+
handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, tt.value, time.Second, "test-1")
- assert.Equal(t, tt.expected, result.Inspect())
+ result := newEvalResult(handler, complexValue, time.Second, "test-id")
+
+ // Type should be MAP
+ assert.Equal(t, data.MAP, result.Type())
+
+ // Inspect should convert to JSON
+ inspectResult := result.Inspect()
+ require.Contains(t, inspectResult, "string")
+ require.Contains(t, inspectResult, "text")
+ require.Contains(t, inspectResult, "number")
+ require.Contains(t, inspectResult, "42")
+ require.Contains(t, inspectResult, "boolean")
+ require.Contains(t, inspectResult, "true")
+ require.Contains(t, inspectResult, "null")
+ require.Contains(t, inspectResult, "array")
+ require.Contains(t, inspectResult, "map")
+ require.Contains(t, inspectResult, "nested")
+ require.Contains(t, inspectResult, "value")
+
+ // Interface should return the original complex structure
+ assert.Equal(t, complexValue, result.Interface())
})
- }
+ })
+
+ t.Run("Interface", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ expectedValue any
+ }{
+ {"nil value", nil, nil},
+ {"string value", "test", "test"},
+ {"int value", 42, 42},
+ {"bool value", true, true},
+ {"map value", map[string]any{"key": "value"}, map[string]any{"key": "value"}},
+ {"list value", []any{1, 2, 3}, []any{1, 2, 3}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, time.Second, "test-id")
+
+ // Interface should return the original value
+ assert.Equal(t, tt.expectedValue, result.Interface())
+ })
+ }
+ })
+
+ t.Run("Metadata", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "short execution time",
+ value: "test string",
+ execTime: 123 * time.Millisecond,
+ versionID: "test-script-9876",
+ },
+ {
+ name: "long execution time",
+ value: 42,
+ execTime: 3 * time.Second,
+ versionID: "test-script-1234",
+ },
+ {
+ name: "microsecond execution time",
+ value: true,
+ execTime: 500 * time.Microsecond,
+ versionID: "test-script-5678",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, tt.value, tt.execTime, tt.versionID)
+
+ // Test GetScriptExeID
+ assert.Equal(t, tt.versionID, result.GetScriptExeID())
+
+ // Test GetExecTime
+ assert.Equal(t, tt.execTime.String(), result.GetExecTime())
+ })
+ }
+ })
+
+ t.Run("NilHandler", func(t *testing.T) {
+ tests := []struct {
+ name string
+ value any
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "string value",
+ value: "test value",
+ execTime: 100 * time.Millisecond,
+ versionID: "test-id",
+ },
+ {
+ name: "numeric value",
+ value: 42,
+ execTime: 2 * time.Second,
+ versionID: "numeric-test-id",
+ },
+ {
+ name: "boolean value",
+ value: true,
+ execTime: 50 * time.Millisecond,
+ versionID: "boolean-test-id",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create with nil handler
+ result := newEvalResult(nil, tt.value, tt.execTime, tt.versionID)
+
+ // Should create default handler and logger
+ require.NotNil(t, result)
+ require.NotNil(t, result.logHandler)
+ require.NotNil(t, result.logger)
+
+ // Should still store all values correctly
+ assert.Equal(t, tt.value, result.value)
+ assert.Equal(t, tt.execTime, result.execTime)
+ assert.Equal(t, tt.versionID, result.scriptExeID)
+ })
+ }
+ })
}
diff --git a/machines/extism/internal/converters_test.go b/machines/extism/internal/converters_test.go
index a54e92b..9d2a850 100644
--- a/machines/extism/internal/converters_test.go
+++ b/machines/extism/internal/converters_test.go
@@ -60,9 +60,7 @@ func TestConvertToExtismFormat(t *testing.T) {
}
for _, tt := range tests {
- tt := tt
t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
result, err := ConvertToExtismFormat(tt.input)
if tt.wantErr {
diff --git a/machines/extism/testdata/integration_test.go b/machines/extism/testdata/integration_test.go
index 86f56f9..bc8c982 100644
--- a/machines/extism/testdata/integration_test.go
+++ b/machines/extism/testdata/integration_test.go
@@ -18,6 +18,7 @@ import (
var testWasmBytes []byte
func TestExtismWasmIntegration(t *testing.T) {
+ t.Parallel()
// Create manifest from wasm bytes
manifest := extismSDK.Manifest{
Wasm: []extismSDK.Wasm{
diff --git a/machines/mocks/evaluatorResponse_test.go b/machines/mocks/evaluatorResponse_test.go
index 5bcc4ea..af5e63b 100644
--- a/machines/mocks/evaluatorResponse_test.go
+++ b/machines/mocks/evaluatorResponse_test.go
@@ -11,12 +11,14 @@ import (
// TestEvaluatorResponseImplementsInterface verifies at compile time
// that our mock EvaluatorResponse implements the engine.EvaluatorResponse interface.
func TestEvaluatorResponseImplementsInterface(t *testing.T) {
+ t.Parallel()
// This is a compile-time check - if it doesn't compile, the test fails
var _ engine.EvaluatorResponse = (*EvaluatorResponse)(nil)
}
// TestEvaluatorResponseType tests the Type method for different value types
func TestEvaluatorResponseType(t *testing.T) {
+ t.Parallel()
tests := []struct {
name string
mockVal any
@@ -71,6 +73,7 @@ func TestEvaluatorResponseType(t *testing.T) {
// TestEvaluatorResponseInspect tests the Inspect method
func TestEvaluatorResponseInspect(t *testing.T) {
+ t.Parallel()
mockResp := new(EvaluatorResponse)
expected := "test string representation"
@@ -89,6 +92,7 @@ func TestEvaluatorResponseInspect(t *testing.T) {
// TestEvaluatorResponseInterface tests the Interface method
func TestEvaluatorResponseInterface(t *testing.T) {
+ t.Parallel()
tests := []struct {
name string
mockVal any
@@ -138,6 +142,7 @@ func TestEvaluatorResponseInterface(t *testing.T) {
// TestEvaluatorResponseScriptExeID tests the GetScriptExeID method
func TestEvaluatorResponseScriptExeID(t *testing.T) {
+ t.Parallel()
mockResp := new(EvaluatorResponse)
expected := "script-v1.0.0"
@@ -156,6 +161,7 @@ func TestEvaluatorResponseScriptExeID(t *testing.T) {
// TestEvaluatorResponseExecTime tests the GetExecTime method
func TestEvaluatorResponseExecTime(t *testing.T) {
+ t.Parallel()
mockResp := new(EvaluatorResponse)
expected := "100ms"
@@ -174,6 +180,7 @@ func TestEvaluatorResponseExecTime(t *testing.T) {
// TestEvaluatorResponsePanicOnInvalidType tests the Type method when an invalid type is provided
func TestEvaluatorResponsePanicOnInvalidType(t *testing.T) {
+ t.Parallel()
// Create the mock
mockResp := new(EvaluatorResponse)
@@ -188,6 +195,7 @@ func TestEvaluatorResponsePanicOnInvalidType(t *testing.T) {
// TestEvaluatorResponseFullUsage tests all methods together in a realistic usage scenario
func TestEvaluatorResponseFullUsage(t *testing.T) {
+ t.Parallel()
// Create the mock
mockResp := new(EvaluatorResponse)
diff --git a/machines/mocks/evaluator_test.go b/machines/mocks/evaluator_test.go
index c0ed3b5..481b024 100644
--- a/machines/mocks/evaluator_test.go
+++ b/machines/mocks/evaluator_test.go
@@ -9,6 +9,7 @@ import (
// TestEvaluatorImplementsEvaluatorWithPrep verifies at compile time
// that our mock Evaluator implements the EvaluatorWithPrep interface.
func TestEvaluatorImplementsEvaluatorWithPrep(t *testing.T) {
+ t.Parallel()
// This is a compile-time check - if it doesn't compile, the test fails
var _ engine.EvaluatorWithPrep = (*Evaluator)(nil)
}
diff --git a/machines/risor/compiler/compiler_test.go b/machines/risor/compiler/compiler_test.go
index 7292729..5245c16 100644
--- a/machines/risor/compiler/compiler_test.go
+++ b/machines/risor/compiler/compiler_test.go
@@ -40,91 +40,66 @@ func (m *mockScriptReaderCloser) Close() error {
return args.Error(0)
}
-type testCase struct {
- name string
- script string
- globals []string
- err error
-}
-
-// execute a single unit test
-func runTestCase(t *testing.T, tt testCase) {
- t.Helper()
+func TestNewCompiler(t *testing.T) {
t.Parallel()
- // Create compiler with options
- comp, err := NewCompiler(
- WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
- WithGlobals(tt.globals),
- )
- require.NoError(t, err, "Failed to create compiler")
-
- reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
- if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
- mockReader.On("Close").Return(nil)
- } else {
- t.Fatal("Failed to create mock reader")
- }
-
- // Execute test
- execContent, err := comp.Compile(reader)
-
- if tt.err != nil {
- require.Error(t, err, "Expected an error but got none")
- require.Nil(t, execContent, "Expected execContent to be nil")
- require.True(t, errors.Is(err, tt.err), "Expected error %v, got %v", tt.err, err)
- return
- }
+ t.Run("basic creation", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ require.Equal(t, "risor.Compiler", comp.String())
+ })
- require.NoError(t, err, "Did not expect an error but got one")
- require.NotNil(t, execContent, "Expected execContent to be non-nil")
- require.Equal(t, tt.script, execContent.GetSource(), "Script content does not match")
+ t.Run("with globals", func(t *testing.T) {
+ globals := []string{"request", "response"}
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(globals),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
- // Check that the bytecode is correct
- risorExec, ok := execContent.(*executable)
- require.True(t, ok, "Expected execContent to be a *Executable")
- require.NotNil(t, risorExec.GetRisorByteCode(), "Expected bytecode to be non-nil")
+ t.Run("with logger", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+ logger := slog.New(handler)
+ comp, err := NewCompiler(WithLogger(logger))
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
- // Verify mock expectations
- if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
- mockReader.AssertExpectations(t)
- }
+ t.Run("defaults", func(t *testing.T) {
+ comp, err := NewCompiler()
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
}
-func TestCompiler(t *testing.T) {
+func TestCompiler_Compile(t *testing.T) {
t.Parallel()
- tests := []testCase{
- {
- name: "valid script",
- script: `print("Hello, World!")`,
- globals: []string{"request"},
- },
- {
- name: "syntax error - missing closing parenthesis",
- script: `print("Hello, World!"`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- {
- name: "empty script",
- script: ``,
- globals: []string{"request"},
- err: ErrContentNil,
- },
- {
- name: "undefined global",
- script: `print(undefined_global)`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- {
- name: "with multiple globals",
- script: `print(request, response)`,
- globals: []string{"request", "response"},
- },
- {
- name: "complex valid script with global override",
- script: `
+
+ t.Run("success cases", func(t *testing.T) {
+ successTests := []struct {
+ name string
+ script string
+ globals []string
+ }{
+ {
+ name: "valid script",
+ script: `print("Hello, World!")`,
+ globals: []string{"request"},
+ },
+ {
+ name: "with multiple globals",
+ script: `print(request, response)`,
+ globals: []string{"request", "response"},
+ },
+ {
+ name: "complex valid script with global override",
+ script: `
request = true
func main() {
if request {
@@ -135,11 +110,11 @@ func main() {
}
main()
`,
- globals: []string{"request"},
- },
- {
- name: "complex valid script with condition",
- script: `
+ globals: []string{"request"},
+ },
+ {
+ name: "complex valid script with condition",
+ script: `
func main() {
if condition {
print("Yes")
@@ -149,22 +124,190 @@ func main() {
}
main()
`,
- globals: []string{"condition"},
- },
- {
- name: "script using undefined global",
- script: `print(undefined)`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- }
+ globals: []string{"condition"},
+ },
+ }
+
+ for _, tt := range successTests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create compiler with options
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(tt.globals),
+ )
+ require.NoError(t, err, "Failed to create compiler")
+
+ reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.On("Close").Return(nil)
+ } else {
+ t.Fatal("Failed to create mock reader")
+ }
+
+ // Execute test
+ execContent, err := comp.Compile(reader)
+ require.NoError(t, err, "Did not expect an error but got one")
+ require.NotNil(t, execContent, "Expected execContent to be non-nil")
+ require.Equal(
+ t,
+ tt.script,
+ execContent.GetSource(),
+ "Script content does not match",
+ )
+
+ // Check that the bytecode is correct
+ risorExec, ok := execContent.(*executable)
+ require.True(t, ok, "Expected execContent to be a *Executable")
+ require.NotNil(t, risorExec.GetRisorByteCode(), "Expected bytecode to be non-nil")
+
+ // Verify mock expectations
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.AssertExpectations(t)
+ }
+ })
+ }
+ })
+
+ t.Run("error cases", func(t *testing.T) {
+ errorTests := []struct {
+ name string
+ script string
+ globals []string
+ err error
+ }{
+ {
+ name: "syntax error - missing closing parenthesis",
+ script: `print("Hello, World!"`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ {
+ name: "empty script",
+ script: ``,
+ globals: []string{"request"},
+ err: ErrContentNil,
+ },
+ {
+ name: "undefined global",
+ script: `print(undefined_global)`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ {
+ name: "script using undefined global",
+ script: `print(undefined)`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ }
+
+ for _, tt := range errorTests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create compiler with options
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(tt.globals),
+ )
+ require.NoError(t, err, "Failed to create compiler")
+
+ reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.On("Close").Return(nil)
+ } else {
+ t.Fatal("Failed to create mock reader")
+ }
+
+ // Execute test
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.True(t, errors.Is(err, tt.err), "Expected error %v, got %v", tt.err, err)
+
+ // Verify mock expectations
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.AssertExpectations(t)
+ }
+ })
+ }
+
+ t.Run("nil reader", func(t *testing.T) {
+ comp, err := NewCompiler(WithLogHandler(slog.NewTextHandler(os.Stdout, nil)))
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- runTestCase(t, tt)
+ execContent, err := comp.Compile(nil)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.True(t, errors.Is(err, ErrContentNil), "Expected error to be ErrContentNil")
})
- }
+
+ t.Run("io error", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals([]string{"ctx"}),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
+
+ // Create a reader that will return an error
+ reader := &mockErrorReader{}
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.Contains(
+ t,
+ err.Error(),
+ "failed to read script",
+ "Expected error to contain 'failed to read script'",
+ )
+ })
+
+ t.Run("close error", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
+
+ // Create a reader that will return an error on close
+ reader := newMockScriptReaderCloser(`print("Hello, World!")`)
+ reader.On("Close").Return(errors.New("test error")).Once()
+
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.Contains(
+ t,
+ err.Error(),
+ "failed to close reader",
+ "Expected error to contain 'failed to close reader'",
+ )
+ })
+ })
+
+ t.Run("direct bytecode compilation", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals([]string{"ctx"}),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
+
+ // Here we test that we can directly call the compile method with a byteslice
+ scriptBytes := []byte(`print("Hello, World!")`)
+ executable, err := comp.compile(scriptBytes)
+ require.NoError(t, err, "Did not expect an error but got one")
+ require.NotNil(t, executable, "Expected execContent to be non-nil")
+ require.Equal(
+ t,
+ string(scriptBytes),
+ executable.GetSource(),
+ "Script content does not match",
+ )
+
+ // Check that the bytecode is valid
+ require.NotNil(t, executable.GetRisorByteCode(), "Expected bytecode to be non-nil")
+ })
}
func TestCompilerOptions(t *testing.T) {
diff --git a/machines/risor/compiler/executable_test.go b/machines/risor/compiler/executable_test.go
index ec00d8e..793d0f1 100644
--- a/machines/risor/compiler/executable_test.go
+++ b/machines/risor/compiler/executable_test.go
@@ -4,106 +4,76 @@ import (
"testing"
risorCompiler "github.com/risor-io/risor/compiler"
+ machineTypes "github.com/robbyt/go-polyscript/machines/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-// TestNewExecutableValid tests creating an Executable with valid content and bytecode
-func TestNewExecutableValid(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &risorCompiler.Code{}
-
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
- assert.Equal(t, content, executable.GetSource())
- assert.Equal(t, bytecode, executable.GetByteCode())
- assert.Equal(t, bytecode, executable.GetRisorByteCode())
-}
-
-// TestNewExecutableNilContent tests creating an Executable with nil content
-func TestNewExecutableNilContent(t *testing.T) {
- bytecode := &risorCompiler.Code{}
-
- executable := newExecutable(nil, bytecode)
- require.Nil(t, executable)
-}
-
-// TestNewExecutableNilByteCode tests creating an Executable with nil bytecode
-func TestNewExecutableNilByteCode(t *testing.T) {
- content := "print('Hello, World!')"
-
- executable := newExecutable([]byte(content), nil)
- require.Nil(t, executable)
-}
-
-// TestNewExecutableNilContentAndByteCode tests creating an Executable with nil content and bytecode
-func TestNewExecutableNilContentAndByteCode(t *testing.T) {
- executable := newExecutable(nil, nil)
- require.Nil(t, executable)
-}
-
-// TestExecutable_GetBody tests the GetBody method of Executable
-func TestExecutable_GetBody(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &risorCompiler.Code{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- body := executable.GetSource()
- assert.Equal(t, content, body)
-}
-
-// TestExecutable_GetByteCode tests the GetByteCode method of Executable
-func TestExecutable_GetByteCode(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &risorCompiler.Code{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- code := executable.GetByteCode()
- assert.Equal(t, bytecode, code)
-
- // Test type assertion
- _, ok := code.(*risorCompiler.Code)
- assert.True(t, ok)
-}
-
-// TestExecutable_GetRisorByteCode tests the GetRisorByteCode method of Executable
-func TestExecutable_GetRisorByteCode(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &risorCompiler.Code{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- code := executable.GetRisorByteCode()
- assert.Equal(t, bytecode, code)
-}
-
-func TestNewExecutable(t *testing.T) {
- t.Run("valid creation", func(t *testing.T) {
- content := "print('test')"
- bytecode := &risorCompiler.Code{}
-
- exe := newExecutable([]byte(content), bytecode)
- require.NotNil(t, exe)
- assert.Equal(t, content, exe.GetSource())
- assert.Equal(t, bytecode, exe.ByteCode)
+// TestExecutable tests the functionality of Executable
+func TestExecutable(t *testing.T) {
+ t.Parallel()
+
+ // Test creation scenarios
+ t.Run("Creation", func(t *testing.T) {
+ t.Run("valid creation", func(t *testing.T) {
+ content := "print('Hello, World!')"
+ bytecode := &risorCompiler.Code{}
+
+ exe := newExecutable([]byte(content), bytecode)
+ require.NotNil(t, exe)
+ assert.Equal(t, content, exe.GetSource())
+ assert.Equal(t, bytecode, exe.GetByteCode())
+ assert.Equal(t, bytecode, exe.GetRisorByteCode())
+ assert.Equal(t, machineTypes.Risor, exe.GetMachineType())
+ })
+
+ t.Run("nil content", func(t *testing.T) {
+ bytecode := &risorCompiler.Code{}
+ exe := newExecutable(nil, bytecode)
+ assert.Nil(t, exe)
+ })
+
+ t.Run("nil bytecode", func(t *testing.T) {
+ content := "print('test')"
+ exe := newExecutable([]byte(content), nil)
+ assert.Nil(t, exe)
+ })
+
+ t.Run("both nil", func(t *testing.T) {
+ exe := newExecutable(nil, nil)
+ assert.Nil(t, exe)
+ })
})
- t.Run("nil content", func(t *testing.T) {
+ // Test getters
+ t.Run("Getters", func(t *testing.T) {
+ content := "print('Hello, World!')"
bytecode := &risorCompiler.Code{}
- exe := newExecutable(nil, bytecode)
- assert.Nil(t, exe)
- })
-
- t.Run("nil bytecode", func(t *testing.T) {
- content := "print('test')"
- exe := newExecutable([]byte(content), nil)
- assert.Nil(t, exe)
- })
-
- t.Run("both nil", func(t *testing.T) {
- exe := newExecutable(nil, nil)
- assert.Nil(t, exe)
+ executable := newExecutable([]byte(content), bytecode)
+ require.NotNil(t, executable)
+
+ t.Run("GetSource", func(t *testing.T) {
+ source := executable.GetSource()
+ assert.Equal(t, content, source)
+ })
+
+ t.Run("GetByteCode", func(t *testing.T) {
+ code := executable.GetByteCode()
+ assert.Equal(t, bytecode, code)
+
+ // Test type assertion
+ _, ok := code.(*risorCompiler.Code)
+ assert.True(t, ok)
+ })
+
+ t.Run("GetRisorByteCode", func(t *testing.T) {
+ code := executable.GetRisorByteCode()
+ assert.Equal(t, bytecode, code)
+ })
+
+ t.Run("GetMachineType", func(t *testing.T) {
+ machineType := executable.GetMachineType()
+ assert.Equal(t, machineTypes.Risor, machineType)
+ })
})
}
diff --git a/machines/risor/compiler/options_test.go b/machines/risor/compiler/options_test.go
index 4a2dc2b..7e3703c 100644
--- a/machines/risor/compiler/options_test.go
+++ b/machines/risor/compiler/options_test.go
@@ -9,276 +9,305 @@ import (
"github.com/stretchr/testify/require"
)
-func TestWithGlobals(t *testing.T) {
- // Test that WithGlobals properly sets the globals field
- globals := []string{"ctx", "print"}
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithGlobals(globals)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, globals, c.globals)
-
- // Test with nil globals
- c = &Compiler{}
- c.applyDefaults()
- nilOpt := WithGlobals(nil)
- err = nilOpt(c)
-
- require.NoError(t, err)
- require.Nil(t, c.globals)
-
- // Test with empty globals
- c = &Compiler{}
- c.applyDefaults()
- emptyOpt := WithGlobals([]string{})
- err = emptyOpt(c)
-
- require.NoError(t, err)
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
-}
-
-func TestWithCtxGlobal(t *testing.T) {
- // Test with empty globals
- c1 := &Compiler{globals: []string{}}
- opt := WithCtxGlobal()
- err := opt(c1)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx}, c1.globals)
-
- // Test with existing globals not containing ctx
- c2 := &Compiler{globals: []string{"request", "response"}}
- err = opt(c2)
-
- require.NoError(t, err)
- require.Equal(t, []string{"request", "response", constants.Ctx}, c2.globals)
-
- // Test with globals already containing ctx
- c3 := &Compiler{globals: []string{constants.Ctx, "request"}}
- err = opt(c3)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx, "request"}, c3.globals)
- require.Len(t, c3.globals, 2) // Should not add duplicate
-
- // Test with nil globals
- c4 := &Compiler{globals: nil}
- err = opt(c4)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx}, c4.globals)
-}
-
-func TestLoggerConfiguration(t *testing.T) {
- t.Run("default initialization", func(t *testing.T) {
- // Create a compiler with default settings
- c, err := NewCompiler()
- require.NoError(t, err)
-
- // Verify that both logHandler and logger are set
- require.NotNil(t, c.logHandler, "logHandler should be initialized")
- require.NotNil(t, c.logger, "logger should be initialized")
- })
-
- t.Run("with explicit log handler", func(t *testing.T) {
- // Create a custom handler
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
-
- // Create compiler with the handler
- c, err := NewCompiler(WithLogHandler(customHandler))
- require.NoError(t, err)
-
- // Verify handler was set and used to create logger
- require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
- require.NotNil(t, c.logger, "logger should be created from handler")
-
- // Test logging works with the custom handler
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with explicit logger", func(t *testing.T) {
- // Create a custom logger
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
- customLogger := slog.New(customHandler)
-
- // Create compiler with the logger
- c, err := NewCompiler(WithLogger(customLogger))
- require.NoError(t, err)
-
- // Verify logger was set
- require.Equal(t, customLogger, c.logger, "custom logger should be set")
- require.NotNil(t, c.logHandler, "handler should be extracted from logger")
-
- // Test logging works with the custom logger
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with both logger options, last one wins", func(t *testing.T) {
- // Create two buffers to verify which one receives logs
- var handlerBuf, loggerBuf bytes.Buffer
- customHandler := slog.NewTextHandler(&handlerBuf, nil)
- customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
-
- // Case 1: Handler then Logger
- c1, err := NewCompiler(
- WithLogHandler(customHandler),
- WithLogger(customLogger),
- )
- require.NoError(t, err)
- require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
- c1.logger.Info("test message")
- require.Contains(t, loggerBuf.String(), "test message", "logger buffer should receive logs")
- require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
-
- // Clear buffers
- handlerBuf.Reset()
- loggerBuf.Reset()
-
- // Case 2: Logger then Handler
- c2, err := NewCompiler(
- WithLogger(customLogger),
- WithLogHandler(customHandler),
- )
- require.NoError(t, err)
- require.Equal(t, customHandler, c2.logHandler, "handler option should take precedence")
- c2.logger.Info("test message")
- require.Contains(
- t,
- handlerBuf.String(),
- "test message",
- "handler buffer should receive logs",
- )
- require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
- })
-}
-
-func TestWithLogHandler(t *testing.T) {
- // Test that WithLogHandler properly sets the handler field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogHandler(handler)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, handler, c.logHandler)
- require.Nil(t, c.logger) // Should clear Logger field
-
- // Test with nil handler
- nilOpt := WithLogHandler(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "log handler cannot be nil")
-}
-
-func TestWithLogger(t *testing.T) {
- // Test that WithLogger properly sets the logger field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
- logger := slog.New(handler)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogger(logger)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, logger, c.logger)
- require.Nil(t, c.logHandler) // Should clear LogHandler field
-
- // Test with nil logger
- nilOpt := WithLogger(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "logger cannot be nil")
-}
-
-func TestApplyDefaults(t *testing.T) {
- t.Run("empty compiler", func(t *testing.T) {
- // Test that defaults are properly applied to an empty compiler
- c := &Compiler{}
- c.applyDefaults()
-
- require.NotNil(t, c.logHandler)
- require.Nil(t, c.logger)
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+// TestCompilerOptionsDetailed tests all compiler options functionality in detail
+func TestCompilerOptionsDetailed(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Globals", func(t *testing.T) {
+ t.Run("WithGlobals", func(t *testing.T) {
+ t.Run("valid globals", func(t *testing.T) {
+ globals := []string{"ctx", "print"}
+
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithGlobals(globals)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, globals, c.globals)
+ })
+
+ t.Run("nil globals", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithGlobals(nil)
+ err := nilOpt(c)
+
+ require.NoError(t, err)
+ require.Nil(t, c.globals)
+ })
+
+ t.Run("empty globals", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ emptyOpt := WithGlobals([]string{})
+ err := emptyOpt(c)
+
+ require.NoError(t, err)
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+ })
+
+ t.Run("WithCtxGlobal", func(t *testing.T) {
+ opt := WithCtxGlobal()
+
+ t.Run("empty globals", func(t *testing.T) {
+ c1 := &Compiler{globals: []string{}}
+ err := opt(c1)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx}, c1.globals)
+ })
+
+ t.Run("existing globals without ctx", func(t *testing.T) {
+ c2 := &Compiler{globals: []string{"request", "response"}}
+ err := opt(c2)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{"request", "response", constants.Ctx}, c2.globals)
+ })
+
+ t.Run("already contains ctx", func(t *testing.T) {
+ c3 := &Compiler{globals: []string{constants.Ctx, "request"}}
+ err := opt(c3)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx, "request"}, c3.globals)
+ require.Len(t, c3.globals, 2) // Should not add duplicate
+ })
+
+ t.Run("nil globals", func(t *testing.T) {
+ c4 := &Compiler{globals: nil}
+ err := opt(c4)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx}, c4.globals)
+ })
+ })
})
- t.Run("nil globals", func(t *testing.T) {
- // Test with a nil globals field
- c := &Compiler{
- globals: nil,
- }
- c.applyDefaults()
-
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+ t.Run("Logger", func(t *testing.T) {
+ t.Run("default initialization", func(t *testing.T) {
+ c, err := NewCompiler()
+ require.NoError(t, err)
+
+ require.NotNil(t, c.logHandler, "logHandler should be initialized")
+ require.NotNil(t, c.logger, "logger should be initialized")
+ })
+
+ t.Run("with explicit log handler", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+
+ c, err := NewCompiler(WithLogHandler(customHandler))
+ require.NoError(t, err)
+
+ require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
+ require.NotNil(t, c.logger, "logger should be created from handler")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("with explicit logger", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+ customLogger := slog.New(customHandler)
+
+ c, err := NewCompiler(WithLogger(customLogger))
+ require.NoError(t, err)
+
+ require.Equal(t, customLogger, c.logger, "custom logger should be set")
+ require.NotNil(t, c.logHandler, "handler should be extracted from logger")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("option precedence", func(t *testing.T) {
+ var handlerBuf, loggerBuf bytes.Buffer
+ customHandler := slog.NewTextHandler(&handlerBuf, nil)
+ customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
+
+ t.Run("handler then logger", func(t *testing.T) {
+ c1, err := NewCompiler(
+ WithLogHandler(customHandler),
+ WithLogger(customLogger),
+ )
+ require.NoError(t, err)
+ require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
+ c1.logger.Info("test message")
+ require.Contains(
+ t,
+ loggerBuf.String(),
+ "test message",
+ "logger buffer should receive logs",
+ )
+ require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
+ })
+
+ // Clear buffers
+ handlerBuf.Reset()
+ loggerBuf.Reset()
+
+ t.Run("logger then handler", func(t *testing.T) {
+ c2, err := NewCompiler(
+ WithLogger(customLogger),
+ WithLogHandler(customHandler),
+ )
+ require.NoError(t, err)
+ require.Equal(
+ t,
+ customHandler,
+ c2.logHandler,
+ "handler option should take precedence",
+ )
+ c2.logger.Info("test message")
+ require.Contains(
+ t,
+ handlerBuf.String(),
+ "test message",
+ "handler buffer should receive logs",
+ )
+ require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
+ })
+ })
+
+ t.Run("WithLogHandler option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+
+ t.Run("valid handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogHandler(handler)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, handler, c.logHandler)
+ require.Nil(t, c.logger) // Should clear Logger field
+ })
+
+ t.Run("nil handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogHandler(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "log handler cannot be nil")
+ })
+ })
+
+ t.Run("WithLogger option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+ logger := slog.New(handler)
+
+ t.Run("valid logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogger(logger)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, logger, c.logger)
+ require.Nil(t, c.logHandler) // Should clear LogHandler field
+ })
+
+ t.Run("nil logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogger(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "logger cannot be nil")
+ })
+ })
})
- t.Run("preserve non-nil globals", func(t *testing.T) {
- // Test that non-nil globals are preserved
- globals := []string{"test", "globals"}
- c := &Compiler{
- globals: globals,
- }
- c.applyDefaults()
-
- require.Equal(t, globals, c.globals)
- })
-
- t.Run("preserve empty globals", func(t *testing.T) {
- // Test that empty but non-nil globals are preserved
- c := &Compiler{
- globals: []string{},
- }
- c.applyDefaults()
-
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+ t.Run("Defaults and Validation", func(t *testing.T) {
+ t.Run("defaults - empty compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ require.NotNil(t, c.logHandler)
+ require.Nil(t, c.logger)
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+
+ t.Run("defaults - globals handling", func(t *testing.T) {
+ t.Run("nil globals", func(t *testing.T) {
+ c := &Compiler{
+ globals: nil,
+ }
+ c.applyDefaults()
+
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+
+ t.Run("preserve non-nil globals", func(t *testing.T) {
+ globals := []string{"test", "globals"}
+ c := &Compiler{
+ globals: globals,
+ }
+ c.applyDefaults()
+
+ require.Equal(t, globals, c.globals)
+ })
+
+ t.Run("preserve empty globals", func(t *testing.T) {
+ c := &Compiler{
+ globals: []string{},
+ }
+ c.applyDefaults()
+
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+ })
+
+ t.Run("validation", func(t *testing.T) {
+ t.Run("valid compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+
+ t.Run("missing logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ c.logHandler = nil
+ c.logger = nil
+
+ err := c.validate()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "either log handler or logger must be specified")
+ })
+
+ t.Run("with log handler only", func(t *testing.T) {
+ c := &Compiler{}
+ c.logHandler = slog.NewTextHandler(bytes.NewBuffer(nil), nil)
+ c.logger = nil
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+
+ t.Run("with logger only", func(t *testing.T) {
+ c := &Compiler{}
+ c.logHandler = nil
+ c.logger = slog.New(slog.NewTextHandler(bytes.NewBuffer(nil), nil))
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+ })
})
}
-
-func TestValidate(t *testing.T) {
- // Test validation with empty compiler after defaults
- c := &Compiler{}
- c.applyDefaults()
-
- err := c.validate()
- require.NoError(t, err)
-
- // Test validation with manually cleared logger and handler
- c.logHandler = nil
- c.logger = nil
-
- err = c.validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), "either log handler or logger must be specified")
-
- // Test validation with either logger or handler
- c = &Compiler{}
- c.logHandler = slog.NewTextHandler(bytes.NewBuffer(nil), nil)
- c.logger = nil
-
- err = c.validate()
- require.NoError(t, err)
-
- c = &Compiler{}
- c.logHandler = nil
- c.logger = slog.New(slog.NewTextHandler(bytes.NewBuffer(nil), nil))
-
- err = c.validate()
- require.NoError(t, err)
-}
diff --git a/machines/risor/evaluator/bytecodeEvaluator_test.go b/machines/risor/evaluator/bytecodeEvaluator_test.go
index 1df550a..7ea0f82 100644
--- a/machines/risor/evaluator/bytecodeEvaluator_test.go
+++ b/machines/risor/evaluator/bytecodeEvaluator_test.go
@@ -18,6 +18,7 @@ import (
"github.com/robbyt/go-polyscript/internal/helpers"
"github.com/robbyt/go-polyscript/machines/risor/compiler"
"github.com/robbyt/go-polyscript/machines/types"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@@ -74,437 +75,488 @@ func (m *MockContent) GetMachineType() types.Type {
return types.Risor
}
-// TestValidScript tests evaluating valid Risor scripts
-func TestValidScript(t *testing.T) {
- t.Parallel()
- handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
- Level: slog.LevelDebug,
- })
- slog.SetDefault(slog.New(handler))
-
- // Define the test script
- scriptContent := `
-func handle(request) {
- if request == nil {
- return error("request is nil")
- }
- if request["Method"] == "POST" {
- return "post"
- }
- if request["URL_Path"] == "/hello" {
- return true
+// Helper function to create a test executable unit
+func createTestExecutable(
+ handler slog.Handler,
+ ld loader.Loader,
+ globals []string,
+ provider data.Provider,
+) (*script.ExecutableUnit, error) {
+ c, err := compiler.NewCompiler(
+ compiler.WithLogHandler(handler),
+ compiler.WithGlobals(globals),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create compiler: %w", err)
}
- return false
-}
-print(ctx)
-handle(ctx["request"])
-`
- ld, err := loader.NewFromString(scriptContent)
- require.NoError(t, err)
-
- // Create a context provider to use with our test context
- ctxProvider := data.NewContextProvider(constants.EvalData)
-
- exe, err := createTestExecutable(handler, ld, []string{constants.Ctx}, ctxProvider)
- require.NoError(t, err)
-
- evaluator := NewBytecodeEvaluator(handler, exe)
- require.NotNil(t, evaluator)
-
- t.Run("get request", func(t *testing.T) {
- // Create the HttpRequest data object
- req := httptest.NewRequest("GET", "/hello", nil)
- rMap, err := helpers.RequestToMap(req)
- require.NoError(t, err)
- require.NotNil(t, rMap)
- require.Equal(t, "/hello", rMap["URL_Path"])
-
- evalData := map[string]any{
- constants.Request: rMap,
- }
-
- ctx := context.WithValue(context.Background(), constants.EvalData, evalData)
-
- // Evaluate the script with the provided HttpRequest
- response, err := evaluator.Eval(ctx)
- require.NoError(t, err)
- require.NotNil(t, response)
-
- // Assert the response
- require.Equal(t, data.Types("bool"), response.Type())
- require.Equal(t, "true", response.Inspect())
-
- // Check the value
- boolValue, ok := response.Interface().(bool)
- require.True(t, ok)
- require.True(t, boolValue)
- })
-
- t.Run("post request", func(t *testing.T) {
- // Create the HttpRequest data object
- req := httptest.NewRequest("POST", "/hello", nil)
- rMap, err := helpers.RequestToMap(req)
- require.NoError(t, err)
- require.NotNil(t, rMap)
- require.Equal(t, "/hello", rMap["URL_Path"])
-
- evalData := map[string]any{
- constants.Request: rMap,
- }
- ctx := context.WithValue(context.Background(), constants.EvalData, evalData)
-
- // Evaluate the script with the provided HttpRequest
- response, err := evaluator.Eval(ctx)
- require.NoError(t, err)
- require.NotNil(t, response)
-
- // Assert the response
- require.Equal(t, data.Types("string"), response.Type())
- require.Equal(t, "\"post\"", response.Inspect())
+ reader, err := ld.GetReader()
+ if err != nil {
+ return nil, err
+ }
- // Check the value
- strValue, ok := response.Interface().(string)
- require.True(t, ok)
- require.Equal(t, "post", strValue)
- })
-}
+ content, err := c.Compile(reader)
+ if err != nil {
+ return nil, err
+ }
-// TestString tests the String method
-func TestString(t *testing.T) {
- t.Parallel()
- evaluator := &BytecodeEvaluator{}
- require.Equal(t, "risor.BytecodeEvaluator", evaluator.String())
+ return &script.ExecutableUnit{
+ ID: "test-id",
+ Content: content,
+ DataProvider: provider,
+ }, nil
}
-// TestPrepareContext tests the PrepareContext method
-func TestPrepareContext(t *testing.T) {
+// TestBytecodeEvaluator_Evaluate tests evaluating Risor scripts
+func TestBytecodeEvaluator_Evaluate(t *testing.T) {
t.Parallel()
- handler := slog.NewTextHandler(os.Stderr, nil)
-
- t.Run("with provider", func(t *testing.T) {
- // Setup the mock provider
- mockProvider := &MockProvider{}
- enrichedCtx := context.WithValue(context.Background(), constants.EvalData, "enriched")
- mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).Return(enrichedCtx, nil)
-
- // Create an executable unit
- exe := &script.ExecutableUnit{DataProvider: mockProvider}
-
- // Create the evaluator
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
- }
-
- // Call PrepareContext
- ctx := context.Background()
- data := map[string]any{"test": "data"}
- result, err := evaluator.PrepareContext(ctx, data)
-
- // Verify results
- require.NoError(t, err)
- require.Equal(t, enrichedCtx, result)
- mockProvider.AssertExpectations(t)
- })
-
- t.Run("with provider error", func(t *testing.T) {
- // Setup the mock provider
- mockProvider := &MockProvider{}
- expectedErr := fmt.Errorf("provider error")
- mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).Return(nil, expectedErr)
-
- // Create an executable unit
- exe := &script.ExecutableUnit{DataProvider: mockProvider}
-
- // Create the evaluator
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
- }
-
- // Call PrepareContext
- ctx := context.Background()
- data := map[string]any{"test": "data"}
- _, err := evaluator.PrepareContext(ctx, data)
-
- // Verify error is returned
- require.Error(t, err)
- require.ErrorIs(t, err, expectedErr)
- mockProvider.AssertExpectations(t)
- })
- t.Run("nil provider", func(t *testing.T) {
- // Create an executable unit without a provider
- exe := &script.ExecutableUnit{DataProvider: nil}
-
- // Create the evaluator
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
+ // Define a test script that handles HTTP requests
+ testScript := `
+ func handle(request) {
+ if request == nil {
+ return error("request is nil")
}
-
- // Call PrepareContext
- ctx := context.Background()
- data := map[string]any{"test": "data"}
- _, err := evaluator.PrepareContext(ctx, data)
-
- // Verify error is returned
- require.Error(t, err)
- require.Contains(t, err.Error(), "no data provider available")
- })
-
- t.Run("nil executable unit", func(t *testing.T) {
- // Create the evaluator without an executable unit
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: nil,
- logHandler: handler,
- logger: slog.New(handler),
+ if request["Method"] == "POST" {
+ return "post"
}
-
- // Call PrepareContext
- ctx := context.Background()
- data := map[string]any{"test": "data"}
- _, err := evaluator.PrepareContext(ctx, data)
-
- // Verify error is returned
- require.Error(t, err)
- require.Contains(t, err.Error(), "no data provider available")
- })
-}
-
-// TestEval tests edge cases for the Eval method
-func TestEval(t *testing.T) {
- t.Parallel()
- handler := slog.NewTextHandler(os.Stderr, nil)
-
- t.Run("nil executable unit", func(t *testing.T) {
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: nil,
- logHandler: handler,
- logger: slog.New(handler),
+ if request["URL_Path"] == "/hello" {
+ return true
}
-
- ctx := context.Background()
- result, err := evaluator.Eval(ctx)
-
- require.Error(t, err)
- require.Nil(t, result)
- require.Contains(t, err.Error(), "executable unit is nil")
- })
-
- t.Run("nil bytecode", func(t *testing.T) {
- // Create an executable unit with nil bytecode
- exe := &script.ExecutableUnit{
- ID: "test-id",
- Content: &MockContent{
- Content: nil,
+ return false
+ }
+ print(ctx)
+ handle(ctx["request"])
+ `
+
+ t.Run("success cases", func(t *testing.T) {
+ tests := []struct {
+ name string
+ script string
+ requestMethod string
+ urlPath string
+ expectedType data.Types
+ expectedResult string
+ expectedValue any
+ }{
+ {
+ name: "GET request to /hello",
+ script: testScript,
+ requestMethod: "GET",
+ urlPath: "/hello",
+ expectedType: data.Types("bool"),
+ expectedResult: "true",
+ expectedValue: true,
},
- }
-
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
- }
-
- ctx := context.Background()
- result, err := evaluator.Eval(ctx)
-
- require.Error(t, err)
- require.Nil(t, result)
- require.Contains(t, err.Error(), "bytecode is nil")
- })
-
- t.Run("empty execution id", func(t *testing.T) {
- // Create an executable unit with empty ID
- exe := &script.ExecutableUnit{
- ID: "",
- Content: &MockContent{
- Content: &risorCompiler.Code{},
+ {
+ name: "POST request",
+ script: testScript,
+ requestMethod: "POST",
+ urlPath: "/hello",
+ expectedType: data.Types("string"),
+ expectedResult: "\"post\"",
+ expectedValue: "post",
+ },
+ {
+ name: "GET request to unknown path",
+ script: testScript,
+ requestMethod: "GET",
+ urlPath: "/unknown",
+ expectedType: data.Types("bool"),
+ expectedResult: "false",
+ expectedValue: false,
},
}
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Set up the environment
+ handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ })
+
+ // Create the loader and provider
+ ld, err := loader.NewFromString(tt.script)
+ require.NoError(t, err)
+ ctxProvider := data.NewContextProvider(constants.EvalData)
+
+ // Create executable unit and evaluator
+ exe, err := createTestExecutable(handler, ld, []string{constants.Ctx}, ctxProvider)
+ require.NoError(t, err)
+ evaluator := NewBytecodeEvaluator(handler, exe)
+ require.NotNil(t, evaluator)
+
+ // Create the request data
+ req := httptest.NewRequest(tt.requestMethod, tt.urlPath, nil)
+ rMap, err := helpers.RequestToMap(req)
+ require.NoError(t, err)
+ require.NotNil(t, rMap)
+
+ // Create the context with eval data
+ evalData := map[string]any{
+ constants.Request: rMap,
+ }
+ ctx := context.WithValue(context.Background(), constants.EvalData, evalData)
+
+ // Execute the script
+ response, err := evaluator.Eval(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, response)
+
+ // Verify the results
+ require.Equal(t, tt.expectedType, response.Type())
+ require.Equal(t, tt.expectedResult, response.Inspect())
+
+ // Type-specific verification
+ switch actualValue := response.Interface().(type) {
+ case bool:
+ expected, ok := tt.expectedValue.(bool)
+ require.True(t, ok)
+ require.Equal(t, expected, actualValue)
+ case string:
+ expected, ok := tt.expectedValue.(string)
+ require.True(t, ok)
+ require.Equal(t, expected, actualValue)
+ default:
+ require.Equal(t, tt.expectedValue, actualValue)
+ }
+ })
}
-
- ctx := context.Background()
- result, err := evaluator.Eval(ctx)
-
- require.Error(t, err)
- require.Nil(t, result)
- require.Contains(t, err.Error(), "exeID is empty")
})
- t.Run("wrong bytecode type", func(t *testing.T) {
- // Create an executable unit with wrong bytecode type
- exe := &script.ExecutableUnit{
- ID: "test-id",
- Content: &MockContent{
- Content: "not a risor bytecode",
+ t.Run("error cases", func(t *testing.T) {
+ tests := []struct {
+ name string
+ setupExe func() *script.ExecutableUnit
+ errorMessage string
+ }{
+ {
+ name: "nil executable unit",
+ setupExe: func() *script.ExecutableUnit {
+ return nil
+ },
+ errorMessage: "executable unit is nil",
+ },
+ {
+ name: "nil bytecode",
+ setupExe: func() *script.ExecutableUnit {
+ return &script.ExecutableUnit{
+ ID: "test-id",
+ Content: &MockContent{
+ Content: nil,
+ },
+ }
+ },
+ errorMessage: "bytecode is nil",
+ },
+ {
+ name: "empty execution id",
+ setupExe: func() *script.ExecutableUnit {
+ return &script.ExecutableUnit{
+ ID: "",
+ Content: &MockContent{
+ Content: &risorCompiler.Code{},
+ },
+ }
+ },
+ errorMessage: "exeID is empty",
+ },
+ {
+ name: "wrong bytecode type",
+ setupExe: func() *script.ExecutableUnit {
+ return &script.ExecutableUnit{
+ ID: "test-id",
+ Content: &MockContent{
+ Content: "not a risor bytecode",
+ },
+ }
+ },
+ errorMessage: "unable to type assert bytecode",
},
}
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stderr, nil)
+ exe := tt.setupExe()
+
+ evaluator := &BytecodeEvaluator{
+ ctxKey: constants.Ctx,
+ execUnit: exe,
+ logHandler: handler,
+ logger: slog.New(handler),
+ }
+
+ ctx := context.Background()
+ result, err := evaluator.Eval(ctx)
+
+ require.Error(t, err)
+ require.Nil(t, result)
+ require.Contains(t, err.Error(), tt.errorMessage)
+ })
}
-
- ctx := context.Background()
- result, err := evaluator.Eval(ctx)
-
- require.Error(t, err)
- require.Nil(t, result)
- require.Contains(t, err.Error(), "unable to type assert bytecode")
})
-}
-// TestLoadInputData tests the loadInputData method
-func TestLoadInputData(t *testing.T) {
- t.Parallel()
- handler := slog.NewTextHandler(os.Stderr, nil)
-
- t.Run("nil provider", func(t *testing.T) {
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: nil,
- logHandler: handler,
- logger: slog.New(handler),
- }
-
- ctx := context.Background()
- data, err := evaluator.loadInputData(ctx)
-
- require.NoError(t, err)
- require.NotNil(t, data)
- require.Empty(t, data)
- })
-
- t.Run("with provider error", func(t *testing.T) {
- // Setup the mock provider
- mockProvider := &MockProvider{}
- expectedErr := fmt.Errorf("provider error")
- mockProvider.On("GetData", mock.Anything).Return(nil, expectedErr)
-
- // Create an executable unit
- exe := &script.ExecutableUnit{
- DataProvider: mockProvider,
+ t.Run("load input data tests", func(t *testing.T) {
+ tests := []struct {
+ name string
+ setupExe func() *script.ExecutableUnit
+ setupCtx func() context.Context
+ expectError bool
+ errorMessage string
+ expectEmpty bool
+ }{
+ {
+ name: "nil provider",
+ setupExe: func() *script.ExecutableUnit {
+ return nil
+ },
+ setupCtx: func() context.Context {
+ return context.Background()
+ },
+ expectError: false,
+ expectEmpty: true,
+ },
+ {
+ name: "with provider error",
+ setupExe: func() *script.ExecutableUnit {
+ mockProvider := &MockProvider{}
+ expectedErr := fmt.Errorf("provider error")
+ mockProvider.On("GetData", mock.Anything).Return(nil, expectedErr)
+
+ return &script.ExecutableUnit{
+ DataProvider: mockProvider,
+ }
+ },
+ setupCtx: func() context.Context {
+ return context.Background()
+ },
+ expectError: true,
+ errorMessage: "provider error",
+ expectEmpty: true,
+ },
+ {
+ name: "with empty data",
+ setupExe: func() *script.ExecutableUnit {
+ mockProvider := &MockProvider{}
+ emptyData := map[string]any{}
+ mockProvider.On("GetData", mock.Anything).Return(emptyData, nil)
+
+ return &script.ExecutableUnit{
+ DataProvider: mockProvider,
+ }
+ },
+ setupCtx: func() context.Context {
+ return context.Background()
+ },
+ expectError: false,
+ expectEmpty: true,
+ },
+ {
+ name: "with valid data",
+ setupExe: func() *script.ExecutableUnit {
+ mockProvider := &MockProvider{}
+ validData := map[string]any{"test": "data"}
+ mockProvider.On("GetData", mock.Anything).Return(validData, nil)
+
+ return &script.ExecutableUnit{
+ DataProvider: mockProvider,
+ }
+ },
+ setupCtx: func() context.Context {
+ return context.Background()
+ },
+ expectError: false,
+ expectEmpty: false,
+ },
}
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stderr, nil)
+ exe := tt.setupExe()
+ ctx := tt.setupCtx()
+
+ evaluator := &BytecodeEvaluator{
+ ctxKey: constants.Ctx,
+ execUnit: exe,
+ logHandler: handler,
+ logger: slog.New(handler),
+ }
+
+ data, err := evaluator.loadInputData(ctx)
+
+ if tt.expectError {
+ require.Error(t, err)
+ if tt.errorMessage != "" {
+ require.Contains(t, err.Error(), tt.errorMessage)
+ }
+ require.Nil(t, data)
+ } else {
+ require.NoError(t, err)
+ if tt.expectEmpty {
+ assert.Empty(t, data)
+ } else {
+ assert.NotEmpty(t, data)
+ }
+ }
+
+ // Verify mock expectations if we have a mockProvider
+ if exe != nil && exe.DataProvider != nil {
+ if mockProvider, ok := exe.DataProvider.(*MockProvider); ok {
+ mockProvider.AssertExpectations(t)
+ }
+ }
+ })
}
-
- ctx := context.Background()
- data, err := evaluator.loadInputData(ctx)
-
- require.Error(t, err)
- require.Equal(t, expectedErr, err)
- require.Nil(t, data)
- mockProvider.AssertExpectations(t)
})
- t.Run("with empty data", func(t *testing.T) {
- // Setup the mock provider
- mockProvider := &MockProvider{}
- emptyData := map[string]any{}
- mockProvider.On("GetData", mock.Anything).Return(emptyData, nil)
-
- // Create an executable unit
- exe := &script.ExecutableUnit{
- DataProvider: mockProvider,
- }
-
- evaluator := &BytecodeEvaluator{
- ctxKey: constants.Ctx,
- execUnit: exe,
- logHandler: handler,
- logger: slog.New(handler),
- }
-
- ctx := context.Background()
- data, err := evaluator.loadInputData(ctx)
-
- require.NoError(t, err)
- require.Empty(t, data)
- mockProvider.AssertExpectations(t)
+ t.Run("metadata tests", func(t *testing.T) {
+ // Test String method
+ t.Run("String method", func(t *testing.T) {
+ evaluator := &BytecodeEvaluator{}
+ require.Equal(t, "risor.BytecodeEvaluator", evaluator.String())
+ })
+
+ // Test constructor with various options
+ t.Run("constructor options", func(t *testing.T) {
+ tests := []struct {
+ name string
+ handler slog.Handler
+ checkLogger bool
+ }{
+ {
+ name: "with handler",
+ handler: slog.NewTextHandler(os.Stderr, nil),
+ checkLogger: true,
+ },
+ {
+ name: "with nil handler",
+ handler: nil,
+ checkLogger: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ exe := &script.ExecutableUnit{}
+ evaluator := NewBytecodeEvaluator(tt.handler, exe)
+
+ require.NotNil(t, evaluator)
+ require.Equal(t, constants.Ctx, evaluator.ctxKey)
+ require.NotNil(t, evaluator.logger)
+ require.NotNil(t, evaluator.logHandler)
+
+ if tt.checkLogger && tt.handler != nil {
+ require.Equal(t, tt.handler, evaluator.logHandler)
+ }
+ })
+ }
+ })
})
}
-// TestNewBytecodeEvaluator tests creating a new BytecodeEvaluator
-func TestNewBytecodeEvaluator(t *testing.T) {
+// TestBytecodeEvaluator_PrepareContext tests the PrepareContext method with various scenarios
+func TestBytecodeEvaluator_PrepareContext(t *testing.T) {
t.Parallel()
- t.Run("with handler", func(t *testing.T) {
- handler := slog.NewTextHandler(os.Stderr, nil)
- exe := &script.ExecutableUnit{}
-
- evaluator := NewBytecodeEvaluator(handler, exe)
-
- require.NotNil(t, evaluator)
- require.Equal(t, constants.Ctx, evaluator.ctxKey)
- require.NotNil(t, evaluator.logger)
- require.Equal(t, handler, evaluator.logHandler)
- })
-
- t.Run("with nil handler", func(t *testing.T) {
- exe := &script.ExecutableUnit{}
-
- evaluator := NewBytecodeEvaluator(nil, exe)
-
- require.NotNil(t, evaluator)
- require.Equal(t, constants.Ctx, evaluator.ctxKey)
- require.NotNil(t, evaluator.logger)
- require.NotNil(t, evaluator.logHandler)
- })
-}
-
-// Helper function to create a test executable unit
-func createTestExecutable(
- handler slog.Handler,
- ld loader.Loader,
- globals []string,
- provider data.Provider,
-) (*script.ExecutableUnit, error) {
- c, err := compiler.NewCompiler(
- compiler.WithLogHandler(handler),
- compiler.WithGlobals(globals),
- )
- if err != nil {
- return nil, fmt.Errorf("failed to create compiler: %w", err)
- }
-
- reader, err := ld.GetReader()
- if err != nil {
- return nil, err
+ // The test cases
+ tests := []struct {
+ name string
+ setupExe func(t *testing.T) *script.ExecutableUnit
+ inputs []any
+ wantError bool
+ errorMessage string
+ }{
+ {
+ name: "with successful provider",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+
+ mockProvider := &MockProvider{}
+ enrichedCtx := context.WithValue(
+ context.Background(),
+ constants.EvalData,
+ "enriched",
+ )
+ mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).
+ Return(enrichedCtx, nil)
+
+ return &script.ExecutableUnit{DataProvider: mockProvider}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: false,
+ },
+ {
+ name: "with provider error",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+
+ mockProvider := &MockProvider{}
+ expectedErr := fmt.Errorf("provider error")
+ mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).
+ Return(nil, expectedErr)
+
+ return &script.ExecutableUnit{DataProvider: mockProvider}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "provider error",
+ },
+ {
+ name: "nil provider",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+ return &script.ExecutableUnit{DataProvider: nil}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "no data provider available",
+ },
+ {
+ name: "nil executable unit",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+ return nil
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "no data provider available",
+ },
}
- content, err := c.Compile(reader)
- if err != nil {
- return nil, err
+ // Run the test cases
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stderr, nil)
+ exe := tt.setupExe(t)
+
+ evaluator := &BytecodeEvaluator{
+ ctxKey: constants.Ctx,
+ execUnit: exe,
+ logHandler: handler,
+ logger: slog.New(handler),
+ }
+
+ ctx := context.Background()
+ result, err := evaluator.PrepareContext(ctx, tt.inputs...)
+
+ if tt.wantError {
+ require.Error(t, err)
+ if tt.errorMessage != "" {
+ require.Contains(t, err.Error(), tt.errorMessage)
+ }
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ }
+
+ // If using mocks, verify expectations
+ if exe != nil && exe.DataProvider != nil {
+ if mockProvider, ok := exe.DataProvider.(*MockProvider); ok {
+ mockProvider.AssertExpectations(t)
+ }
+ }
+ })
}
-
- return &script.ExecutableUnit{
- ID: "test-id",
- Content: content,
- DataProvider: provider,
- }, nil
}
diff --git a/machines/risor/evaluator/response_test.go b/machines/risor/evaluator/response_test.go
index 1d8cc2c..f693416 100644
--- a/machines/risor/evaluator/response_test.go
+++ b/machines/risor/evaluator/response_test.go
@@ -80,100 +80,286 @@ func (m *RisorObjectMock) Compare(other rObj.Object) (int, error) {
return args.Int(0), args.Error(1)
}
-func TestNewEvalResult(t *testing.T) {
- mockObj := new(RisorObjectMock)
+// TestResponseMethods tests all the methods of the EvaluatorResponse interface
+func TestResponseMethods(t *testing.T) {
+ t.Parallel()
- execTime := 100 * time.Millisecond
- versionID := "test-version-1"
+ t.Run("Creation", func(t *testing.T) {
+ tests := []struct {
+ name string
+ setupMock func() *RisorObjectMock
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "with valid object",
+ setupMock: func() *RisorObjectMock {
+ mockObj := new(RisorObjectMock)
+ return mockObj
+ },
+ execTime: 100 * time.Millisecond,
+ versionID: "test-version-1",
+ },
+ {
+ name: "with longer execution time",
+ setupMock: func() *RisorObjectMock {
+ mockObj := new(RisorObjectMock)
+ return mockObj
+ },
+ execTime: 2 * time.Second,
+ versionID: "test-version-2",
+ },
+ }
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockObj, execTime, versionID)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := tt.setupMock()
+ handler := slog.NewTextHandler(os.Stdout, nil)
- require.NotNil(t, result)
- require.Equal(t, mockObj, result.Object)
+ result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID)
- require.Equal(t, execTime, result.execTime)
- assert.Equal(t, execTime.String(), result.GetExecTime())
+ // Verify basic properties
+ require.NotNil(t, result)
+ require.Equal(t, mockObj, result.Object)
+ require.Equal(t, tt.execTime, result.execTime)
+ require.Equal(t, tt.versionID, result.scriptExeID)
- require.Equal(t, versionID, result.scriptExeID)
- require.Equal(t, versionID, result.GetScriptExeID())
- require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
+ // Verify interface implementation
+ require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
- mockObj.AssertExpectations(t)
-}
+ // Verify metadata methods
+ assert.Equal(t, tt.execTime.String(), result.GetExecTime())
+ assert.Equal(t, tt.versionID, result.GetScriptExeID())
+ })
+ }
+ })
-func TestExecResult_Type(t *testing.T) {
- testCases := []struct {
- name string
- typeStr string
- expected data.Types
- }{
- {"string type", string(data.STRING), data.STRING},
- {"int type", string(data.INT), data.INT},
- {"bool type", string(data.BOOL), data.BOOL},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- mockObj := new(RisorObjectMock)
- mockObj.On("Type").Return(rObj.Type(tc.typeStr))
-
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockObj, time.Second, "version-1")
- assert.Equal(t, tc.expected, result.Type())
-
- mockObj.AssertExpectations(t)
- })
- }
-}
+ t.Run("Type", func(t *testing.T) {
+ tests := []struct {
+ name string
+ typeStr string
+ expected data.Types
+ }{
+ {"string type", string(data.STRING), data.STRING},
+ {"int type", string(data.INT), data.INT},
+ {"bool type", string(data.BOOL), data.BOOL},
+ {"float type", string(data.FLOAT), data.FLOAT},
+ {"list type", string(data.LIST), data.LIST},
+ {"map type", string(data.MAP), data.MAP},
+ {"none type", string(data.NONE), data.NONE},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ mockObj.On("Type").Return(rObj.Type(tt.typeStr))
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockObj, time.Second, "version-1")
+
+ // Check the result type
+ assert.Equal(t, tt.expected, result.Type())
+
+ // Verify mock expectations
+ mockObj.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("String", func(t *testing.T) {
+ tests := []struct {
+ name string
+ mockType rObj.Type
+ mockString string
+ execTime time.Duration
+ versionID string
+ expected string
+ }{
+ {
+ name: "string object",
+ mockType: rObj.Type("string"),
+ mockString: "hello",
+ execTime: 100 * time.Millisecond,
+ versionID: "v1.0.0",
+ expected: "ExecResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
+ },
+ {
+ name: "integer object",
+ mockType: rObj.Type("integer"),
+ mockString: "42",
+ execTime: 200 * time.Millisecond,
+ versionID: "v2.0.0",
+ expected: "ExecResult{Type: integer, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
+ },
+ {
+ name: "boolean object",
+ mockType: rObj.Type("boolean"),
+ mockString: "true",
+ execTime: 50 * time.Millisecond,
+ versionID: "v3.0.0",
+ expected: "ExecResult{Type: boolean, Value: true, ExecTime: 50ms, ScriptExeID: v3.0.0}",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ mockObj.On("Type").Return(tt.mockType)
+ mockObj.On("String").Return(tt.mockString)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID)
+
+ // Check string representation
+ actual := result.String()
+ assert.Equal(t, tt.expected, actual)
+
+ // Verify mock expectations
+ mockObj.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("Inspect", func(t *testing.T) {
+ tests := []struct {
+ name string
+ mockInspect string
+ expectedInspect string
+ }{
+ {
+ name: "string value",
+ mockInspect: "\"test string\"",
+ expectedInspect: "\"test string\"",
+ },
+ {
+ name: "number value",
+ mockInspect: "42",
+ expectedInspect: "42",
+ },
+ {
+ name: "boolean value",
+ mockInspect: "true",
+ expectedInspect: "true",
+ },
+ {
+ name: "complex value",
+ mockInspect: "{\"key\":\"value\"}",
+ expectedInspect: "{\"key\":\"value\"}",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ mockObj.On("Inspect").Return(tt.mockInspect)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockObj, time.Second, "test-1")
+
+ // Check inspect result
+ assert.Equal(t, tt.expectedInspect, result.Inspect())
+
+ // Verify mock expectations
+ mockObj.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("Interface", func(t *testing.T) {
+ tests := []struct {
+ name string
+ mockValue any
+ }{
+ {
+ name: "string value",
+ mockValue: "test string",
+ },
+ {
+ name: "number value",
+ mockValue: 42,
+ },
+ {
+ name: "boolean value",
+ mockValue: true,
+ },
+ {
+ name: "map value",
+ mockValue: map[string]any{"key": "value"},
+ },
+ {
+ name: "nil value",
+ mockValue: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ mockObj.On("Interface").Return(tt.mockValue)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockObj, time.Second, "test-1")
+
+ // The Interface method should return the original value
+ actual := result.Interface()
+ assert.Equal(t, tt.mockValue, actual)
+
+ // Verify mock expectations
+ mockObj.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("NilHandler", func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ execTime := 100 * time.Millisecond
+ versionID := "test-version-1"
+
+ // Create with nil handler
+ result := newEvalResult(nil, mockObj, execTime, versionID)
+
+ // Should create default handler and logger
+ require.NotNil(t, result)
+ require.NotNil(t, result.logHandler)
+ require.NotNil(t, result.logger)
+
+ // Should still store all values correctly
+ assert.Equal(t, mockObj, result.Object)
+ assert.Equal(t, execTime, result.execTime)
+ assert.Equal(t, versionID, result.scriptExeID)
+ })
+
+ t.Run("Metadata", func(t *testing.T) {
+ tests := []struct {
+ name string
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "short execution time",
+ execTime: 123 * time.Millisecond,
+ versionID: "test-script-9876",
+ },
+ {
+ name: "long execution time",
+ execTime: 3 * time.Second,
+ versionID: "test-script-1234",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockObj := new(RisorObjectMock)
+ handler := slog.NewTextHandler(os.Stdout, nil)
+
+ result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID)
+
+ // Test GetScriptExeID
+ assert.Equal(t, tt.versionID, result.GetScriptExeID())
-func TestExecResult_String(t *testing.T) {
- testCases := []struct {
- name string
- mockType rObj.Type
- mockString string
- execTime time.Duration
- versionID string
- expected string
- }{
- {
- name: "simple string object",
- mockType: rObj.Type("string"),
- mockString: "hello",
- execTime: 100 * time.Millisecond,
- versionID: "v1.0.0",
- expected: "ExecResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
- },
- {
- name: "integer object",
- mockType: rObj.Type("integer"),
- mockString: "42",
- execTime: 200 * time.Millisecond,
- versionID: "v2.0.0",
- expected: "ExecResult{Type: integer, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
- },
- {
- name: "boolean object",
- mockType: rObj.Type("boolean"),
- mockString: "true",
- execTime: 50 * time.Millisecond,
- versionID: "v3.0.0",
- expected: "ExecResult{Type: boolean, Value: true, ExecTime: 50ms, ScriptExeID: v3.0.0}",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- mockObj := new(RisorObjectMock)
- mockObj.On("Type").Return(tc.mockType)
- mockObj.On("String").Return(tc.mockString)
-
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockObj, tc.execTime, tc.versionID)
- actual := result.String()
- assert.Equal(t, tc.expected, actual)
-
- mockObj.AssertExpectations(t)
- })
- }
+ // Test GetExecTime
+ assert.Equal(t, tt.execTime.String(), result.GetExecTime())
+ })
+ }
+ })
}
diff --git a/machines/starlark/compiler/compiler_test.go b/machines/starlark/compiler/compiler_test.go
index 9fe914c..bc44381 100644
--- a/machines/starlark/compiler/compiler_test.go
+++ b/machines/starlark/compiler/compiler_test.go
@@ -51,50 +51,71 @@ func (m *mockErrorReader) Close() error {
return nil
}
-func TestCompiler(t *testing.T) {
+func TestNewCompiler(t *testing.T) {
t.Parallel()
- tests := []struct {
- name string
- script string
- globals []string
- err error
- }{
- {
- name: "valid script",
- script: `print("Hello, World!")`,
- globals: []string{"request"},
- },
- {
- name: "syntax error - missing closing parenthesis",
- script: `print("Hello, World!"`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- {
- name: "empty script",
- script: ``,
- globals: []string{"request"},
- err: ErrContentNil,
- },
- {
- name: "only comments",
- script: `# This is just a comment`,
- globals: []string{"request"},
- },
- {
- name: "undefined global",
- script: `print(undefined_global)`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- {
- name: "with multiple globals",
- script: `print(request, response)`,
- globals: []string{"request", "response"},
- },
- {
- name: "complex valid script with global override",
- script: `
+
+ t.Run("basic creation", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ require.Equal(t, "starlark.Compiler", comp.String())
+ })
+
+ t.Run("with globals", func(t *testing.T) {
+ globals := []string{"request", "response"}
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(globals),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
+
+ t.Run("with ctx global", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithCtxGlobal(),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
+
+ t.Run("defaults", func(t *testing.T) {
+ comp, err := NewCompiler()
+ require.NoError(t, err)
+ require.NotNil(t, comp)
+ })
+}
+
+func TestCompiler_Compile(t *testing.T) {
+ t.Parallel()
+
+ t.Run("success cases", func(t *testing.T) {
+ successTests := []struct {
+ name string
+ script string
+ globals []string
+ }{
+ {
+ name: "valid script",
+ script: `print("Hello, World!")`,
+ globals: []string{"request"},
+ },
+ {
+ name: "only comments",
+ script: `# This is just a comment`,
+ globals: []string{"request"},
+ },
+ {
+ name: "with multiple globals",
+ script: `print(request, response)`,
+ globals: []string{"request", "response"},
+ },
+ {
+ name: "complex valid script with global override",
+ script: `
request = True
def main():
if request:
@@ -103,11 +124,11 @@ def main():
print("No")
main()
`,
- globals: []string{"request"},
- },
- {
- name: "complex valid script with condition",
- script: `
+ globals: []string{"request"},
+ },
+ {
+ name: "complex valid script with condition",
+ script: `
def main():
if condition:
print("Yes")
@@ -115,55 +136,161 @@ def main():
print("No")
main()
`,
- globals: []string{"condition"},
- },
- {
- name: "script using undefined global",
- script: `print(undefined)`,
- globals: []string{"request"},
- err: ErrValidationFailed,
- },
- }
+ globals: []string{"condition"},
+ },
+ }
+
+ for _, tt := range successTests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create compiler with options
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(tt.globals),
+ )
+ require.NoError(t, err, "Failed to create compiler")
+
+ reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.On("Close").Return(nil)
+ } else {
+ t.Fatal("Failed to create mock reader")
+ }
+
+ // Execute test
+ execContent, err := comp.Compile(reader)
+ require.NoError(t, err, "Did not expect an error but got one")
+ require.NotNil(t, execContent, "Expected execContent to be non-nil")
+ require.Equal(
+ t,
+ tt.script,
+ execContent.GetSource(),
+ "Script content does not match",
+ )
+
+ // Verify mock expectations
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.AssertExpectations(t)
+ }
+ })
+ }
+ })
+
+ t.Run("error cases", func(t *testing.T) {
+ errorTests := []struct {
+ name string
+ script string
+ globals []string
+ err error
+ }{
+ {
+ name: "syntax error - missing closing parenthesis",
+ script: `print("Hello, World!"`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ {
+ name: "empty script",
+ script: ``,
+ globals: []string{"request"},
+ err: ErrContentNil,
+ },
+ {
+ name: "undefined global",
+ script: `print(undefined_global)`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ {
+ name: "script using undefined global",
+ script: `print(undefined)`,
+ globals: []string{"request"},
+ err: ErrValidationFailed,
+ },
+ }
+
+ for _, tt := range errorTests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create compiler with options
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ WithGlobals(tt.globals),
+ )
+ require.NoError(t, err, "Failed to create compiler")
+
+ reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.On("Close").Return(nil)
+ } else {
+ t.Fatal("Failed to create mock reader")
+ }
+
+ // Execute test
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.True(t, errors.Is(err, tt.err), "Expected error %v, got %v", tt.err, err)
+
+ // Verify mock expectations
+ if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
+ mockReader.AssertExpectations(t)
+ }
+ })
+ }
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
+ t.Run("nil reader", func(t *testing.T) {
+ comp, err := NewCompiler(WithLogHandler(slog.NewTextHandler(os.Stdout, nil)))
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
- // Create compiler with options
+ execContent, err := comp.Compile(nil)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.True(t, errors.Is(err, ErrContentNil), "Expected error to be ErrContentNil")
+ })
+
+ t.Run("io error", func(t *testing.T) {
comp, err := NewCompiler(
WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
- WithGlobals(tt.globals),
+ WithGlobals([]string{"ctx"}),
)
- require.NoError(t, err, "Failed to create compiler")
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
- reader := io.ReadCloser(newMockScriptReaderCloser(tt.script))
- if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
- mockReader.On("Close").Return(nil)
- } else {
- t.Fatal("Failed to create mock reader")
- }
-
- // Execute test
+ // Create a reader that will return an error
+ reader := &mockErrorReader{}
execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.Contains(
+ t,
+ err.Error(),
+ "failed to read script",
+ "Expected error to contain 'failed to read script'",
+ )
+ })
- if tt.err != nil {
- require.Error(t, err, "Expected an error but got none")
- require.Nil(t, execContent, "Expected execContent to be nil")
- require.True(t, errors.Is(err, tt.err), "Expected error %v, got %v", tt.err, err)
- return
- }
+ t.Run("close error", func(t *testing.T) {
+ comp, err := NewCompiler(
+ WithLogHandler(slog.NewTextHandler(os.Stdout, nil)),
+ )
+ require.NoError(t, err)
+ require.NotNil(t, comp, "Expected compiler to be non-nil")
- require.NoError(t, err, "Did not expect an error but got one")
- require.NotNil(t, execContent, "Expected execContent to be non-nil")
- require.Equal(t, tt.script, execContent.GetSource(), "Script content does not match")
+ // Create a reader that will return an error on close
+ reader := newMockScriptReaderCloser(`print("Hello, World!")`)
+ reader.On("Close").Return(errors.New("test error")).Once()
- // Verify mock expectations
- if mockReader, ok := reader.(*mockScriptReaderCloser); ok {
- mockReader.AssertExpectations(t)
- }
+ execContent, err := comp.Compile(reader)
+ require.Error(t, err, "Expected an error but got none")
+ require.Nil(t, execContent, "Expected execContent to be nil")
+ require.Contains(
+ t,
+ err.Error(),
+ "failed to close reader",
+ "Expected error to contain 'failed to close reader'",
+ )
})
- }
+ })
}
func TestCompilerOptions(t *testing.T) {
diff --git a/machines/starlark/compiler/executable_test.go b/machines/starlark/compiler/executable_test.go
index b11dd71..3eb1e81 100644
--- a/machines/starlark/compiler/executable_test.go
+++ b/machines/starlark/compiler/executable_test.go
@@ -9,94 +9,71 @@ import (
starlarkLib "go.starlark.net/starlark"
)
-func TestNewExecutableValid(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &starlarkLib.Program{}
-
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
- assert.Equal(t, content, executable.GetSource())
- assert.Equal(t, bytecode, executable.GetByteCode())
- assert.Equal(t, bytecode, executable.GetStarlarkByteCode())
- assert.Equal(t, machineTypes.Starlark, executable.GetMachineType())
-}
-
-func TestNewExecutableNilContent(t *testing.T) {
- bytecode := &starlarkLib.Program{}
- executable := newExecutable(nil, bytecode)
- require.Nil(t, executable)
-}
-
-func TestNewExecutableNilByteCode(t *testing.T) {
- content := "print('Hello, World!')"
- executable := newExecutable([]byte(content), nil)
- require.Nil(t, executable)
-}
-
-func TestNewExecutableNilContentAndByteCode(t *testing.T) {
- executable := newExecutable(nil, nil)
- require.Nil(t, executable)
-}
-
-func TestExecutable_GetSource(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &starlarkLib.Program{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- source := executable.GetSource()
- assert.Equal(t, content, source)
-}
-
-func TestExecutable_GetByteCode(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &starlarkLib.Program{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- code := executable.GetByteCode()
- assert.Equal(t, bytecode, code)
-
- // Test type assertion
- _, ok := code.(*starlarkLib.Program)
- assert.True(t, ok)
-}
-
-func TestExecutable_GetStarlarkByteCode(t *testing.T) {
- content := "print('Hello, World!')"
- bytecode := &starlarkLib.Program{}
- executable := newExecutable([]byte(content), bytecode)
- require.NotNil(t, executable)
-
- code := executable.GetStarlarkByteCode()
- assert.Equal(t, bytecode, code)
-}
-
-func TestNewExecutable(t *testing.T) {
- t.Run("valid creation", func(t *testing.T) {
- content := "print('test')"
- bytecode := &starlarkLib.Program{}
-
- exe := newExecutable([]byte(content), bytecode)
- require.NotNil(t, exe)
- assert.Equal(t, content, exe.GetSource())
- assert.Equal(t, bytecode, exe.ByteCode)
+// TestExecutable tests the functionality of Executable
+func TestExecutable(t *testing.T) {
+ t.Parallel()
+
+ // Test creation scenarios
+ t.Run("Creation", func(t *testing.T) {
+ t.Run("valid creation", func(t *testing.T) {
+ content := "print('Hello, World!')"
+ bytecode := &starlarkLib.Program{}
+
+ exe := newExecutable([]byte(content), bytecode)
+ require.NotNil(t, exe)
+ assert.Equal(t, content, exe.GetSource())
+ assert.Equal(t, bytecode, exe.GetByteCode())
+ assert.Equal(t, bytecode, exe.GetStarlarkByteCode())
+ assert.Equal(t, machineTypes.Starlark, exe.GetMachineType())
+ })
+
+ t.Run("nil content", func(t *testing.T) {
+ bytecode := &starlarkLib.Program{}
+ exe := newExecutable(nil, bytecode)
+ assert.Nil(t, exe)
+ })
+
+ t.Run("nil bytecode", func(t *testing.T) {
+ content := "print('test')"
+ exe := newExecutable([]byte(content), nil)
+ assert.Nil(t, exe)
+ })
+
+ t.Run("both nil", func(t *testing.T) {
+ exe := newExecutable(nil, nil)
+ assert.Nil(t, exe)
+ })
})
- t.Run("nil content", func(t *testing.T) {
+ // Test getters
+ t.Run("Getters", func(t *testing.T) {
+ content := "print('Hello, World!')"
bytecode := &starlarkLib.Program{}
- exe := newExecutable(nil, bytecode)
- assert.Nil(t, exe)
- })
-
- t.Run("nil bytecode", func(t *testing.T) {
- content := "print('test')"
- exe := newExecutable([]byte(content), nil)
- assert.Nil(t, exe)
- })
-
- t.Run("both nil", func(t *testing.T) {
- exe := newExecutable(nil, nil)
- assert.Nil(t, exe)
+ executable := newExecutable([]byte(content), bytecode)
+ require.NotNil(t, executable)
+
+ t.Run("GetSource", func(t *testing.T) {
+ source := executable.GetSource()
+ assert.Equal(t, content, source)
+ })
+
+ t.Run("GetByteCode", func(t *testing.T) {
+ code := executable.GetByteCode()
+ assert.Equal(t, bytecode, code)
+
+ // Test type assertion
+ _, ok := code.(*starlarkLib.Program)
+ assert.True(t, ok)
+ })
+
+ t.Run("GetStarlarkByteCode", func(t *testing.T) {
+ code := executable.GetStarlarkByteCode()
+ assert.Equal(t, bytecode, code)
+ })
+
+ t.Run("GetMachineType", func(t *testing.T) {
+ machineType := executable.GetMachineType()
+ assert.Equal(t, machineTypes.Starlark, machineType)
+ })
})
}
diff --git a/machines/starlark/compiler/options_test.go b/machines/starlark/compiler/options_test.go
index 4a2dc2b..7e3703c 100644
--- a/machines/starlark/compiler/options_test.go
+++ b/machines/starlark/compiler/options_test.go
@@ -9,276 +9,305 @@ import (
"github.com/stretchr/testify/require"
)
-func TestWithGlobals(t *testing.T) {
- // Test that WithGlobals properly sets the globals field
- globals := []string{"ctx", "print"}
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithGlobals(globals)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, globals, c.globals)
-
- // Test with nil globals
- c = &Compiler{}
- c.applyDefaults()
- nilOpt := WithGlobals(nil)
- err = nilOpt(c)
-
- require.NoError(t, err)
- require.Nil(t, c.globals)
-
- // Test with empty globals
- c = &Compiler{}
- c.applyDefaults()
- emptyOpt := WithGlobals([]string{})
- err = emptyOpt(c)
-
- require.NoError(t, err)
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
-}
-
-func TestWithCtxGlobal(t *testing.T) {
- // Test with empty globals
- c1 := &Compiler{globals: []string{}}
- opt := WithCtxGlobal()
- err := opt(c1)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx}, c1.globals)
-
- // Test with existing globals not containing ctx
- c2 := &Compiler{globals: []string{"request", "response"}}
- err = opt(c2)
-
- require.NoError(t, err)
- require.Equal(t, []string{"request", "response", constants.Ctx}, c2.globals)
-
- // Test with globals already containing ctx
- c3 := &Compiler{globals: []string{constants.Ctx, "request"}}
- err = opt(c3)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx, "request"}, c3.globals)
- require.Len(t, c3.globals, 2) // Should not add duplicate
-
- // Test with nil globals
- c4 := &Compiler{globals: nil}
- err = opt(c4)
-
- require.NoError(t, err)
- require.Equal(t, []string{constants.Ctx}, c4.globals)
-}
-
-func TestLoggerConfiguration(t *testing.T) {
- t.Run("default initialization", func(t *testing.T) {
- // Create a compiler with default settings
- c, err := NewCompiler()
- require.NoError(t, err)
-
- // Verify that both logHandler and logger are set
- require.NotNil(t, c.logHandler, "logHandler should be initialized")
- require.NotNil(t, c.logger, "logger should be initialized")
- })
-
- t.Run("with explicit log handler", func(t *testing.T) {
- // Create a custom handler
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
-
- // Create compiler with the handler
- c, err := NewCompiler(WithLogHandler(customHandler))
- require.NoError(t, err)
-
- // Verify handler was set and used to create logger
- require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
- require.NotNil(t, c.logger, "logger should be created from handler")
-
- // Test logging works with the custom handler
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with explicit logger", func(t *testing.T) {
- // Create a custom logger
- var buf bytes.Buffer
- customHandler := slog.NewTextHandler(&buf, nil)
- customLogger := slog.New(customHandler)
-
- // Create compiler with the logger
- c, err := NewCompiler(WithLogger(customLogger))
- require.NoError(t, err)
-
- // Verify logger was set
- require.Equal(t, customLogger, c.logger, "custom logger should be set")
- require.NotNil(t, c.logHandler, "handler should be extracted from logger")
-
- // Test logging works with the custom logger
- c.logger.Info("test message")
- require.Contains(t, buf.String(), "test message", "log message should be in buffer")
- })
-
- t.Run("with both logger options, last one wins", func(t *testing.T) {
- // Create two buffers to verify which one receives logs
- var handlerBuf, loggerBuf bytes.Buffer
- customHandler := slog.NewTextHandler(&handlerBuf, nil)
- customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
-
- // Case 1: Handler then Logger
- c1, err := NewCompiler(
- WithLogHandler(customHandler),
- WithLogger(customLogger),
- )
- require.NoError(t, err)
- require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
- c1.logger.Info("test message")
- require.Contains(t, loggerBuf.String(), "test message", "logger buffer should receive logs")
- require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
-
- // Clear buffers
- handlerBuf.Reset()
- loggerBuf.Reset()
-
- // Case 2: Logger then Handler
- c2, err := NewCompiler(
- WithLogger(customLogger),
- WithLogHandler(customHandler),
- )
- require.NoError(t, err)
- require.Equal(t, customHandler, c2.logHandler, "handler option should take precedence")
- c2.logger.Info("test message")
- require.Contains(
- t,
- handlerBuf.String(),
- "test message",
- "handler buffer should receive logs",
- )
- require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
- })
-}
-
-func TestWithLogHandler(t *testing.T) {
- // Test that WithLogHandler properly sets the handler field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogHandler(handler)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, handler, c.logHandler)
- require.Nil(t, c.logger) // Should clear Logger field
-
- // Test with nil handler
- nilOpt := WithLogHandler(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "log handler cannot be nil")
-}
-
-func TestWithLogger(t *testing.T) {
- // Test that WithLogger properly sets the logger field
- var buf bytes.Buffer
- handler := slog.NewTextHandler(&buf, nil)
- logger := slog.New(handler)
-
- c := &Compiler{}
- c.applyDefaults()
- opt := WithLogger(logger)
- err := opt(c)
-
- require.NoError(t, err)
- require.Equal(t, logger, c.logger)
- require.Nil(t, c.logHandler) // Should clear LogHandler field
-
- // Test with nil logger
- nilOpt := WithLogger(nil)
- err = nilOpt(c)
-
- require.Error(t, err)
- require.Contains(t, err.Error(), "logger cannot be nil")
-}
-
-func TestApplyDefaults(t *testing.T) {
- t.Run("empty compiler", func(t *testing.T) {
- // Test that defaults are properly applied to an empty compiler
- c := &Compiler{}
- c.applyDefaults()
-
- require.NotNil(t, c.logHandler)
- require.Nil(t, c.logger)
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+// TestCompilerOptionsDetailed tests all compiler options functionality in detail
+func TestCompilerOptionsDetailed(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Globals", func(t *testing.T) {
+ t.Run("WithGlobals", func(t *testing.T) {
+ t.Run("valid globals", func(t *testing.T) {
+ globals := []string{"ctx", "print"}
+
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithGlobals(globals)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, globals, c.globals)
+ })
+
+ t.Run("nil globals", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithGlobals(nil)
+ err := nilOpt(c)
+
+ require.NoError(t, err)
+ require.Nil(t, c.globals)
+ })
+
+ t.Run("empty globals", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ emptyOpt := WithGlobals([]string{})
+ err := emptyOpt(c)
+
+ require.NoError(t, err)
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+ })
+
+ t.Run("WithCtxGlobal", func(t *testing.T) {
+ opt := WithCtxGlobal()
+
+ t.Run("empty globals", func(t *testing.T) {
+ c1 := &Compiler{globals: []string{}}
+ err := opt(c1)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx}, c1.globals)
+ })
+
+ t.Run("existing globals without ctx", func(t *testing.T) {
+ c2 := &Compiler{globals: []string{"request", "response"}}
+ err := opt(c2)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{"request", "response", constants.Ctx}, c2.globals)
+ })
+
+ t.Run("already contains ctx", func(t *testing.T) {
+ c3 := &Compiler{globals: []string{constants.Ctx, "request"}}
+ err := opt(c3)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx, "request"}, c3.globals)
+ require.Len(t, c3.globals, 2) // Should not add duplicate
+ })
+
+ t.Run("nil globals", func(t *testing.T) {
+ c4 := &Compiler{globals: nil}
+ err := opt(c4)
+
+ require.NoError(t, err)
+ require.Equal(t, []string{constants.Ctx}, c4.globals)
+ })
+ })
})
- t.Run("nil globals", func(t *testing.T) {
- // Test with a nil globals field
- c := &Compiler{
- globals: nil,
- }
- c.applyDefaults()
-
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+ t.Run("Logger", func(t *testing.T) {
+ t.Run("default initialization", func(t *testing.T) {
+ c, err := NewCompiler()
+ require.NoError(t, err)
+
+ require.NotNil(t, c.logHandler, "logHandler should be initialized")
+ require.NotNil(t, c.logger, "logger should be initialized")
+ })
+
+ t.Run("with explicit log handler", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+
+ c, err := NewCompiler(WithLogHandler(customHandler))
+ require.NoError(t, err)
+
+ require.Equal(t, customHandler, c.logHandler, "custom handler should be set")
+ require.NotNil(t, c.logger, "logger should be created from handler")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("with explicit logger", func(t *testing.T) {
+ var buf bytes.Buffer
+ customHandler := slog.NewTextHandler(&buf, nil)
+ customLogger := slog.New(customHandler)
+
+ c, err := NewCompiler(WithLogger(customLogger))
+ require.NoError(t, err)
+
+ require.Equal(t, customLogger, c.logger, "custom logger should be set")
+ require.NotNil(t, c.logHandler, "handler should be extracted from logger")
+
+ c.logger.Info("test message")
+ require.Contains(t, buf.String(), "test message", "log message should be in buffer")
+ })
+
+ t.Run("option precedence", func(t *testing.T) {
+ var handlerBuf, loggerBuf bytes.Buffer
+ customHandler := slog.NewTextHandler(&handlerBuf, nil)
+ customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil))
+
+ t.Run("handler then logger", func(t *testing.T) {
+ c1, err := NewCompiler(
+ WithLogHandler(customHandler),
+ WithLogger(customLogger),
+ )
+ require.NoError(t, err)
+ require.Equal(t, customLogger, c1.logger, "logger option should take precedence")
+ c1.logger.Info("test message")
+ require.Contains(
+ t,
+ loggerBuf.String(),
+ "test message",
+ "logger buffer should receive logs",
+ )
+ require.Empty(t, handlerBuf.String(), "handler buffer should not receive logs")
+ })
+
+ // Clear buffers
+ handlerBuf.Reset()
+ loggerBuf.Reset()
+
+ t.Run("logger then handler", func(t *testing.T) {
+ c2, err := NewCompiler(
+ WithLogger(customLogger),
+ WithLogHandler(customHandler),
+ )
+ require.NoError(t, err)
+ require.Equal(
+ t,
+ customHandler,
+ c2.logHandler,
+ "handler option should take precedence",
+ )
+ c2.logger.Info("test message")
+ require.Contains(
+ t,
+ handlerBuf.String(),
+ "test message",
+ "handler buffer should receive logs",
+ )
+ require.Empty(t, loggerBuf.String(), "logger buffer should not receive logs")
+ })
+ })
+
+ t.Run("WithLogHandler option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+
+ t.Run("valid handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogHandler(handler)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, handler, c.logHandler)
+ require.Nil(t, c.logger) // Should clear Logger field
+ })
+
+ t.Run("nil handler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogHandler(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "log handler cannot be nil")
+ })
+ })
+
+ t.Run("WithLogger option", func(t *testing.T) {
+ var buf bytes.Buffer
+ handler := slog.NewTextHandler(&buf, nil)
+ logger := slog.New(handler)
+
+ t.Run("valid logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ opt := WithLogger(logger)
+ err := opt(c)
+
+ require.NoError(t, err)
+ require.Equal(t, logger, c.logger)
+ require.Nil(t, c.logHandler) // Should clear LogHandler field
+ })
+
+ t.Run("nil logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ nilOpt := WithLogger(nil)
+ err := nilOpt(c)
+
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "logger cannot be nil")
+ })
+ })
})
- t.Run("preserve non-nil globals", func(t *testing.T) {
- // Test that non-nil globals are preserved
- globals := []string{"test", "globals"}
- c := &Compiler{
- globals: globals,
- }
- c.applyDefaults()
-
- require.Equal(t, globals, c.globals)
- })
-
- t.Run("preserve empty globals", func(t *testing.T) {
- // Test that empty but non-nil globals are preserved
- c := &Compiler{
- globals: []string{},
- }
- c.applyDefaults()
-
- require.NotNil(t, c.globals)
- require.Empty(t, c.globals)
+ t.Run("Defaults and Validation", func(t *testing.T) {
+ t.Run("defaults - empty compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ require.NotNil(t, c.logHandler)
+ require.Nil(t, c.logger)
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+
+ t.Run("defaults - globals handling", func(t *testing.T) {
+ t.Run("nil globals", func(t *testing.T) {
+ c := &Compiler{
+ globals: nil,
+ }
+ c.applyDefaults()
+
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+
+ t.Run("preserve non-nil globals", func(t *testing.T) {
+ globals := []string{"test", "globals"}
+ c := &Compiler{
+ globals: globals,
+ }
+ c.applyDefaults()
+
+ require.Equal(t, globals, c.globals)
+ })
+
+ t.Run("preserve empty globals", func(t *testing.T) {
+ c := &Compiler{
+ globals: []string{},
+ }
+ c.applyDefaults()
+
+ require.NotNil(t, c.globals)
+ require.Empty(t, c.globals)
+ })
+ })
+
+ t.Run("validation", func(t *testing.T) {
+ t.Run("valid compiler", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+
+ t.Run("missing logger", func(t *testing.T) {
+ c := &Compiler{}
+ c.applyDefaults()
+ c.logHandler = nil
+ c.logger = nil
+
+ err := c.validate()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "either log handler or logger must be specified")
+ })
+
+ t.Run("with log handler only", func(t *testing.T) {
+ c := &Compiler{}
+ c.logHandler = slog.NewTextHandler(bytes.NewBuffer(nil), nil)
+ c.logger = nil
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+
+ t.Run("with logger only", func(t *testing.T) {
+ c := &Compiler{}
+ c.logHandler = nil
+ c.logger = slog.New(slog.NewTextHandler(bytes.NewBuffer(nil), nil))
+
+ err := c.validate()
+ require.NoError(t, err)
+ })
+ })
})
}
-
-func TestValidate(t *testing.T) {
- // Test validation with empty compiler after defaults
- c := &Compiler{}
- c.applyDefaults()
-
- err := c.validate()
- require.NoError(t, err)
-
- // Test validation with manually cleared logger and handler
- c.logHandler = nil
- c.logger = nil
-
- err = c.validate()
- require.Error(t, err)
- require.Contains(t, err.Error(), "either log handler or logger must be specified")
-
- // Test validation with either logger or handler
- c = &Compiler{}
- c.logHandler = slog.NewTextHandler(bytes.NewBuffer(nil), nil)
- c.logger = nil
-
- err = c.validate()
- require.NoError(t, err)
-
- c = &Compiler{}
- c.logHandler = nil
- c.logger = slog.New(slog.NewTextHandler(bytes.NewBuffer(nil), nil))
-
- err = c.validate()
- require.NoError(t, err)
-}
diff --git a/machines/starlark/evaluator/bytecodeEvaluator_test.go b/machines/starlark/evaluator/bytecodeEvaluator_test.go
index d76a4b6..87bc5a0 100644
--- a/machines/starlark/evaluator/bytecodeEvaluator_test.go
+++ b/machines/starlark/evaluator/bytecodeEvaluator_test.go
@@ -2,6 +2,7 @@ package evaluator
import (
"context"
+ "fmt"
"log/slog"
"net/http/httptest"
"os"
@@ -13,14 +14,70 @@ import (
"github.com/robbyt/go-polyscript/execution/script/loader"
"github.com/robbyt/go-polyscript/internal/helpers"
"github.com/robbyt/go-polyscript/machines/starlark/compiler"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
-// TestValidScript tests evaluating valid scripts
-func TestValidScript(t *testing.T) {
+// evalBuilder is a helper function to create a test executor and evaluator
+func evalBuilder(t *testing.T, scriptContent string) (*script.ExecutableUnit, *BytecodeEvaluator) {
+ t.Helper()
+ loader, err := loader.NewFromString(scriptContent)
+ require.NoError(t, err, "Failed to create new loader")
+
+ // Create test logger
+ handler := slog.NewTextHandler(os.Stdout, nil)
+
+ // Create a context provider to use with our test context
+ ctxProvider := data.NewContextProvider(constants.EvalData)
+
+ // Create compiler with options
+ compiler, err := compiler.NewCompiler(
+ compiler.WithLogHandler(handler),
+ compiler.WithCtxGlobal(),
+ )
+ require.NoError(t, err, "Failed to create compiler")
+
+ exe, err := script.NewExecutableUnit(
+ handler,
+ scriptContent,
+ loader,
+ compiler,
+ ctxProvider,
+ )
+ require.NoError(t, err, "Failed to create new version")
+
+ evaluator := NewBytecodeEvaluator(handler, exe)
+ require.NotNil(t, evaluator, "BytecodeEvaluator should not be nil")
+
+ return exe, evaluator
+}
+
+// Mock the data.Provider interface
+type MockProvider struct {
+ mock.Mock
+}
+
+func (m *MockProvider) GetData(ctx context.Context) (map[string]any, error) {
+ args := m.Called(ctx)
+ if data, ok := args.Get(0).(map[string]any); ok {
+ return data, args.Error(1)
+ }
+ return nil, args.Error(1)
+}
+
+func (m *MockProvider) AddDataToContext(ctx context.Context, data ...any) (context.Context, error) {
+ args := m.Called(ctx, data)
+ if ctx, ok := args.Get(0).(context.Context); ok {
+ return ctx, args.Error(1)
+ }
+ return ctx, args.Error(1)
+}
+
+// TestBytecodeEvaluator_Evaluate tests evaluating starlark scripts
+func TestBytecodeEvaluator_Evaluate(t *testing.T) {
t.Parallel()
- // Defines a Starlark script that can handle HTTP requests
+ // Define a Starlark script that can handle HTTP requests
scriptContent := `
def request_handler(request):
if request == None:
@@ -35,71 +92,48 @@ print(ctx)
_ = request_handler(ctx.get("request"))
`
- evalBuilder := func(t *testing.T, scriptContent string) (*script.ExecutableUnit, *BytecodeEvaluator) {
- t.Helper()
- loader, err := loader.NewFromString(scriptContent)
- require.NoError(t, err, "Failed to create new loader")
-
- // Create test logger
- handler := slog.NewTextHandler(os.Stdout, nil)
-
- // Create a context provider to use with our test context
- ctxProvider := data.NewContextProvider(constants.EvalData)
-
- // Create compiler with options
- compiler, err := compiler.NewCompiler(
- compiler.WithLogHandler(handler),
- compiler.WithCtxGlobal(),
- )
- require.NoError(t, err, "Failed to create compiler")
-
- exe, err := script.NewExecutableUnit(
- handler,
- scriptContent,
- loader,
- compiler,
- ctxProvider,
- )
- require.NoError(t, err, "Failed to create new version")
-
- evaluator := NewBytecodeEvaluator(handler, exe)
- require.NotNil(t, evaluator, "BytecodeEvaluator should not be nil")
-
- return exe, evaluator
- }
-
- t.Run("get request", func(t *testing.T) {
+ t.Run("success cases", func(t *testing.T) {
tests := []struct {
name string
script string
+ requestMethod string
+ urlPath string
expected string
expectedObject any
- urlPath string
}{
{
- name: "Handles /hello",
+ name: "GET request to /hello",
script: scriptContent,
+ requestMethod: "GET",
+ urlPath: "/hello",
expected: "True",
expectedObject: true,
- urlPath: "/hello",
},
{
- name: "Handles other paths",
+ name: "GET request to other path",
script: scriptContent,
+ requestMethod: "GET",
+ urlPath: "/other",
expected: "False",
expectedObject: false,
- urlPath: "/other",
+ },
+ {
+ name: "POST request",
+ script: scriptContent,
+ requestMethod: "POST",
+ urlPath: "/hello",
+ expected: "\"post\"",
+ expectedObject: "post",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup the test
- exe, evaluator := evalBuilder(t, tt.script)
- _ = exe // We no longer need to pass this to Eval
+ _, evaluator := evalBuilder(t, tt.script)
// Create the HttpRequest data object
- req := httptest.NewRequest("GET", tt.urlPath, nil)
+ req := httptest.NewRequest(tt.requestMethod, tt.urlPath, nil)
rMap, err := helpers.RequestToMap(req)
require.NoError(t, err, "Failed to create HttpRequest data object")
@@ -123,31 +157,157 @@ _ = request_handler(ctx.get("request"))
}
})
- t.Run("post request", func(t *testing.T) {
- // Setup the test
- exe, evaluator := evalBuilder(t, scriptContent)
- _ = exe // We no longer need to pass this to Eval
+ t.Run("error cases", func(t *testing.T) {
+ // Test nil executable unit
+ t.Run("nil executable unit", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ evaluator := NewBytecodeEvaluator(handler, nil)
- // Create the HttpRequest data object
- req := httptest.NewRequest("POST", "/hello", nil)
- rMap, err := helpers.RequestToMap(req)
- require.NoError(t, err, "Failed to create HttpRequest data object")
+ response, err := evaluator.Eval(context.Background())
+ require.Error(t, err)
+ require.Nil(t, response)
+ require.Contains(t, err.Error(), "executable unit is nil")
+ })
- evalData := map[string]any{
- constants.Request: rMap,
- }
+ // Test content nil
+ t.Run("content nil", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ exe := &script.ExecutableUnit{
+ ID: "test-nil-content",
+ Content: nil, // Deliberately nil content
+ }
+ evaluator := NewBytecodeEvaluator(handler, exe)
- ctx := context.WithValue(context.Background(), constants.EvalData, evalData)
+ response, err := evaluator.Eval(context.Background())
+ require.Error(t, err)
+ require.Nil(t, response)
+ require.Contains(t, err.Error(), "content is nil")
+ })
- // Evaluate the script with the provided HttpRequest
- response, err := evaluator.Eval(ctx)
- require.NoError(t, err, "Did not expect an error")
- require.NotNil(t, response, "Response should not be nil")
+ // Test script with execution error
+ t.Run("script execution error", func(t *testing.T) {
+ // Create a script that will intentionally cause an error
+ scriptContent := `
+def invalid_func():
+ # This will cause a runtime error
+ fail("intentional error")
- // Assert the string representation of the response
- require.Equal(t, "\"post\"", response.Inspect())
+invalid_func()
+`
+ _, evaluator := evalBuilder(t, scriptContent)
+ response, err := evaluator.Eval(context.Background())
+ require.Error(t, err)
+ require.Nil(t, response)
+ require.Contains(t, err.Error(), "intentional error")
+ })
+ })
- // Assert the actual value of the response
- require.Equal(t, "post", response.Interface())
+ t.Run("metadata tests", func(t *testing.T) {
+ // Test String() representation
+ t.Run("String method", func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ evaluator := NewBytecodeEvaluator(handler, nil)
+ require.Equal(t, "starlark.BytecodeEvaluator", evaluator.String())
+ })
})
}
+
+// TestBytecodeEvaluator_PrepareContext tests the PrepareContext method with various scenarios
+func TestBytecodeEvaluator_PrepareContext(t *testing.T) {
+ t.Parallel()
+
+ // Test cases
+ tests := []struct {
+ name string
+ setupExe func(t *testing.T) *script.ExecutableUnit
+ inputs []any
+ wantError bool
+ errorMessage string
+ }{
+ {
+ name: "with successful provider",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+
+ mockProvider := &MockProvider{}
+ enrichedCtx := context.WithValue(
+ context.Background(),
+ constants.EvalData,
+ "enriched",
+ )
+ mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).
+ Return(enrichedCtx, nil)
+
+ return &script.ExecutableUnit{DataProvider: mockProvider}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: false,
+ },
+ {
+ name: "with provider error",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+
+ mockProvider := &MockProvider{}
+ expectedErr := fmt.Errorf("provider error")
+ mockProvider.On("AddDataToContext", mock.Anything, mock.Anything).
+ Return(nil, expectedErr)
+
+ return &script.ExecutableUnit{DataProvider: mockProvider}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "provider error",
+ },
+ {
+ name: "nil provider",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+ return &script.ExecutableUnit{DataProvider: nil}
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "no data provider available",
+ },
+ {
+ name: "nil executable unit",
+ setupExe: func(t *testing.T) *script.ExecutableUnit {
+ t.Helper()
+ return nil
+ },
+ inputs: []any{map[string]any{"test": "data"}},
+ wantError: true,
+ errorMessage: "no data provider available",
+ },
+ }
+
+ // Run the test cases
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ handler := slog.NewTextHandler(os.Stderr, nil)
+ exe := tt.setupExe(t)
+
+ evaluator := NewBytecodeEvaluator(handler, exe)
+
+ ctx := context.Background()
+ result, err := evaluator.PrepareContext(ctx, tt.inputs...)
+
+ if tt.wantError {
+ require.Error(t, err)
+ if tt.errorMessage != "" {
+ require.Contains(t, err.Error(), tt.errorMessage)
+ }
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ }
+
+ // If using mocks, verify expectations
+ if exe != nil && exe.DataProvider != nil {
+ if mockProvider, ok := exe.DataProvider.(*MockProvider); ok {
+ mockProvider.AssertExpectations(t)
+ }
+ }
+ })
+ }
+}
diff --git a/machines/starlark/evaluator/response_test.go b/machines/starlark/evaluator/response_test.go
index 56fc5c0..2f99423 100644
--- a/machines/starlark/evaluator/response_test.go
+++ b/machines/starlark/evaluator/response_test.go
@@ -43,104 +43,251 @@ func (m *StarlarkValueMock) Freeze() {
m.Called()
}
-func TestNewEvalResult(t *testing.T) {
- mockVal := new(StarlarkValueMock)
- execTime := 100 * time.Millisecond
- versionID := "test-version-1"
-
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockVal, execTime, versionID)
-
- require.NotNil(t, result)
- require.Equal(t, mockVal, result.Value)
- require.Equal(t, execTime, result.execTime)
- assert.Equal(t, execTime.String(), result.GetExecTime())
- require.Equal(t, versionID, result.scriptExeID)
- require.Equal(t, versionID, result.GetScriptExeID())
- require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
-
- mockVal.AssertExpectations(t)
-}
+// TestResponseMethods tests all the methods of the EvaluatorResponse interface
+func TestResponseMethods(t *testing.T) {
+ t.Parallel()
-func TestExecResult_Type(t *testing.T) {
- testCases := []struct {
- name string
- typeStr string
- expected data.Types
- }{
- {"none type", "NoneType", data.NONE},
- {"string type", "string", data.STRING},
- {"int type", "int", data.INT},
- {"float type", "float", data.FLOAT},
- {"bool type", "bool", data.BOOL},
- {"list type", "list", data.LIST},
- {"tuple type", "tuple", data.TUPLE},
- {"dict type", "dict", data.MAP},
- {"set type", "set", data.SET},
- {"function type", "function", data.FUNCTION},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- mockVal := new(StarlarkValueMock)
- mockVal.On("Type").Return(tc.typeStr)
-
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockVal, time.Second, "version-1")
- assert.Equal(t, tc.expected, result.Type())
-
- mockVal.AssertExpectations(t)
- })
- }
-}
+ t.Run("Creation", func(t *testing.T) {
+ tests := []struct {
+ name string
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "with standard values",
+ execTime: 100 * time.Millisecond,
+ versionID: "test-version-1",
+ },
+ {
+ name: "with longer execution time",
+ execTime: 5 * time.Second,
+ versionID: "test-version-2",
+ },
+ {
+ name: "with microsecond execution time",
+ execTime: 750 * time.Microsecond,
+ versionID: "test-version-micro",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+ handler := slog.NewTextHandler(os.Stdout, nil)
+
+ result := newEvalResult(handler, mockVal, tt.execTime, tt.versionID)
+
+ require.NotNil(t, result)
+ require.Equal(t, mockVal, result.Value)
+ require.Equal(t, tt.execTime, result.execTime)
+ assert.Equal(t, tt.execTime.String(), result.GetExecTime())
+ require.Equal(t, tt.versionID, result.scriptExeID)
+ require.Equal(t, tt.versionID, result.GetScriptExeID())
+ require.Implements(t, (*engine.EvaluatorResponse)(nil), result)
+
+ mockVal.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("Type", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ typeStr string
+ expected data.Types
+ }{
+ {"none type", "NoneType", data.NONE},
+ {"string type", "string", data.STRING},
+ {"int type", "int", data.INT},
+ {"float type", "float", data.FLOAT},
+ {"bool type", "bool", data.BOOL},
+ {"list type", "list", data.LIST},
+ {"tuple type", "tuple", data.TUPLE},
+ {"dict type", "dict", data.MAP},
+ {"set type", "set", data.SET},
+ {"function type", "function", data.FUNCTION},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+ mockVal.On("Type").Return(tc.typeStr)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockVal, time.Second, "version-1")
+ assert.Equal(t, tc.expected, result.Type())
+
+ mockVal.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("String", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ mockType string
+ mockString string
+ execTime time.Duration
+ versionID string
+ expected string
+ }{
+ {
+ name: "string value",
+ mockType: "string",
+ mockString: "hello",
+ execTime: 100 * time.Millisecond,
+ versionID: "v1.0.0",
+ expected: "ExecResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
+ },
+ {
+ name: "int value",
+ mockType: "int",
+ mockString: "42",
+ execTime: 200 * time.Millisecond,
+ versionID: "v2.0.0",
+ expected: "ExecResult{Type: int, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
+ },
+ {
+ name: "bool value",
+ mockType: "bool",
+ mockString: "True",
+ execTime: 50 * time.Millisecond,
+ versionID: "v3.0.0",
+ expected: "ExecResult{Type: bool, Value: True, ExecTime: 50ms, ScriptExeID: v3.0.0}",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+ mockVal.On("Type").Return(tc.mockType)
+ mockVal.On("String").Return(tc.mockString)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockVal, tc.execTime, tc.versionID)
+ actual := result.String()
+ assert.Equal(t, tc.expected, actual)
+
+ mockVal.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("Inspect", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ mockStringVal string
+ expectedInspect string
+ }{
+ {
+ name: "string value",
+ mockStringVal: "\"test string\"",
+ expectedInspect: "\"test string\"",
+ },
+ {
+ name: "number value",
+ mockStringVal: "42",
+ expectedInspect: "42",
+ },
+ {
+ name: "boolean value",
+ mockStringVal: "True",
+ expectedInspect: "True",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+ mockVal.On("String").Return(tc.mockStringVal)
+
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockVal, time.Second, "test-1")
+
+ assert.Equal(t, tc.expectedInspect, result.Inspect())
+ mockVal.AssertExpectations(t)
+ })
+ }
+ })
+
+ t.Run("Metadata", func(t *testing.T) {
+ tests := []struct {
+ name string
+ execTime time.Duration
+ scriptID string
+ }{
+ {
+ name: "short execution time",
+ execTime: 123 * time.Millisecond,
+ scriptID: "test-script-9876",
+ },
+ {
+ name: "long execution time",
+ execTime: 3 * time.Second,
+ scriptID: "test-script-1234",
+ },
+ {
+ name: "zero execution time",
+ execTime: 0,
+ scriptID: "test-script-zero",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+ handler := slog.NewTextHandler(os.Stdout, nil)
+ result := newEvalResult(handler, mockVal, tt.execTime, tt.scriptID)
+
+ // Test GetScriptExeID
+ assert.Equal(t, tt.scriptID, result.GetScriptExeID())
+
+ // Test GetExecTime
+ assert.Equal(t, tt.execTime.String(), result.GetExecTime())
+ })
+ }
+ })
+
+ t.Run("NilHandler", func(t *testing.T) {
+ tests := []struct {
+ name string
+ execTime time.Duration
+ versionID string
+ }{
+ {
+ name: "standard case",
+ execTime: 100 * time.Millisecond,
+ versionID: "test-version-1",
+ },
+ {
+ name: "long execution time",
+ execTime: 3 * time.Second,
+ versionID: "test-version-2",
+ },
+ {
+ name: "very short execution time",
+ execTime: 5 * time.Microsecond,
+ versionID: "test-version-micro",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockVal := new(StarlarkValueMock)
+
+ // Create with nil handler
+ result := newEvalResult(nil, mockVal, tt.execTime, tt.versionID)
+
+ // Should create default handler and logger
+ require.NotNil(t, result)
+ require.NotNil(t, result.logHandler)
+ require.NotNil(t, result.logger)
-func TestExecResult_String(t *testing.T) {
- testCases := []struct {
- name string
- mockType string
- mockString string
- execTime time.Duration
- versionID string
- expected string
- }{
- {
- name: "string value",
- mockType: "string",
- mockString: "hello",
- execTime: 100 * time.Millisecond,
- versionID: "v1.0.0",
- expected: "ExecResult{Type: string, Value: hello, ExecTime: 100ms, ScriptExeID: v1.0.0}",
- },
- {
- name: "int value",
- mockType: "int",
- mockString: "42",
- execTime: 200 * time.Millisecond,
- versionID: "v2.0.0",
- expected: "ExecResult{Type: int, Value: 42, ExecTime: 200ms, ScriptExeID: v2.0.0}",
- },
- {
- name: "bool value",
- mockType: "bool",
- mockString: "True",
- execTime: 50 * time.Millisecond,
- versionID: "v3.0.0",
- expected: "ExecResult{Type: bool, Value: True, ExecTime: 50ms, ScriptExeID: v3.0.0}",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- mockVal := new(StarlarkValueMock)
- mockVal.On("Type").Return(tc.mockType)
- mockVal.On("String").Return(tc.mockString)
-
- handler := slog.NewTextHandler(os.Stdout, nil)
- result := newEvalResult(handler, mockVal, tc.execTime, tc.versionID)
- actual := result.String()
- assert.Equal(t, tc.expected, actual)
-
- mockVal.AssertExpectations(t)
- })
- }
+ // Should still store all values correctly
+ assert.Equal(t, mockVal, result.Value)
+ assert.Equal(t, tt.execTime, result.execTime)
+ assert.Equal(t, tt.versionID, result.scriptExeID)
+ })
+ }
+ })
}
diff --git a/machines/starlark/internal/converters_test.go b/machines/starlark/internal/converters_test.go
index dbf036a..f9ac71e 100644
--- a/machines/starlark/internal/converters_test.go
+++ b/machines/starlark/internal/converters_test.go
@@ -10,622 +10,606 @@ import (
)
func TestConvertStarlarkValueToInterface(t *testing.T) {
- t.Run("primitive types", func(t *testing.T) {
- tests := []struct {
- name string
- input starlarkLib.Value
- expected any
- }{
- {
- name: "nil value",
- input: nil,
- expected: nil,
- },
- {
- name: "bool true",
- input: starlarkLib.Bool(true),
- expected: true,
- },
- {
- name: "bool false",
- input: starlarkLib.Bool(false),
- expected: false,
- },
- {
- name: "int",
- input: starlarkLib.MakeInt(42),
- expected: int64(42),
- },
- {
- name: "float",
- input: starlarkLib.Float(3.14),
- expected: float64(3.14),
- },
- {
- name: "string",
- input: starlarkLib.String("hello"),
- expected: "hello",
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ input starlarkLib.Value
+ expected any
+ wantErr bool
+ }{
+ // Primitive types
+ {
+ name: "nil value",
+ input: nil,
+ expected: nil,
+ wantErr: false,
+ },
+ {
+ name: "bool true",
+ input: starlarkLib.Bool(true),
+ expected: true,
+ wantErr: false,
+ },
+ {
+ name: "bool false",
+ input: starlarkLib.Bool(false),
+ expected: false,
+ wantErr: false,
+ },
+ {
+ name: "int",
+ input: starlarkLib.MakeInt(42),
+ expected: int64(42),
+ wantErr: false,
+ },
+ {
+ name: "float",
+ input: starlarkLib.Float(3.14),
+ expected: float64(3.14),
+ wantErr: false,
+ },
+ {
+ name: "string",
+ input: starlarkLib.String("hello"),
+ expected: "hello",
+ wantErr: false,
+ },
+
+ // List types
+ {
+ name: "empty list",
+ input: starlarkLib.NewList(nil),
+ expected: []any{},
+ wantErr: false,
+ },
+ {
+ name: "mixed type list",
+ input: starlarkLib.NewList([]starlarkLib.Value{
+ starlarkLib.MakeInt(1),
+ starlarkLib.String("two"),
+ starlarkLib.Bool(true),
+ }),
+ expected: []any{int64(1), "two", true},
+ wantErr: false,
+ },
+ {
+ name: "nested list",
+ input: func() *starlarkLib.List {
+ inner := starlarkLib.NewList([]starlarkLib.Value{
+ starlarkLib.MakeInt(1),
+ starlarkLib.MakeInt(2),
+ })
+ outer := starlarkLib.NewList([]starlarkLib.Value{inner})
+ return outer
+ }(),
+ expected: []any{[]any{int64(1), int64(2)}},
+ wantErr: false,
+ },
+
+ // Dict types
+ {
+ name: "empty dict",
+ input: starlarkLib.NewDict(0),
+ expected: map[string]any{},
+ wantErr: false,
+ },
+ {
+ name: "string keys dict",
+ input: func() *starlarkLib.Dict {
+ d := starlarkLib.NewDict(1)
+ if err := d.SetKey(starlarkLib.String("key"), starlarkLib.MakeInt(42)); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ return d
+ }(),
+ expected: map[string]any{"key": int64(42)},
+ wantErr: false,
+ },
+ {
+ name: "nested dict",
+ input: func() *starlarkLib.Dict {
+ inner := starlarkLib.NewDict(1)
+ if err := inner.SetKey(starlarkLib.String("inner"), starlarkLib.MakeInt(1)); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ outer := starlarkLib.NewDict(1)
+ if err := outer.SetKey(starlarkLib.String("outer"), inner); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ return outer
+ }(),
+ expected: map[string]any{
+ "outer": map[string]any{
+ "inner": int64(1),
+ },
},
- }
+ wantErr: false,
+ },
+
+ // Error cases
+ {
+ name: "dict with invalid entry",
+ input: func() *starlarkLib.Dict {
+ d := starlarkLib.NewDict(1)
+ // Create an invalid entry that will fail Get()
+ err := d.Clear() // This creates an inconsistent state
+ if err != nil {
+ t.Fatalf("Failed to clear dict: %v", err)
+ }
+ return d
+ }(),
+ expected: map[string]any{},
+ wantErr: false, // Note: Current implementation doesn't return an error for this case
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt // Capture range variable
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := ConvertStarlarkValueToInterface(tt.input)
+
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertStarlarkValueToInterface(tt.input)
- require.NoError(t, err)
- require.Equal(t, tt.expected, result)
- })
- }
- })
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+}
- t.Run("list types", func(t *testing.T) {
- tests := []struct {
- name string
- input *starlarkLib.List
- expected []any
- }{
- {
- name: "empty list",
- input: starlarkLib.NewList(nil),
- expected: []any{},
- },
- {
- name: "mixed type list",
- input: func() *starlarkLib.List {
- l := starlarkLib.NewList([]starlarkLib.Value{
- starlarkLib.MakeInt(1),
- starlarkLib.String("two"),
- starlarkLib.Bool(true),
- })
- return l
- }(),
- expected: []any{int64(1), "two", true},
+func TestConvertToStarlarkFormat(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ input map[string]any
+ expected starlarkLib.StringDict
+ wantErr bool
+ }{
+ // Basic types
+ {
+ name: "empty map",
+ input: map[string]any{},
+ expected: starlarkLib.StringDict{
+ constants.Ctx: starlarkLib.NewDict(0),
+ },
+ wantErr: false,
+ },
+ {
+ name: "simple types",
+ input: map[string]any{
+ "bool": true,
+ "int": 42,
+ "float": 3.14,
+ "string": "hello",
},
- {
- name: "nested list",
- input: func() *starlarkLib.List {
- inner := starlarkLib.NewList([]starlarkLib.Value{
- starlarkLib.MakeInt(1),
- starlarkLib.MakeInt(2),
- })
- outer := starlarkLib.NewList([]starlarkLib.Value{inner})
- return outer
+ expected: starlarkLib.StringDict{
+ constants.Ctx: func() *starlarkLib.Dict {
+ d := starlarkLib.NewDict(4)
+ if err := d.SetKey(starlarkLib.String("bool"), starlarkLib.Bool(true)); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ if err := d.SetKey(starlarkLib.String("int"), starlarkLib.MakeInt(42)); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ if err := d.SetKey(starlarkLib.String("float"), starlarkLib.Float(3.14)); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ if err := d.SetKey(starlarkLib.String("string"), starlarkLib.String("hello")); err != nil {
+ t.Fatalf("Failed to set key: %v", err)
+ }
+ return d
}(),
- expected: []any{[]any{int64(1), int64(2)}},
},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertStarlarkValueToInterface(tt.input)
- require.NoError(t, err)
- require.Equal(t, tt.expected, result)
- })
- }
- })
-
- t.Run("dict types", func(t *testing.T) {
- tests := []struct {
- name string
- input *starlarkLib.Dict
- expected map[string]any
- }{
- {
- name: "empty dict",
- input: starlarkLib.NewDict(0),
- expected: map[string]any{},
+ wantErr: false,
+ },
+ {
+ name: "with nil value",
+ input: map[string]any{
+ "nil": nil,
},
- {
- name: "string keys dict",
- input: func() *starlarkLib.Dict {
+ expected: starlarkLib.StringDict{
+ constants.Ctx: func() *starlarkLib.Dict {
d := starlarkLib.NewDict(1)
- require.NoError(t, d.SetKey(starlarkLib.String("key"), starlarkLib.MakeInt(42)))
+ if err := d.SetKey(starlarkLib.String("nil"), starlarkLib.None); err != nil {
+ t.Fatalf("Failed to set nil key: %v", err)
+ }
return d
}(),
- expected: map[string]any{"key": int64(42)},
- },
- {
- name: "nested dict",
- input: func() *starlarkLib.Dict {
- inner := starlarkLib.NewDict(1)
- require.NoError(
- t,
- inner.SetKey(starlarkLib.String("inner"), starlarkLib.MakeInt(1)),
- )
-
- outer := starlarkLib.NewDict(1)
- require.NoError(t, outer.SetKey(starlarkLib.String("outer"), inner))
- return outer
- }(),
- expected: map[string]any{
- "outer": map[string]any{
- "inner": int64(1),
- },
- },
},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertStarlarkValueToInterface(tt.input)
- require.NoError(t, err)
- require.Equal(t, tt.expected, result)
- })
- }
- })
+ wantErr: false,
+ },
- t.Run("error cases", func(t *testing.T) {
- tests := []struct {
- name string
- input func() *starlarkLib.Dict
- }{
- {
- name: "dict with invalid entry",
- input: func() *starlarkLib.Dict {
+ // Complex types
+ {
+ name: "with URL",
+ input: map[string]any{
+ "url": &url.URL{Scheme: "https", Host: "localhost:8080"},
+ },
+ expected: starlarkLib.StringDict{
+ constants.Ctx: func() *starlarkLib.Dict {
d := starlarkLib.NewDict(1)
- // Create an invalid entry that will fail Get()
- err := d.Clear() // This creates an inconsistent state
- require.NoError(t, err)
+ u := &url.URL{Scheme: "https", Host: "localhost:8080"}
+ if err := d.SetKey(starlarkLib.String("url"), starlarkLib.String(u.String())); err != nil {
+ t.Fatalf("Failed to set url key: %v", err)
+ }
return d
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertStarlarkValueToInterface(tt.input())
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Empty(t, result.(map[string]any))
- })
- }
- })
-}
-
-func TestConvertToStarlarkFormat(t *testing.T) {
- t.Run("basic types", func(t *testing.T) {
- tests := []struct {
- name string
- input map[string]any
- expected starlarkLib.StringDict
- wantErr bool
- }{
- {
- name: "empty map",
- input: map[string]any{},
- expected: starlarkLib.StringDict{
- constants.Ctx: starlarkLib.NewDict(0),
- },
- },
- {
- name: "simple types",
- input: map[string]any{
- "bool": true,
- "int": 42,
- "float": 3.14,
- "string": "hello",
- },
- expected: func() starlarkLib.StringDict {
- d := starlarkLib.NewDict(4)
- require.NoError(t, d.SetKey(starlarkLib.String("bool"), starlarkLib.Bool(true)))
- require.NoError(t, d.SetKey(starlarkLib.String("int"), starlarkLib.MakeInt(42)))
- require.NoError(
- t,
- d.SetKey(starlarkLib.String("float"), starlarkLib.Float(3.14)),
- )
- require.NoError(
- t,
- d.SetKey(starlarkLib.String("string"), starlarkLib.String("hello")),
- )
- return starlarkLib.StringDict{constants.Ctx: d}
}(),
},
- {
- name: "with nil value",
- input: map[string]any{
- "nil": nil,
- },
- expected: starlarkLib.StringDict{
- constants.Ctx: func() *starlarkLib.Dict {
- d := starlarkLib.NewDict(1)
- require.NoError(t, d.SetKey(starlarkLib.String("nil"), starlarkLib.None))
- return d
- }(),
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertToStarlarkFormat(tt.input)
- if tt.wantErr {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- require.Equal(t, len(tt.expected), len(result))
-
- // Get ctx value and verify it's a dict
- ctxVal, ok := result[constants.Ctx].(*starlarkLib.Dict)
- require.True(t, ok)
-
- // Compare the dict contents
- expectedCtx := tt.expected[constants.Ctx].(*starlarkLib.Dict)
- require.Equal(t, expectedCtx.Len(), ctxVal.Len())
-
- for _, k := range expectedCtx.Keys() {
- expectedVal, found, err := expectedCtx.Get(k)
- require.NoError(t, err)
- require.True(t, found)
- actualVal, found, err := ctxVal.Get(k)
- require.NoError(t, err)
- require.True(t, found)
- require.Equal(t, expectedVal.String(), actualVal.String())
- }
- })
- }
- })
-
- t.Run("complex types", func(t *testing.T) {
- tests := []struct {
- name string
- input map[string]any
- expected starlarkLib.StringDict
- wantErr bool
- }{
- {
- name: "with URL",
- input: map[string]any{
- "url": &url.URL{Scheme: "https", Host: "example.com"},
+ wantErr: false,
+ },
+ {
+ name: "with headers",
+ input: map[string]any{
+ "headers": map[string][]string{
+ "Accept": {"text/plain", "application/json"},
},
- expected: func() starlarkLib.StringDict {
- d := starlarkLib.NewDict(1)
- u := &url.URL{Scheme: "https", Host: "example.com"}
- require.NoError(
- t,
- d.SetKey(starlarkLib.String("url"), starlarkLib.String(u.String())),
- )
- return starlarkLib.StringDict{constants.Ctx: d}
- }(),
},
- {
- name: "with headers",
- input: map[string]any{
- "headers": map[string][]string{
- "Accept": {"text/plain", "application/json"},
- },
- },
- expected: func() starlarkLib.StringDict {
+ expected: starlarkLib.StringDict{
+ constants.Ctx: func() *starlarkLib.Dict {
d := starlarkLib.NewDict(1)
- // Create inner dict for headers
headers := starlarkLib.NewDict(1)
- // Create list for Accept values
acceptList := starlarkLib.NewList([]starlarkLib.Value{
starlarkLib.String("text/plain"),
starlarkLib.String("application/json"),
})
- // Set Accept list in headers dict
- require.NoError(t, headers.SetKey(starlarkLib.String("Accept"), acceptList))
- // Set headers dict in outer dict
- require.NoError(t, d.SetKey(starlarkLib.String("headers"), headers))
- return starlarkLib.StringDict{constants.Ctx: d}
+ if err := headers.SetKey(starlarkLib.String("Accept"), acceptList); err != nil {
+ t.Fatalf("Failed to set Accept key: %v", err)
+ }
+ if err := d.SetKey(starlarkLib.String("headers"), headers); err != nil {
+ t.Fatalf("Failed to set headers key: %v", err)
+ }
+ return d
}(),
},
- {
- name: "nested structures",
- input: map[string]any{
- "nested": map[string]any{
- "list": []any{1, "two", true},
- },
+ wantErr: false,
+ },
+ {
+ name: "nested structures",
+ input: map[string]any{
+ "nested": map[string]any{
+ "list": []any{1, "two", true},
},
- expected: func() starlarkLib.StringDict {
+ },
+ expected: starlarkLib.StringDict{
+ constants.Ctx: func() *starlarkLib.Dict {
inner := starlarkLib.NewDict(1)
l := starlarkLib.NewList([]starlarkLib.Value{
starlarkLib.MakeInt(1),
starlarkLib.String("two"),
starlarkLib.Bool(true),
})
- require.NoError(t, inner.SetKey(starlarkLib.String("list"), l))
+ if err := inner.SetKey(starlarkLib.String("list"), l); err != nil {
+ t.Fatalf("Failed to set list key: %v", err)
+ }
d := starlarkLib.NewDict(1)
- require.NoError(t, d.SetKey(starlarkLib.String("nested"), inner))
- return starlarkLib.StringDict{constants.Ctx: d}
+ if err := d.SetKey(starlarkLib.String("nested"), inner); err != nil {
+ t.Fatalf("Failed to set nested key: %v", err)
+ }
+ return d
}(),
},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertToStarlarkFormat(tt.input)
- if tt.wantErr {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- require.Equal(t, len(tt.expected), len(result))
-
- // Get ctx value and verify it's a dict
- ctxVal, ok := result[constants.Ctx].(*starlarkLib.Dict)
- require.True(t, ok, "Expected ctx value to be a dict")
-
- // Compare the dict contents
- expectedCtx := tt.expected[constants.Ctx].(*starlarkLib.Dict)
- require.Equal(t, expectedCtx.Len(), ctxVal.Len(), "Dict lengths should match")
-
- for _, k := range expectedCtx.Keys() {
- expectedVal, found, err := expectedCtx.Get(k)
- require.NoError(t, err)
- require.True(t, found)
-
- actualVal, found, err := ctxVal.Get(k)
- require.NoError(t, err)
- require.True(t, found)
-
- require.Equal(t, expectedVal.String(), actualVal.String())
- }
- })
- }
- })
-
- t.Run("error cases", func(t *testing.T) {
- tests := []struct {
- name string
- input map[string]any
- wantErr bool
- }{
- {
- name: "unsupported type",
- input: map[string]any{
- "chan": make(chan int),
- },
- wantErr: true,
- },
- {
- name: "mixed valid and invalid",
- input: map[string]any{
- "valid": "value",
- "invalid": make(chan int),
- },
- wantErr: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := ConvertToStarlarkFormat(tt.input)
+ wantErr: false,
+ },
+
+ // Error cases
+ {
+ name: "unsupported type",
+ input: map[string]any{
+ "chan": make(chan int),
+ },
+ wantErr: true,
+ },
+ {
+ name: "mixed valid and invalid",
+ input: map[string]any{
+ "valid": "value",
+ "invalid": make(chan int),
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt // Capture range variable
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := ConvertToStarlarkFormat(tt.input)
+
+ if tt.wantErr {
require.Error(t, err)
+ // Without defined error sentinel values, we can directly check the message
+ // In a more ideal harmonization, we would define and use error sentinels
require.Contains(t, err.Error(), "failed to convert input value")
- })
- }
- })
-}
+ return
+ }
-func TestConvertToStarlarkValue(t *testing.T) {
- t.Run("primitive types", func(t *testing.T) {
- tests := []struct {
- name string
- input any
- expected starlarkLib.Value
- }{
- {
- name: "nil",
- input: nil,
- expected: starlarkLib.None,
- },
- {
- name: "bool true",
- input: true,
- expected: starlarkLib.Bool(true),
- },
- {
- name: "bool false",
- input: false,
- expected: starlarkLib.Bool(false),
- },
- {
- name: "int",
- input: 42,
- expected: starlarkLib.MakeInt(42),
- },
- {
- name: "int64",
- input: int64(42),
- expected: starlarkLib.MakeInt64(42),
- },
- {
- name: "float64",
- input: 3.14,
- expected: starlarkLib.Float(3.14),
- },
- {
- name: "string",
- input: "hello",
- expected: starlarkLib.String("hello"),
- },
- }
+ require.NoError(t, err)
+ require.Equal(t, len(tt.expected), len(result))
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertToStarlarkValue(tt.input)
- require.NoError(t, err)
- require.Equal(t, tt.expected.String(), result.String())
- require.Equal(t, tt.expected.Type(), result.Type())
- })
- }
- })
+ // Get ctx value and verify it's a dict
+ ctxVal, ok := result[constants.Ctx].(*starlarkLib.Dict)
+ require.True(t, ok)
- t.Run("URL type", func(t *testing.T) {
- tests := []struct {
- name string
- input *url.URL
- expected starlarkLib.Value
- }{
- {
- name: "simple URL",
- input: &url.URL{
- Scheme: "https",
- Host: "example.com",
- },
- expected: starlarkLib.String("https://example.com"),
- },
- {
- name: "complex URL",
- input: &url.URL{
- Scheme: "https",
- Host: "example.com",
- Path: "/path",
- RawQuery: "q=search",
- },
- expected: starlarkLib.String("https://example.com/path?q=search"),
- },
- }
+ // Compare the dict contents
+ expectedCtx := tt.expected[constants.Ctx].(*starlarkLib.Dict)
+ require.Equal(t, expectedCtx.Len(), ctxVal.Len())
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertToStarlarkValue(tt.input)
+ for _, k := range expectedCtx.Keys() {
+ expectedVal, found, err := expectedCtx.Get(k)
require.NoError(t, err)
- require.Equal(t, tt.expected.String(), result.String())
- require.Equal(t, tt.expected.Type(), result.Type())
- })
- }
- })
-
- t.Run("slice types", func(t *testing.T) {
- tests := []struct {
- name string
- input []any
- expected func() *starlarkLib.List
- }{
- {
- name: "empty slice",
- input: []any{},
- expected: func() *starlarkLib.List {
- return starlarkLib.NewList([]starlarkLib.Value{})
- },
- },
- {
- name: "mixed types",
- input: []any{42, "hello", true},
- expected: func() *starlarkLib.List {
- return starlarkLib.NewList([]starlarkLib.Value{
- starlarkLib.MakeInt(42),
- starlarkLib.String("hello"),
- starlarkLib.Bool(true),
- })
- },
- },
- }
+ require.True(t, found)
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result, err := ConvertToStarlarkValue(tt.input)
+ actualVal, found, err := ctxVal.Get(k)
require.NoError(t, err)
- expected := tt.expected()
- require.Equal(t, expected.String(), result.String())
- require.Equal(t, expected.Len(), result.(*starlarkLib.List).Len())
- })
- }
- })
+ require.True(t, found)
- t.Run("map types", func(t *testing.T) {
- t.Run("map[string][]string type", func(t *testing.T) {
- input := map[string][]string{
- "headers": {"value1", "value2"},
- "empty": {},
+ require.Equal(t, expectedVal.String(), actualVal.String())
}
- result, err := ConvertToStarlarkValue(input)
- require.NoError(t, err)
- dict := result.(*starlarkLib.Dict)
-
- // Check headers
- headersVal, found, err := dict.Get(starlarkLib.String("headers"))
- require.NoError(t, err)
- require.True(t, found)
- headersList := headersVal.(*starlarkLib.List)
- require.Equal(t, 2, headersList.Len())
- val1 := starlarkLib.String("value1")
- val2 := starlarkLib.String("value2")
- require.Equal(t, val1, headersList.Index(0))
- require.Equal(t, val2, headersList.Index(1))
-
- // Check empty
- emptyVal, found, err := dict.Get(starlarkLib.String("empty"))
- require.NoError(t, err)
- require.True(t, found)
- emptyList := emptyVal.(*starlarkLib.List)
- require.Equal(t, 0, emptyList.Len())
})
+ }
+}
- t.Run("map[string]any type", func(t *testing.T) {
- input := map[string]any{
- "int": 42,
- "str": "hello",
- "nested": map[string]any{"key": "value"},
+func TestConvertToStarlarkValue(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ input any
+ expected starlarkLib.Value
+ checkFn func(t *testing.T, expected, actual starlarkLib.Value)
+ wantErr bool
+ errMsg string
+ }{
+ // Primitive types
+ {
+ name: "nil",
+ input: nil,
+ expected: starlarkLib.None,
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "bool true",
+ input: true,
+ expected: starlarkLib.Bool(true),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "bool false",
+ input: false,
+ expected: starlarkLib.Bool(false),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "int",
+ input: 42,
+ expected: starlarkLib.MakeInt(42),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "int64",
+ input: int64(42),
+ expected: starlarkLib.MakeInt64(42),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "float64",
+ input: 3.14,
+ expected: starlarkLib.Float(3.14),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "string",
+ input: "hello",
+ expected: starlarkLib.String("hello"),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+
+ // URL types
+ {
+ name: "simple URL",
+ input: &url.URL{
+ Scheme: "https",
+ Host: "localhost:8080",
+ },
+ expected: starlarkLib.String("https://localhost:8080"),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+ {
+ name: "complex URL",
+ input: &url.URL{
+ Scheme: "https",
+ Host: "localhost:8080",
+ Path: "/path",
+ RawQuery: "q=search",
+ },
+ expected: starlarkLib.String("https://localhost:8080/path?q=search"),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(t, expected.Type(), actual.Type())
+ },
+ },
+
+ // Slice types
+ {
+ name: "empty slice",
+ input: []any{},
+ expected: starlarkLib.NewList([]starlarkLib.Value{}),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(
+ t,
+ expected.(*starlarkLib.List).Len(),
+ actual.(*starlarkLib.List).Len(),
+ )
+ },
+ },
+ {
+ name: "mixed type slice",
+ input: []any{42, "hello", true},
+ expected: starlarkLib.NewList([]starlarkLib.Value{
+ starlarkLib.MakeInt(42),
+ starlarkLib.String("hello"),
+ starlarkLib.Bool(true),
+ }),
+ checkFn: func(t *testing.T, expected, actual starlarkLib.Value) {
+ t.Helper()
+ require.Equal(t, expected.String(), actual.String())
+ require.Equal(
+ t,
+ expected.(*starlarkLib.List).Len(),
+ actual.(*starlarkLib.List).Len(),
+ )
+ },
+ },
+
+ // Map types - We'll test these separately as they need special verification
+
+ // Error cases
+ {
+ name: "unsupported type",
+ input: make(chan int),
+ wantErr: true,
+ errMsg: "unsupported type chan int",
+ },
+ {
+ name: "invalid nested type",
+ input: []any{
+ make(chan int),
+ },
+ wantErr: true,
+ errMsg: "failed to convert list element",
+ },
+ {
+ name: "invalid map value",
+ input: map[string]any{
+ "chan": make(chan int),
+ },
+ wantErr: true,
+ errMsg: "failed to convert dict value",
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt // Capture range variable
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := ConvertToStarlarkValue(tt.input)
+
+ if tt.wantErr {
+ require.Error(t, err)
+ if tt.errMsg != "" {
+ require.Contains(t, err.Error(), tt.errMsg)
+ }
+ return
}
- result, err := ConvertToStarlarkValue(input)
- require.NoError(t, err)
- dict := result.(*starlarkLib.Dict)
-
- // Check int value
- intVal, found, err := dict.Get(starlarkLib.String("int"))
- require.NoError(t, err)
- require.True(t, found)
- expectedInt := starlarkLib.MakeInt(42)
- require.Equal(t, expectedInt, intVal)
-
- // Check string value
- strVal, found, err := dict.Get(starlarkLib.String("str"))
require.NoError(t, err)
- require.True(t, found)
- expectedStr := starlarkLib.String("hello")
- require.Equal(t, expectedStr, strVal)
-
- // Check nested dict
- nestedVal, found, err := dict.Get(starlarkLib.String("nested"))
- require.NoError(t, err)
- require.True(t, found)
- nestedDict := nestedVal.(*starlarkLib.Dict)
-
- keyVal, found, err := nestedDict.Get(starlarkLib.String("key"))
- require.NoError(t, err)
- require.True(t, found)
- expectedKeyVal := starlarkLib.String("value")
- require.Equal(t, expectedKeyVal, keyVal)
+ if tt.checkFn != nil {
+ tt.checkFn(t, tt.expected, result)
+ }
})
- })
+ }
- t.Run("error cases", func(t *testing.T) {
- tests := []struct {
- name string
- input any
- errorMsg string
- }{
- {
- name: "unsupported type",
- input: make(chan int),
- errorMsg: "unsupported type chan int",
- },
- {
- name: "invalid nested type",
- input: []any{
- make(chan int),
- },
- errorMsg: "failed to convert list element",
- },
- {
- name: "invalid map value",
- input: map[string]any{
- "chan": make(chan int),
- },
- errorMsg: "failed to convert dict value",
- },
+ // Test map types separately as they need more detailed verification
+ t.Run("map[string][]string type", func(t *testing.T) {
+ input := map[string][]string{
+ "headers": {"value1", "value2"},
+ "empty": {},
}
+ result, err := ConvertToStarlarkValue(input)
+ require.NoError(t, err)
+ dict := result.(*starlarkLib.Dict)
+
+ // Check headers
+ headersVal, found, err := dict.Get(starlarkLib.String("headers"))
+ require.NoError(t, err)
+ require.True(t, found)
+ headersList := headersVal.(*starlarkLib.List)
+ require.Equal(t, 2, headersList.Len())
+ val1 := starlarkLib.String("value1")
+ val2 := starlarkLib.String("value2")
+ require.Equal(t, val1, headersList.Index(0))
+ require.Equal(t, val2, headersList.Index(1))
+
+ // Check empty
+ emptyVal, found, err := dict.Get(starlarkLib.String("empty"))
+ require.NoError(t, err)
+ require.True(t, found)
+ emptyList := emptyVal.(*starlarkLib.List)
+ require.Equal(t, 0, emptyList.Len())
+ })
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := ConvertToStarlarkValue(tt.input)
- require.Error(t, err)
- require.Contains(t, err.Error(), tt.errorMsg)
- })
+ t.Run("map[string]any type", func(t *testing.T) {
+ input := map[string]any{
+ "int": 42,
+ "str": "hello",
+ "nested": map[string]any{"key": "value"},
}
+
+ result, err := ConvertToStarlarkValue(input)
+ require.NoError(t, err)
+ dict := result.(*starlarkLib.Dict)
+
+ // Check int value
+ intVal, found, err := dict.Get(starlarkLib.String("int"))
+ require.NoError(t, err)
+ require.True(t, found)
+ expectedInt := starlarkLib.MakeInt(42)
+ require.Equal(t, expectedInt, intVal)
+
+ // Check string value
+ strVal, found, err := dict.Get(starlarkLib.String("str"))
+ require.NoError(t, err)
+ require.True(t, found)
+ expectedStr := starlarkLib.String("hello")
+ require.Equal(t, expectedStr, strVal)
+
+ // Check nested dict
+ nestedVal, found, err := dict.Get(starlarkLib.String("nested"))
+ require.NoError(t, err)
+ require.True(t, found)
+ nestedDict := nestedVal.(*starlarkLib.Dict)
+
+ keyVal, found, err := nestedDict.Get(starlarkLib.String("key"))
+ require.NoError(t, err)
+ require.True(t, found)
+ expectedKeyVal := starlarkLib.String("value")
+ require.Equal(t, expectedKeyVal, keyVal)
})
}
diff --git a/polyscript_test.go b/polyscript_test.go
index a44355e..cc42878 100644
--- a/polyscript_test.go
+++ b/polyscript_test.go
@@ -19,6 +19,7 @@ import (
"github.com/robbyt/go-polyscript/execution/data"
"github.com/robbyt/go-polyscript/execution/script/loader"
extismCompiler "github.com/robbyt/go-polyscript/machines/extism/compiler"
+ "github.com/robbyt/go-polyscript/machines/mocks"
risorCompiler "github.com/robbyt/go-polyscript/machines/risor/compiler"
starlarkCompiler "github.com/robbyt/go-polyscript/machines/starlark/compiler"
"github.com/robbyt/go-polyscript/machines/types"
@@ -43,44 +44,6 @@ func withCompositeProvider(staticData map[string]any) any {
))
}
-// Create a mock evaluator response
-type mockResponse struct {
- value any
-}
-
-func (m mockResponse) Interface() any {
- return m.value
-}
-
-func (m mockResponse) GetScriptExeID() string {
- return "mock-script-id"
-}
-
-func (m mockResponse) GetExecTime() string {
- return "1ms"
-}
-
-func (m mockResponse) Inspect() string {
- return "mock-response"
-}
-
-func (m mockResponse) Type() data.Types {
- return data.NONE
-}
-
-// mockEvaluator implements engine.Evaluator for testing
-type mockEvaluator struct {
- mock.Mock
-}
-
-func (m *mockEvaluator) Eval(ctx context.Context) (engine.EvaluatorResponse, error) {
- args := m.Called(ctx)
- if args.Get(0) == nil {
- return nil, args.Error(1)
- }
- return mockResponse{value: args.Get(0)}, args.Error(1)
-}
-
// mockPreparer implements engine.EvalDataPreparer for testing
type mockPreparer struct {
mock.Mock
@@ -176,8 +139,6 @@ func TestMachineEvaluators(t *testing.T) {
for _, tc := range tests {
tc := tc // Capture for parallel execution
t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
// Create a loader
l, err := loader.NewFromString(tc.content)
require.NoError(t, err)
@@ -277,8 +238,6 @@ func TestNewEvaluator(t *testing.T) {
for _, tc := range tests {
tc := tc // Capture for parallel execution
t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
var evaluator engine.EvaluatorWithPrep
var err error
@@ -350,8 +309,6 @@ func TestFromStringLoaders(t *testing.T) {
for _, tc := range tests {
tc := tc // Capture for parallel execution
t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
evaluator, err := tc.creator(tc.content, tc.options...)
if tc.expectError {
@@ -368,8 +325,6 @@ func TestFromStringLoaders(t *testing.T) {
// Test invalid option in string loader
t.Run("FromRisorString - Invalid Option", func(t *testing.T) {
- t.Parallel()
-
_, err := FromRisorString(
"print('test')",
func(cfg *options.Config) error {
@@ -471,8 +426,6 @@ _ = result`
for _, tc := range tests {
tc := tc // Capture for parallel execution
t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
evaluator, err := tc.loaderFunc(tc.filePath, tc.options...)
if tc.expectError {
@@ -497,8 +450,6 @@ func TestDataProviders(t *testing.T) {
t.Parallel()
t.Run("withCompositeProvider", func(t *testing.T) {
- t.Parallel()
-
// Create a simple script that uses composite data
script := `print(ctx["static_key"], ", ", ctx["input_data"]["dynamic_key"])`
@@ -532,8 +483,6 @@ func TestEvalHelpers(t *testing.T) {
t.Parallel()
t.Run("PrepareAndEval", func(t *testing.T) {
- t.Parallel()
-
// Create a simple Risor evaluator
script := `
name := ctx["input_data"]["name"]
@@ -584,7 +533,7 @@ func TestEvalHelpers(t *testing.T) {
t.Run("PrepareContext error", func(t *testing.T) {
// Create mocks for testing error cases
mockPrepCtx := &mockPreparer{}
- mockEval := &mockEvaluator{}
+ mockEval := &mocks.Evaluator{}
// Create context and data
ctx := context.Background()
@@ -613,7 +562,7 @@ func TestEvalHelpers(t *testing.T) {
t.Run("Eval error", func(t *testing.T) {
// Create mocks for testing error cases
mockPrepCtx := &mockPreparer{}
- mockEval := &mockEvaluator{}
+ mockEval := &mocks.Evaluator{}
// Create context and data
ctx := context.Background()
@@ -625,7 +574,8 @@ func TestEvalHelpers(t *testing.T) {
mockPrepCtx.On("PrepareContext", ctx, []any{data}).Return(enrichedCtx, nil)
// Mock Eval to fail
- mockEval.On("Eval", enrichedCtx).Return(nil, errors.New("eval error"))
+ mockEval.On("Eval", enrichedCtx).
+ Return((*mocks.EvaluatorResponse)(nil), errors.New("eval error"))
// Create a mock evaluator that implements both interfaces
mockEvalWithPrep := struct {
@@ -646,8 +596,6 @@ func TestEvalHelpers(t *testing.T) {
})
t.Run("EvalAndExtractMap", func(t *testing.T) {
- t.Parallel()
-
// Create a simple Risor evaluator
script := `
{
@@ -714,11 +662,12 @@ func TestEvalHelpers(t *testing.T) {
// Test with evaluation error
t.Run("Eval error", func(t *testing.T) {
- mockEval := &mockEvaluator{}
+ mockEval := &mocks.Evaluator{}
ctx := context.Background()
// Mock Eval to return an error
- mockEval.On("Eval", ctx).Return(nil, errors.New("eval error"))
+ mockEval.On("Eval", ctx).
+ Return((*mocks.EvaluatorResponse)(nil), errors.New("eval error"))
// EvalAndExtractMap should return the error
_, err = evalAndExtractMap(t, ctx, mockEval)
@@ -770,8 +719,6 @@ _ = result`
}
t.Run("FromRisorStringWithData", func(t *testing.T) {
- t.Parallel()
-
// Test script
risorScript := `
// Access static data
@@ -818,8 +765,6 @@ _ = result`
})
t.Run("FromStarlarkStringWithData", func(t *testing.T) {
- t.Parallel()
-
// Create evaluator
starlarkEval, err := FromStarlarkStringWithData(
starlarkFileContent,
@@ -847,23 +792,7 @@ _ = result`
assert.Equal(t, int64(30), starlarkTimeout, "timeout should be 30")
})
- t.Run("FromRisorFileWithData", func(t *testing.T) {
- t.Parallel()
-
- // Skip this test and mark as passing - will be tested separately
- t.Skip("Test refactored to use simpler test approach")
- })
-
- t.Run("FromStarlarkFileWithData", func(t *testing.T) {
- t.Parallel()
-
- // Skip this test and mark as passing - will be tested separately
- t.Skip("Test refactored to use simpler test approach")
- })
-
t.Run("FromExtismFileWithData", func(t *testing.T) {
- t.Parallel()
-
// Create evaluator with static data that includes input
extismEval, err := FromExtismFileWithData(
wasmPath,
@@ -1049,8 +978,6 @@ func TestCreateEvaluatorEdgeCases(t *testing.T) {
// Test validation error in newEvaluator
t.Run("Configuration Validation Error", func(t *testing.T) {
- t.Parallel()
-
// Try to create an evaluator without a loader
_, err := NewRisorEvaluator()
require.Error(t, err)
@@ -1059,8 +986,6 @@ func TestCreateEvaluatorEdgeCases(t *testing.T) {
// Test option application error
t.Run("Option Error", func(t *testing.T) {
- t.Parallel()
-
// Create an invalid option that returns an error
invalidOption := func(cfg *options.Config) error {
return errors.New("custom invalid option error")