mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(plugins): make generated mock stubs nil-safe for nilable returns (#5909)
The ndpgen mock accessors asserted return types unconditionally, so a plugin test using Return(nil, ...) — the natural way to model an empty result or a pagination terminal page — panicked on the untyped-nil type assertion instead of returning the zero value. Guard pointer, slice, map and any returns in both the host-service client stubs and the PDK stub, and regenerate.
This commit is contained in:
parent
a0bf78cdea
commit
5a5311a9c0
@ -27,32 +27,46 @@ func hostFuncMap(svc Service) template.FuncMap {
|
||||
// Uses private (lowercase) type names for request/response structs.
|
||||
func clientFuncMap(svc Service) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"lower": strings.ToLower,
|
||||
"title": strings.Title,
|
||||
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
||||
"requestType": func(m Method) string { return m.ClientRequestTypeName(svc.Name) },
|
||||
"responseType": func(m Method) string { return m.ClientResponseTypeName(svc.Name) },
|
||||
"formatDoc": formatDoc,
|
||||
"mockReturnValues": mockReturnValues,
|
||||
"lower": strings.ToLower,
|
||||
"title": strings.Title,
|
||||
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
||||
"requestType": func(m Method) string { return m.ClientRequestTypeName(svc.Name) },
|
||||
"responseType": func(m Method) string { return m.ClientResponseTypeName(svc.Name) },
|
||||
"formatDoc": formatDoc,
|
||||
"mockReturnBody": mockReturnBody,
|
||||
}
|
||||
}
|
||||
|
||||
// mockReturnValues generates the testify mock return value accessors for a method.
|
||||
// For example: args.String(0), args.Bool(1), args.Error(2)
|
||||
func mockReturnValues(m Method) string {
|
||||
// mockReturnBody generates the testify mock body lines that extract return values.
|
||||
// Nil-able returns are guarded so tests can use Return(nil, ...) without the
|
||||
// type-assertion panic testify produces on an untyped nil.
|
||||
func mockReturnBody(m Method) string {
|
||||
var b strings.Builder
|
||||
var parts []string
|
||||
idx := 0
|
||||
|
||||
for _, r := range m.Returns {
|
||||
parts = append(parts, mockAccessor(r.Type, idx))
|
||||
idx++
|
||||
for idx, r := range m.Returns {
|
||||
if isNilableType(r.Type) {
|
||||
name := fmt.Sprintf("r%d", idx)
|
||||
fmt.Fprintf(&b, "\tvar %s %s\n\tif v := args.Get(%d); v != nil {\n\t\t%s = v.(%s)\n\t}\n", name, r.Type, idx, name, r.Type)
|
||||
parts = append(parts, name)
|
||||
} else {
|
||||
parts = append(parts, mockAccessor(r.Type, idx))
|
||||
}
|
||||
}
|
||||
|
||||
if m.HasError {
|
||||
parts = append(parts, fmt.Sprintf("args.Error(%d)", idx))
|
||||
parts = append(parts, fmt.Sprintf("args.Error(%d)", len(m.Returns)))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
b.WriteString("\treturn " + strings.Join(parts, ", "))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// isNilableType reports whether a nil mock return value is a valid intent for the
|
||||
// type, rather than a malformed test expectation.
|
||||
func isNilableType(typ string) bool {
|
||||
return strings.HasPrefix(typ, "*") || strings.HasPrefix(typ, "[]") ||
|
||||
strings.HasPrefix(typ, "map[") || typ == "any" || typ == "interface{}"
|
||||
}
|
||||
|
||||
// mockAccessor returns the testify mock accessor call for a given type and index.
|
||||
@ -725,7 +739,7 @@ func pdkFuncMap() template.FuncMap {
|
||||
"returnList": pdkReturnList,
|
||||
"argList": pdkArgList,
|
||||
"argListWithReceiver": pdkArgListWithReceiver,
|
||||
"mockReturns": pdkMockReturns,
|
||||
"mockReturnBody": pdkMockReturnBody,
|
||||
"constValue": pdkConstValue,
|
||||
"stubTypeUnderlying": stubTypeUnderlying,
|
||||
"methodReceiver": pdkMethodReceiver,
|
||||
@ -834,12 +848,20 @@ func pdkMethodReceiver(receiver, typeName string) string {
|
||||
}
|
||||
|
||||
// pdkMockReturns generates the mock return accessors for a function.
|
||||
func pdkMockReturns(returns []PDKReturn) string {
|
||||
func pdkMockReturnBody(returns []PDKReturn) string {
|
||||
var b strings.Builder
|
||||
var parts []string
|
||||
for i, r := range returns {
|
||||
parts = append(parts, mockAccessorForType(r.Type, i))
|
||||
if isNilableType(r.Type) {
|
||||
name := fmt.Sprintf("r%d", i)
|
||||
fmt.Fprintf(&b, "\tvar %s %s\n\tif v := args.Get(%d); v != nil {\n\t\t%s = v.(%s)\n\t}\n", name, r.Type, i, name, r.Type)
|
||||
parts = append(parts, name)
|
||||
} else {
|
||||
parts = append(parts, mockAccessorForType(r.Type, i))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
b.WriteString("\treturn " + strings.Join(parts, ", "))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// mockAccessorForType returns the testify mock accessor for a type.
|
||||
|
||||
@ -748,8 +748,9 @@ var _ = Describe("Generator", func() {
|
||||
// Check bool return uses args.Bool(1)
|
||||
Expect(codeStr).To(ContainSubstring("args.Bool(1)"))
|
||||
|
||||
// Check []byte return uses args.Get(0).([]byte)
|
||||
Expect(codeStr).To(ContainSubstring("args.Get(0).([]byte)"))
|
||||
// []byte is nil-able, so it gets a guarded extraction
|
||||
Expect(codeStr).To(ContainSubstring("var r0 []byte"))
|
||||
Expect(codeStr).To(ContainSubstring("r0 = v.([]byte)"))
|
||||
|
||||
// Check error returns use args.Error(N)
|
||||
Expect(codeStr).To(ContainSubstring("args.Error("))
|
||||
@ -1699,6 +1700,97 @@ var _ = Describe("Rust Generation", func() {
|
||||
Expect(codeStr).NotTo(ContainSubstring("use base64"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("nil-safe mock accessors", func() {
|
||||
Describe("GenerateClientGoStub", func() {
|
||||
It("guards pointer, slice and map returns so Return(nil, ...) does not panic", func() {
|
||||
svc := Service{
|
||||
Name: "Paged",
|
||||
Interface: "PagedService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "GetPage",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("query", "string")},
|
||||
Returns: []Param{NewParam("items", "[]Item"), NewParam("next", "*PageOptions")},
|
||||
},
|
||||
{
|
||||
Name: "GetLabels",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("id", "string")},
|
||||
Returns: []Param{NewParam("labels", "map[string]string")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientGoStub(svc, "host")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = format.Source(code)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
Expect(codeStr).To(ContainSubstring("var r0 []Item"))
|
||||
Expect(codeStr).To(ContainSubstring("var r1 *PageOptions"))
|
||||
Expect(codeStr).To(ContainSubstring("if v := args.Get(1); v != nil {"))
|
||||
Expect(codeStr).To(ContainSubstring("r1 = v.(*PageOptions)"))
|
||||
Expect(codeStr).To(ContainSubstring("return r0, r1, args.Error(2)"))
|
||||
Expect(codeStr).To(ContainSubstring("var r0 map[string]string"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("args.Get(0).([]Item)"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("args.Get(1).(*PageOptions)"))
|
||||
})
|
||||
|
||||
It("keeps non-nilable returns as inline accessors", func() {
|
||||
svc := Service{
|
||||
Name: "Counter",
|
||||
Interface: "CounterService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "Count",
|
||||
HasError: true,
|
||||
Params: []Param{NewParam("id", "string")},
|
||||
Returns: []Param{NewParam("count", "int64"), NewParam("name", "string")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientGoStub(svc, "host")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
Expect(codeStr).To(ContainSubstring("return args.Get(0).(int64), args.String(1), args.Error(2)"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("var r0"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GeneratePDKGoStub", func() {
|
||||
It("guards pointer and slice returns", func() {
|
||||
symbols := &PDKSymbols{
|
||||
Functions: []PDKFunc{
|
||||
{
|
||||
Name: "NewHTTPRequest",
|
||||
Params: []PDKParam{{Name: "method", Type: "HTTPMethod"}, {Name: "url", Type: "string"}},
|
||||
Returns: []PDKReturn{{Type: "*HTTPRequest"}},
|
||||
},
|
||||
{
|
||||
Name: "Input",
|
||||
Returns: []PDKReturn{{Type: "[]byte"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GeneratePDKGoStub(symbols)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = format.Source(code)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
Expect(codeStr).To(ContainSubstring("var r0 *HTTPRequest"))
|
||||
Expect(codeStr).To(ContainSubstring("r0 = v.(*HTTPRequest)"))
|
||||
Expect(codeStr).To(ContainSubstring("var r0 []byte"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("return args.Get(0).(*HTTPRequest)"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func writeFile(path, content string) error {
|
||||
|
||||
@ -51,7 +51,7 @@ var {{.Service.Name}}Mock = &mock{{.Service.Name}}Service{}
|
||||
// {{.Name}} is the mock method for {{$.Service.Name}}{{.Name}}.
|
||||
func (m *mock{{$.Service.Name}}Service) {{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}} {{$p.Type}}{{end}}) {{.ReturnSignature}} {
|
||||
args := m.Called({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}}{{end}})
|
||||
return {{mockReturnValues .}}
|
||||
{{mockReturnBody .}}
|
||||
}
|
||||
|
||||
// {{$.Service.Name}}{{.Name}} delegates to the mock instance.
|
||||
|
||||
@ -34,7 +34,7 @@ func ResetMock() {
|
||||
func {{.Name}}({{paramList .Params}}){{returnList .Returns}} {
|
||||
{{- if .Returns}}
|
||||
args := PDKMock.Called({{argList .Params}})
|
||||
return {{mockReturns .Returns}}
|
||||
{{mockReturnBody .Returns}}
|
||||
{{- else}}
|
||||
PDKMock.Called({{argList .Params}})
|
||||
{{- end}}
|
||||
|
||||
27
plugins/pdk/go/host/mock_nil_returns_test.go
Normal file
27
plugins/pdk/go/host/mock_nil_returns_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
//go:build !wasip1
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Return(nil, ...) models "no result"; the generated accessors must yield the
|
||||
// zero value instead of panicking on the type assertion.
|
||||
func TestMockNilPointerReturn(t *testing.T) {
|
||||
HTTPMock.On("Send", HTTPRequest{URL: "http://nil.example"}).Return(nil, nil)
|
||||
|
||||
resp, err := HTTPSend(HTTPRequest{URL: "http://nil.example"})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp)
|
||||
}
|
||||
|
||||
func TestMockNilSliceReturn(t *testing.T) {
|
||||
LibraryMock.On("GetAllLibraries").Return(nil, nil)
|
||||
|
||||
libs, err := LibraryGetAllLibraries()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, libs)
|
||||
}
|
||||
@ -154,7 +154,11 @@ func CacheSetBytes(key string, value []byte, ttlSeconds int64) error {
|
||||
// GetBytes is the mock method for CacheGetBytes.
|
||||
func (m *mockCacheService) GetBytes(key string) ([]byte, bool, error) {
|
||||
args := m.Called(key)
|
||||
return args.Get(0).([]byte), args.Bool(1), args.Error(2)
|
||||
var r0 []byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]byte)
|
||||
}
|
||||
return r0, args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
// CacheGetBytes delegates to the mock instance.
|
||||
|
||||
@ -59,7 +59,11 @@ func ConfigGetInt(key string) (int64, bool) {
|
||||
// Keys is the mock method for ConfigKeys.
|
||||
func (m *mockConfigService) Keys(prefix string) []string {
|
||||
args := m.Called(prefix)
|
||||
return args.Get(0).([]string)
|
||||
var r0 []string
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]string)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigKeys delegates to the mock instance.
|
||||
|
||||
@ -43,7 +43,11 @@ var HTTPMock = &mockHTTPService{}
|
||||
// Send is the mock method for HTTPSend.
|
||||
func (m *mockHTTPService) Send(request HTTPRequest) (*HTTPResponse, error) {
|
||||
args := m.Called(request)
|
||||
return args.Get(0).(*HTTPResponse), args.Error(1)
|
||||
var r0 *HTTPResponse
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.(*HTTPResponse)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// HTTPSend delegates to the mock instance.
|
||||
|
||||
@ -64,7 +64,11 @@ func KVStoreSetWithTTL(key string, value []byte, ttlSeconds int64) error {
|
||||
// Get is the mock method for KVStoreGet.
|
||||
func (m *mockKVStoreService) Get(key string) ([]byte, bool, error) {
|
||||
args := m.Called(key)
|
||||
return args.Get(0).([]byte), args.Bool(1), args.Error(2)
|
||||
var r0 []byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]byte)
|
||||
}
|
||||
return r0, args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
// KVStoreGet delegates to the mock instance.
|
||||
@ -81,7 +85,11 @@ func KVStoreGet(key string) ([]byte, bool, error) {
|
||||
// GetMany is the mock method for KVStoreGetMany.
|
||||
func (m *mockKVStoreService) GetMany(keys []string) (map[string][]byte, error) {
|
||||
args := m.Called(keys)
|
||||
return args.Get(0).(map[string][]byte), args.Error(1)
|
||||
var r0 map[string][]byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.(map[string][]byte)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// KVStoreGetMany delegates to the mock instance.
|
||||
@ -116,7 +124,11 @@ func KVStoreHas(key string) (bool, error) {
|
||||
// List is the mock method for KVStoreList.
|
||||
func (m *mockKVStoreService) List(prefix string) ([]string, error) {
|
||||
args := m.Called(prefix)
|
||||
return args.Get(0).([]string), args.Error(1)
|
||||
var r0 []string
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]string)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// KVStoreList delegates to the mock instance.
|
||||
|
||||
@ -39,7 +39,11 @@ var LibraryMock = &mockLibraryService{}
|
||||
// GetLibrary is the mock method for LibraryGetLibrary.
|
||||
func (m *mockLibraryService) GetLibrary(id int32) (*Library, error) {
|
||||
args := m.Called(id)
|
||||
return args.Get(0).(*Library), args.Error(1)
|
||||
var r0 *Library
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.(*Library)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// LibraryGetLibrary delegates to the mock instance.
|
||||
@ -56,7 +60,11 @@ func LibraryGetLibrary(id int32) (*Library, error) {
|
||||
// GetAllLibraries is the mock method for LibraryGetAllLibraries.
|
||||
func (m *mockLibraryService) GetAllLibraries() ([]Library, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]Library), args.Error(1)
|
||||
var r0 []Library
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]Library)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// LibraryGetAllLibraries delegates to the mock instance.
|
||||
|
||||
@ -31,7 +31,11 @@ var MatcherMock = &mockMatcherService{}
|
||||
// MatchSongs is the mock method for MatcherMatchSongs.
|
||||
func (m *mockMatcherService) MatchSongs(songs []types.SongRef, opts MatchOptions) ([]*types.Track, error) {
|
||||
args := m.Called(songs, opts)
|
||||
return args.Get(0).([]*types.Track), args.Error(1)
|
||||
var r0 []*types.Track
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]*types.Track)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// MatcherMatchSongs delegates to the mock instance.
|
||||
|
||||
@ -39,7 +39,11 @@ func SubsonicAPICall(uri string) (string, error) {
|
||||
// CallRaw is the mock method for SubsonicAPICallRaw.
|
||||
func (m *mockSubsonicAPIService) CallRaw(uri string) (string, []byte, error) {
|
||||
args := m.Called(uri)
|
||||
return args.String(0), args.Get(1).([]byte), args.Error(2)
|
||||
var r1 []byte
|
||||
if v := args.Get(1); v != nil {
|
||||
r1 = v.([]byte)
|
||||
}
|
||||
return args.String(0), r1, args.Error(2)
|
||||
}
|
||||
|
||||
// SubsonicAPICallRaw delegates to the mock instance.
|
||||
|
||||
@ -70,7 +70,11 @@ func TaskEnqueue(queueName string, payload []byte) (string, error) {
|
||||
// Get is the mock method for TaskGet.
|
||||
func (m *mockTaskService) Get(taskID string) (*TaskInfo, error) {
|
||||
args := m.Called(taskID)
|
||||
return args.Get(0).(*TaskInfo), args.Error(1)
|
||||
var r0 *TaskInfo
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.(*TaskInfo)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// TaskGet delegates to the mock instance.
|
||||
|
||||
@ -33,7 +33,11 @@ var UsersMock = &mockUsersService{}
|
||||
// GetUsers is the mock method for UsersGetUsers.
|
||||
func (m *mockUsersService) GetUsers() ([]User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]User), args.Error(1)
|
||||
var r0 []User
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]User)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// UsersGetUsers delegates to the mock instance.
|
||||
@ -49,7 +53,11 @@ func UsersGetUsers() ([]User, error) {
|
||||
// GetAdmins is the mock method for UsersGetAdmins.
|
||||
func (m *mockUsersService) GetAdmins() ([]User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]User), args.Error(1)
|
||||
var r0 []User
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]User)
|
||||
}
|
||||
return r0, args.Error(1)
|
||||
}
|
||||
|
||||
// UsersGetAdmins delegates to the mock instance.
|
||||
|
||||
@ -62,7 +62,11 @@ func GetConfig(key string) (string, bool) {
|
||||
// GetVar GetVar returns the byte slice (if any) associated with `key`.
|
||||
func GetVar(key string) []byte {
|
||||
args := PDKMock.Called(key)
|
||||
return args.Get(0).([]byte)
|
||||
var r0 []byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]byte)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetVarInt GetVarInt returns the int associated with `key` (or 0 if none).
|
||||
@ -74,7 +78,11 @@ func GetVarInt(key string) int {
|
||||
// Input Input returns a slice of bytes from the host.
|
||||
func Input() []byte {
|
||||
args := PDKMock.Called()
|
||||
return args.Get(0).([]byte)
|
||||
var r0 []byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]byte)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// InputJSON InputJSON returns unmartialed JSON data from the host "input".
|
||||
@ -108,7 +116,11 @@ func LogMemory(level LogLevel, m Memory) {
|
||||
// NewHTTPRequest NewHTTPRequest returns a new `HTTPRequest`.
|
||||
func NewHTTPRequest(method HTTPMethod, url string) *HTTPRequest {
|
||||
args := PDKMock.Called(method, url)
|
||||
return args.Get(0).(*HTTPRequest)
|
||||
var r0 *HTTPRequest
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.(*HTTPRequest)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
func NewMemory(offset uint64, length uint64) Memory {
|
||||
args := PDKMock.Called(offset, length)
|
||||
@ -139,7 +151,11 @@ func OutputString(s string) {
|
||||
// ParamBytes ParamBytes returns bytes from Extism host memory given an offset.
|
||||
func ParamBytes(offset uint64) []byte {
|
||||
args := PDKMock.Called(offset)
|
||||
return args.Get(0).([]byte)
|
||||
var r0 []byte
|
||||
if v := args.Get(0); v != nil {
|
||||
r0 = v.([]byte)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// ParamString ParamString returns UTF-8 string data from Extism host memory given an offset.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user