Skip to content

Commit f2b4b6c

Browse files
committed
nits, comments and more tests
1 parent c16a604 commit f2b4b6c

File tree

2 files changed

+81
-13
lines changed

2 files changed

+81
-13
lines changed

dbos/workflow.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -757,24 +757,25 @@ func RunAsStep[R any](ctx DBOSContext, fn any, input ...any) (R, error) {
757757
return *new(R), newStepExecutionError("", "", "workflow state not found in context: are you running this step within a workflow?")
758758
}
759759
workflowID := wfState.workflowID
760-
761-
// Resolve step name in separate safeExecute to use it outside of main execution
762-
stepNameResult, stepNameErr := safeExecute(func() (any, error) {
760+
761+
// Resolve step name in safeExecute
762+
stepNameResult, err := safeExecute(func() (any, error) {
763763
return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name(), nil
764764
}, workflowID, "", nil)
765-
766-
if stepNameErr != nil {
767-
return *new(R), stepNameErr
765+
766+
if err != nil {
767+
return *new(R), err
768768
}
769-
770-
stepName := stepNameResult.(string)
771-
769+
770+
stepName, ok := stepNameResult.(string)
771+
if !ok { // This should never happen, but we check for safety
772+
return *new(R), newStepExecutionError(workflowID, "", fmt.Sprintf("unexpected step function type: expected string, got %T", stepNameResult))
773+
}
774+
772775
// Type-erase the function based on its actual type
773776
typeErasedFn := func(ctx context.Context) (any, error) {
774777
return safeExecute(func() (any, error) {
775-
// All reflection operations are protected
776-
777-
// Prepare the arguments -- we must call fnValue.Call() with a slice of reflect.Value
778+
// Prepare the arguments
778779
fnType := reflect.TypeOf(fn)
779780
typedInput := make([]reflect.Value, fnType.NumIn())
780781
typedInput[0] = reflect.ValueOf(ctx) // First argument is always context.Context
@@ -787,9 +788,10 @@ func RunAsStep[R any](ctx DBOSContext, fn any, input ...any) (R, error) {
787788
fnValue := reflect.ValueOf(fn)
788789
results := fnValue.Call(typedInput)
789790

790-
// Convert the results to the expected types
791+
// Convert the results to the expected types (R and error)
791792
var result any
792793
if results[0].IsValid() && results[0].CanInterface() {
794+
// If the result is a pointer or interface, check if it's nil before casting
793795
if results[0].Kind() == reflect.Ptr || results[0].Kind() == reflect.Interface {
794796
if !results[0].IsNil() {
795797
result = results[0].Interface().(R) // Cast the result to the expected type R
@@ -800,6 +802,7 @@ func RunAsStep[R any](ctx DBOSContext, fn any, input ...any) (R, error) {
800802
}
801803
var err error
802804
if results[1].IsValid() && results[1].CanInterface() {
805+
// Handle the case were the return value is an interface or pointer that implements the error interface
803806
if results[1].Kind() == reflect.Ptr || results[1].Kind() == reflect.Interface {
804807
if !results[1].IsNil() {
805808
err = results[1].Interface().(error)
@@ -820,6 +823,8 @@ func RunAsStep[R any](ctx DBOSContext, fn any, input ...any) (R, error) {
820823
result, err := ctx.RunAsStep(ctx, typeErasedFn)
821824

822825
// Type-check and cast the result with panic recovery
826+
// The result should already be of type R (casted in the inner function), but ctx.RunAsStep returns an `any` type, so we need to cast it back to R
827+
// err is the step error, if any, and already of type error
823828
typedResult, castErr := safeExecute(func() (any, error) {
824829
if casted, ok := result.(R); ok {
825830
return casted, nil

dbos/workflows_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,8 @@ func TestSteps(t *testing.T) {
867867
t.Run("NilReturnHandling", func(t *testing.T) {
868868
nilPointerReturn := func(ctx context.Context) (*string, error) { return nil, nil }
869869
nilInterfaceReturn := func(ctx context.Context) (any, error) { return nil, nil }
870+
nilConcreteReturn := func(ctx context.Context) (string, error) { return "", nil } // Zero value, not nil
871+
trueNilNilReturn := func(ctx context.Context) (interface{}, error) { return nil, nil } // Both nil
870872

871873
// Test that nil pointers fail with type casting error (as expected)
872874
testWorkflowNilPointer := func(dbosCtx DBOSContext, input string) (string, error) {
@@ -917,6 +919,67 @@ func TestSteps(t *testing.T) {
917919
if result2 != "nil-interface-error-expected" {
918920
t.Fatalf("expected result 'nil-interface-error-expected', got %q", result2)
919921
}
922+
923+
// Test concrete type with zero value (should work)
924+
t.Run("ConcreteZeroValue", func(t *testing.T) {
925+
testWorkflowZeroValue := func(dbosCtx DBOSContext, input string) (string, error) {
926+
result, err := RunAsStep[string](dbosCtx, nilConcreteReturn)
927+
if err != nil {
928+
return "zero-value-error", err
929+
}
930+
return fmt.Sprintf("zero-value-success: %q", result), nil
931+
}
932+
RegisterWorkflow(dbosCtx, testWorkflowZeroValue)
933+
934+
handle, err := RunAsWorkflow(dbosCtx, testWorkflowZeroValue, "input")
935+
if err != nil {
936+
t.Fatal("failed to run workflow with zero value:", err)
937+
}
938+
939+
result, err := handle.GetResult()
940+
if err != nil {
941+
t.Fatal("failed to get result from zero value workflow:", err)
942+
}
943+
944+
if result != "zero-value-success: \"\"" {
945+
t.Fatalf("expected result 'zero-value-success: \"\"', got %q", result)
946+
}
947+
})
948+
949+
// Test true (nil, nil) return - both result and error are nil
950+
t.Run("TrueNilNilReturn", func(t *testing.T) {
951+
testWorkflowNilNil := func(dbosCtx DBOSContext, input string) (string, error) {
952+
result, err := RunAsStep[interface{}](dbosCtx, trueNilNilReturn)
953+
if err != nil {
954+
// This is expected due to the type casting issue with nil interface{}
955+
if strings.Contains(err.Error(), "unexpected result type") {
956+
return "nil-nil-type-error-expected", nil
957+
}
958+
return "nil-nil-other-error", err
959+
}
960+
if result == nil {
961+
return "nil-nil-success: got nil result", nil
962+
}
963+
return fmt.Sprintf("nil-nil-unexpected: got %v", result), nil
964+
}
965+
RegisterWorkflow(dbosCtx, testWorkflowNilNil)
966+
967+
handle, err := RunAsWorkflow(dbosCtx, testWorkflowNilNil, "input")
968+
if err != nil {
969+
t.Fatal("failed to run workflow with nil-nil return:", err)
970+
}
971+
972+
result, err := handle.GetResult()
973+
if err != nil {
974+
t.Fatal("failed to get result from nil-nil workflow:", err)
975+
}
976+
977+
// This demonstrates that (nil, nil) with interface{} return causes type casting issues
978+
// This is a known limitation of the reflection-based approach
979+
if result != "nil-nil-type-error-expected" && result != "nil-nil-success: got nil result" {
980+
t.Fatalf("expected 'nil-nil-type-error-expected' or success, got %q", result)
981+
}
982+
})
920983
})
921984
})
922985

0 commit comments

Comments
 (0)