From e3d9a4510e5482dcd199156a4337067318397a56 Mon Sep 17 00:00:00 2001 From: RT Date: Wed, 9 Apr 2025 21:49:31 -0400 Subject: [PATCH 01/15] add and adjust tests --- engine/evaluatorResponse_test.go | 150 ++++ engine/evaluator_test.go | 229 +++++- engine/executionPackage_test.go | 123 +++ examples/data-prep/risor/main.go | 6 +- examples/data-prep/starlark/main.go | 8 +- execution/data/README.md | 8 +- execution/data/compositeProvider_test.go | 281 ++----- execution/data/contextProvider_test.go | 246 ++---- execution/data/data_helpers_test.go | 102 +++ execution/data/provider_test.go | 60 -- execution/data/staticProvider_test.go | 228 +++--- execution/script/loader/fromDisk_test.go | 94 ++- execution/script/loader/fromHTTP.go | 8 +- execution/script/loader/fromHTTP_test.go | 740 +++++++++--------- execution/script/loader/fromString_test.go | 89 +-- .../script/loader/httpauth/basic_test.go | 10 +- .../script/loader/httpauth/header_test.go | 16 +- .../script/loader/httpauth/noauth_test.go | 8 +- execution/script/loader/loader_test.go | 101 +++ .../evaluator/bytecodeEvaluator_test.go | 343 +++++++- .../evaluator/response_comprehensive_test.go | 169 ++++ machines/starlark/internal/converters_test.go | 12 +- 22 files changed, 1962 insertions(+), 1069 deletions(-) create mode 100644 engine/evaluatorResponse_test.go create mode 100644 engine/executionPackage_test.go create mode 100644 execution/data/data_helpers_test.go create mode 100644 execution/script/loader/loader_test.go create mode 100644 machines/extism/evaluator/response_comprehensive_test.go diff --git a/engine/evaluatorResponse_test.go b/engine/evaluatorResponse_test.go new file mode 100644 index 0000000..028974f --- /dev/null +++ b/engine/evaluatorResponse_test.go @@ -0,0 +1,150 @@ +package engine_test + +import ( + "testing" + + "github.com/robbyt/go-polyscript/execution/data" + "github.com/robbyt/go-polyscript/machines/mocks" + "github.com/stretchr/testify/assert" +) + +// TestEvaluatorResponseInterface tests all methods of the EvaluatorResponse interface +func TestEvaluatorResponseInterface(t *testing.T) { + // Create a mock implementation of EvaluatorResponse + 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") + }) + } + }) + + // Test Inspect method + 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) { + // Create a mock implementation + 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 (common in go-polyscript) + result := mockResponse.Interface() + if 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") + } else { + t.Fail() + } + + // 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() + if 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") + } else { + t.Fail() + } + + // Verify all expected assertions + mockResponse.AssertExpectations(t) +} diff --git a/engine/evaluator_test.go b/engine/evaluator_test.go index 36b6751..49fa6dc 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,14 +10,111 @@ 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/require" ) +// mockEvaluatorImplementation is a minimal struct implementing engine.Evaluator for testing +type mockEvaluatorImplementation struct { + evalFunc func(ctx context.Context) (engine.EvaluatorResponse, error) +} + +func (m *mockEvaluatorImplementation) Eval(ctx context.Context) (engine.EvaluatorResponse, error) { + return m.evalFunc(ctx) +} + +// mockEvalDataPreparerImplementation is a minimal struct implementing engine.EvalDataPreparer for testing +type mockEvalDataPreparerImplementation struct { + prepareFunc func(ctx context.Context, data ...any) (context.Context, error) +} + +func (m *mockEvalDataPreparerImplementation) PrepareContext( + ctx context.Context, + data ...any, +) (context.Context, error) { + return m.prepareFunc(ctx, data...) +} + +// mockEvaluatorWithPrepImplementation is a minimal struct implementing engine.EvaluatorWithPrep for testing +type mockEvaluatorWithPrepImplementation struct { + evalFunc func(ctx context.Context) (engine.EvaluatorResponse, error) + prepareFunc func(ctx context.Context, data ...any) (context.Context, error) +} + +func (m *mockEvaluatorWithPrepImplementation) Eval( + ctx context.Context, +) (engine.EvaluatorResponse, error) { + return m.evalFunc(ctx) +} + +func (m *mockEvaluatorWithPrepImplementation) PrepareContext( + ctx context.Context, + data ...any, +) (context.Context, error) { + return m.prepareFunc(ctx, data...) +} + +func TestEvaluatorInterface(t *testing.T) { + // 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") + + // Define a type for the context key to avoid collision + type contextKey string + testKey := contextKey("test-key") + + // Create a mock evaluator implementation + evaluator := &mockEvaluatorImplementation{ + evalFunc: func(ctx context.Context) (engine.EvaluatorResponse, error) { + // Verify that context is passed correctly + _, hasKey := ctx.Value(testKey).(string) + if !hasKey { + return nil, errors.New("context key missing") + } + return mockResponse, nil + }, + } + + // Test the Eval method with a context containing a test key + ctx := context.WithValue(context.Background(), testKey, "test-value") + 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 + evaluator.evalFunc = func(ctx context.Context) (engine.EvaluatorResponse, error) { + return nil, errors.New("evaluation error") + } + + response, err = evaluator.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) { // Create a logger for testing handler := slog.NewTextHandler(os.Stdout, nil) @@ -39,7 +137,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,6 +175,135 @@ method + " " + greeting ) } +func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { + // Define a type for the context key to avoid collision + type dataKey string + + // Create a mock data preparer implementation + dataPreparer := &mockEvalDataPreparerImplementation{ + prepareFunc: func(ctx context.Context, data ...any) (context.Context, error) { + // Simple implementation to store data in context + enrichedCtx := ctx + for i, item := range data { + key := dataKey(fmt.Sprintf("data-%d", i)) + enrichedCtx = context.WithValue(enrichedCtx, key, item) + } + return enrichedCtx, nil + }, + } + + // Test with various data types + ctx := context.Background() + data1 := "string data" + data2 := map[string]any{"key": "value"} + data3 := 123 + + enrichedCtx, err := dataPreparer.PrepareContext(ctx, data1, data2, data3) + require.NoError(t, err, "PrepareContext should not return an error") + require.NotNil(t, enrichedCtx, "Enriched context should not be nil") + + // Verify data was stored correctly + assert.Equal( + t, + data1, + enrichedCtx.Value(dataKey("data-0")), + "First data item should be stored correctly", + ) + assert.Equal( + t, + data2, + enrichedCtx.Value(dataKey("data-1")), + "Second data item should be stored correctly", + ) + assert.Equal( + t, + data3, + enrichedCtx.Value(dataKey("data-2")), + "Third data item should be stored correctly", + ) + + // Test error case + dataPreparer.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { + return ctx, errors.New("preparation error") + } + + _, err = dataPreparer.PrepareContext(ctx, "test") + assert.Error(t, err, "Should return an error") + assert.Contains(t, err.Error(), "preparation error", "Error message should be preserved") +} + +func TestEvaluatorWithPrepInterface(t *testing.T) { + // 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") + + // Define a type for the context key to avoid collision + type prepKey string + prepDataKey := prepKey("prepared-data") + + // Create a mock combined implementation + combinedEvaluator := &mockEvaluatorWithPrepImplementation{ + evalFunc: func(ctx context.Context) (engine.EvaluatorResponse, error) { + // Check if context has prepared data + value, ok := ctx.Value(prepDataKey).(string) + if !ok || value != "test-value" { + return nil, errors.New("context not properly prepared") + } + return mockResponse, nil + }, + prepareFunc: func(ctx context.Context, data ...any) (context.Context, error) { + // Store the prepared data in context + return context.WithValue(ctx, prepDataKey, "test-value"), nil + }, + } + + // Test the full workflow: prepare context then evaluate + ctx := context.Background() + + // First prepare the context + enrichedCtx, err := combinedEvaluator.PrepareContext(ctx, "test data") + require.NoError(t, err, "PrepareContext should not return an error") + require.NotNil(t, enrichedCtx, "Enriched context should not be nil") + + // Then evaluate with the enriched context + response, err := combinedEvaluator.Eval(enrichedCtx) + 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 + combinedEvaluator.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { + return ctx, errors.New("preparation error") + } + + _, err = combinedEvaluator.PrepareContext(ctx, "test data") + assert.Error(t, err, "Should return an error when preparation fails") + + // Test error in evaluation + combinedEvaluator.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { + return context.WithValue(ctx, prepDataKey, "test-value"), nil + } + combinedEvaluator.evalFunc = func(ctx context.Context) (engine.EvaluatorResponse, error) { + return nil, errors.New("evaluation error") + } + + enrichedCtx, prepErr := combinedEvaluator.PrepareContext(ctx, "test data") + require.NoError(t, prepErr, "PrepareContext should not return an error") + _, err = combinedEvaluator.Eval(enrichedCtx) + assert.Error(t, err, "Should return an error when evaluation fails") +} + func TestEvaluatorWithPrepErrors(t *testing.T) { // 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..0bc4d0f --- /dev/null +++ b/engine/executionPackage_test.go @@ -0,0 +1,123 @@ +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) { + // 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) { + // 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) { + // 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) { + // 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/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..c62d336 100644 --- a/execution/data/compositeProvider_test.go +++ b/execution/data/compositeProvider_test.go @@ -118,8 +118,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 +142,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 +171,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{ @@ -242,14 +247,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") + assertMapContainsExpected(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 + verifyDataConsistency(t, provider, ctx) }) } } @@ -261,13 +262,13 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { 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") @@ -276,19 +277,19 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { 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) @@ -300,43 +301,43 @@ 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 @@ -352,66 +353,41 @@ 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) }) @@ -419,24 +395,21 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { 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 +420,7 @@ func TestCompositeProvider_NestedStructures(t *testing.T) { tests := []struct { name string setupProviders func() *CompositeProvider + setupContext func() context.Context expectedResult map[string]any }{ { @@ -470,6 +444,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 +480,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 +491,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 +514,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", @@ -644,59 +541,19 @@ func TestCompositeProvider_NestedStructures(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) - } - } + assertMapContainsExpected(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() diff --git a/execution/data/contextProvider_test.go b/execution/data/contextProvider_test.go index 335b158..39855dc 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 @@ -14,55 +15,39 @@ func TestContextProvider_Creation(t *testing.T) { 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") }) } @@ -72,6 +57,7 @@ func TestContextProvider_GetData(t *testing.T) { t.Run("empty context key", func(t *testing.T) { t.Parallel() + provider := NewContextProvider("") ctx := context.Background() @@ -83,6 +69,7 @@ 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 +78,14 @@ 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 + verifyDataConsistency(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 +93,14 @@ 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 + verifyDataConsistency(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 +108,14 @@ 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 + verifyDataConsistency(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") @@ -128,6 +127,7 @@ 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) @@ -144,20 +144,25 @@ func TestContextProvider_AddDataToContext(t *testing.T) { 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) @@ -167,12 +172,13 @@ 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) @@ -187,13 +193,16 @@ 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) @@ -207,31 +216,14 @@ 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, req) - assert.NoError(t, err) + newCtx, err := provider.AddDataToContext(ctx, createTestRequest()) + + 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 +235,18 @@ 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") @@ -289,14 +255,18 @@ 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 +276,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 @@ -332,14 +284,13 @@ func TestContextProvider_DataIntegration(t *testing.T) { t.Run("single map data item", func(t *testing.T) { t.Parallel() - provider := NewContextProvider(constants.EvalData) - // Start with empty context + provider := NewContextProvider(constants.EvalData) 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 +305,29 @@ 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..b6dd4d1 --- /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"}, + } +) + +// 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 +} + +// assertMapContainsExpected asserts that a map contains all expected key/value pairs +func assertMapContainsExpected(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 { + assertMapContainsExpected(t, expectedMap, actualMap) + } else { + assert.Equal(t, expectedValue, actualValue, "Value should match for key: %s", key) + } + } +} + +// verifyDataConsistency checks if multiple calls to GetData return consistent results +func verifyDataConsistency(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/provider_test.go b/execution/data/provider_test.go index 6b2b395..f2b18f8 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() diff --git a/execution/data/staticProvider_test.go b/execution/data/staticProvider_test.go index 7f67aeb..0759bd6 100644 --- a/execution/data/staticProvider_test.go +++ b/execution/data/staticProvider_test.go @@ -6,116 +6,130 @@ 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 { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + 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 { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + 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 + verifyDataConsistency(t, provider, ctx) + }) + } } // TestStaticProvider_AddDataToContext tests that StaticProvider properly rejects all context updates @@ -124,15 +138,16 @@ func TestStaticProvider_AddDataToContext(t *testing.T) { 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) @@ -142,43 +157,44 @@ 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, createTestRequest()) 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/loader/fromDisk_test.go b/execution/script/loader/fromDisk_test.go index 94b5f62..3bde798 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" @@ -15,10 +14,12 @@ func TestNewFromDisk(t *testing.T) { t.Parallel() t.Run("valid paths", func(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() absPath := filepath.Join(tempDir, "test.js") - cases := []struct { + tests := []struct { name string path string wantPath string @@ -35,34 +36,45 @@ 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) { + t.Parallel() + 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 { + t.Parallel() + + 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) { + t.Parallel() + loader, err := NewFromDisk(tc.path) require.Error(t, err) require.ErrorIs(t, err, ErrSchemeUnsupported) @@ -72,7 +84,9 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("relative paths", func(t *testing.T) { - cases := []struct { + t.Parallel() + + tests := []struct { name string path string }{ @@ -81,8 +95,11 @@ 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) { + t.Parallel() + loader, err := NewFromDisk(tc.path) require.Error(t, err) require.ErrorIs(t, err, ErrScriptNotAvailable) @@ -92,7 +109,9 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("empty or invalid paths", func(t *testing.T) { - cases := []struct { + t.Parallel() + + tests := []struct { name string path string }{ @@ -103,8 +122,11 @@ 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) { + t.Parallel() + if tc.path == "\\" && runtime.GOOS != "windows" { t.Skip("Skipping Windows-specific test on non-Windows platform") } @@ -117,6 +139,8 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("url parsing errors", func(t *testing.T) { + t.Parallel() + loader, err := NewFromDisk("file://[invalid-url") require.Error(t, err) require.ErrorContains(t, err, "relative paths are not supported") @@ -124,6 +148,8 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("non-file scheme", func(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() absPath := filepath.Join(tempDir, "test.js") loader, err := NewFromDisk("http://" + absPath) @@ -137,6 +163,8 @@ func TestFromDisk_GetReader(t *testing.T) { t.Parallel() t.Run("read file contents", func(t *testing.T) { + t.Parallel() + // Setup test file tempDir := t.TempDir() testContent := "test content\nwith multiple lines" @@ -153,23 +181,15 @@ 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) { + t.Parallel() + // 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,28 +199,12 @@ 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) { + t.Parallel() + tempDir := t.TempDir() nonExistingFile := filepath.Join(tempDir, "nonexisting.js") @@ -218,6 +222,8 @@ func TestFromDisk_GetSourceURL(t *testing.T) { t.Parallel() t.Run("valid source URL", func(t *testing.T) { + t.Parallel() + // Setup test file tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.risor") @@ -238,6 +244,8 @@ func TestFromDisk_String(t *testing.T) { t.Parallel() t.Run("string representation with content", func(t *testing.T) { + t.Parallel() + // Setup test file tempDir := t.TempDir() testContent := "test content for string method" @@ -262,6 +270,8 @@ func TestFromDisk_String(t *testing.T) { }) t.Run("string representation with non-existent file", func(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() nonExistingFile := filepath.Join(tempDir, "nonexisting.js") 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..c18bcfd 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,94 @@ 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) { + t.Parallel() -// mockResponseBody implements io.ReadCloser for testing -type mockResponseBody struct { - io.Reader - closeFunc func() error - closed bool -} + // 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() -func (m *mockResponseBody) Close() error { - if m.closed { - return nil - } - m.closed = true + testURL := tlsServer.URL + "/script.js" - if m.closeFunc != nil { - return m.closeFunc() - } - return nil -} + // 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) -// 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), - } -} + // 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) -func TestNewFromHTTP(t *testing.T) { - t.Parallel() + // Use TLS server's client to accept its certificate + loader.client = tlsServer.Client() - 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", - }, - } + // Additional verification + verifyLoader(t, loader, testURL) + }) - 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 - } + t.Run("Valid HTTP URL", func(t *testing.T) { + t.Parallel() + // 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) - 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) - }) - } + })) + defer server.Close() + + testURL := server.URL + "/script.js" + + 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) { + t.Parallel() + + 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) { + t.Parallel() + + 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 +108,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 +115,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 +125,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 +138,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 +150,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 +163,52 @@ 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) { + t.Parallel() + + // 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 +217,24 @@ func TestFromHTTP_TLSConfig(t *testing.T) { t.Parallel() t.Run("with insecure skip verify", func(t *testing.T) { + t.Parallel() + + // 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 +247,31 @@ func TestFromHTTP_TLSConfig(t *testing.T) { }) t.Run("with custom TLS config", func(t *testing.T) { + t.Parallel() + + // 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 +285,20 @@ func TestFromHTTP_TLSConfig(t *testing.T) { }) t.Run("TLSConfig takes precedence over InsecureSkipVerify", func(t *testing.T) { + t.Parallel() + + // 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 +307,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 +320,29 @@ 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) { + t.Parallel() + + // 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 +353,226 @@ 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) { + t.Parallel() - 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 - }, - } + // 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() - var loader *FromHTTP - var err error + testURL := server.URL + "/script.js" - // Start with default options and apply modifier if provided - options := DefaultHTTPOptions() - if tt.optionsModifier != nil { - options = tt.optionsModifier(options) - } + loader, err := NewFromHTTP(testURL) + require.NoError(t, err) - loader, err = NewFromHTTPWithOptions(tt.url, options) - require.NoError(t, err, "Failed to create HTTP loader") + // Use real server with helper + reader, err := loader.GetReader() + require.NoError(t, err) + verifyReaderContent(t, reader, testScript) + }) - // Replace the client with our mock - loader.client = mockClient + t.Run("unauthorized error", func(t *testing.T) { + t.Parallel() - reader, err := loader.GetReader() - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - require.Contains(t, err.Error(), tt.errorContains) - } - return - } + 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() + + testURL := server.URL + "/auth" + + 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) { + t.Parallel() + + 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) { + t.Parallel() + + // 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) { + t.Parallel() - 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", - }, - } + // 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) { - loader, err := NewFromHTTP(tt.url) + testURL := server.URL + "/script.js" + + loader, err := NewFromHTTP(testURL) + require.NoError(t, err) + + 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) { + t.Parallel() + + // 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) { + t.Parallel() + // 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) { + t.Parallel() + + // 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 +580,14 @@ func TestFromHTTP_String(t *testing.T) { }) t.Run("string representation with network error", func(t *testing.T) { - testURL := "https://example.com/script.js" + t.Parallel() + + // 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 +595,21 @@ func TestFromHTTP_String(t *testing.T) { }) t.Run("string representation with HTTP error", func(t *testing.T) { - testURL := "https://example.com/script.js" + t.Parallel() + + // 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) @@ -608,6 +634,8 @@ func TestHTTPOptionsWithMethods(t *testing.T) { t.Parallel() t.Run("option method chaining", func(t *testing.T) { + t.Parallel() + // Test chaining of option methods options := DefaultHTTPOptions(). WithTimeout(60*time.Second). @@ -623,6 +651,8 @@ func TestHTTPOptionsWithMethods(t *testing.T) { }) t.Run("with header auth", func(t *testing.T) { + t.Parallel() + headers := map[string]string{ "X-API-Key": "api-key-123", "X-Custom-Key": "custom-value", @@ -637,6 +667,8 @@ func TestHTTPOptionsWithMethods(t *testing.T) { }) t.Run("with no auth", func(t *testing.T) { + t.Parallel() + // Start with basic auth options := DefaultHTTPOptions().WithBasicAuth("user", "pass") @@ -653,17 +685,23 @@ func TestFromHTTP_GetSourceURL(t *testing.T) { t.Parallel() t.Run("source URL", func(t *testing.T) { - testURL := "https://example.com/script.js" + t.Parallel() + + // 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..5446ba1 100644 --- a/execution/script/loader/fromString_test.go +++ b/execution/script/loader/fromString_test.go @@ -12,15 +12,17 @@ func TestNewFromString(t *testing.T) { t.Parallel() t.Run("valid content", func(t *testing.T) { - cases := []struct { + t.Parallel() + + 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 +31,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,8 +46,11 @@ 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) { + t.Parallel() + loader, err := NewFromString(tc.content) require.NoError(t, err) require.NotNil(t, loader) @@ -54,12 +59,17 @@ 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 { + t.Parallel() + + tests := []struct { name string content string }{ @@ -73,8 +83,11 @@ 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) { + t.Parallel() + loader, err := NewFromString(tc.content) require.Error(t, err) require.ErrorIs(t, err, ErrScriptNotAvailable) @@ -82,21 +95,14 @@ 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) { t.Parallel() t.Run("read content", func(t *testing.T) { + t.Parallel() + content := "test content\nwith multiple lines" loader, err := NewFromString(content) require.NoError(t, err) @@ -104,42 +110,22 @@ 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; }" - loader, err := NewFromString(content) - require.NoError(t, err) + t.Parallel() - // 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) + content := FunctionContent + loader, err := NewFromString(content) 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) { + t.Parallel() + content := "line1\nline2\nline3\nline4\nline5" loader, err := NewFromString(content) require.NoError(t, err) @@ -168,7 +154,9 @@ func TestFromString_GetSourceURL(t *testing.T) { t.Parallel() t.Run("source url", func(t *testing.T) { - content := "test content" + t.Parallel() + + content := SimpleContent loader, err := NewFromString(content) require.NoError(t, err) @@ -184,6 +172,8 @@ func TestFromString_GetSourceURL(t *testing.T) { }) t.Run("unique urls for different content", func(t *testing.T) { + t.Parallel() + loader1, err := NewFromString("content one") require.NoError(t, err) @@ -199,8 +189,10 @@ func TestFromString_String(t *testing.T) { t.Parallel() t.Run("string representation", func(t *testing.T) { + t.Parallel() + // Test with different content lengths - testCases := []struct { + tests := []struct { name string content string shouldMatch string @@ -217,8 +209,11 @@ 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) { + t.Parallel() + 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..21701e8 100644 --- a/execution/script/loader/httpauth/basic_test.go +++ b/execution/script/loader/httpauth/basic_test.go @@ -17,7 +17,7 @@ func TestBasicAuth(t *testing.T) { 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) @@ -37,7 +37,7 @@ func TestBasicAuth(t *testing.T) { 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) @@ -56,7 +56,7 @@ func TestBasicAuth(t *testing.T) { 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() @@ -77,7 +77,7 @@ func TestBasicAuth(t *testing.T) { 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()) @@ -96,7 +96,7 @@ func TestBasicAuth(t *testing.T) { 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..8934bd2 100644 --- a/execution/script/loader/httpauth/header_test.go +++ b/execution/script/loader/httpauth/header_test.go @@ -22,7 +22,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) @@ -39,7 +39,7 @@ func TestHeaderAuth(t *testing.T) { 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) @@ -54,7 +54,7 @@ func TestHeaderAuth(t *testing.T) { 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) @@ -69,7 +69,7 @@ func TestHeaderAuth(t *testing.T) { 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) @@ -86,7 +86,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) ctx := context.Background() @@ -104,7 +104,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) ctx, cancel := context.WithCancel(context.Background()) @@ -123,7 +123,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) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) @@ -152,7 +152,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..c803cb5 100644 --- a/execution/script/loader/httpauth/noauth_test.go +++ b/execution/script/loader/httpauth/noauth_test.go @@ -21,7 +21,7 @@ func TestNoAuth(t *testing.T) { 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) @@ -34,7 +34,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() @@ -47,7 +47,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()) @@ -61,7 +61,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/extism/evaluator/bytecodeEvaluator_test.go b/machines/extism/evaluator/bytecodeEvaluator_test.go index 07bb916..b94c766 100644 --- a/machines/extism/evaluator/bytecodeEvaluator_test.go +++ b/machines/extism/evaluator/bytecodeEvaluator_test.go @@ -7,15 +7,49 @@ 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" ) +// MockCompiledPlugin is a mock implementation of adapters.CompiledPlugin +type MockCompiledPlugin struct { + mock.Mock +} + +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) +} + func TestLoadInputData(t *testing.T) { t.Parallel() @@ -146,14 +180,18 @@ func TestBytecodeEvaluatorInvalidInputs(t *testing.T) { func TestNilHandlerFallback(t *testing.T) { // Test that the evaluator handles nil handlers by creating a default + + // 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: &mockExecutableContent{ - machineType: machineTypes.Extism, - source: "test wasm", - bytecode: []byte{0x00, 0x61, 0x73, 0x6D}, - }, + Content: content, } // Create with nil handler @@ -217,15 +255,23 @@ func TestBasicExecution(t *testing.T) { // Create context provider ctxProvider := data.NewContextProvider(constants.EvalData) + // Create mock plugin + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + exitCode: 1, // Will cause an error + 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 real compiler.Executable with our mock plugin + content := createMockExecutable(mockPlugin, "main") + // 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: content, } evaluator := NewBytecodeEvaluator(handler, exe) @@ -236,7 +282,7 @@ func TestBasicExecution(t *testing.T) { ctx = context.WithValue(ctx, constants.EvalData, evalData) _, err := evaluator.Eval(ctx) - // We expect an error since our mock WASM isn't valid + // We expect an error since our mock returns an error assert.Error(t, err) } @@ -254,14 +300,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 +319,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 +337,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 +365,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"}}, @@ -379,7 +429,7 @@ func (m *mockErrProvider) AddDataToContext( return ctx, m.err } -// mockPluginInstance is a mock implementation of the testPluginInstance interface +// mockPluginInstance is a mock implementation of the adapters.PluginInstance interface type mockPluginInstance struct { exitCode uint32 output []byte @@ -407,6 +457,15 @@ func (m *mockPluginInstance) CallWithContext( return m.exitCode, m.output, m.callErr } +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 @@ -655,6 +714,218 @@ func TestStaticAndDynamicDataCombination(t *testing.T) { } */ +// TestBytecodeEvaluator_Cancel tests the behavior when a context is cancelled +func TestBytecodeEvaluator_Cancel(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) + + // Create a real compiler.Executable with our mock plugin + content := createMockExecutable(mockPlugin, "main") + + // Create executor unit + handler := slog.NewTextHandler(os.Stdout, nil) + execUnit := &script.ExecutableUnit{ + ID: "test-cancel", + Content: content, + DataProvider: data.NewContextProvider(constants.EvalData), + } + + evaluator := NewBytecodeEvaluator(handler, execUnit) + + // Add test data to context + ctx = context.WithValue(ctx, constants.EvalData, map[string]any{"test": "data"}) + + // Call Eval, which should be cancelled during execution + result, err := evaluator.Eval(ctx) + + // Should get a cancellation error + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "execution") + + // Instance should have been called + mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything) + + // Instance should have been closed + assert.True(t, mockInstance.wasClosed) +} + +// TestBytecodeEvaluator_Exec tests the exec method directly +func TestBytecodeEvaluator_Exec(t *testing.T) { + // Define test cases + tests := []struct { + name string + setupMocks func() (*MockCompiledPlugin, *mockPluginInstance) + entryPoint string + inputData map[string]any + expectedValue any + expectError bool + errorContains string + }{ + { + name: "successful execution", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + exitCode: 0, + output: []byte(`{"result":"success"}`), + } + mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectedValue: map[string]any{"result": "success"}, + expectError: false, + }, + { + name: "error creating instance", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + wasClosed: true, // Mock as if it was closed since the exec method closes the instance on error + } + mockPlugin.On("Instance", mock.Anything, mock.Anything). + Return(mockInstance, errors.New("instance creation error")) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectError: true, + errorContains: "failed to create plugin instance", + }, + { + name: "execution error", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + callErr: errors.New("execution error"), + } + mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectError: true, + errorContains: "execution error", + }, + { + name: "non-zero exit code", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + exitCode: 1, + output: []byte(`{"error":"something went wrong"}`), + } + mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectError: true, + errorContains: "non-zero exit code", + }, + { + name: "context cancellation", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + callErr: context.Canceled, + } + mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectError: true, + errorContains: "execution", + }, + { + name: "close error (should still succeed)", + setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { + mockPlugin := new(MockCompiledPlugin) + mockInstance := &mockPluginInstance{ + exitCode: 0, + output: []byte(`{"result":"success"}`), + closeErr: errors.New("close error"), + } + mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) + mockPlugin.On("Close", mock.Anything).Return(nil) + return mockPlugin, mockInstance + }, + entryPoint: "main", + inputData: map[string]any{"test": "data"}, + expectedValue: map[string]any{"result": "success"}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mocks + mockPlugin, mockInstance := tt.setupMocks() + + // Create handler and evaluator + handler := slog.NewTextHandler(os.Stdout, nil) + + // Create the unit under test + evaluator := NewBytecodeEvaluator(handler, nil) + + // Convert input data to JSON + inputJSON, err := mockLoadInputData(tt.inputData) + require.NoError(t, err) + + // Call the exec method directly + result, err := evaluator.exec( + context.Background(), + mockPlugin, + tt.entryPoint, + adapters.NewPluginInstanceConfig(), + inputJSON, + ) + + // Verify instance was called + mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything) + + // Check for expected errors + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + + // Check result + resultValue := result.Interface() + assert.Equal(t, tt.expectedValue, resultValue) + } + + // Close should always be called + assert.True(t, mockInstance.wasClosed, "Instance should be closed") + }) + } +} + // TestExtismDirectInputFormat tests how input data is formatted for Extism func TestExtismDirectInputFormat(t *testing.T) { // Create a test map that simulates data from our providers @@ -681,3 +952,11 @@ func TestExtismDirectInputFormat(t *testing.T) { expected := `{"initial":"top-level-value","input_data":{"input":"API User","request":{}}}` assert.JSONEq(t, expected, string(jsonBytes)) } + +// Helper function to create mock input data JSON +func mockLoadInputData(data map[string]any) ([]byte, error) { + if data == nil { + return []byte("{}"), nil + } + return internal.ConvertToExtismFormat(data) +} diff --git a/machines/extism/evaluator/response_comprehensive_test.go b/machines/extism/evaluator/response_comprehensive_test.go new file mode 100644 index 0000000..f1760c4 --- /dev/null +++ b/machines/extism/evaluator/response_comprehensive_test.go @@ -0,0 +1,169 @@ +package evaluator + +import ( + "log/slog" + "os" + "testing" + "time" + + "github.com/robbyt/go-polyscript/execution/data" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestNewEvalResultNilHandler tests the behavior when a nil handler is provided +func TestNewEvalResultNilHandler(t *testing.T) { + // Create with nil handler + result := newEvalResult(nil, "test value", 100*time.Millisecond, "test-id") + + // 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, "test value", result.value) + assert.Equal(t, 100*time.Millisecond, result.execTime) + assert.Equal(t, "test-id", result.scriptExeID) +} + +// TestExecResult_UnknownType tests handling of unknown value types +func TestExecResult_UnknownType(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, customValue, time.Second, "test-id") + + // Should return ERROR for unknown types + assert.Equal(t, data.ERROR, result.Type()) +} + +// TestExecResult_Interface tests the Interface method +func TestExecResult_Interface(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()) + }) + } +} + +// TestExecResult_GetMetadata tests the metadata methods +func TestExecResult_GetMetadata(t *testing.T) { + handler := slog.NewTextHandler(os.Stdout, nil) + execTime := 123 * time.Millisecond + scriptID := "test-script-9876" + + result := newEvalResult(handler, "test", execTime, scriptID) + + // Test GetScriptExeID + assert.Equal(t, scriptID, result.GetScriptExeID()) + + // Test GetExecTime + assert.Equal(t, execTime.String(), result.GetExecTime()) +} + +// TestExecResult_InspectWithInvalidJSON tests what happens when JSON marshaling fails +func TestExecResult_InspectWithInvalidJSON(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 + } + + handler := slog.NewTextHandler(os.Stdout, nil) + result := newEvalResult(handler, badMap, time.Second, "test-id") + + // Should fall back to default string representation + inspectResult := result.Inspect() + assert.Contains(t, inspectResult, "map[") +} + +// TestExecResult_NestedComplexValues tests the handling of nested complex values +func TestExecResult_NestedComplexValues(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, 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()) +} + +// TestExecResult_StringRepresentation tests various string representation cases +func TestExecResult_StringRepresentation(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") + }) + } +} diff --git a/machines/starlark/internal/converters_test.go b/machines/starlark/internal/converters_test.go index dbf036a..98be903 100644 --- a/machines/starlark/internal/converters_test.go +++ b/machines/starlark/internal/converters_test.go @@ -275,11 +275,11 @@ func TestConvertToStarlarkFormat(t *testing.T) { { name: "with URL", input: map[string]any{ - "url": &url.URL{Scheme: "https", Host: "example.com"}, + "url": &url.URL{Scheme: "https", Host: "localhost:8080"}, }, expected: func() starlarkLib.StringDict { d := starlarkLib.NewDict(1) - u := &url.URL{Scheme: "https", Host: "example.com"} + u := &url.URL{Scheme: "https", Host: "localhost:8080"} require.NoError( t, d.SetKey(starlarkLib.String("url"), starlarkLib.String(u.String())), @@ -462,19 +462,19 @@ func TestConvertToStarlarkValue(t *testing.T) { name: "simple URL", input: &url.URL{ Scheme: "https", - Host: "example.com", + Host: "localhost:8080", }, - expected: starlarkLib.String("https://example.com"), + expected: starlarkLib.String("https://localhost:8080"), }, { name: "complex URL", input: &url.URL{ Scheme: "https", - Host: "example.com", + Host: "localhost:8080", Path: "/path", RawQuery: "q=search", }, - expected: starlarkLib.String("https://example.com/path?q=search"), + expected: starlarkLib.String("https://localhost:8080/path?q=search"), }, } From e6bab350c2aa76eddc865523ea7034adaccae5bf Mon Sep 17 00:00:00 2001 From: RT Date: Wed, 9 Apr 2025 22:45:57 -0400 Subject: [PATCH 02/15] remove extra t.Parallel() calls --- execution/data/compositeProvider_test.go | 28 ++---------- execution/data/contextProvider_test.go | 44 ++----------------- execution/data/data_helpers_test.go | 14 +++--- execution/data/prepareContext_test.go | 20 +-------- execution/data/provider_test.go | 23 +--------- execution/data/staticProvider_test.go | 4 +- execution/script/loader/fromDisk_test.go | 32 -------------- .../evaluator/bytecodeEvaluator_test.go | 10 ----- polyscript_test.go | 40 ----------------- 9 files changed, 19 insertions(+), 196 deletions(-) diff --git a/execution/data/compositeProvider_test.go b/execution/data/compositeProvider_test.go index c62d336..507b858 100644 --- a/execution/data/compositeProvider_test.go +++ b/execution/data/compositeProvider_test.go @@ -51,8 +51,6 @@ 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( @@ -233,8 +231,6 @@ 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") @@ -247,10 +243,10 @@ func TestCompositeProvider_GetData(t *testing.T) { } assert.NoError(t, err, "Should not return error for valid providers") - assertMapContainsExpected(t, tt.expectedData, result) + assertMapContainsExpectedHelper(t, tt.expectedData, result) // Verify data consistency across calls - verifyDataConsistency(t, provider, ctx) + getDataCheckHelper(t, provider, ctx) }) } } @@ -260,8 +256,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { t.Parallel() t.Run("empty providers list", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider() require.NotNil(t, provider) @@ -275,8 +269,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("single context provider succeeds", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider(NewContextProvider(constants.EvalData)) require.NotNil(t, provider) @@ -299,8 +291,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("single static provider always errors", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider(NewStaticProvider(simpleData)) require.NotNil(t, provider) @@ -320,8 +310,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("mixed providers (static fails, context succeeds)", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider( NewStaticProvider(simpleData), NewContextProvider(constants.EvalData), @@ -351,8 +339,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("all providers fail", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider( NewStaticProvider(simpleData), newMockErrorProvider(), @@ -369,8 +355,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("nil providers are skipped", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider( nil, NewContextProvider(constants.EvalData), @@ -393,8 +377,6 @@ func TestCompositeProvider_AddDataToContext(t *testing.T) { }) t.Run("composite with only static providers", func(t *testing.T) { - t.Parallel() - provider := NewCompositeProvider( NewStaticProvider(map[string]any{"key1": "value1"}), NewStaticProvider(map[string]any{"key2": "value2"}), @@ -538,8 +520,6 @@ 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() ctx := tt.setupContext() @@ -548,7 +528,7 @@ func TestCompositeProvider_NestedStructures(t *testing.T) { require.NoError(t, err, "GetData should not error with valid providers") // Verify all expected values are present - assertMapContainsExpected(t, tt.expectedResult, result) + assertMapContainsExpectedHelper(t, tt.expectedResult, result) }) } } @@ -670,8 +650,6 @@ 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 39855dc..795a025 100644 --- a/execution/data/contextProvider_test.go +++ b/execution/data/contextProvider_test.go @@ -14,8 +14,6 @@ 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, @@ -29,8 +27,6 @@ func TestContextProvider_Creation(t *testing.T) { }) t.Run("custom context key", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider("custom_key") assert.Equal(t, constants.ContextKey("custom_key"), provider.contextKey, @@ -40,8 +36,6 @@ func TestContextProvider_Creation(t *testing.T) { }) t.Run("empty context key", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider("") assert.Equal(t, constants.ContextKey(""), provider.contextKey, @@ -56,8 +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() @@ -68,8 +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() @@ -80,12 +70,10 @@ func TestContextProvider_GetData(t *testing.T) { assert.Empty(t, result, "Result map should be empty") // Verify data consistency - verifyDataConsistency(t, provider, ctx) + 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) @@ -95,12 +83,10 @@ func TestContextProvider_GetData(t *testing.T) { assert.Equal(t, simpleData, result, "Result should match expected data") // Verify data consistency - verifyDataConsistency(t, provider, ctx) + 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) @@ -110,12 +96,10 @@ func TestContextProvider_GetData(t *testing.T) { assert.Equal(t, complexData, result, "Result should match expected data") // Verify data consistency - verifyDataConsistency(t, provider, ctx) + 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") @@ -126,8 +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,8 +125,6 @@ func TestContextProvider_AddDataToContext(t *testing.T) { t.Parallel() t.Run("empty context key", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider("") ctx := context.Background() @@ -155,8 +135,6 @@ func TestContextProvider_AddDataToContext(t *testing.T) { }) t.Run("nil input data", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider(constants.EvalData) ctx := context.Background() @@ -171,8 +149,6 @@ func TestContextProvider_AddDataToContext(t *testing.T) { }) t.Run("simple map data", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider(constants.EvalData) ctx := context.Background() @@ -192,8 +168,6 @@ 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() @@ -215,12 +189,10 @@ func TestContextProvider_AddDataToContext(t *testing.T) { }) t.Run("HTTP request data", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider(constants.EvalData) ctx := context.Background() - newCtx, err := provider.AddDataToContext(ctx, createTestRequest()) + newCtx, err := provider.AddDataToContext(ctx, createTestRequestHelper()) assert.NoError(t, err, "Should not return error with HTTP request") assert.NotEqual(t, ctx, newCtx, "Context should be modified") @@ -236,8 +208,6 @@ func TestContextProvider_AddDataToContext(t *testing.T) { }) t.Run("unsupported data type", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider(constants.EvalData) ctx := context.Background() @@ -254,8 +224,6 @@ 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() @@ -283,8 +251,6 @@ func TestContextProvider_DataIntegration(t *testing.T) { t.Parallel() t.Run("single map data item", func(t *testing.T) { - t.Parallel() - provider := NewContextProvider(constants.EvalData) ctx := context.Background() @@ -306,8 +272,6 @@ func TestContextProvider_DataIntegration(t *testing.T) { }) 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 diff --git a/execution/data/data_helpers_test.go b/execution/data/data_helpers_test.go index b6dd4d1..da1bf08 100644 --- a/execution/data/data_helpers_test.go +++ b/execution/data/data_helpers_test.go @@ -33,8 +33,8 @@ var ( } ) -// createTestRequest creates a standard HTTP request for testing -func createTestRequest() *http.Request { +// 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"}, @@ -68,8 +68,8 @@ func newMockErrorProvider() *MockProvider { return provider } -// assertMapContainsExpected asserts that a map contains all expected key/value pairs -func assertMapContainsExpected(t *testing.T, expected, actual map[string]any) { +// 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) @@ -82,15 +82,15 @@ func assertMapContainsExpected(t *testing.T, expected, actual map[string]any) { actualMap, actualIsMap := actualValue.(map[string]any) if expectedIsMap && actualIsMap { - assertMapContainsExpected(t, expectedMap, actualMap) + assertMapContainsExpectedHelper(t, expectedMap, actualMap) } else { assert.Equal(t, expectedValue, actualValue, "Value should match for key: %s", key) } } } -// verifyDataConsistency checks if multiple calls to GetData return consistent results -func verifyDataConsistency(t *testing.T, provider Provider, ctx context.Context) { +// 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) 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 f2b18f8..4ae50de 100644 --- a/execution/data/provider_test.go +++ b/execution/data/provider_test.go @@ -36,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() @@ -51,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() @@ -62,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) @@ -77,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() @@ -87,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") @@ -98,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), @@ -130,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() @@ -140,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(), @@ -159,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() @@ -177,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() @@ -197,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) @@ -218,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() @@ -230,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), @@ -260,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(), @@ -275,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() @@ -302,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{ @@ -319,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", @@ -339,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", @@ -365,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"}} @@ -375,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) @@ -384,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 0759bd6..3e5c158 100644 --- a/execution/data/staticProvider_test.go +++ b/execution/data/staticProvider_test.go @@ -127,7 +127,7 @@ func TestStaticProvider_GetData(t *testing.T) { } // Verify data consistency - verifyDataConsistency(t, provider, ctx) + getDataCheckHelper(t, provider, ctx) }) } } @@ -175,7 +175,7 @@ func TestStaticProvider_AddDataToContext(t *testing.T) { provider := NewStaticProvider(simpleData) ctx := context.Background() - newCtx, err := provider.AddDataToContext(ctx, createTestRequest()) + 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") diff --git a/execution/script/loader/fromDisk_test.go b/execution/script/loader/fromDisk_test.go index 3bde798..4ecc1c1 100644 --- a/execution/script/loader/fromDisk_test.go +++ b/execution/script/loader/fromDisk_test.go @@ -14,8 +14,6 @@ func TestNewFromDisk(t *testing.T) { t.Parallel() t.Run("valid paths", func(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() absPath := filepath.Join(tempDir, "test.js") @@ -39,8 +37,6 @@ func TestNewFromDisk(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - loader, err := NewFromDisk(tc.path) require.NoError(t, err) require.NotNil(t, loader) @@ -54,8 +50,6 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("invalid schemes", func(t *testing.T) { - t.Parallel() - tests := []struct { name string path string @@ -73,8 +67,6 @@ func TestNewFromDisk(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - loader, err := NewFromDisk(tc.path) require.Error(t, err) require.ErrorIs(t, err, ErrSchemeUnsupported) @@ -84,8 +76,6 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("relative paths", func(t *testing.T) { - t.Parallel() - tests := []struct { name string path string @@ -98,8 +88,6 @@ func TestNewFromDisk(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - loader, err := NewFromDisk(tc.path) require.Error(t, err) require.ErrorIs(t, err, ErrScriptNotAvailable) @@ -109,8 +97,6 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("empty or invalid paths", func(t *testing.T) { - t.Parallel() - tests := []struct { name string path string @@ -125,8 +111,6 @@ func TestNewFromDisk(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if tc.path == "\\" && runtime.GOOS != "windows" { t.Skip("Skipping Windows-specific test on non-Windows platform") } @@ -139,8 +123,6 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("url parsing errors", func(t *testing.T) { - t.Parallel() - loader, err := NewFromDisk("file://[invalid-url") require.Error(t, err) require.ErrorContains(t, err, "relative paths are not supported") @@ -148,8 +130,6 @@ func TestNewFromDisk(t *testing.T) { }) t.Run("non-file scheme", func(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() absPath := filepath.Join(tempDir, "test.js") loader, err := NewFromDisk("http://" + absPath) @@ -163,8 +143,6 @@ func TestFromDisk_GetReader(t *testing.T) { t.Parallel() t.Run("read file contents", func(t *testing.T) { - t.Parallel() - // Setup test file tempDir := t.TempDir() testContent := "test content\nwith multiple lines" @@ -185,8 +163,6 @@ func TestFromDisk_GetReader(t *testing.T) { }) t.Run("multiple reads from same loader", func(t *testing.T) { - t.Parallel() - // Setup test file tempDir := t.TempDir() testContent := FunctionContent @@ -203,8 +179,6 @@ func TestFromDisk_GetReader(t *testing.T) { }) t.Run("file not found", func(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() nonExistingFile := filepath.Join(tempDir, "nonexisting.js") @@ -222,8 +196,6 @@ func TestFromDisk_GetSourceURL(t *testing.T) { t.Parallel() t.Run("valid source URL", func(t *testing.T) { - t.Parallel() - // Setup test file tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.risor") @@ -244,8 +216,6 @@ func TestFromDisk_String(t *testing.T) { t.Parallel() t.Run("string representation with content", func(t *testing.T) { - t.Parallel() - // Setup test file tempDir := t.TempDir() testContent := "test content for string method" @@ -270,8 +240,6 @@ func TestFromDisk_String(t *testing.T) { }) t.Run("string representation with non-existent file", func(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() nonExistingFile := filepath.Join(tempDir, "nonexisting.js") diff --git a/machines/extism/evaluator/bytecodeEvaluator_test.go b/machines/extism/evaluator/bytecodeEvaluator_test.go index b94c766..d40e948 100644 --- a/machines/extism/evaluator/bytecodeEvaluator_test.go +++ b/machines/extism/evaluator/bytecodeEvaluator_test.go @@ -84,8 +84,6 @@ func TestLoadInputData(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) // Create a context provider @@ -139,8 +137,6 @@ func TestBytecodeEvaluatorInvalidInputs(t *testing.T) { // Test case: nil bytecode t.Run("nil bytecode", func(t *testing.T) { - t.Parallel() - mockContent := &mockExecutableContent{ machineType: machineTypes.Extism, source: "invalid wasm", @@ -159,8 +155,6 @@ func TestBytecodeEvaluatorInvalidInputs(t *testing.T) { // Test case: invalid content type t.Run("invalid content type", func(t *testing.T) { - t.Parallel() - mockContent := &mockExecutableContent{ machineType: machineTypes.Extism, source: "invalid wasm", @@ -388,8 +382,6 @@ 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) @@ -555,8 +547,6 @@ func TestExecHelper(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() - mockInstance, ctx, cancel := tt.setup() defer cancel() diff --git a/polyscript_test.go b/polyscript_test.go index a44355e..c10a5d1 100644 --- a/polyscript_test.go +++ b/polyscript_test.go @@ -176,8 +176,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 +275,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 +346,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 +362,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 +463,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 +487,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 +520,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"] @@ -646,8 +632,6 @@ func TestEvalHelpers(t *testing.T) { }) t.Run("EvalAndExtractMap", func(t *testing.T) { - t.Parallel() - // Create a simple Risor evaluator script := ` { @@ -770,8 +754,6 @@ _ = result` } t.Run("FromRisorStringWithData", func(t *testing.T) { - t.Parallel() - // Test script risorScript := ` // Access static data @@ -818,8 +800,6 @@ _ = result` }) t.Run("FromStarlarkStringWithData", func(t *testing.T) { - t.Parallel() - // Create evaluator starlarkEval, err := FromStarlarkStringWithData( starlarkFileContent, @@ -847,23 +827,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 +1013,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 +1021,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") From e1eccf3bda8aa29f64e3aed84cad912d79d0b5f3 Mon Sep 17 00:00:00 2001 From: RT Date: Wed, 9 Apr 2025 22:54:11 -0400 Subject: [PATCH 03/15] consolidate response_test code --- .../evaluator/response_comprehensive_test.go | 169 ------------------ machines/extism/evaluator/response_test.go | 157 ++++++++++++++++ 2 files changed, 157 insertions(+), 169 deletions(-) delete mode 100644 machines/extism/evaluator/response_comprehensive_test.go diff --git a/machines/extism/evaluator/response_comprehensive_test.go b/machines/extism/evaluator/response_comprehensive_test.go deleted file mode 100644 index f1760c4..0000000 --- a/machines/extism/evaluator/response_comprehensive_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package evaluator - -import ( - "log/slog" - "os" - "testing" - "time" - - "github.com/robbyt/go-polyscript/execution/data" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestNewEvalResultNilHandler tests the behavior when a nil handler is provided -func TestNewEvalResultNilHandler(t *testing.T) { - // Create with nil handler - result := newEvalResult(nil, "test value", 100*time.Millisecond, "test-id") - - // 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, "test value", result.value) - assert.Equal(t, 100*time.Millisecond, result.execTime) - assert.Equal(t, "test-id", result.scriptExeID) -} - -// TestExecResult_UnknownType tests handling of unknown value types -func TestExecResult_UnknownType(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, customValue, time.Second, "test-id") - - // Should return ERROR for unknown types - assert.Equal(t, data.ERROR, result.Type()) -} - -// TestExecResult_Interface tests the Interface method -func TestExecResult_Interface(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()) - }) - } -} - -// TestExecResult_GetMetadata tests the metadata methods -func TestExecResult_GetMetadata(t *testing.T) { - handler := slog.NewTextHandler(os.Stdout, nil) - execTime := 123 * time.Millisecond - scriptID := "test-script-9876" - - result := newEvalResult(handler, "test", execTime, scriptID) - - // Test GetScriptExeID - assert.Equal(t, scriptID, result.GetScriptExeID()) - - // Test GetExecTime - assert.Equal(t, execTime.String(), result.GetExecTime()) -} - -// TestExecResult_InspectWithInvalidJSON tests what happens when JSON marshaling fails -func TestExecResult_InspectWithInvalidJSON(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 - } - - handler := slog.NewTextHandler(os.Stdout, nil) - result := newEvalResult(handler, badMap, time.Second, "test-id") - - // Should fall back to default string representation - inspectResult := result.Inspect() - assert.Contains(t, inspectResult, "map[") -} - -// TestExecResult_NestedComplexValues tests the handling of nested complex values -func TestExecResult_NestedComplexValues(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, 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()) -} - -// TestExecResult_StringRepresentation tests various string representation cases -func TestExecResult_StringRepresentation(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") - }) - } -} diff --git a/machines/extism/evaluator/response_test.go b/machines/extism/evaluator/response_test.go index f62c52b..274f579 100644 --- a/machines/extism/evaluator/response_test.go +++ b/machines/extism/evaluator/response_test.go @@ -188,3 +188,160 @@ func TestExecResult_Inspect(t *testing.T) { }) } } + +// TestNewEvalResultNilHandler tests the behavior when a nil handler is provided +func TestNewEvalResultNilHandler(t *testing.T) { + // Create with nil handler + result := newEvalResult(nil, "test value", 100*time.Millisecond, "test-id") + + // 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, "test value", result.value) + assert.Equal(t, 100*time.Millisecond, result.execTime) + assert.Equal(t, "test-id", result.scriptExeID) +} + +// TestExecResult_UnknownType tests handling of unknown value types +func TestExecResult_UnknownType(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, customValue, time.Second, "test-id") + + // Should return ERROR for unknown types + assert.Equal(t, data.ERROR, result.Type()) +} + +// TestExecResult_Interface tests the Interface method +func TestExecResult_Interface(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()) + }) + } +} + +// TestExecResult_GetMetadata tests the metadata methods +func TestExecResult_GetMetadata(t *testing.T) { + handler := slog.NewTextHandler(os.Stdout, nil) + execTime := 123 * time.Millisecond + scriptID := "test-script-9876" + + result := newEvalResult(handler, "test", execTime, scriptID) + + // Test GetScriptExeID + assert.Equal(t, scriptID, result.GetScriptExeID()) + + // Test GetExecTime + assert.Equal(t, execTime.String(), result.GetExecTime()) +} + +// TestExecResult_InspectWithInvalidJSON tests what happens when JSON marshaling fails +func TestExecResult_InspectWithInvalidJSON(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 + } + + handler := slog.NewTextHandler(os.Stdout, nil) + result := newEvalResult(handler, badMap, time.Second, "test-id") + + // Should fall back to default string representation + inspectResult := result.Inspect() + assert.Contains(t, inspectResult, "map[") +} + +// TestExecResult_NestedComplexValues tests the handling of nested complex values +func TestExecResult_NestedComplexValues(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, 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()) +} + +// TestExecResult_StringRepresentation tests various string representation cases +func TestExecResult_StringRepresentation(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") + }) + } +} From 143cb5ffae436fc164d0f506ebd4c1ad3ced66f8 Mon Sep 17 00:00:00 2001 From: RT Date: Wed, 9 Apr 2025 23:07:59 -0400 Subject: [PATCH 04/15] use the mocks package for polyscript_test --- polyscript_test.go | 51 ++++++++-------------------------------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/polyscript_test.go b/polyscript_test.go index c10a5d1..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 @@ -570,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() @@ -599,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() @@ -611,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 { @@ -698,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) From 384e5d3e33e4cea38a9666e3db9eef706c6b6a29 Mon Sep 17 00:00:00 2001 From: RT Date: Wed, 9 Apr 2025 23:54:41 -0400 Subject: [PATCH 05/15] fix up evaluator_test --- engine/evaluator_test.go | 173 ++++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 93 deletions(-) diff --git a/engine/evaluator_test.go b/engine/evaluator_test.go index 49fa6dc..996a751 100644 --- a/engine/evaluator_test.go +++ b/engine/evaluator_test.go @@ -17,47 +17,42 @@ import ( "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" ) -// mockEvaluatorImplementation is a minimal struct implementing engine.Evaluator for testing -type mockEvaluatorImplementation struct { - evalFunc func(ctx context.Context) (engine.EvaluatorResponse, error) +// mockDataPreparer is a mock implementation of engine.EvalDataPreparer +type mockDataPreparer struct { + mock.Mock } -func (m *mockEvaluatorImplementation) Eval(ctx context.Context) (engine.EvaluatorResponse, error) { - return m.evalFunc(ctx) -} - -// mockEvalDataPreparerImplementation is a minimal struct implementing engine.EvalDataPreparer for testing -type mockEvalDataPreparerImplementation struct { - prepareFunc func(ctx context.Context, data ...any) (context.Context, error) -} - -func (m *mockEvalDataPreparerImplementation) PrepareContext( +func (m *mockDataPreparer) PrepareContext( ctx context.Context, data ...any, ) (context.Context, error) { - return m.prepareFunc(ctx, data...) + args := m.Called(ctx, data) + return args.Get(0).(context.Context), args.Error(1) } -// mockEvaluatorWithPrepImplementation is a minimal struct implementing engine.EvaluatorWithPrep for testing -type mockEvaluatorWithPrepImplementation struct { - evalFunc func(ctx context.Context) (engine.EvaluatorResponse, error) - prepareFunc func(ctx context.Context, data ...any) (context.Context, error) +// mockEvaluatorWithPreparer creates an evaluator implementation that satisfies both interfaces +type mockEvaluatorWithPreparer struct { + mock.Mock } -func (m *mockEvaluatorWithPrepImplementation) Eval( - ctx context.Context, -) (engine.EvaluatorResponse, error) { - return m.evalFunc(ctx) +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 *mockEvaluatorWithPrepImplementation) PrepareContext( +func (m *mockEvaluatorWithPreparer) PrepareContext( ctx context.Context, data ...any, ) (context.Context, error) { - return m.prepareFunc(ctx, data...) + args := m.Called(ctx, data) + return args.Get(0).(context.Context), args.Error(1) } func TestEvaluatorInterface(t *testing.T) { @@ -73,20 +68,18 @@ func TestEvaluatorInterface(t *testing.T) { type contextKey string testKey := contextKey("test-key") - // Create a mock evaluator implementation - evaluator := &mockEvaluatorImplementation{ - evalFunc: func(ctx context.Context) (engine.EvaluatorResponse, error) { - // Verify that context is passed correctly - _, hasKey := ctx.Value(testKey).(string) - if !hasKey { - return nil, errors.New("context key missing") - } - return mockResponse, nil - }, - } - - // Test the Eval method with a context containing a 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") @@ -105,11 +98,11 @@ func TestEvaluatorInterface(t *testing.T) { assert.Equal(t, "test result", response.Inspect(), "Inspect() should return expected value") // Test error case - evaluator.evalFunc = func(ctx context.Context) (engine.EvaluatorResponse, error) { - return nil, errors.New("evaluation error") - } + errorEvaluator := new(mocks.Evaluator) + errorEvaluator.On("Eval", mock.Anything). + Return((*mocks.EvaluatorResponse)(nil), errors.New("evaluation error")) - response, err = evaluator.Eval(context.Background()) + 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") @@ -179,18 +172,8 @@ func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { // Define a type for the context key to avoid collision type dataKey string - // Create a mock data preparer implementation - dataPreparer := &mockEvalDataPreparerImplementation{ - prepareFunc: func(ctx context.Context, data ...any) (context.Context, error) { - // Simple implementation to store data in context - enrichedCtx := ctx - for i, item := range data { - key := dataKey(fmt.Sprintf("data-%d", i)) - enrichedCtx = context.WithValue(enrichedCtx, key, item) - } - return enrichedCtx, nil - }, - } + // Create a mock data preparer + dataPreparer := &mockDataPreparer{} // Test with various data types ctx := context.Background() @@ -198,36 +181,47 @@ func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { data2 := map[string]any{"key": "value"} data3 := 123 - enrichedCtx, err := dataPreparer.PrepareContext(ctx, data1, data2, data3) + // Create enriched context with the test data + enrichedCtx := ctx + for i, item := range []any{data1, data2, data3} { + key := dataKey(fmt.Sprintf("data-%d", i)) + enrichedCtx = context.WithValue(enrichedCtx, key, item) + } + + // 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, enrichedCtx, "Enriched context should not be nil") + require.NotNil(t, resultCtx, "Enriched context should not be nil") // Verify data was stored correctly assert.Equal( t, data1, - enrichedCtx.Value(dataKey("data-0")), + resultCtx.Value(dataKey("data-0")), "First data item should be stored correctly", ) assert.Equal( t, data2, - enrichedCtx.Value(dataKey("data-1")), + resultCtx.Value(dataKey("data-1")), "Second data item should be stored correctly", ) assert.Equal( t, data3, - enrichedCtx.Value(dataKey("data-2")), + resultCtx.Value(dataKey("data-2")), "Third data item should be stored correctly", ) // Test error case - dataPreparer.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { - return ctx, errors.New("preparation error") - } + errorPreparer := &mockDataPreparer{} + errorPreparer.On("PrepareContext", ctx, []any{"test"}). + Return(ctx, errors.New("preparation error")) - _, err = dataPreparer.PrepareContext(ctx, "test") + _, err = errorPreparer.PrepareContext(ctx, "test") assert.Error(t, err, "Should return an error") assert.Contains(t, err.Error(), "preparation error", "Error message should be preserved") } @@ -246,31 +240,26 @@ func TestEvaluatorWithPrepInterface(t *testing.T) { prepDataKey := prepKey("prepared-data") // Create a mock combined implementation - combinedEvaluator := &mockEvaluatorWithPrepImplementation{ - evalFunc: func(ctx context.Context) (engine.EvaluatorResponse, error) { - // Check if context has prepared data - value, ok := ctx.Value(prepDataKey).(string) - if !ok || value != "test-value" { - return nil, errors.New("context not properly prepared") - } - return mockResponse, nil - }, - prepareFunc: func(ctx context.Context, data ...any) (context.Context, error) { - // Store the prepared data in context - return context.WithValue(ctx, prepDataKey, "test-value"), nil - }, - } + combinedEvaluator := &mockEvaluatorWithPreparer{} - // Test the full workflow: prepare context then evaluate + // Define context and test data ctx := context.Background() + enrichedCtx := context.WithValue(ctx, prepDataKey, "test-value") - // First prepare the context - enrichedCtx, err := combinedEvaluator.PrepareContext(ctx, "test data") + // 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, enrichedCtx, "Enriched context should not be nil") + require.NotNil(t, resultCtx, "Enriched context should not be nil") // Then evaluate with the enriched context - response, err := combinedEvaluator.Eval(enrichedCtx) + 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") @@ -283,24 +272,22 @@ func TestEvaluatorWithPrepInterface(t *testing.T) { ) // Test error in preparation - combinedEvaluator.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { - return ctx, errors.New("preparation error") - } + prepErrorEvaluator := &mockEvaluatorWithPreparer{} + prepErrorEvaluator.On("PrepareContext", ctx, []any{"test data"}). + Return(ctx, errors.New("preparation error")) - _, err = combinedEvaluator.PrepareContext(ctx, "test data") + _, err = prepErrorEvaluator.PrepareContext(ctx, "test data") assert.Error(t, err, "Should return an error when preparation fails") // Test error in evaluation - combinedEvaluator.prepareFunc = func(ctx context.Context, data ...any) (context.Context, error) { - return context.WithValue(ctx, prepDataKey, "test-value"), nil - } - combinedEvaluator.evalFunc = func(ctx context.Context) (engine.EvaluatorResponse, error) { - return nil, errors.New("evaluation error") - } + 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")) - enrichedCtx, prepErr := combinedEvaluator.PrepareContext(ctx, "test data") + evalCtx, prepErr := evalErrorEvaluator.PrepareContext(ctx, "test data") require.NoError(t, prepErr, "PrepareContext should not return an error") - _, err = combinedEvaluator.Eval(enrichedCtx) + _, err = evalErrorEvaluator.Eval(evalCtx) assert.Error(t, err, "Should return an error when evaluation fails") } From 94b5aef175d76db038d2fa035c1677f709168340 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 00:21:13 -0400 Subject: [PATCH 06/15] more test cleanup --- execution/script/compiler_test.go | 3 - machines/extism/compiler/compiler_test.go | 48 ++-- .../compiler/internal/compile/compile_test.go | 180 ++++++++------- machines/extism/compiler/options_test.go | 205 +++++++++++++----- 4 files changed, 272 insertions(+), 164 deletions(-) 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/machines/extism/compiler/compiler_test.go b/machines/extism/compiler/compiler_test.go index d098d56..77e9ece 100644 --- a/machines/extism/compiler/compiler_test.go +++ b/machines/extism/compiler/compiler_test.go @@ -30,7 +30,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,6 +65,18 @@ func (m *mockScriptReaderCloser) Close() error { return args.Error(0) } +func TestCompiler_String(t *testing.T) { + t.Parallel() + + // Create a compiler to test the String method + comp := createTestCompiler(t, "test_function") + + // Test String method + result := comp.String() + require.NotEmpty(t, result) + require.Contains(t, result, "Compiler") +} + func TestCompiler(t *testing.T) { t.Parallel() @@ -215,71 +227,58 @@ func TestCompiler(t *testing.T) { t.Run("nil content", func(t *testing.T) { t.Parallel() - // Create compiler using functional options comp, err := NewCompiler( WithEntryPoint("main"), - WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + 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) + require.True(t, errors.Is(err, ErrContentNil)) }) t.Run("empty content", func(t *testing.T) { t.Parallel() - // Create compiler using functional options comp, err := NewCompiler( WithEntryPoint("main"), - WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + WithLogHandler(slog.NewTextHandler(io.Discard, nil)), ) 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) + require.ErrorIs(t, err, ErrContentNil) - // Verify mock expectations reader.AssertExpectations(t) }) t.Run("invalid wasm binary", func(t *testing.T) { t.Parallel() - // Create compiler using functional options comp, err := NewCompiler( WithEntryPoint("main"), - WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), + WithLogHandler(slog.NewTextHandler(io.Discard, nil)), ) 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) + require.ErrorIs(t, err, ErrValidationFailed) - // Verify mock expectations reader.AssertExpectations(t) }) @@ -287,26 +286,21 @@ func TestCompiler(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)), + 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.Error(t, err) require.Nil(t, execContent) - require.True(t, errors.Is(err, ErrValidationFailed), - "Expected error %v, got %v", ErrValidationFailed, err) + require.ErrorIs(t, err, ErrValidationFailed) - // Verify mock expectations reader.AssertExpectations(t) }) } diff --git a/machines/extism/compiler/internal/compile/compile_test.go b/machines/extism/compiler/internal/compile/compile_test.go index ba4e220..789e5a5 100644 --- a/machines/extism/compiler/internal/compile/compile_test.go +++ b/machines/extism/compiler/internal/compile/compile_test.go @@ -121,90 +121,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 +260,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..85cbfdd 100644 --- a/machines/extism/compiler/options_test.go +++ b/machines/extism/compiler/options_test.go @@ -13,7 +13,7 @@ import ( ) func TestWithEntryPoint(t *testing.T) { - // Test that WithEntryPoint properly sets the entry point + t.Parallel() entryPoint := "custom_entrypoint" c := &Compiler{ @@ -35,6 +35,7 @@ func TestWithEntryPoint(t *testing.T) { } func TestLoggerConfiguration(t *testing.T) { + t.Parallel() t.Run("default initialization", func(t *testing.T) { // Create a compiler with default settings c, err := NewCompiler() @@ -122,7 +123,7 @@ func TestLoggerConfiguration(t *testing.T) { } func TestWithLogHandler(t *testing.T) { - // Test that WithLogHandler properly sets the handler field + t.Parallel() var buf bytes.Buffer handler := slog.NewTextHandler(&buf, nil) @@ -144,7 +145,7 @@ func TestWithLogHandler(t *testing.T) { } func TestWithLogger(t *testing.T) { - // Test that WithLogger properly sets the logger field + t.Parallel() var buf bytes.Buffer handler := slog.NewTextHandler(&buf, nil) logger := slog.New(handler) @@ -167,81 +168,170 @@ func TestWithLogger(t *testing.T) { } func TestWithWASIEnabled(t *testing.T) { - // Test that WithWASIEnabled properly sets the EnableWASI field - c := &Compiler{ - options: &compile.Settings{}, - } - c.applyDefaults() + t.Parallel() - // Test enabling WASI - enableOpt := WithWASIEnabled(true) - err := enableOpt(c) + // Test with options initialized + t.Run("options initialized", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{}, + } + c.applyDefaults() - require.NoError(t, err) - require.True(t, c.options.EnableWASI) + // Test enabling WASI + enableOpt := WithWASIEnabled(true) + err := enableOpt(c) - // Test disabling WASI - disableOpt := WithWASIEnabled(false) - err = disableOpt(c) + require.NoError(t, err) + require.True(t, c.options.EnableWASI) - require.NoError(t, err) - require.False(t, c.options.EnableWASI) + // Test disabling WASI + disableOpt := WithWASIEnabled(false) + err = disableOpt(c) + + require.NoError(t, err) + require.False(t, c.options.EnableWASI) + }) + + // Note: the WithWASIEnabled function doesn't check if options is nil + // because applyDefaults initializes it. This test just verifies its behavior + // with a nil options to ensure the code path is covered. + t.Run("with nil options", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + + // We initialize options first (as applyDefaults would do) + c.options = &compile.Settings{} + + opt := WithWASIEnabled(true) + err := opt(c) + + require.NoError(t, err) + require.True(t, c.options.EnableWASI) + }) } func TestWithRuntimeConfig(t *testing.T) { - // Test that WithRuntimeConfig properly sets the RuntimeConfig field - runtimeConfig := wazero.NewRuntimeConfig() + t.Parallel() - c := &Compiler{ - options: &compile.Settings{}, - } - c.applyDefaults() - opt := WithRuntimeConfig(runtimeConfig) - err := opt(c) + // Test with normal runtime config + t.Run("normal runtime config", func(t *testing.T) { + runtimeConfig := wazero.NewRuntimeConfig() - require.NoError(t, err) - require.Equal(t, runtimeConfig, c.options.RuntimeConfig) + 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) + t.Run("nil runtime config", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{}, + } + c.applyDefaults() - require.Error(t, err) - require.Contains(t, err.Error(), "runtime config cannot be nil") + nilOpt := WithRuntimeConfig(nil) + err := nilOpt(c) + + require.Error(t, err) + require.Contains(t, err.Error(), "runtime config cannot be nil") + }) + + // Note: the WithRuntimeConfig function doesn't check if options is nil + // because applyDefaults initializes it. This test just verifies its behavior + // with a nil options to ensure the code path is covered. + t.Run("with nil options", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + + // We initialize options first (as applyDefaults would do) + c.options = &compile.Settings{} + runtimeConfig := wazero.NewRuntimeConfig() + + opt := WithRuntimeConfig(runtimeConfig) + err := opt(c) + + require.NoError(t, err) + require.Equal(t, runtimeConfig, c.options.RuntimeConfig) + }) } 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} + t.Parallel() + + // Test with valid host functions + 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") - c := &Compiler{ - options: &compile.Settings{}, - } - c.applyDefaults() - opt := WithHostFunctions(hostFuncs) - err := opt(c) + hostFuncs := []extismSDK.HostFunction{testHostFn} - require.NoError(t, err) - require.Equal(t, hostFuncs, c.options.HostFunctions) + 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) + t.Run("empty host functions", func(t *testing.T) { + c := &Compiler{ + options: &compile.Settings{}, + } + c.applyDefaults() - require.NoError(t, err) - require.Empty(t, c.options.HostFunctions) + emptyOpt := WithHostFunctions([]extismSDK.HostFunction{}) + err := emptyOpt(c) + + require.NoError(t, err) + require.Empty(t, c.options.HostFunctions) + }) + + // Note: the WithHostFunctions function doesn't check if options is nil + // because applyDefaults initializes it. This test just verifies its behavior + // with a nil options to ensure the code path is covered. + t.Run("with nil options", func(t *testing.T) { + c := &Compiler{ + options: nil, + } + + // We initialize options first (as applyDefaults would do) + 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) + }) } func TestApplyDefaults(t *testing.T) { + t.Parallel() t.Run("empty compiler", func(t *testing.T) { // Test that defaults are properly applied to an empty compiler c := &Compiler{} @@ -308,7 +398,7 @@ func TestApplyDefaults(t *testing.T) { } func TestValidate(t *testing.T) { - // Test validation with proper defaults + t.Parallel() c := &Compiler{} c.applyDefaults() @@ -345,7 +435,7 @@ func TestValidate(t *testing.T) { } func TestWithContext(t *testing.T) { - // Test that WithContext properly sets the Context field + t.Parallel() ctx := context.Background() c := &Compiler{} @@ -367,6 +457,7 @@ func TestWithContext(t *testing.T) { } func TestGetEntryPointName(t *testing.T) { + t.Parallel() t.Run("normal value", func(t *testing.T) { // Test with a normal value c := &Compiler{ From ec92ae248ff25dda06b0d0a4d57361d149ab269c Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 00:54:11 -0400 Subject: [PATCH 07/15] update tests for extism/compiler --- machines/extism/compiler/compiler_test.go | 399 +++++++++--------- machines/extism/compiler/executable_test.go | 12 +- machines/extism/compiler/options_test.go | 431 +++++++++----------- 3 files changed, 387 insertions(+), 455 deletions(-) diff --git a/machines/extism/compiler/compiler_test.go b/machines/extism/compiler/compiler_test.go index 77e9ece..bb8fcab 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" @@ -80,227 +79,193 @@ func TestCompiler_String(t *testing.T) { func TestCompiler(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 - comp, err := NewCompiler( - WithEntryPoint(entryPoint), - WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), - WithRuntimeConfig(wazero.NewRuntimeConfig()), - ) - 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) - }) - - t.Run("nil content", func(t *testing.T) { - t.Parallel() - - 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.True(t, errors.Is(err, ErrContentNil)) - }) - - t.Run("empty content", func(t *testing.T) { + // Success cases + t.Run("success cases", func(t *testing.T) { t.Parallel() - 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("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) + }) }) - t.Run("invalid wasm binary", func(t *testing.T) { + // Error cases + t.Run("error cases", func(t *testing.T) { t.Parallel() - 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) { - t.Parallel() - 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) + 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..bb8bf18 100644 --- a/machines/extism/compiler/executable_test.go +++ b/machines/extism/compiler/executable_test.go @@ -108,33 +108,23 @@ func TestNewExecutable(t *testing.T) { func TestExecutable_Close(t *testing.T) { t.Parallel() - // Test data + ctx := context.Background() wasmBytes := []byte("mock wasm bytes") entryPoint := "run" - ctx := context.Background() - // 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) - - // Verify closed state assert.True(t, exe.closed.Load()) - // Verify idempotent close - should not call plugin Close again err = exe.Close(ctx) assert.NoError(t, err) - // Verify expectations mockPlugin.AssertExpectations(t) } diff --git a/machines/extism/compiler/options_test.go b/machines/extism/compiler/options_test.go index 85cbfdd..8116a5b 100644 --- a/machines/extism/compiler/options_test.go +++ b/machines/extism/compiler/options_test.go @@ -36,89 +36,93 @@ func TestWithEntryPoint(t *testing.T) { func TestLoggerConfiguration(t *testing.T) { t.Parallel() - 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) + // Basic initialization and configuration + t.Run("creation and configuration", func(t *testing.T) { + t.Parallel() - // Create compiler with the handler - c, err := NewCompiler(WithLogHandler(customHandler)) - require.NoError(t, err) + 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") + }) - // 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") + t.Run("with explicit log handler", func(t *testing.T) { + var buf bytes.Buffer + customHandler := slog.NewTextHandler(&buf, nil) - // 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") - }) + 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") - 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) + c.logger.Info("test message") + require.Contains(t, buf.String(), "test message", "log message should be in buffer") + }) - // Create compiler with the logger - c, err := NewCompiler(WithLogger(customLogger)) - require.NoError(t, err) + t.Run("with explicit logger", func(t *testing.T) { + var buf bytes.Buffer + customHandler := slog.NewTextHandler(&buf, nil) + customLogger := slog.New(customHandler) - // 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") + c, err := NewCompiler(WithLogger(customLogger)) + require.NoError(t, err) - // 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") + 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("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") + // Testing precedence rules + t.Run("option precedence", func(t *testing.T) { + t.Parallel() + + t.Run("last option wins", func(t *testing.T) { + var handlerBuf, loggerBuf bytes.Buffer + customHandler := slog.NewTextHandler(&handlerBuf, nil) + customLogger := slog.New(slog.NewTextHandler(&loggerBuf, nil)) + + // Case 1: Handler then Logger (logger wins) + 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 (handler wins) + 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") + }) }) } @@ -167,166 +171,139 @@ func TestWithLogger(t *testing.T) { require.Contains(t, err.Error(), "logger cannot be nil") } -func TestWithWASIEnabled(t *testing.T) { +func TestRuntimeOptions(t *testing.T) { t.Parallel() - // Test with options initialized - t.Run("options initialized", func(t *testing.T) { - 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) - }) - - // Note: the WithWASIEnabled function doesn't check if options is nil - // because applyDefaults initializes it. This test just verifies its behavior - // with a nil options to ensure the code path is covered. - t.Run("with nil options", func(t *testing.T) { - c := &Compiler{ - options: nil, - } - - // We initialize options first (as applyDefaults would do) - c.options = &compile.Settings{} - - opt := WithWASIEnabled(true) - err := opt(c) - - require.NoError(t, err) - require.True(t, c.options.EnableWASI) + t.Run("WASI options", func(t *testing.T) { + t.Parallel() + + 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) + }) }) -} -func TestWithRuntimeConfig(t *testing.T) { - t.Parallel() - - // Test with normal runtime config - 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("runtime config", func(t *testing.T) { + t.Parallel() + + 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) + }) }) - // Test with nil runtime config - 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") - }) - - // Note: the WithRuntimeConfig function doesn't check if options is nil - // because applyDefaults initializes it. This test just verifies its behavior - // with a nil options to ensure the code path is covered. - t.Run("with nil options", func(t *testing.T) { - c := &Compiler{ - options: nil, - } - - // We initialize options first (as applyDefaults would do) - c.options = &compile.Settings{} - runtimeConfig := wazero.NewRuntimeConfig() - - opt := WithRuntimeConfig(runtimeConfig) - err := opt(c) - - require.NoError(t, err) - require.Equal(t, runtimeConfig, c.options.RuntimeConfig) - }) -} - -func TestWithHostFunctions(t *testing.T) { - t.Parallel() - - // Test with valid host functions - 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) - }) - - // Test with empty host functions - 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) - }) - - // Note: the WithHostFunctions function doesn't check if options is nil - // because applyDefaults initializes it. This test just verifies its behavior - // with a nil options to ensure the code path is covered. - t.Run("with nil options", func(t *testing.T) { - c := &Compiler{ - options: nil, - } - - // We initialize options first (as applyDefaults would do) - 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("host functions", func(t *testing.T) { + t.Parallel() + + 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) + }) }) } From 937ee25d8f25f99aaa715185af2f144b5101a416 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 01:05:57 -0400 Subject: [PATCH 08/15] more test cleanup --- execution/data/compositeProvider_test.go | 4 - execution/data/staticProvider_test.go | 4 - .../evaluator/bytecodeEvaluator_test.go | 3 - machines/extism/evaluator/response_test.go | 4 - machines/extism/internal/converters_test.go | 1 - machines/risor/compiler/compiler_test.go | 1 - .../risor/evaluator/bytecodeEvaluator_test.go | 826 ++++++++++-------- machines/risor/evaluator/response_test.go | 258 +++++- machines/starlark/compiler/compiler_test.go | 1 - 9 files changed, 686 insertions(+), 416 deletions(-) diff --git a/execution/data/compositeProvider_test.go b/execution/data/compositeProvider_test.go index 507b858..459229e 100644 --- a/execution/data/compositeProvider_test.go +++ b/execution/data/compositeProvider_test.go @@ -49,7 +49,6 @@ func TestCompositeProvider_Creation(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { composite := NewCompositeProvider(tt.providers...) require.NotNil(t, composite, "CompositeProvider should never be nil") @@ -229,7 +228,6 @@ func TestCompositeProvider_GetData(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { provider := tt.setupProvider() require.NotNil(t, provider, "Provider should never be nil") @@ -518,7 +516,6 @@ func TestCompositeProvider_NestedStructures(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { composite := tt.setupProviders() ctx := tt.setupContext() @@ -648,7 +645,6 @@ func TestCompositeProvider_DeepMerge(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { result := deepMerge(tt.src, tt.dst) assert.Equal(t, tt.expected, result, tt.description) diff --git a/execution/data/staticProvider_test.go b/execution/data/staticProvider_test.go index 3e5c158..5720cb4 100644 --- a/execution/data/staticProvider_test.go +++ b/execution/data/staticProvider_test.go @@ -41,10 +41,7 @@ func TestStaticProvider_Creation(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() - provider := NewStaticProvider(tt.inputData) require.NotNil(t, provider, "Provider should never be nil") @@ -94,7 +91,6 @@ func TestStaticProvider_GetData(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() diff --git a/machines/extism/evaluator/bytecodeEvaluator_test.go b/machines/extism/evaluator/bytecodeEvaluator_test.go index d40e948..9576a62 100644 --- a/machines/extism/evaluator/bytecodeEvaluator_test.go +++ b/machines/extism/evaluator/bytecodeEvaluator_test.go @@ -82,7 +82,6 @@ func TestLoadInputData(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { handler := slog.NewTextHandler(os.Stdout, nil) @@ -380,7 +379,6 @@ func TestPrepareContext(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { handler := slog.NewTextHandler(os.Stdout, nil) exe := tt.setupExe(t) @@ -545,7 +543,6 @@ func TestExecHelper(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { mockInstance, ctx, cancel := tt.setup() defer cancel() diff --git a/machines/extism/evaluator/response_test.go b/machines/extism/evaluator/response_test.go index 274f579..df8e917 100644 --- a/machines/extism/evaluator/response_test.go +++ b/machines/extism/evaluator/response_test.go @@ -53,7 +53,6 @@ func TestNewEvalResult(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) @@ -97,7 +96,6 @@ func TestExecResult_Type(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) @@ -147,7 +145,6 @@ func TestExecResult_String(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) @@ -179,7 +176,6 @@ func TestExecResult_Inspect(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) diff --git a/machines/extism/internal/converters_test.go b/machines/extism/internal/converters_test.go index a54e92b..b39f470 100644 --- a/machines/extism/internal/converters_test.go +++ b/machines/extism/internal/converters_test.go @@ -60,7 +60,6 @@ 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) diff --git a/machines/risor/compiler/compiler_test.go b/machines/risor/compiler/compiler_test.go index 7292729..e0d7e89 100644 --- a/machines/risor/compiler/compiler_test.go +++ b/machines/risor/compiler/compiler_test.go @@ -160,7 +160,6 @@ main() } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { runTestCase(t, tt) }) diff --git a/machines/risor/evaluator/bytecodeEvaluator_test.go b/machines/risor/evaluator/bytecodeEvaluator_test.go index 1df550a..42f3094 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,407 +75,494 @@ func (m *MockContent) GetMachineType() types.Type { return types.Risor } -// TestValidScript tests evaluating valid Risor scripts -func TestValidScript(t *testing.T) { +// TestBytecodeEvaluator_Success tests evaluating valid Risor scripts +func TestBytecodeEvaluator_Success(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" + tests := []struct { + name string + script string + requestMethod string + urlPath string + expectedType data.Types + expectedResult string + expectedValue any + }{ + { + name: "GET request to /hello", + script: ` + func handle(request) { + if request == nil { + return error("request is nil") + } + if request["Method"] == "POST" { + return "post" + } + if request["URL_Path"] == "/hello" { + return true + } + return false + } + print(ctx) + handle(ctx["request"]) + `, + requestMethod: "GET", + urlPath: "/hello", + expectedType: data.Types("bool"), + expectedResult: "true", + expectedValue: true, + }, + { + name: "POST request", + script: ` + func handle(request) { + if request == nil { + return error("request is nil") + } + if request["Method"] == "POST" { + return "post" + } + if request["URL_Path"] == "/hello" { + return true + } + return false + } + print(ctx) + handle(ctx["request"]) + `, + requestMethod: "POST", + urlPath: "/hello", + expectedType: data.Types("string"), + expectedResult: "\"post\"", + expectedValue: "post", + }, + { + name: "GET request to unknown path", + script: ` + func handle(request) { + if request == nil { + return error("request is nil") + } + if request["Method"] == "POST" { + return "post" + } + if request["URL_Path"] == "/hello" { + return true + } + return false + } + print(ctx) + handle(ctx["request"]) + `, + requestMethod: "GET", + urlPath: "/unknown", + expectedType: data.Types("bool"), + expectedResult: "false", + expectedValue: false, + }, } - if request["URL_Path"] == "/hello" { - return true - } - 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()) - - // Check the value - strValue, ok := response.Interface().(string) - require.True(t, ok) - require.Equal(t, "post", strValue) - }) -} -// TestString tests the String method -func TestString(t *testing.T) { - t.Parallel() - evaluator := &BytecodeEvaluator{} - require.Equal(t, "risor.BytecodeEvaluator", evaluator.String()) + 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) + } + }) + } } -// TestPrepareContext tests the PrepareContext method -func TestPrepareContext(t *testing.T) { +// TestBytecodeEvaluator_Metadata tests string representation and metadata methods +func TestBytecodeEvaluator_Metadata(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), - } - - // 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), - } - - // 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") + // Test String method + t.Run("String method", func(t *testing.T) { + evaluator := &BytecodeEvaluator{} + require.Equal(t, "risor.BytecodeEvaluator", evaluator.String()) }) } -// TestEval tests edge cases for the Eval method -func TestEval(t *testing.T) { +// TestBytecodeEvaluator_PrepareContext tests the PrepareContext method with various scenarios +func TestBytecodeEvaluator_PrepareContext(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), - } - - 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, + // 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} }, - } - - 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{}, + 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} }, - } - - evaluator := &BytecodeEvaluator{ - ctxKey: constants.Ctx, - execUnit: exe, - logHandler: handler, - logger: slog.New(handler), - } + 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", + }, + } - ctx := context.Background() - result, err := evaluator.Eval(ctx) + // 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) + } + } + }) + } +} - require.Error(t, err) - require.Nil(t, result) - require.Contains(t, err.Error(), "exeID is empty") - }) +// TestBytecodeEvaluator_Errors tests error conditions for the Eval method +func TestBytecodeEvaluator_Errors(t *testing.T) { + t.Parallel() - 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", + tests := []struct { + name string + setupExe func() *script.ExecutableUnit + errorMessage string + }{ + { + name: "nil executable unit", + setupExe: func() *script.ExecutableUnit { + return nil }, - } - - evaluator := &BytecodeEvaluator{ - ctxKey: constants.Ctx, - execUnit: exe, - logHandler: handler, - logger: slog.New(handler), - } - - ctx := context.Background() - result, err := evaluator.Eval(ctx) + 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", + }, + } - require.Error(t, err) - require.Nil(t, result) - require.Contains(t, err.Error(), "unable to type assert bytecode") - }) + 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) + }) + } } -// TestLoadInputData tests the loadInputData method -func TestLoadInputData(t *testing.T) { +// TestBytecodeEvaluator_LoadInputData tests the loadInputData method +func TestBytecodeEvaluator_LoadInputData(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, - } - - evaluator := &BytecodeEvaluator{ - ctxKey: constants.Ctx, - execUnit: exe, - logHandler: handler, - logger: slog.New(handler), - } - - ctx := context.Background() - data, err := evaluator.loadInputData(ctx) - - require.Error(t, err) - require.Equal(t, expectedErr, err) - require.Nil(t, data) - mockProvider.AssertExpectations(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, + }, + } - 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) - }) + 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) + } + } + }) + } } -// TestNewBytecodeEvaluator tests creating a new BytecodeEvaluator -func TestNewBytecodeEvaluator(t *testing.T) { +// TestBytecodeEvaluator_New tests creating a new BytecodeEvaluator with various options +func TestBytecodeEvaluator_New(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) - }) + 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, + }, + } - t.Run("with nil handler", func(t *testing.T) { - exe := &script.ExecutableUnit{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exe := &script.ExecutableUnit{} + evaluator := NewBytecodeEvaluator(tt.handler, exe) - 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) - 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) + } + }) + } } // Helper function to create a test executable unit diff --git a/machines/risor/evaluator/response_test.go b/machines/risor/evaluator/response_test.go index 1d8cc2c..8661101 100644 --- a/machines/risor/evaluator/response_test.go +++ b/machines/risor/evaluator/response_test.go @@ -80,30 +80,64 @@ func (m *RisorObjectMock) Compare(other rObj.Object) (int, error) { return args.Int(0), args.Error(1) } -func TestNewEvalResult(t *testing.T) { - mockObj := new(RisorObjectMock) +// TestEvalResult_Creation tests creating a new evaluation result +func TestEvalResult_Creation(t *testing.T) { + t.Parallel() - execTime := 100 * time.Millisecond - versionID := "test-version-1" + 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 { +// TestEvalResult_Type tests the Type method +func TestEvalResult_Type(t *testing.T) { + t.Parallel() + + tests := []struct { name string typeStr string expected data.Types @@ -111,24 +145,34 @@ func TestExecResult_Type(t *testing.T) { {"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 _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { mockObj := new(RisorObjectMock) - mockObj.On("Type").Return(rObj.Type(tc.typeStr)) + mockObj.On("Type").Return(rObj.Type(tt.typeStr)) handler := slog.NewTextHandler(os.Stdout, nil) result := newEvalResult(handler, mockObj, time.Second, "version-1") - assert.Equal(t, tc.expected, result.Type()) + // Check the result type + assert.Equal(t, tt.expected, result.Type()) + + // Verify mock expectations mockObj.AssertExpectations(t) }) } } -func TestExecResult_String(t *testing.T) { - testCases := []struct { +// TestEvalResult_String tests the String method +func TestEvalResult_String(t *testing.T) { + t.Parallel() + + tests := []struct { name string mockType rObj.Type mockString string @@ -137,7 +181,7 @@ func TestExecResult_String(t *testing.T) { expected string }{ { - name: "simple string object", + name: "string object", mockType: rObj.Type("string"), mockString: "hello", execTime: 100 * time.Millisecond, @@ -162,18 +206,174 @@ func TestExecResult_String(t *testing.T) { }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { mockObj := new(RisorObjectMock) - mockObj.On("Type").Return(tc.mockType) - mockObj.On("String").Return(tc.mockString) + mockObj.On("Type").Return(tt.mockType) + mockObj.On("String").Return(tt.mockString) handler := slog.NewTextHandler(os.Stdout, nil) - result := newEvalResult(handler, mockObj, tc.execTime, tc.versionID) + result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID) + + // Check string representation actual := result.String() - assert.Equal(t, tc.expected, actual) + assert.Equal(t, tt.expected, actual) + // Verify mock expectations mockObj.AssertExpectations(t) }) } } + +// TestEvalResult_Inspect tests the Inspect method +func TestEvalResult_Inspect(t *testing.T) { + t.Parallel() + + 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) + }) + } +} + +// TestEvalResult_Interface tests the Interface method +func TestEvalResult_Interface(t *testing.T) { + t.Parallel() + + 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) + }) + } +} + +// TestEvalResult_NilHandler tests creating a result with nil handler +func TestEvalResult_NilHandler(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) +} + +// TestEvalResult_Metadata tests all metadata accessors +func TestEvalResult_Metadata(t *testing.T) { + t.Parallel() + + 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()) + + // 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..a59f9c5 100644 --- a/machines/starlark/compiler/compiler_test.go +++ b/machines/starlark/compiler/compiler_test.go @@ -126,7 +126,6 @@ main() } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() From 3f231fcb54eb9226414c0a128c5f9f29922a733f Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 01:30:03 -0400 Subject: [PATCH 09/15] clean up starlark converter tests --- machines/starlark/internal/converters_test.go | 1104 ++++++++--------- 1 file changed, 544 insertions(+), 560 deletions(-) diff --git a/machines/starlark/internal/converters_test.go b/machines/starlark/internal/converters_test.go index 98be903..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", - }, - } - - 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("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}, - }, - { - 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)}}, + 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("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{}, +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: "string keys dict", - input: func() *starlarkLib.Dict { - d := starlarkLib.NewDict(1) - require.NoError(t, d.SetKey(starlarkLib.String("key"), starlarkLib.MakeInt(42))) + 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: 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), - }, - }, + wantErr: false, + }, + { + name: "with nil value", + input: map[string]any{ + "nil": nil, }, - } - - 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("error cases", func(t *testing.T) { - tests := []struct { - name string - input func() *starlarkLib.Dict - }{ - { - name: "dict with invalid entry", - input: func() *starlarkLib.Dict { + 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) + if err := d.SetKey(starlarkLib.String("nil"), starlarkLib.None); err != nil { + t.Fatalf("Failed to set nil 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()) - } - }) - } - }) + wantErr: false, + }, - 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: "localhost:8080"}, - }, - expected: func() starlarkLib.StringDict { + // 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) u := &url.URL{Scheme: "https", Host: "localhost:8080"} - require.NoError( - t, - d.SetKey(starlarkLib.String("url"), starlarkLib.String(u.String())), - ) - return starlarkLib.StringDict{constants.Ctx: d} + if err := d.SetKey(starlarkLib.String("url"), starlarkLib.String(u.String())); err != nil { + t.Fatalf("Failed to set url key: %v", err) + } + return d }(), }, - { - name: "with headers", - input: map[string]any{ - "headers": map[string][]string{ - "Accept": {"text/plain", "application/json"}, - }, + wantErr: false, + }, + { + 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: "localhost:8080", - }, - expected: starlarkLib.String("https://localhost:8080"), - }, - { - 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"), - }, - } + // 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) }) } From 2bcfd2e4d1fc47e03a2e810c0006086aee99f6d2 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 01:58:01 -0400 Subject: [PATCH 10/15] remove extra t.Parallel() calls --- execution/data/staticProvider_test.go | 10 ---- execution/script/loader/fromHTTP_test.go | 46 ------------------- execution/script/loader/fromString_test.go | 22 --------- .../script/loader/httpauth/basic_test.go | 5 -- .../script/loader/httpauth/header_test.go | 14 ------ .../script/loader/httpauth/noauth_test.go | 8 ---- machines/extism/compiler/compiler_test.go | 4 -- .../compiler/internal/compile/compile_test.go | 4 -- machines/extism/compiler/options_test.go | 10 ---- machines/extism/evaluator/response_test.go | 4 -- machines/extism/internal/converters_test.go | 1 - machines/starlark/compiler/compiler_test.go | 2 - 12 files changed, 130 deletions(-) diff --git a/execution/data/staticProvider_test.go b/execution/data/staticProvider_test.go index 5720cb4..2e964cf 100644 --- a/execution/data/staticProvider_test.go +++ b/execution/data/staticProvider_test.go @@ -92,8 +92,6 @@ func TestStaticProvider_GetData(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() - provider := NewStaticProvider(tt.inputData) ctx := context.Background() @@ -133,8 +131,6 @@ 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() @@ -152,8 +148,6 @@ 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() @@ -166,8 +160,6 @@ func TestStaticProvider_AddDataToContext(t *testing.T) { }) t.Run("HTTP request context arg returns error", func(t *testing.T) { - t.Parallel() - provider := NewStaticProvider(simpleData) ctx := context.Background() @@ -180,8 +172,6 @@ func TestStaticProvider_AddDataToContext(t *testing.T) { }) t.Run("multiple args returns error", func(t *testing.T) { - t.Parallel() - provider := NewStaticProvider(simpleData) ctx := context.Background() diff --git a/execution/script/loader/fromHTTP_test.go b/execution/script/loader/fromHTTP_test.go index c18bcfd..0f46570 100644 --- a/execution/script/loader/fromHTTP_test.go +++ b/execution/script/loader/fromHTTP_test.go @@ -17,8 +17,6 @@ func TestNewFromHTTP(t *testing.T) { t.Parallel() t.Run("Valid HTTPS URL", func(t *testing.T) { - t.Parallel() - // Set up TLS server tlsServer := httptest.NewTLSServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -53,8 +51,6 @@ func TestNewFromHTTP(t *testing.T) { }) t.Run("Valid HTTP URL", func(t *testing.T) { - t.Parallel() - // Set up HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -81,8 +77,6 @@ func TestNewFromHTTP(t *testing.T) { }) t.Run("Invalid URL scheme", func(t *testing.T) { - t.Parallel() - testURL := "file:///path/to/script.js" loader, err := NewFromHTTP(testURL) @@ -92,8 +86,6 @@ func TestNewFromHTTP(t *testing.T) { }) t.Run("Invalid URL format", func(t *testing.T) { - t.Parallel() - testURL := "://invalid-url" loader, err := NewFromHTTP(testURL) @@ -166,8 +158,6 @@ func TestNewFromHTTPWithOptions(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - // Create test server for this test case server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -217,8 +207,6 @@ func TestFromHTTP_TLSConfig(t *testing.T) { t.Parallel() t.Run("with insecure skip verify", func(t *testing.T) { - t.Parallel() - // Create test server for this test server := httptest.NewTLSServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -247,8 +235,6 @@ func TestFromHTTP_TLSConfig(t *testing.T) { }) t.Run("with custom TLS config", func(t *testing.T) { - t.Parallel() - // Create test server for this test server := httptest.NewTLSServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -285,8 +271,6 @@ func TestFromHTTP_TLSConfig(t *testing.T) { }) t.Run("TLSConfig takes precedence over InsecureSkipVerify", func(t *testing.T) { - t.Parallel() - // Create test server for this test server := httptest.NewTLSServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -326,8 +310,6 @@ func TestFromHTTP_TLSConfig(t *testing.T) { }) t.Run("no TLS modifications when neither option is set", func(t *testing.T) { - t.Parallel() - // Create test server for this test server := httptest.NewTLSServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -366,8 +348,6 @@ func TestFromHTTP_GetReader(t *testing.T) { // Test with simple basic mocks instead of complex HTTP validation t.Run("successful read", func(t *testing.T) { - t.Parallel() - // Create test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -388,8 +368,6 @@ func TestFromHTTP_GetReader(t *testing.T) { }) t.Run("unauthorized error", func(t *testing.T) { - t.Parallel() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte("Unauthorized")) @@ -409,8 +387,6 @@ func TestFromHTTP_GetReader(t *testing.T) { }) t.Run("not found error", func(t *testing.T) { - t.Parallel() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, err := w.Write([]byte("Not Found")) @@ -430,8 +406,6 @@ func TestFromHTTP_GetReader(t *testing.T) { }) t.Run("network error", func(t *testing.T) { - t.Parallel() - // Use any URL since we'll replace the client with a mock testURL := "https://localhost:8080/script.js" @@ -458,8 +432,6 @@ func TestFromHTTP_GetReaderWithContext(t *testing.T) { const testScript = FunctionContent t.Run("Success - Background Context", func(t *testing.T) { - t.Parallel() - // Create test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -481,8 +453,6 @@ func TestFromHTTP_GetReaderWithContext(t *testing.T) { }) t.Run("Failure - Cancelled Context", func(t *testing.T) { - t.Parallel() - // Create test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -516,8 +486,6 @@ func TestFromHTTP_GetReaderWithContext(t *testing.T) { }) t.Run("Failure - Timeout Context", func(t *testing.T) { - t.Parallel() - // Create test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Delay to ensure timeout happens @@ -558,8 +526,6 @@ func TestFromHTTP_String(t *testing.T) { t.Parallel() t.Run("successful string representation", func(t *testing.T) { - t.Parallel() - // Create test server that returns content server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -580,8 +546,6 @@ func TestFromHTTP_String(t *testing.T) { }) t.Run("string representation with network error", func(t *testing.T) { - t.Parallel() - // Create server that deliberately fails connections (invalid port) testURL := "http://localhost:1" // This port is unlikely to be listening @@ -595,8 +559,6 @@ func TestFromHTTP_String(t *testing.T) { }) t.Run("string representation with HTTP error", func(t *testing.T) { - t.Parallel() - // Create test server that returns an error status server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) @@ -634,8 +596,6 @@ func TestHTTPOptionsWithMethods(t *testing.T) { t.Parallel() t.Run("option method chaining", func(t *testing.T) { - t.Parallel() - // Test chaining of option methods options := DefaultHTTPOptions(). WithTimeout(60*time.Second). @@ -651,8 +611,6 @@ func TestHTTPOptionsWithMethods(t *testing.T) { }) t.Run("with header auth", func(t *testing.T) { - t.Parallel() - headers := map[string]string{ "X-API-Key": "api-key-123", "X-Custom-Key": "custom-value", @@ -667,8 +625,6 @@ func TestHTTPOptionsWithMethods(t *testing.T) { }) t.Run("with no auth", func(t *testing.T) { - t.Parallel() - // Start with basic auth options := DefaultHTTPOptions().WithBasicAuth("user", "pass") @@ -685,8 +641,6 @@ func TestFromHTTP_GetSourceURL(t *testing.T) { t.Parallel() t.Run("source URL", func(t *testing.T) { - t.Parallel() - // Create test server for this test server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) diff --git a/execution/script/loader/fromString_test.go b/execution/script/loader/fromString_test.go index 5446ba1..c1e45f3 100644 --- a/execution/script/loader/fromString_test.go +++ b/execution/script/loader/fromString_test.go @@ -12,8 +12,6 @@ func TestNewFromString(t *testing.T) { t.Parallel() t.Run("valid content", func(t *testing.T) { - t.Parallel() - tests := []struct { name string content string @@ -49,8 +47,6 @@ func TestNewFromString(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - loader, err := NewFromString(tc.content) require.NoError(t, err) require.NotNil(t, loader) @@ -67,8 +63,6 @@ func TestNewFromString(t *testing.T) { }) t.Run("invalid content", func(t *testing.T) { - t.Parallel() - tests := []struct { name string content string @@ -86,8 +80,6 @@ func TestNewFromString(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - loader, err := NewFromString(tc.content) require.Error(t, err) require.ErrorIs(t, err, ErrScriptNotAvailable) @@ -101,8 +93,6 @@ func TestFromString_GetReader(t *testing.T) { t.Parallel() t.Run("read content", func(t *testing.T) { - t.Parallel() - content := "test content\nwith multiple lines" loader, err := NewFromString(content) require.NoError(t, err) @@ -114,8 +104,6 @@ func TestFromString_GetReader(t *testing.T) { }) t.Run("multiple reads from same loader", func(t *testing.T) { - t.Parallel() - content := FunctionContent loader, err := NewFromString(content) require.NoError(t, err) @@ -124,8 +112,6 @@ func TestFromString_GetReader(t *testing.T) { }) t.Run("partial reads", func(t *testing.T) { - t.Parallel() - content := "line1\nline2\nline3\nline4\nline5" loader, err := NewFromString(content) require.NoError(t, err) @@ -154,8 +140,6 @@ func TestFromString_GetSourceURL(t *testing.T) { t.Parallel() t.Run("source url", func(t *testing.T) { - t.Parallel() - content := SimpleContent loader, err := NewFromString(content) require.NoError(t, err) @@ -172,8 +156,6 @@ func TestFromString_GetSourceURL(t *testing.T) { }) t.Run("unique urls for different content", func(t *testing.T) { - t.Parallel() - loader1, err := NewFromString("content one") require.NoError(t, err) @@ -189,8 +171,6 @@ func TestFromString_String(t *testing.T) { t.Parallel() t.Run("string representation", func(t *testing.T) { - t.Parallel() - // Test with different content lengths tests := []struct { name string @@ -212,8 +192,6 @@ func TestFromString_String(t *testing.T) { for _, tc := range tests { tc := tc // Capture range variable t.Run(tc.name, func(t *testing.T) { - t.Parallel() - 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 21701e8..f0cc140 100644 --- a/execution/script/loader/httpauth/basic_test.go +++ b/execution/script/loader/httpauth/basic_test.go @@ -13,7 +13,6 @@ func TestBasicAuth(t *testing.T) { t.Parallel() t.Run("Valid credentials", func(t *testing.T) { - t.Parallel() username := "testuser" password := "testpass" @@ -33,7 +32,6 @@ func TestBasicAuth(t *testing.T) { }) t.Run("Empty username (no auth applied)", func(t *testing.T) { - t.Parallel() username := "" password := "testpass" @@ -52,7 +50,6 @@ func TestBasicAuth(t *testing.T) { }) t.Run("With context", func(t *testing.T) { - t.Parallel() username := "testuser" password := "testpass" @@ -73,7 +70,6 @@ func TestBasicAuth(t *testing.T) { }) t.Run("With cancelled context", func(t *testing.T) { - t.Parallel() username := "testuser" password := "testpass" @@ -92,7 +88,6 @@ func TestBasicAuth(t *testing.T) { }) t.Run("With timeout context", func(t *testing.T) { - t.Parallel() username := "testuser" password := "testpass" diff --git a/execution/script/loader/httpauth/header_test.go b/execution/script/loader/httpauth/header_test.go index 8934bd2..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", @@ -34,8 +32,6 @@ 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()) @@ -49,8 +45,6 @@ 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()) @@ -64,8 +58,6 @@ 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()) @@ -79,8 +71,6 @@ func TestHeaderAuth(t *testing.T) { }) t.Run("With context", func(t *testing.T) { - t.Parallel() - auth := NewHeaderAuth(map[string]string{ "Authorization": "Bearer token123", }) @@ -97,8 +87,6 @@ func TestHeaderAuth(t *testing.T) { }) t.Run("With cancelled context", func(t *testing.T) { - t.Parallel() - auth := NewHeaderAuth(map[string]string{ "Authorization": "Bearer token123", }) @@ -116,8 +104,6 @@ func TestHeaderAuth(t *testing.T) { }) t.Run("With timeout context", func(t *testing.T) { - t.Parallel() - auth := NewHeaderAuth(map[string]string{ "Authorization": "Bearer token123", }) diff --git a/execution/script/loader/httpauth/noauth_test.go b/execution/script/loader/httpauth/noauth_test.go index c803cb5..f6ea355 100644 --- a/execution/script/loader/httpauth/noauth_test.go +++ b/execution/script/loader/httpauth/noauth_test.go @@ -19,8 +19,6 @@ 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, "http://localhost/test", nil) require.NoError(t, err) @@ -32,8 +30,6 @@ func TestNoAuth(t *testing.T) { }) t.Run("With context authentication", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil) require.NoError(t, err) ctx := context.Background() @@ -45,8 +41,6 @@ func TestNoAuth(t *testing.T) { }) t.Run("With cancelled context", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil) require.NoError(t, err) @@ -59,8 +53,6 @@ func TestNoAuth(t *testing.T) { }) t.Run("With timeout context", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest(http.MethodGet, "http://localhost/test", nil) require.NoError(t, err) diff --git a/machines/extism/compiler/compiler_test.go b/machines/extism/compiler/compiler_test.go index bb8fcab..e4ec62f 100644 --- a/machines/extism/compiler/compiler_test.go +++ b/machines/extism/compiler/compiler_test.go @@ -81,8 +81,6 @@ func TestCompiler(t *testing.T) { // Success cases t.Run("success cases", func(t *testing.T) { - t.Parallel() - t.Run("valid wasm binary with existing function", func(t *testing.T) { wasmBytes := readTestWasm(t) entryPoint := "greet" @@ -194,8 +192,6 @@ func TestCompiler(t *testing.T) { // Error cases t.Run("error cases", func(t *testing.T) { - t.Parallel() - t.Run("nil content", func(t *testing.T) { comp, err := NewCompiler( WithEntryPoint("main"), diff --git a/machines/extism/compiler/internal/compile/compile_test.go b/machines/extism/compiler/internal/compile/compile_test.go index 789e5a5..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(), diff --git a/machines/extism/compiler/options_test.go b/machines/extism/compiler/options_test.go index 8116a5b..4ae9e26 100644 --- a/machines/extism/compiler/options_test.go +++ b/machines/extism/compiler/options_test.go @@ -39,8 +39,6 @@ func TestLoggerConfiguration(t *testing.T) { // Basic initialization and configuration t.Run("creation and configuration", func(t *testing.T) { - t.Parallel() - t.Run("default initialization", func(t *testing.T) { c, err := NewCompiler() require.NoError(t, err) @@ -80,8 +78,6 @@ func TestLoggerConfiguration(t *testing.T) { // Testing precedence rules t.Run("option precedence", func(t *testing.T) { - t.Parallel() - t.Run("last option wins", func(t *testing.T) { var handlerBuf, loggerBuf bytes.Buffer customHandler := slog.NewTextHandler(&handlerBuf, nil) @@ -175,8 +171,6 @@ func TestRuntimeOptions(t *testing.T) { t.Parallel() t.Run("WASI options", func(t *testing.T) { - t.Parallel() - t.Run("enable/disable WASI", func(t *testing.T) { c := &Compiler{ options: &compile.Settings{}, @@ -208,8 +202,6 @@ func TestRuntimeOptions(t *testing.T) { }) t.Run("runtime config", func(t *testing.T) { - t.Parallel() - t.Run("normal runtime config", func(t *testing.T) { runtimeConfig := wazero.NewRuntimeConfig() c := &Compiler{ @@ -250,8 +242,6 @@ func TestRuntimeOptions(t *testing.T) { }) t.Run("host functions", func(t *testing.T) { - t.Parallel() - t.Run("valid host functions", func(t *testing.T) { testHostFn := extismSDK.NewHostFunctionWithStack( "test_function", diff --git a/machines/extism/evaluator/response_test.go b/machines/extism/evaluator/response_test.go index df8e917..a05c635 100644 --- a/machines/extism/evaluator/response_test.go +++ b/machines/extism/evaluator/response_test.go @@ -54,7 +54,6 @@ func TestNewEvalResult(t *testing.T) { for _, tt := range tests { 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) require.NotNil(t, result) @@ -97,7 +96,6 @@ func TestExecResult_Type(t *testing.T) { for _, tt := range tests { 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()) @@ -146,7 +144,6 @@ func TestExecResult_String(t *testing.T) { for _, tt := range tests { 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()) @@ -177,7 +174,6 @@ func TestExecResult_Inspect(t *testing.T) { for _, tt := range tests { 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.Inspect()) diff --git a/machines/extism/internal/converters_test.go b/machines/extism/internal/converters_test.go index b39f470..9d2a850 100644 --- a/machines/extism/internal/converters_test.go +++ b/machines/extism/internal/converters_test.go @@ -61,7 +61,6 @@ func TestConvertToExtismFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() result, err := ConvertToExtismFormat(tt.input) if tt.wantErr { diff --git a/machines/starlark/compiler/compiler_test.go b/machines/starlark/compiler/compiler_test.go index a59f9c5..416c98c 100644 --- a/machines/starlark/compiler/compiler_test.go +++ b/machines/starlark/compiler/compiler_test.go @@ -127,8 +127,6 @@ main() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() - // Create compiler with options comp, err := NewCompiler( WithLogHandler(slog.NewTextHandler(os.Stdout, nil)), From c4b7a62473c1b7cfc490d0960675a0f709e8601a Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 02:09:56 -0400 Subject: [PATCH 11/15] readd t.Parallel in some places --- engine/evaluatorResponse_test.go | 2 ++ engine/evaluator_test.go | 5 +++++ engine/executionPackage_test.go | 4 ++++ engine/options/options_test.go | 3 +++ machines/extism/testdata/integration_test.go | 1 + machines/mocks/evaluatorResponse_test.go | 8 ++++++++ machines/mocks/evaluator_test.go | 1 + 7 files changed, 24 insertions(+) diff --git a/engine/evaluatorResponse_test.go b/engine/evaluatorResponse_test.go index 028974f..d65011c 100644 --- a/engine/evaluatorResponse_test.go +++ b/engine/evaluatorResponse_test.go @@ -10,6 +10,7 @@ import ( // TestEvaluatorResponseInterface tests all methods of the EvaluatorResponse interface func TestEvaluatorResponseInterface(t *testing.T) { + t.Parallel() // Create a mock implementation of EvaluatorResponse mockResponse := new(mocks.EvaluatorResponse) @@ -109,6 +110,7 @@ func TestEvaluatorResponseInterface(t *testing.T) { // TestEvaluatorResponseUsage tests how EvaluatorResponse is typically used in real code func TestEvaluatorResponseUsage(t *testing.T) { + t.Parallel() // Create a mock implementation mockResponse := new(mocks.EvaluatorResponse) diff --git a/engine/evaluator_test.go b/engine/evaluator_test.go index 996a751..62c3dc2 100644 --- a/engine/evaluator_test.go +++ b/engine/evaluator_test.go @@ -56,6 +56,7 @@ func (m *mockEvaluatorWithPreparer) PrepareContext( } func TestEvaluatorInterface(t *testing.T) { + t.Parallel() // Create a mock evaluator response mockResponse := new(mocks.EvaluatorResponse) mockResponse.On("Interface").Return("test result") @@ -109,6 +110,7 @@ func TestEvaluatorInterface(t *testing.T) { } func TestEvalDataPreparerInterface(t *testing.T) { + t.Parallel() // Create a logger for testing handler := slog.NewTextHandler(os.Stdout, nil) @@ -169,6 +171,7 @@ method + " " + greeting } func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { + t.Parallel() // Define a type for the context key to avoid collision type dataKey string @@ -227,6 +230,7 @@ func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { } func TestEvaluatorWithPrepInterface(t *testing.T) { + t.Parallel() // Create a mock evaluator response mockResponse := new(mocks.EvaluatorResponse) mockResponse.On("Interface").Return("combined result") @@ -292,6 +296,7 @@ func TestEvaluatorWithPrepInterface(t *testing.T) { } 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 index 0bc4d0f..a50cd9a 100644 --- a/engine/executionPackage_test.go +++ b/engine/executionPackage_test.go @@ -12,6 +12,7 @@ import ( ) func TestNewExecutionPackage(t *testing.T) { + t.Parallel() // Setup test mocks mockEvaluator := new(mocks.Evaluator) mockUnit := &script.ExecutableUnit{} @@ -33,6 +34,7 @@ func TestNewExecutionPackage(t *testing.T) { } func TestExecutionPackage_String(t *testing.T) { + t.Parallel() // Setup test mocks mockEvaluator := new(mocks.Evaluator) mockUnit := &script.ExecutableUnit{} @@ -59,6 +61,7 @@ func TestExecutionPackage_String(t *testing.T) { } func TestExecutionPackage_Getters(t *testing.T) { + t.Parallel() // Setup test cases testCases := []struct { name string @@ -103,6 +106,7 @@ func TestExecutionPackage_Getters(t *testing.T) { } 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") 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/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) } From 3cf5ee39a04b5dfd525380388ed020e47f84b95c Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 14:25:53 -0400 Subject: [PATCH 12/15] minor test cleanup --- engine/evaluatorResponse_test.go | 32 +++++++++++--------------- engine/evaluator_test.go | 39 +++++++++++--------------------- 2 files changed, 26 insertions(+), 45 deletions(-) diff --git a/engine/evaluatorResponse_test.go b/engine/evaluatorResponse_test.go index d65011c..11dfa7d 100644 --- a/engine/evaluatorResponse_test.go +++ b/engine/evaluatorResponse_test.go @@ -6,12 +6,12 @@ import ( "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() - // Create a mock implementation of EvaluatorResponse mockResponse := new(mocks.EvaluatorResponse) // Test Type method with various return types @@ -42,7 +42,6 @@ func TestEvaluatorResponseInterface(t *testing.T) { } }) - // Test Inspect method t.Run("Inspect method", func(t *testing.T) { inspectTests := []struct { name string @@ -111,22 +110,19 @@ func TestEvaluatorResponseInterface(t *testing.T) { // TestEvaluatorResponseUsage tests how EvaluatorResponse is typically used in real code func TestEvaluatorResponseUsage(t *testing.T) { t.Parallel() - // Create a mock implementation 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 (common in go-polyscript) + // Type checking pattern result := mockResponse.Interface() - if 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") - } else { - t.Fail() - } + 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{ @@ -138,14 +134,12 @@ func TestEvaluatorResponseUsage(t *testing.T) { // Type checking for map result = mockResponse.Interface() - if 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") - } else { - t.Fail() - } + 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 62c3dc2..d301532 100644 --- a/engine/evaluator_test.go +++ b/engine/evaluator_test.go @@ -65,7 +65,7 @@ func TestEvaluatorInterface(t *testing.T) { mockResponse.On("Type").Return(data.STRING) mockResponse.On("Inspect").Return("test result") - // Define a type for the context key to avoid collision + // use a custom type for the context key lookup, to avoid lint warnings type contextKey string testKey := contextKey("test-key") @@ -172,10 +172,6 @@ method + " " + greeting func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { t.Parallel() - // Define a type for the context key to avoid collision - type dataKey string - - // Create a mock data preparer dataPreparer := &mockDataPreparer{} // Test with various data types @@ -186,9 +182,11 @@ func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { // 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 @@ -200,33 +198,22 @@ func TestEvalDataPreparerInterfaceDirectImplementation(t *testing.T) { require.NotNil(t, resultCtx, "Enriched context should not be nil") // Verify data was stored correctly - assert.Equal( - t, - data1, - resultCtx.Value(dataKey("data-0")), - "First data item should be stored correctly", - ) - assert.Equal( - t, - data2, - resultCtx.Value(dataKey("data-1")), - "Second data item should be stored correctly", - ) - assert.Equal( - t, - data3, - resultCtx.Value(dataKey("data-2")), - "Third data item should be 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")) - _, err = errorPreparer.PrepareContext(ctx, "test") + ogCtx, err := errorPreparer.PrepareContext(ctx, "test") assert.Error(t, err, "Should return an error") - assert.Contains(t, err.Error(), "preparation error", "Error message should be preserved") + 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) { @@ -239,7 +226,7 @@ func TestEvaluatorWithPrepInterface(t *testing.T) { mockResponse.On("Type").Return(data.STRING) mockResponse.On("Inspect").Return("combined result") - // Define a type for the context key to avoid collision + // use a custom type for the context key lookup, to avoid lint warnings type prepKey string prepDataKey := prepKey("prepared-data") From ccbad0df23948b265012361e504b485043962601 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 17:04:42 -0400 Subject: [PATCH 13/15] harmonize several tests harmonize several tests --- benchmarks/results/comparison.txt | 48 +- benchmarks/results/latest.txt | 2 +- machines/extism/compiler/compiler_test.go | 54 +- machines/extism/compiler/executable_test.go | 138 +- machines/extism/compiler/options_test.go | 721 +++++----- .../evaluator/bytecodeEvaluator_test.go | 1222 +++++++---------- machines/extism/evaluator/response_test.go | 670 ++++----- machines/risor/compiler/compiler_test.go | 334 +++-- machines/risor/compiler/executable_test.go | 158 +-- machines/risor/compiler/options_test.go | 563 ++++---- .../risor/evaluator/bytecodeEvaluator_test.go | 766 +++++------ machines/risor/evaluator/response_test.go | 554 ++++---- machines/starlark/compiler/compiler_test.go | 298 ++-- machines/starlark/compiler/executable_test.go | 149 +- machines/starlark/compiler/options_test.go | 563 ++++---- .../evaluator/bytecodeEvaluator_test.go | 290 +++- machines/starlark/evaluator/response_test.go | 343 +++-- 17 files changed, 3661 insertions(+), 3212 deletions(-) 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/machines/extism/compiler/compiler_test.go b/machines/extism/compiler/compiler_test.go index e4ec62f..0b21eb0 100644 --- a/machines/extism/compiler/compiler_test.go +++ b/machines/extism/compiler/compiler_test.go @@ -64,22 +64,57 @@ func (m *mockScriptReaderCloser) Close() error { return args.Error(0) } -func TestCompiler_String(t *testing.T) { +func TestNewCompiler(t *testing.T) { t.Parallel() - // Create a compiler to test the String method - comp := createTestCompiler(t, "test_function") + t.Run("basic creation", func(t *testing.T) { + comp, err := NewCompiler( + WithEntryPoint("main"), + WithLogHandler(slog.NewTextHandler(io.Discard, nil)), + ) + require.NoError(t, err) + require.NotNil(t, comp) + + // Test String method + result := comp.String() + require.NotEmpty(t, result) + require.Contains(t, result, "Compiler") + }) + + t.Run("with entry point", func(t *testing.T) { + comp, err := NewCompiler( + WithEntryPoint("custom_function"), + WithLogHandler(slog.NewTextHandler(io.Discard, nil)), + ) + require.NoError(t, err) + require.NotNil(t, comp) + }) - // Test String method - result := comp.String() - require.NotEmpty(t, result) - require.Contains(t, result, "Compiler") + t.Run("with custom runtime config", func(t *testing.T) { + comp, err := NewCompiler( + WithEntryPoint("main"), + WithLogHandler(slog.NewTextHandler(io.Discard, nil)), + WithRuntimeConfig(wazero.NewRuntimeConfig()), + ) + require.NoError(t, err) + require.NotNil(t, comp) + }) + + t.Run("with custom logger", func(t *testing.T) { + handler := slog.NewTextHandler(io.Discard, nil) + logger := slog.New(handler) + comp, err := NewCompiler( + WithEntryPoint("main"), + WithLogger(logger), + ) + require.NoError(t, err) + require.NotNil(t, comp) + }) } -func TestCompiler(t *testing.T) { +func TestCompiler_Compile(t *testing.T) { t.Parallel() - // Success cases t.Run("success cases", func(t *testing.T) { t.Run("valid wasm binary with existing function", func(t *testing.T) { wasmBytes := readTestWasm(t) @@ -190,7 +225,6 @@ func TestCompiler(t *testing.T) { }) }) - // Error cases t.Run("error cases", func(t *testing.T) { t.Run("nil content", func(t *testing.T) { comp, err := NewCompiler( diff --git a/machines/extism/compiler/executable_test.go b/machines/extism/compiler/executable_test.go index bb8bf18..6af80ea 100644 --- a/machines/extism/compiler/executable_test.go +++ b/machines/extism/compiler/executable_test.go @@ -62,69 +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) + // 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) + }) }) - // Empty script bytes test - t.Run("empty script bytes", func(t *testing.T) { - exe := NewExecutable(nil, mockPlugin, entryPoint) - assert.Nil(t, exe) - }) + // Test getters + t.Run("Getters", func(t *testing.T) { + wasmBytes := []byte("mock wasm bytes") + entryPoint := "run" + mockPlugin := new(MockCompiledPlugin) - // Nil plugin test - t.Run("nil plugin", func(t *testing.T) { - exe := NewExecutable(wasmBytes, nil, entryPoint) - assert.Nil(t, exe) - }) - - // 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() - - ctx := context.Background() - wasmBytes := []byte("mock wasm bytes") - entryPoint := "run" + // Test Close functionality (specific to Extism) + t.Run("Close", func(t *testing.T) { + ctx := context.Background() + wasmBytes := []byte("mock wasm bytes") + entryPoint := "run" - mockPlugin := new(MockCompiledPlugin) - mockPlugin.On("Close", ctx).Return(nil) + mockPlugin := new(MockCompiledPlugin) + mockPlugin.On("Close", ctx).Return(nil) - exe := NewExecutable(wasmBytes, mockPlugin, entryPoint) - require.NotNil(t, exe) - assert.False(t, exe.closed.Load()) + exe := NewExecutable(wasmBytes, mockPlugin, entryPoint) + require.NotNil(t, exe) + assert.False(t, exe.closed.Load()) - err := exe.Close(ctx) - require.NoError(t, err) - 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()) + }) - 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) + }) - mockPlugin.AssertExpectations(t) + mockPlugin.AssertExpectations(t) + }) } diff --git a/machines/extism/compiler/options_test.go b/machines/extism/compiler/options_test.go index 4ae9e26..6ed2762 100644 --- a/machines/extism/compiler/options_test.go +++ b/machines/extism/compiler/options_test.go @@ -12,33 +12,53 @@ import ( "github.com/tetratelabs/wazero" ) -func TestWithEntryPoint(t *testing.T) { +// TestCompilerOptions tests all compiler options functionality +func TestCompilerOptions(t *testing.T) { t.Parallel() - entryPoint := "custom_entrypoint" - c := &Compiler{ - entryPointName: "", - } - c.applyDefaults() - opt := WithEntryPoint(entryPoint) - err := opt(c) + t.Run("EntryPoint", func(t *testing.T) { + t.Run("valid entry point", func(t *testing.T) { + entryPoint := "custom_entrypoint" - require.NoError(t, err) - require.Equal(t, entryPoint, c.GetEntryPointName()) + c := &Compiler{ + entryPointName: "", + } + c.applyDefaults() + opt := WithEntryPoint(entryPoint) + err := opt(c) - // Test with empty entry point - emptyOpt := WithEntryPoint("") - err = emptyOpt(c) + require.NoError(t, err) + require.Equal(t, entryPoint, c.GetEntryPointName()) + }) - require.Error(t, err) - require.Contains(t, err.Error(), "entry point cannot be empty") -} + t.Run("empty entry point", func(t *testing.T) { + c := &Compiler{ + entryPointName: "existing", + } + c.applyDefaults() + emptyOpt := WithEntryPoint("") + err := emptyOpt(c) -func TestLoggerConfiguration(t *testing.T) { - t.Parallel() + 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()) - // Basic initialization and configuration - t.Run("creation and configuration", func(t *testing.T) { + // Test with empty string + c2 := &Compiler{ + entryPointName: "", + } + require.Equal(t, "", c2.GetEntryPointName()) + }) + }) + + t.Run("Logger", func(t *testing.T) { t.Run("default initialization", func(t *testing.T) { c, err := NewCompiler() require.NoError(t, err) @@ -74,374 +94,361 @@ func TestLoggerConfiguration(t *testing.T) { c.logger.Info("test message") require.Contains(t, buf.String(), "test message", "log message should be in buffer") }) - }) - // Testing precedence rules - t.Run("option precedence", func(t *testing.T) { - t.Run("last option wins", func(t *testing.T) { + 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)) - // Case 1: Handler then Logger (logger wins) - 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") + 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() - // Case 2: Logger then Handler (handler wins) - 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("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") + }) }) - }) -} - -func TestWithLogHandler(t *testing.T) { - t.Parallel() - 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) { - t.Parallel() - 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 TestRuntimeOptions(t *testing.T) { - t.Parallel() - - 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("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("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("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("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("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("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("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("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("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("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 host functions", func(t *testing.T) { - c := &Compiler{ - options: &compile.Settings{}, - } + t.Run("Defaults and Validation", func(t *testing.T) { + t.Run("defaults - empty compiler", func(t *testing.T) { + c := &Compiler{} c.applyDefaults() - emptyOpt := WithHostFunctions([]extismSDK.HostFunction{}) - err := emptyOpt(c) - require.NoError(t, err) + 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("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("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) + }) }) - }) -} - -func TestApplyDefaults(t *testing.T) { - t.Parallel() - 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) - }) - - 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) - }) -} -func TestValidate(t *testing.T) { - t.Parallel() - 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) { - t.Parallel() - 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.Parallel() - 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("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("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 9576a62..dffe475 100644 --- a/machines/extism/evaluator/bytecodeEvaluator_test.go +++ b/machines/extism/evaluator/bytecodeEvaluator_test.go @@ -50,236 +50,565 @@ func createMockExecutable( return compiler.NewExecutable(wasmBytes, mockPlugin, entryPoint) } -func TestLoadInputData(t *testing.T) { - t.Parallel() +// mockErrProvider implements the data.Provider interface and always returns an error +type mockErrProvider struct { + err error +} - 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 *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 +} + +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") + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { 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"]) + }) + + // 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) - 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) + 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) - // Test the loadInputData method - result, err := evaluator.loadInputData(ctx) + content := createMockExecutable(mockPlugin, "main") + exe := &script.ExecutableUnit{ + ID: "test-string-success", + DataProvider: ctxProvider, + Content: content, + } + + 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) { - mockContent := &mockExecutableContent{ - machineType: machineTypes.Extism, - source: "invalid wasm", - bytecode: nil, // Nil bytecode will cause error - } + ctx := context.Background() + _, err := evaluator.Eval(ctx) - handler, exe := setupTest(mockContent) - evaluator := NewBytecodeEvaluator(handler, exe) + require.Error(t, err) + require.Contains(t, err.Error(), "executable unit is nil") + }) - ctx := context.Background() - _, err := evaluator.Eval(ctx) + // 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 + } - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode is nil") - }) + handler := slog.NewTextHandler(os.Stdout, nil) + ctxProvider := data.NewContextProvider(constants.EvalData) - // Test case: 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 module - } + exe := &script.ExecutableUnit{ + ID: "test-case", + Content: mockContent, + DataProvider: ctxProvider, + } - handler, exe := setupTest(mockContent) - evaluator := NewBytecodeEvaluator(handler, exe) + evaluator := NewBytecodeEvaluator(handler, exe) - ctx := context.Background() - _, err := evaluator.Eval(ctx) + ctx := context.Background() + _, err := evaluator.Eval(ctx) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid executable type") - }) -} + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode is nil") + }) -func TestNilHandlerFallback(t *testing.T) { - // Test that the evaluator handles nil handlers by creating a default + // 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 + } - // Create mock plugin - mockPlugin := new(MockCompiledPlugin) - mockPlugin.On("Close", mock.Anything).Return(nil) + handler := slog.NewTextHandler(os.Stdout, nil) + ctxProvider := data.NewContextProvider(constants.EvalData) - // Create a real compiler.Executable with our mock plugin - content := createMockExecutable(mockPlugin, "main") + exe := &script.ExecutableUnit{ + ID: "test-case", + Content: mockContent, + DataProvider: ctxProvider, + } - exe := &script.ExecutableUnit{ - ID: "test-nil-handler", - DataProvider: data.NewContextProvider(constants.EvalData), - Content: content, - } + evaluator := NewBytecodeEvaluator(handler, exe) - // Create with nil handler - evaluator := NewBytecodeEvaluator(nil, exe) + ctx := context.Background() + _, err := evaluator.Eval(ctx) - // Shouldn't panic - require.NotNil(t, evaluator) - require.NotNil(t, evaluator.logger) - require.NotNil(t, evaluator.logHandler) -} + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid executable type") + }) -func TestEvaluatorString(t *testing.T) { - handler := slog.NewTextHandler(os.Stdout, nil) - evaluator := NewBytecodeEvaluator(handler, nil) + // 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) - // Test the string representation - strRep := evaluator.String() - require.Equal(t, "extism.BytecodeEvaluator", strRep) -} + // Create a real compiler.Executable with our mock plugin + content := createMockExecutable(mockPlugin, "main") -func TestEvalWithNilExecutableUnit(t *testing.T) { - handler := slog.NewTextHandler(os.Stdout, nil) - evaluator := NewBytecodeEvaluator(handler, nil) + // Create executor unit + handler := slog.NewTextHandler(os.Stdout, nil) + execUnit := &script.ExecutableUnit{ + ID: "test-cancel", + Content: content, + DataProvider: data.NewContextProvider(constants.EvalData), + } - // Attempt to evaluate with nil executable unit - ctx := context.Background() - _, err := evaluator.Eval(ctx) + evaluator := NewBytecodeEvaluator(handler, execUnit) - // Should get an error - require.Error(t, err) - require.Contains(t, err.Error(), "executable unit is nil") -} + // Add test data to context + ctx = context.WithValue(ctx, constants.EvalData, map[string]any{"test": "data"}) -type mockExecutableContent struct { - machineType machineTypes.Type - source string - bytecode any -} + // Call Eval, which should be cancelled during execution + result, err := evaluator.Eval(ctx) -func (m *mockExecutableContent) GetMachineType() machineTypes.Type { - return m.machineType -} + // Should get a cancellation error + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "execution") -func (m *mockExecutableContent) GetSource() string { - return m.source -} + // Instance should have been called + mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything) -func (m *mockExecutableContent) GetByteCode() any { - return m.bytecode -} + // Instance should have been closed + assert.True(t, mockInstance.wasClosed) + }) -// 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") - } + // 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) + + 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) + + content := createMockExecutable(mockPlugin, "main") + exe := &script.ExecutableUnit{ + ID: "test-error-exit", + DataProvider: ctxProvider, + Content: content, + } - handler := slog.NewTextHandler(os.Stdout, nil) + evaluator := NewBytecodeEvaluator(handler, exe) + ctx := context.Background() + evalData := map[string]any{"test": "data"} + ctx = context.WithValue(ctx, constants.EvalData, evalData) - // Create context provider - ctxProvider := data.NewContextProvider(constants.EvalData) + _, err := evaluator.Eval(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "non-zero exit code") + }) - // Create mock plugin - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - exitCode: 1, // Will cause an error - output: []byte(`{"error":"something went wrong"}`), - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) + // 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, + } - // Create a real compiler.Executable with our mock plugin - content := createMockExecutable(mockPlugin, "main") + evaluator := NewBytecodeEvaluator(handler, exe) + ctx := context.Background() - // Create a mock executable - exe := &script.ExecutableUnit{ - ID: "test-basic", - DataProvider: ctxProvider, - Content: content, - } + _, err := evaluator.Eval(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create plugin instance") + }) + }) - evaluator := NewBytecodeEvaluator(handler, exe) + 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) - // 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) + // Create a real compiler.Executable with our mock plugin + content := createMockExecutable(mockPlugin, "main") - _, err := evaluator.Eval(ctx) - // We expect an error since our mock returns an error - assert.Error(t, err) + 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", + }, + } + + 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 { @@ -402,548 +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 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 -} - -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 -} - -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 { - 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 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"]) -} -*/ - -// TestBytecodeEvaluator_Cancel tests the behavior when a context is cancelled -func TestBytecodeEvaluator_Cancel(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) - - // Create a real compiler.Executable with our mock plugin - content := createMockExecutable(mockPlugin, "main") - - // Create executor unit - handler := slog.NewTextHandler(os.Stdout, nil) - execUnit := &script.ExecutableUnit{ - ID: "test-cancel", - Content: content, - DataProvider: data.NewContextProvider(constants.EvalData), - } - - evaluator := NewBytecodeEvaluator(handler, execUnit) - - // Add test data to context - ctx = context.WithValue(ctx, constants.EvalData, map[string]any{"test": "data"}) - - // Call Eval, which should be cancelled during execution - result, err := evaluator.Eval(ctx) - - // Should get a cancellation error - assert.Error(t, err) - assert.Nil(t, result) - assert.Contains(t, err.Error(), "execution") - - // Instance should have been called - mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything) - - // Instance should have been closed - assert.True(t, mockInstance.wasClosed) -} - -// TestBytecodeEvaluator_Exec tests the exec method directly -func TestBytecodeEvaluator_Exec(t *testing.T) { - // Define test cases - tests := []struct { - name string - setupMocks func() (*MockCompiledPlugin, *mockPluginInstance) - entryPoint string - inputData map[string]any - expectedValue any - expectError bool - errorContains string - }{ - { - name: "successful execution", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - exitCode: 0, - output: []byte(`{"result":"success"}`), - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectedValue: map[string]any{"result": "success"}, - expectError: false, - }, - { - name: "error creating instance", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - wasClosed: true, // Mock as if it was closed since the exec method closes the instance on error - } - mockPlugin.On("Instance", mock.Anything, mock.Anything). - Return(mockInstance, errors.New("instance creation error")) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectError: true, - errorContains: "failed to create plugin instance", - }, - { - name: "execution error", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - callErr: errors.New("execution error"), - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectError: true, - errorContains: "execution error", - }, - { - name: "non-zero exit code", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - exitCode: 1, - output: []byte(`{"error":"something went wrong"}`), - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectError: true, - errorContains: "non-zero exit code", - }, - { - name: "context cancellation", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - callErr: context.Canceled, - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectError: true, - errorContains: "execution", - }, - { - name: "close error (should still succeed)", - setupMocks: func() (*MockCompiledPlugin, *mockPluginInstance) { - mockPlugin := new(MockCompiledPlugin) - mockInstance := &mockPluginInstance{ - exitCode: 0, - output: []byte(`{"result":"success"}`), - closeErr: errors.New("close error"), - } - mockPlugin.On("Instance", mock.Anything, mock.Anything).Return(mockInstance, nil) - mockPlugin.On("Close", mock.Anything).Return(nil) - return mockPlugin, mockInstance - }, - entryPoint: "main", - inputData: map[string]any{"test": "data"}, - expectedValue: map[string]any{"result": "success"}, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create mocks - mockPlugin, mockInstance := tt.setupMocks() - - // Create handler and evaluator - handler := slog.NewTextHandler(os.Stdout, nil) - - // Create the unit under test - evaluator := NewBytecodeEvaluator(handler, nil) - - // Convert input data to JSON - inputJSON, err := mockLoadInputData(tt.inputData) - require.NoError(t, err) - - // Call the exec method directly - result, err := evaluator.exec( - context.Background(), - mockPlugin, - tt.entryPoint, - adapters.NewPluginInstanceConfig(), - inputJSON, - ) - - // Verify instance was called - mockPlugin.AssertCalled(t, "Instance", mock.Anything, mock.Anything) - - // Check for expected errors - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.NotNil(t, result) - - // Check result - resultValue := result.Interface() - assert.Equal(t, tt.expectedValue, resultValue) - } - - // Close should always be called - assert.True(t, mockInstance.wasClosed, "Instance should be closed") - }) - } -} - -// 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)) -} - -// Helper function to create mock input data JSON -func mockLoadInputData(data map[string]any) ([]byte, error) { - if data == nil { - return []byte("{}"), nil - } - return internal.ConvertToExtismFormat(data) -} diff --git a/machines/extism/evaluator/response_test.go b/machines/extism/evaluator/response_test.go index a05c635..3ab8e85 100644 --- a/machines/extism/evaluator/response_test.go +++ b/machines/extism/evaluator/response_test.go @@ -12,328 +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 { - t.Run(tt.name, func(t *testing.T) { + 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") -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 { - 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()) + // Should return ERROR for unknown types + assert.Equal(t, data.ERROR, result.Type()) }) - } -} - -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 { - 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", 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_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 { - 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()) - }) - } -} - -// TestNewEvalResultNilHandler tests the behavior when a nil handler is provided -func TestNewEvalResultNilHandler(t *testing.T) { - // Create with nil handler - result := newEvalResult(nil, "test value", 100*time.Millisecond, "test-id") - - // 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, "test value", result.value) - assert.Equal(t, 100*time.Millisecond, result.execTime) - assert.Equal(t, "test-id", result.scriptExeID) -} - -// TestExecResult_UnknownType tests handling of unknown value types -func TestExecResult_UnknownType(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, customValue, time.Second, "test-id") + result := newEvalResult(handler, badMap, time.Second, "test-id") - // Should return ERROR for unknown types - assert.Equal(t, data.ERROR, result.Type()) -} - -// TestExecResult_Interface tests the Interface method -func TestExecResult_Interface(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()) + // Should fall back to default string representation + inspectResult := result.Inspect() + assert.Contains(t, inspectResult, "map[") }) - } -} -// TestExecResult_GetMetadata tests the metadata methods -func TestExecResult_GetMetadata(t *testing.T) { - handler := slog.NewTextHandler(os.Stdout, nil) - execTime := 123 * time.Millisecond - scriptID := "test-script-9876" - - result := newEvalResult(handler, "test", execTime, scriptID) - - // Test GetScriptExeID - assert.Equal(t, scriptID, result.GetScriptExeID()) - - // Test GetExecTime - assert.Equal(t, execTime.String(), result.GetExecTime()) -} + 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"}, + } -// TestExecResult_InspectWithInvalidJSON tests what happens when JSON marshaling fails -func TestExecResult_InspectWithInvalidJSON(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 - } - - handler := slog.NewTextHandler(os.Stdout, nil) - result := newEvalResult(handler, badMap, time.Second, "test-id") - - // Should fall back to default string representation - inspectResult := result.Inspect() - assert.Contains(t, inspectResult, "map[") -} - -// TestExecResult_NestedComplexValues tests the handling of nested complex values -func TestExecResult_NestedComplexValues(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, 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()) -} - -// TestExecResult_StringRepresentation tests various string representation cases -func TestExecResult_StringRepresentation(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") + 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/risor/compiler/compiler_test.go b/machines/risor/compiler/compiler_test.go index e0d7e89..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,21 +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 { - 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 42f3094..7ea0f82 100644 --- a/machines/risor/evaluator/bytecodeEvaluator_test.go +++ b/machines/risor/evaluator/bytecodeEvaluator_test.go @@ -75,157 +75,384 @@ func (m *MockContent) GetMachineType() types.Type { return types.Risor } -// TestBytecodeEvaluator_Success tests evaluating valid Risor scripts -func TestBytecodeEvaluator_Success(t *testing.T) { +// 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 + } + + content, err := c.Compile(reader) + if err != nil { + return nil, err + } + + return &script.ExecutableUnit{ + ID: "test-id", + Content: content, + DataProvider: provider, + }, nil +} + +// TestBytecodeEvaluator_Evaluate tests evaluating Risor scripts +func TestBytecodeEvaluator_Evaluate(t *testing.T) { t.Parallel() - tests := []struct { - name string - script string - requestMethod string - urlPath string - expectedType data.Types - expectedResult string - expectedValue any - }{ - { - name: "GET request to /hello", - script: ` - func handle(request) { - if request == nil { - return error("request is nil") - } - if request["Method"] == "POST" { - return "post" - } - if request["URL_Path"] == "/hello" { - return true - } - return false - } - print(ctx) - handle(ctx["request"]) - `, - requestMethod: "GET", - urlPath: "/hello", - expectedType: data.Types("bool"), - expectedResult: "true", - expectedValue: true, - }, - { - name: "POST request", - script: ` - func handle(request) { - if request == nil { - return error("request is nil") - } - if request["Method"] == "POST" { - return "post" + // Define a test script that handles HTTP requests + testScript := ` + func handle(request) { + if request == nil { + return error("request is nil") + } + if request["Method"] == "POST" { + return "post" + } + if request["URL_Path"] == "/hello" { + return true + } + 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, + }, + { + 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, + }, + } + + 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, } - if request["URL_Path"] == "/hello" { - return true + 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) } - return false - } - print(ctx) - handle(ctx["request"]) - `, - requestMethod: "POST", - urlPath: "/hello", - expectedType: data.Types("string"), - expectedResult: "\"post\"", - expectedValue: "post", - }, - { - name: "GET request to unknown path", - script: ` - func handle(request) { - if request == nil { - return error("request is nil") + }) + } + }) + + 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", + }, + } + + 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), } - if request["Method"] == "POST" { - return "post" + + ctx := context.Background() + result, err := evaluator.Eval(ctx) + + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), tt.errorMessage) + }) + } + }) + + 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, + }, + } + + 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), } - if request["URL_Path"] == "/hello" { - return true + + 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) + } } - return false - } - print(ctx) - handle(ctx["request"]) - `, - requestMethod: "GET", - urlPath: "/unknown", - expectedType: data.Types("bool"), - expectedResult: "false", - expectedValue: false, - }, - } - 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, + // Verify mock expectations if we have a mockProvider + if exe != nil && exe.DataProvider != nil { + if mockProvider, ok := exe.DataProvider.(*MockProvider); ok { + mockProvider.AssertExpectations(t) + } + } }) + } + }) - // 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) - } + 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()) }) - } -} -// TestBytecodeEvaluator_Metadata tests string representation and metadata methods -func TestBytecodeEvaluator_Metadata(t *testing.T) { - t.Parallel() + // 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) - // Test String method - t.Run("String method", func(t *testing.T) { - evaluator := &BytecodeEvaluator{} - require.Equal(t, "risor.BytecodeEvaluator", evaluator.String()) + 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) + } + }) + } + }) }) } @@ -333,266 +560,3 @@ func TestBytecodeEvaluator_PrepareContext(t *testing.T) { }) } } - -// TestBytecodeEvaluator_Errors tests error conditions for the Eval method -func TestBytecodeEvaluator_Errors(t *testing.T) { - t.Parallel() - - 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", - }, - } - - 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) - }) - } -} - -// TestBytecodeEvaluator_LoadInputData tests the loadInputData method -func TestBytecodeEvaluator_LoadInputData(t *testing.T) { - t.Parallel() - - 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, - }, - } - - 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) - } - } - }) - } -} - -// TestBytecodeEvaluator_New tests creating a new BytecodeEvaluator with various options -func TestBytecodeEvaluator_New(t *testing.T) { - t.Parallel() - - 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) - } - }) - } -} - -// 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 - } - - content, err := c.Compile(reader) - if err != nil { - return nil, err - } - - 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 8661101..f693416 100644 --- a/machines/risor/evaluator/response_test.go +++ b/machines/risor/evaluator/response_test.go @@ -80,300 +80,286 @@ func (m *RisorObjectMock) Compare(other rObj.Object) (int, error) { return args.Int(0), args.Error(1) } -// TestEvalResult_Creation tests creating a new evaluation result -func TestEvalResult_Creation(t *testing.T) { +// TestResponseMethods tests all the methods of the EvaluatorResponse interface +func TestResponseMethods(t *testing.T) { t.Parallel() - tests := []struct { - name string - setupMock func() *RisorObjectMock - execTime time.Duration - versionID string - }{ - { - name: "with valid object", - setupMock: func() *RisorObjectMock { - mockObj := new(RisorObjectMock) - return mockObj + 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", }, - execTime: 100 * time.Millisecond, - versionID: "test-version-1", - }, - { - name: "with longer execution time", - setupMock: func() *RisorObjectMock { + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockObj := tt.setupMock() + handler := slog.NewTextHandler(os.Stdout, nil) + + result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID) + + // 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) + + // Verify interface implementation + require.Implements(t, (*engine.EvaluatorResponse)(nil), result) + + // Verify metadata methods + assert.Equal(t, tt.execTime.String(), result.GetExecTime()) + assert.Equal(t, tt.versionID, result.GetScriptExeID()) + }) + } + }) + + 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) - return mockObj + 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}", }, - execTime: 2 * time.Second, - versionID: "test-version-2", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockObj := tt.setupMock() - handler := slog.NewTextHandler(os.Stdout, nil) - - result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID) - - // 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) - - // Verify interface implementation - require.Implements(t, (*engine.EvaluatorResponse)(nil), result) - - // Verify metadata methods - assert.Equal(t, tt.execTime.String(), result.GetExecTime()) - assert.Equal(t, tt.versionID, result.GetScriptExeID()) - }) - } -} - -// TestEvalResult_Type tests the Type method -func TestEvalResult_Type(t *testing.T) { - t.Parallel() - - 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) - }) - } -} - -// TestEvalResult_String tests the String method -func TestEvalResult_String(t *testing.T) { - t.Parallel() - - 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) - }) - } -} - -// TestEvalResult_Inspect tests the Inspect method -func TestEvalResult_Inspect(t *testing.T) { - t.Parallel() - - 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) - }) - } -} - -// TestEvalResult_Interface tests the Interface method -func TestEvalResult_Interface(t *testing.T) { - t.Parallel() + { + 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}", + }, + } - 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) - }) - } -} + 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\"}", + }, + } -// TestEvalResult_NilHandler tests creating a result with nil handler -func TestEvalResult_NilHandler(t *testing.T) { - mockObj := new(RisorObjectMock) - execTime := 100 * time.Millisecond - versionID := "test-version-1" + 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, + }, + } - // Create with nil handler - result := newEvalResult(nil, mockObj, execTime, versionID) + 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", + }, + } - // Should create default handler and logger - require.NotNil(t, result) - require.NotNil(t, result.logHandler) - require.NotNil(t, result.logger) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockObj := new(RisorObjectMock) + handler := slog.NewTextHandler(os.Stdout, nil) - // Should still store all values correctly - assert.Equal(t, mockObj, result.Object) - assert.Equal(t, execTime, result.execTime) - assert.Equal(t, versionID, result.scriptExeID) -} + result := newEvalResult(handler, mockObj, tt.execTime, tt.versionID) -// TestEvalResult_Metadata tests all metadata accessors -func TestEvalResult_Metadata(t *testing.T) { - t.Parallel() + // Test GetScriptExeID + assert.Equal(t, tt.versionID, result.GetScriptExeID()) - 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()) - - // Test GetExecTime - assert.Equal(t, tt.execTime.String(), result.GetExecTime()) - }) - } + // 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 416c98c..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,52 +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) + } + }) + } + + 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 { - t.Run(tt.name, func(t *testing.T) { - // 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) + }) + } + }) } From 26139e1dca9528e9073e1495a76fbdd15c22e4ac Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 19:43:25 -0400 Subject: [PATCH 14/15] add testing guidelines for machines --- machines/TESTING.md | 192 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 machines/TESTING.md 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 From 384206ba3e8d47424251ae662c4ab674bb2f773f Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 10 Apr 2025 20:50:07 -0400 Subject: [PATCH 15/15] add benchmark results --- .../benchmark_2025-04-10_14-26-23.json | 45 +++++++++++++++++++ .../results/benchmark_2025-04-10_14-26-23.txt | 13 ++++++ 2 files changed, 58 insertions(+) create mode 100644 benchmarks/results/benchmark_2025-04-10_14-26-23.json create mode 100644 benchmarks/results/benchmark_2025-04-10_14-26-23.txt 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