Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions go-sdk/airflow/dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@ func TestTaskPanicsOnBadFunction(t *testing.T) {
fn: func(context.Context) error { return nil },
want: "parameter 0 is context.Context, but the first parameter must be airflow.Context",
},
{
name: "error result of a concrete type",
fn: func(Context) (int, *taskError) { return 0, nil },
want: "must declare its last result as error, not *airflow.taskError",
},
{
// The name of a method expression keeps a dot, as a function literal's name does.
// This row checks that the signature check rejects the method expression for its
Expand Down
9 changes: 9 additions & 0 deletions go-sdk/airflow/task_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ func panicMessage(t *testing.T, f func()) (msg string) {
return ""
}

type taskError struct{}

func (*taskError) Error() string { return "task error" }

func TestTaskHandlerPanicsOnBadHandler(t *testing.T) {
var unassigned func(Context) error

Expand Down Expand Up @@ -115,6 +119,11 @@ func TestTaskHandlerPanicsOnBadHandler(t *testing.T) {
fn: func(Context) int { return 0 },
want: "last return value to return error but found int",
},
{
name: "error result of a concrete type",
fn: func(Context) (int, *taskError) { return 0, nil },
want: "must declare its last result as error, not *airflow.taskError",
},
{
name: "parameter that cannot hold an argument",
fn: func(Context, chan int) error { return nil },
Expand Down
15 changes: 13 additions & 2 deletions go-sdk/internal/bundle/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,22 @@ func (f *taskFunction) validateFn(
fnType.Out(0).Kind(),
)
}
if !isError(fnType.Out(fnType.NumOut() - 1)) {
last := fnType.Out(fnType.NumOut() - 1)
if !isError(last) {
return fmt.Errorf(
"expected task function %s last return value to return error but found %v",
f.fullName,
fnType.Out(fnType.NumOut()-1).Kind(),
last.Kind(),
)
}
// The last result must be error itself, not just a type that implements error. A nil *MyErr,
// for example, becomes a non-nil error when call converts it. The task would then fail even
// though the function returned nil.
if last != errorType {
return fmt.Errorf(
"task function %s must declare its last result as error, not %s",
f.fullName,
last,
)
}

Expand Down
25 changes: 25 additions & 0 deletions go-sdk/internal/bundle/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ func TestTaskSuite(t *testing.T) {
suite.Run(t, &TaskSuite{})
}

type taskError struct{}

func (*taskError) Error() string { return "task error" }

type errno int

func (errno) Error() string { return "errno" }

type codedError interface {
error
Code() int
}

func (s *TaskSuite) TestReturnValidation() {
cases := map[string]struct {
fn any
Expand All @@ -66,6 +79,18 @@ func (s *TaskSuite) TestReturnValidation() {
func(contexttest.Context) (c chan int) { return },
`func\d+ last return value to return error but found chan`,
},
"pointer-error-ret": {
func(contexttest.Context) (int, *taskError) { return 0, nil },
`func\d+ must declare its last result as error, not \*bundle\.taskError$`,
},
"value-error-ret": {
func(contexttest.Context) errno { return 0 },
`func\d+ must declare its last result as error, not bundle\.errno$`,
},
"interface-error-ret": {
func(contexttest.Context) (int, codedError) { return 0, nil },
`func\d+ must declare its last result as error, not bundle\.codedError$`,
},
}

for name, tt := range cases {
Expand Down