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")