Skip to content
This repository has been archived by the owner on Jun 27, 2023. It is now read-only.

gomock generics support enhancement #663

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

bradleygore
Copy link

@bradleygore bradleygore commented Jul 11, 2022

Related Issue(s)

The related issues can be found in the 1.7.0 Milestone doc: https://github.com/golang/mock/milestone/5, specifically issues #643 and #649

Goals

  • handle embedded interfaces from src pkg
  • handle embedded interfaces from external pkg
  • handle generic input parameters on generated mock
  • handle generic output parameters on generate mock

Hiccups/Hangups

Navigation and Data Flow

There were some parts that I found challenging to navigate - especially this being my first dive into this codebase. Much respect to the work that's been done - not trying to criticize - but some helpful comments would make it a bit easier for contributors. I tried to add useful comments to what I've added in this PR without being overly verbose - tough to balance b/c some of this stuff is complex.

Build/Test Processes

I didn't see instructions for linting or adding of tests, etc... so I just tried to do what the github workflow does, which is run ./ci/test.sh.

I am unable to get that to run nor am I able to do go generate ./... to re-generate all of the mocks in the gomock/internal/tests/... dirs. I get the following errors (even on main branch):

 ~/git/bdg-gomock   main  ./ci/test.sh 
~/git/bdg-gomock ~/git/bdg-gomock
~/git/bdg-gomock
~/git/bdg-gomock/mockgen/internal/tests/generics ~/git/bdg-gomock
~/git/bdg-gomock
/var/folders/3v/d4pfdq752wj3y14x_136dvww0000gn/T/tmp.ZAh3Kj6D /var/folders/3v/d4pfdq752wj3y14x_136dvww0000gn/T/tmp.ZAh3Kj6D
2022/07/11 15:00:11 Loading input failed: input.go:13:5: failed parsing returns: input.go:13:9: bad array size: strconv.Atoi: parsing "": invalid syntax
mockgen/internal/tests/const_array_length/input.go:5: running "mockgen": exit status 1
2022/07/11 15:00:14 Loading input failed: bugreport.go:31:2: unknown embedded interface Foo
mockgen/internal/tests/import_embedded_interface/bugreport.go:17: running "mockgen": exit status 1
package command-line-arguments
        prog.go:14:2: use of internal package github.com/golang/mock/mockgen/internal/tests/internal_pkg/subdir/internal/pkg not allowed

 ✘  ~/git/bdg-gomock   main  go generate ./...
2022/07/11 15:00:41 Loading input failed: input.go:13:5: failed parsing returns: input.go:13:9: bad array size: strconv.Atoi: parsing "": invalid syntax
mockgen/internal/tests/const_array_length/input.go:5: running "mockgen": exit status 1
2022/07/11 15:00:45 Loading input failed: bugreport.go:31:2: unknown embedded interface Foo
mockgen/internal/tests/import_embedded_interface/bugreport.go:17: running "mockgen": exit status 1
package command-line-arguments
        prog.go:14:2: use of internal package github.com/golang/mock/mockgen/internal/tests/internal_pkg/subdir/internal/pkg not allowed

Are we supposed to run go generate ./... from the root (or anywhere else)?

Proving Grounds

Though I had some hangups trying to prove this out, I was able to prove out reasonably enough that this works via the following mechanisms:

Ran against my test repo

I was able to successfully run this against the CustomWorker interface in my repo for testing gomock w/ generics: https://github.com/bradleygore/gomock-generics-issue/blob/master/workers/custom.go

I did this using a debug entry in VSCode (so I could debug it and troubleshoot as needed) of:

{
            "name": "Mockgen CustomWorker",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}/mockgen",
            "args": [
                "-source", "${userHome}/git/gomock-generics-issue/workers/custom.go",
                "-destination", "${userHome}/git/gomock-generics-issue/workers/mocks/CustomWorker.go",
                "-imports", "iface=../iface",
                "CustomWorker"
            ]
        }

and it successfully generate this file (which looks right to me):

Mock CustomWorker
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/bgore/git/gomock-generics-issue/workers/custom.go

// Package mock_workers is a generated GoMock package.
package mock_workers

import (
	workers "gomock-generics-issue/workers"
	reflect "reflect"

	iface "../iface"
	gomock "github.com/golang/mock/gomock"
)

// MockCustomWorker is a mock of CustomWorker interface.
type MockCustomWorker struct {
	ctrl     *gomock.Controller
	recorder *MockCustomWorkerMockRecorder
}

// MockCustomWorkerMockRecorder is the mock recorder for MockCustomWorker.
type MockCustomWorkerMockRecorder struct {
	mock *MockCustomWorker
}

// NewMockCustomWorker creates a new mock instance.
func NewMockCustomWorker(ctrl *gomock.Controller) *MockCustomWorker {
	mock := &MockCustomWorker{ctrl: ctrl}
	mock.recorder = &MockCustomWorkerMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCustomWorker) EXPECT() *MockCustomWorkerMockRecorder {
	return m.recorder
}

// DoWork mocks base method.
func (m *MockCustomWorker) DoWork(arg0 ...interface{}) iface.WorkResult[workers.CustomWorkDetail] {
	m.ctrl.T.Helper()
	varargs := []interface{}{}
	for _, a := range arg0 {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "DoWork", varargs...)
	ret0, _ := ret[0].(iface.WorkResult[workers.CustomWorkDetail])
	return ret0
}

// DoWork indicates an expected call of DoWork.
func (mr *MockCustomWorkerMockRecorder) DoWork(arg0 ...interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoWork", reflect.TypeOf((*MockCustomWorker)(nil).DoWork), arg0...)
}

// HaveGoodTimes mocks base method.
func (m *MockCustomWorker) HaveGoodTimes() {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "HaveGoodTimes")
}

// HaveGoodTimes indicates an expected call of HaveGoodTimes.
func (mr *MockCustomWorkerMockRecorder) HaveGoodTimes() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HaveGoodTimes", reflect.TypeOf((*MockCustomWorker)(nil).HaveGoodTimes))
}

gomock generics test

I added a new interface to mockgen/internal/tests/generics/generics.go of:

type EmbeddingIface interface {
	Bar[other.Three, error]
	other.Otherer[StructType, other.Five]
	LocalFunc() error
}

and used the following VSCode debug entry to execute these updates against it:

{
    "name": "Mockgen generics",
    "type": "go",
    "request": "launch",
    "mode": "auto",
    "program": "${workspaceFolder}/mockgen",
    "args": [
        "-source", "${workspaceFolder}/mockgen/internal/tests/generics/generics.go",
        "-destination", "${workspaceFolder}/mockgen/_local/EmbeddedIfaceGenerics.go",
        "-package", "source",
        "-imports", "other=./other",
        "EmbeddingIface"
    ]
}

and this is the resulting output:

mock_generics_test.go
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/bgore/git/bdg-gomock/mockgen/internal/tests/generics/generics.go

// Package source is a generated GoMock package.
package source

import (
	reflect "reflect"

	"github.com/golang/mock/mockgen/internal/tests/generics/other"
	gomock "github.com/golang/mock/gomock"
	generics "github.com/golang/mock/mockgen/internal/tests/generics"
)

// MockBar is a mock of Bar interface.
type MockBar[T any, R any] struct {
	ctrl     *gomock.Controller
	recorder *MockBarMockRecorder[T, R]
}

// MockBarMockRecorder is the mock recorder for MockBar.
type MockBarMockRecorder[T any, R any] struct {
	mock *MockBar[T, R]
}

// NewMockBar creates a new mock instance.
func NewMockBar[T any, R any](ctrl *gomock.Controller) *MockBar[T, R] {
	mock := &MockBar[T, R]{ctrl: ctrl}
	mock.recorder = &MockBarMockRecorder[T, R]{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBar[T, R]) EXPECT() *MockBarMockRecorder[T, R] {
	return m.recorder
}

// Eight mocks base method.
func (m *MockBar[T, R]) Eight(arg0 T) other.Two[T, R] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eight", arg0)
	ret0, _ := ret[0].(other.Two[T, R])
	return ret0
}

// Eight indicates an expected call of Eight.
func (mr *MockBarMockRecorder[T, R]) Eight(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eight", reflect.TypeOf((*MockBar[T, R])(nil).Eight), arg0)
}

// Eighteen mocks base method.
func (m *MockBar[T, R]) Eighteen() (generics.Iface[*other.Five], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eighteen")
	ret0, _ := ret[0].(generics.Iface[*other.Five])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Eighteen indicates an expected call of Eighteen.
func (mr *MockBarMockRecorder[T, R]) Eighteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eighteen", reflect.TypeOf((*MockBar[T, R])(nil).Eighteen))
}

// Eleven mocks base method.
func (m *MockBar[T, R]) Eleven() (*other.One[T], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eleven")
	ret0, _ := ret[0].(*other.One[T])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Eleven indicates an expected call of Eleven.
func (mr *MockBarMockRecorder[T, R]) Eleven() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eleven", reflect.TypeOf((*MockBar[T, R])(nil).Eleven))
}

// Fifteen mocks base method.
func (m *MockBar[T, R]) Fifteen() (generics.Iface[generics.StructType], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fifteen")
	ret0, _ := ret[0].(generics.Iface[generics.StructType])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Fifteen indicates an expected call of Fifteen.
func (mr *MockBarMockRecorder[T, R]) Fifteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fifteen", reflect.TypeOf((*MockBar[T, R])(nil).Fifteen))
}

// Five mocks base method.
func (m *MockBar[T, R]) Five(arg0 T) generics.Baz[T] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Five", arg0)
	ret0, _ := ret[0].(generics.Baz[T])
	return ret0
}

// Five indicates an expected call of Five.
func (mr *MockBarMockRecorder[T, R]) Five(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Five", reflect.TypeOf((*MockBar[T, R])(nil).Five), arg0)
}

// Four mocks base method.
func (m *MockBar[T, R]) Four(arg0 T) generics.Foo[T, R] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Four", arg0)
	ret0, _ := ret[0].(generics.Foo[T, R])
	return ret0
}

// Four indicates an expected call of Four.
func (mr *MockBarMockRecorder[T, R]) Four(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Four", reflect.TypeOf((*MockBar[T, R])(nil).Four), arg0)
}

// Fourteen mocks base method.
func (m *MockBar[T, R]) Fourteen() (*generics.Foo[generics.StructType, generics.StructType2], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fourteen")
	ret0, _ := ret[0].(*generics.Foo[generics.StructType, generics.StructType2])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Fourteen indicates an expected call of Fourteen.
func (mr *MockBarMockRecorder[T, R]) Fourteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fourteen", reflect.TypeOf((*MockBar[T, R])(nil).Fourteen))
}

// Nine mocks base method.
func (m *MockBar[T, R]) Nine(arg0 generics.Iface[T]) {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "Nine", arg0)
}

// Nine indicates an expected call of Nine.
func (mr *MockBarMockRecorder[T, R]) Nine(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nine", reflect.TypeOf((*MockBar[T, R])(nil).Nine), arg0)
}

// Nineteen mocks base method.
func (m *MockBar[T, R]) Nineteen() generics.AliasType {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Nineteen")
	ret0, _ := ret[0].(generics.AliasType)
	return ret0
}

// Nineteen indicates an expected call of Nineteen.
func (mr *MockBarMockRecorder[T, R]) Nineteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nineteen", reflect.TypeOf((*MockBar[T, R])(nil).Nineteen))
}

// One mocks base method.
func (m *MockBar[T, R]) One(arg0 string) string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "One", arg0)
	ret0, _ := ret[0].(string)
	return ret0
}

// One indicates an expected call of One.
func (mr *MockBarMockRecorder[T, R]) One(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "One", reflect.TypeOf((*MockBar[T, R])(nil).One), arg0)
}

// Seven mocks base method.
func (m *MockBar[T, R]) Seven(arg0 T) other.One[T] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Seven", arg0)
	ret0, _ := ret[0].(other.One[T])
	return ret0
}

// Seven indicates an expected call of Seven.
func (mr *MockBarMockRecorder[T, R]) Seven(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seven", reflect.TypeOf((*MockBar[T, R])(nil).Seven), arg0)
}

// Seventeen mocks base method.
func (m *MockBar[T, R]) Seventeen() (*generics.Foo[other.Three, other.Four], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Seventeen")
	ret0, _ := ret[0].(*generics.Foo[other.Three, other.Four])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Seventeen indicates an expected call of Seventeen.
func (mr *MockBarMockRecorder[T, R]) Seventeen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seventeen", reflect.TypeOf((*MockBar[T, R])(nil).Seventeen))
}

// Six mocks base method.
func (m *MockBar[T, R]) Six(arg0 T) *generics.Baz[T] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Six", arg0)
	ret0, _ := ret[0].(*generics.Baz[T])
	return ret0
}

// Six indicates an expected call of Six.
func (mr *MockBarMockRecorder[T, R]) Six(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Six", reflect.TypeOf((*MockBar[T, R])(nil).Six), arg0)
}

// Sixteen mocks base method.
func (m *MockBar[T, R]) Sixteen() (generics.Baz[other.Three], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Sixteen")
	ret0, _ := ret[0].(generics.Baz[other.Three])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Sixteen indicates an expected call of Sixteen.
func (mr *MockBarMockRecorder[T, R]) Sixteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sixteen", reflect.TypeOf((*MockBar[T, R])(nil).Sixteen))
}

// Ten mocks base method.
func (m *MockBar[T, R]) Ten(arg0 *T) {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "Ten", arg0)
}

// Ten indicates an expected call of Ten.
func (mr *MockBarMockRecorder[T, R]) Ten(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ten", reflect.TypeOf((*MockBar[T, R])(nil).Ten), arg0)
}

// Thirteen mocks base method.
func (m *MockBar[T, R]) Thirteen() (generics.Baz[generics.StructType], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Thirteen")
	ret0, _ := ret[0].(generics.Baz[generics.StructType])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Thirteen indicates an expected call of Thirteen.
func (mr *MockBarMockRecorder[T, R]) Thirteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Thirteen", reflect.TypeOf((*MockBar[T, R])(nil).Thirteen))
}

// Three mocks base method.
func (m *MockBar[T, R]) Three(arg0 T) R {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Three", arg0)
	ret0, _ := ret[0].(R)
	return ret0
}

// Three indicates an expected call of Three.
func (mr *MockBarMockRecorder[T, R]) Three(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Three", reflect.TypeOf((*MockBar[T, R])(nil).Three), arg0)
}

// Twelve mocks base method.
func (m *MockBar[T, R]) Twelve() (*other.Two[T, R], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Twelve")
	ret0, _ := ret[0].(*other.Two[T, R])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Twelve indicates an expected call of Twelve.
func (mr *MockBarMockRecorder[T, R]) Twelve() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Twelve", reflect.TypeOf((*MockBar[T, R])(nil).Twelve))
}

// Twenty mocks base method.
func (m *MockBar[T, R]) Twenty(arg0 *other.One[T]) *other.Two[T, R] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Twenty", arg0)
	ret0, _ := ret[0].(*other.Two[T, R])
	return ret0
}

// Twenty indicates an expected call of Twenty.
func (mr *MockBarMockRecorder[T, R]) Twenty(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Twenty", reflect.TypeOf((*MockBar[T, R])(nil).Twenty), arg0)
}

// TwentyOne mocks base method.
func (m *MockBar[T, R]) TwentyOne(arg0 *string) *other.Two[*T, *R] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "TwentyOne", arg0)
	ret0, _ := ret[0].(*other.Two[*T, *R])
	return ret0
}

// TwentyOne indicates an expected call of TwentyOne.
func (mr *MockBarMockRecorder[T, R]) TwentyOne(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TwentyOne", reflect.TypeOf((*MockBar[T, R])(nil).TwentyOne), arg0)
}

// Two mocks base method.
func (m *MockBar[T, R]) Two(arg0 T) string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Two", arg0)
	ret0, _ := ret[0].(string)
	return ret0
}

// Two indicates an expected call of Two.
func (mr *MockBarMockRecorder[T, R]) Two(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Two", reflect.TypeOf((*MockBar[T, R])(nil).Two), arg0)
}

// MockIface is a mock of Iface interface.
type MockIface[T any] struct {
	ctrl     *gomock.Controller
	recorder *MockIfaceMockRecorder[T]
}

// MockIfaceMockRecorder is the mock recorder for MockIface.
type MockIfaceMockRecorder[T any] struct {
	mock *MockIface[T]
}

// NewMockIface creates a new mock instance.
func NewMockIface[T any](ctrl *gomock.Controller) *MockIface[T] {
	mock := &MockIface[T]{ctrl: ctrl}
	mock.recorder = &MockIfaceMockRecorder[T]{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIface[T]) EXPECT() *MockIfaceMockRecorder[T] {
	return m.recorder
}

// MockEmbeddingIface is a mock of EmbeddingIface interface.
type MockEmbeddingIface struct {
	ctrl     *gomock.Controller
	recorder *MockEmbeddingIfaceMockRecorder
}

// MockEmbeddingIfaceMockRecorder is the mock recorder for MockEmbeddingIface.
type MockEmbeddingIfaceMockRecorder struct {
	mock *MockEmbeddingIface
}

// NewMockEmbeddingIface creates a new mock instance.
func NewMockEmbeddingIface(ctrl *gomock.Controller) *MockEmbeddingIface {
	mock := &MockEmbeddingIface{ctrl: ctrl}
	mock.recorder = &MockEmbeddingIfaceMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockEmbeddingIface) EXPECT() *MockEmbeddingIfaceMockRecorder {
	return m.recorder
}

// DoR mocks base method.
func (m *MockEmbeddingIface) DoR(arg0 other.Five) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "DoR", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// DoR indicates an expected call of DoR.
func (mr *MockEmbeddingIfaceMockRecorder) DoR(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoR", reflect.TypeOf((*MockEmbeddingIface)(nil).DoR), arg0)
}

// DoT mocks base method.
func (m *MockEmbeddingIface) DoT(arg0 generics.StructType) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "DoT", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// DoT indicates an expected call of DoT.
func (mr *MockEmbeddingIfaceMockRecorder) DoT(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoT", reflect.TypeOf((*MockEmbeddingIface)(nil).DoT), arg0)
}

// Eight mocks base method.
func (m *MockEmbeddingIface) Eight(arg0 other.Three) other.Two[other.Three, error] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eight", arg0)
	ret0, _ := ret[0].(other.Two[other.Three, error])
	return ret0
}

// Eight indicates an expected call of Eight.
func (mr *MockEmbeddingIfaceMockRecorder) Eight(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eight", reflect.TypeOf((*MockEmbeddingIface)(nil).Eight), arg0)
}

// Eighteen mocks base method.
func (m *MockEmbeddingIface) Eighteen() (generics.Iface[*other.Five], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eighteen")
	ret0, _ := ret[0].(generics.Iface[*other.Five])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Eighteen indicates an expected call of Eighteen.
func (mr *MockEmbeddingIfaceMockRecorder) Eighteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eighteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Eighteen))
}

// Eleven mocks base method.
func (m *MockEmbeddingIface) Eleven() (*other.One[other.Three], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Eleven")
	ret0, _ := ret[0].(*other.One[other.Three])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Eleven indicates an expected call of Eleven.
func (mr *MockEmbeddingIfaceMockRecorder) Eleven() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eleven", reflect.TypeOf((*MockEmbeddingIface)(nil).Eleven))
}

// Fifteen mocks base method.
func (m *MockEmbeddingIface) Fifteen() (generics.Iface[generics.StructType], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fifteen")
	ret0, _ := ret[0].(generics.Iface[generics.StructType])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Fifteen indicates an expected call of Fifteen.
func (mr *MockEmbeddingIfaceMockRecorder) Fifteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fifteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Fifteen))
}

// Five mocks base method.
func (m *MockEmbeddingIface) Five(arg0 other.Three) generics.Baz[other.Three] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Five", arg0)
	ret0, _ := ret[0].(generics.Baz[other.Three])
	return ret0
}

// Five indicates an expected call of Five.
func (mr *MockEmbeddingIfaceMockRecorder) Five(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Five", reflect.TypeOf((*MockEmbeddingIface)(nil).Five), arg0)
}

// Four mocks base method.
func (m *MockEmbeddingIface) Four(arg0 other.Three) generics.Foo[other.Three, error] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Four", arg0)
	ret0, _ := ret[0].(generics.Foo[other.Three, error])
	return ret0
}

// Four indicates an expected call of Four.
func (mr *MockEmbeddingIfaceMockRecorder) Four(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Four", reflect.TypeOf((*MockEmbeddingIface)(nil).Four), arg0)
}

// Fourteen mocks base method.
func (m *MockEmbeddingIface) Fourteen() (*generics.Foo[generics.StructType, generics.StructType2], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Fourteen")
	ret0, _ := ret[0].(*generics.Foo[generics.StructType, generics.StructType2])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Fourteen indicates an expected call of Fourteen.
func (mr *MockEmbeddingIfaceMockRecorder) Fourteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fourteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Fourteen))
}

// LocalFunc mocks base method.
func (m *MockEmbeddingIface) LocalFunc() error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "LocalFunc")
	ret0, _ := ret[0].(error)
	return ret0
}

// LocalFunc indicates an expected call of LocalFunc.
func (mr *MockEmbeddingIfaceMockRecorder) LocalFunc() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalFunc", reflect.TypeOf((*MockEmbeddingIface)(nil).LocalFunc))
}

// MakeThem mocks base method.
func (m *MockEmbeddingIface) MakeThem() (generics.StructType, other.Five, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "MakeThem")
	ret0, _ := ret[0].(generics.StructType)
	ret1, _ := ret[1].(other.Five)
	ret2, _ := ret[2].(error)
	return ret0, ret1, ret2
}

// MakeThem indicates an expected call of MakeThem.
func (mr *MockEmbeddingIfaceMockRecorder) MakeThem() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeThem", reflect.TypeOf((*MockEmbeddingIface)(nil).MakeThem))
}

// Nine mocks base method.
func (m *MockEmbeddingIface) Nine(arg0 generics.Iface[other.Three]) {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "Nine", arg0)
}

// Nine indicates an expected call of Nine.
func (mr *MockEmbeddingIfaceMockRecorder) Nine(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nine", reflect.TypeOf((*MockEmbeddingIface)(nil).Nine), arg0)
}

// Nineteen mocks base method.
func (m *MockEmbeddingIface) Nineteen() generics.AliasType {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Nineteen")
	ret0, _ := ret[0].(generics.AliasType)
	return ret0
}

// Nineteen indicates an expected call of Nineteen.
func (mr *MockEmbeddingIfaceMockRecorder) Nineteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nineteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Nineteen))
}

// One mocks base method.
func (m *MockEmbeddingIface) One(arg0 string) string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "One", arg0)
	ret0, _ := ret[0].(string)
	return ret0
}

// One indicates an expected call of One.
func (mr *MockEmbeddingIfaceMockRecorder) One(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "One", reflect.TypeOf((*MockEmbeddingIface)(nil).One), arg0)
}

// Seven mocks base method.
func (m *MockEmbeddingIface) Seven(arg0 other.Three) other.One[other.Three] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Seven", arg0)
	ret0, _ := ret[0].(other.One[other.Three])
	return ret0
}

// Seven indicates an expected call of Seven.
func (mr *MockEmbeddingIfaceMockRecorder) Seven(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seven", reflect.TypeOf((*MockEmbeddingIface)(nil).Seven), arg0)
}

// Seventeen mocks base method.
func (m *MockEmbeddingIface) Seventeen() (*generics.Foo[other.Three, other.Four], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Seventeen")
	ret0, _ := ret[0].(*generics.Foo[other.Three, other.Four])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Seventeen indicates an expected call of Seventeen.
func (mr *MockEmbeddingIfaceMockRecorder) Seventeen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seventeen", reflect.TypeOf((*MockEmbeddingIface)(nil).Seventeen))
}

// Six mocks base method.
func (m *MockEmbeddingIface) Six(arg0 other.Three) *generics.Baz[other.Three] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Six", arg0)
	ret0, _ := ret[0].(*generics.Baz[other.Three])
	return ret0
}

// Six indicates an expected call of Six.
func (mr *MockEmbeddingIfaceMockRecorder) Six(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Six", reflect.TypeOf((*MockEmbeddingIface)(nil).Six), arg0)
}

// Sixteen mocks base method.
func (m *MockEmbeddingIface) Sixteen() (generics.Baz[other.Three], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Sixteen")
	ret0, _ := ret[0].(generics.Baz[other.Three])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Sixteen indicates an expected call of Sixteen.
func (mr *MockEmbeddingIfaceMockRecorder) Sixteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sixteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Sixteen))
}

// Ten mocks base method.
func (m *MockEmbeddingIface) Ten(arg0 *other.Three) {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "Ten", arg0)
}

// Ten indicates an expected call of Ten.
func (mr *MockEmbeddingIfaceMockRecorder) Ten(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ten", reflect.TypeOf((*MockEmbeddingIface)(nil).Ten), arg0)
}

// Thirteen mocks base method.
func (m *MockEmbeddingIface) Thirteen() (generics.Baz[generics.StructType], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Thirteen")
	ret0, _ := ret[0].(generics.Baz[generics.StructType])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Thirteen indicates an expected call of Thirteen.
func (mr *MockEmbeddingIfaceMockRecorder) Thirteen() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Thirteen", reflect.TypeOf((*MockEmbeddingIface)(nil).Thirteen))
}

// Three mocks base method.
func (m *MockEmbeddingIface) Three(arg0 other.Three) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Three", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// Three indicates an expected call of Three.
func (mr *MockEmbeddingIfaceMockRecorder) Three(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Three", reflect.TypeOf((*MockEmbeddingIface)(nil).Three), arg0)
}

// Twelve mocks base method.
func (m *MockEmbeddingIface) Twelve() (*other.Two[other.Three, error], error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Twelve")
	ret0, _ := ret[0].(*other.Two[other.Three, error])
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Twelve indicates an expected call of Twelve.
func (mr *MockEmbeddingIfaceMockRecorder) Twelve() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Twelve", reflect.TypeOf((*MockEmbeddingIface)(nil).Twelve))
}

// Twenty mocks base method.
func (m *MockEmbeddingIface) Twenty(arg0 *other.One[other.Three]) *other.Two[other.Three, error] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Twenty", arg0)
	ret0, _ := ret[0].(*other.Two[other.Three, error])
	return ret0
}

// Twenty indicates an expected call of Twenty.
func (mr *MockEmbeddingIfaceMockRecorder) Twenty(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Twenty", reflect.TypeOf((*MockEmbeddingIface)(nil).Twenty), arg0)
}

// TwentyOne mocks base method.
func (m *MockEmbeddingIface) TwentyOne(arg0 *string) *other.Two[*other.Three, *error] {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "TwentyOne", arg0)
	ret0, _ := ret[0].(*other.Two[*other.Three, *error])
	return ret0
}

// TwentyOne indicates an expected call of TwentyOne.
func (mr *MockEmbeddingIfaceMockRecorder) TwentyOne(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TwentyOne", reflect.TypeOf((*MockEmbeddingIface)(nil).TwentyOne), arg0)
}

// Two mocks base method.
func (m *MockEmbeddingIface) Two(arg0 other.Three) string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Two", arg0)
	ret0, _ := ret[0].(string)
	return ret0
}

// Two indicates an expected call of Two.
func (mr *MockEmbeddingIfaceMockRecorder) Two(arg0 interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Two", reflect.TypeOf((*MockEmbeddingIface)(nil).Two), arg0)
}

@google-cla
Copy link

google-cla bot commented Jul 11, 2022

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@sodul
Copy link
Contributor

sodul commented Jul 13, 2022

Thanks for improving the support for Generics @bradleygore. I see that the new case is fairly complex, with 3-4 layers of nested for loops which is quite deep. Is this something that could be extracted to one or more separate methods? Go is not my primary language so I can't really comment on the rest of the logic.

@codyoss We are eagerly waiting for full support of Go 1.18 in the next release of mockgen, ideally with #604 in it ;-).

Thanks!

@bradleygore
Copy link
Author

@sodul - I went ahead and consolidated down the functionality to retrieve interface model definition (whether imported or same pkg). PR updated.

@bradleygore
Copy link
Author

Hey @codyoss - anything I can do to help this PR move forward?

@codyoss
Copy link
Member

codyoss commented Aug 11, 2022

Thanks for the PR I will take a look at this today!

@codyoss
Copy link
Member

codyoss commented Aug 11, 2022

First off thanks for taking a stab at getting this all implemented. I have not looked too in depth yet but I thought I would provide some early feedback.

  1. It does not look like the current generated code compiles when I ran locally. Please feel free to re-run and check-in the updates. The CI works be validating what is checked in matches what a local generator run produces. Here is an example of one of the build error: func (m *MockEmbeddingIface) Eight(arg0 T) other.Two[other.Three, error] . There is no type T for input params, this should be swapped to an other.Three.
  2. I turned on running CI for this PR and it looks like it does not compile for 1.15. I would like to bump up the min version soon, but it would not be passed 1.17 even if I did. The error from CI is mockgen/parse.go:327:29: IndexListExpr not declared by package ast. To keep compatibility for 1.15 we need to separate pre 1.18 code from 1.18+ code. This is why in a previous PR I added the generic_go118.go and generic_notgo118.go files. We need to hide newer types in the 1.18 impl and for older versions need no-op impls.

Thanks again and I am happy to work with you to push this along. Please let me know if you have any questions.

@codyoss
Copy link
Member

codyoss commented Aug 11, 2022

Also, I totally agree about how un-approachable some of these files are to edit. In the future I would love to do some internal refactors to improve readability. If you have motivation for that(in a separate PR) we are always accept contributions :)

@codyoss
Copy link
Member

codyoss commented Aug 11, 2022

Also also just noticed #669 that is doing some of the same work. The impl works for embedding with interfaces where all parameters are type parameters but not when they are instantiated types.

@bradleygore
Copy link
Author

Hey @codyoss - I will work on this. I noticed the different 1.18 file and honestly didn't even recognize that the ast types I was referencing was part of the new types to support generics 😅 I think I can fairly quickly separate it out and hope to have some time next week to put on this.

@bradleygore
Copy link
Author

bradleygore commented Aug 12, 2022

Hey @codyoss - I went ahead and pushed an update that splits out the generic-specific stuff to the _go118 file and puts an empty impl in the _notgo118 file. I am able to build locally with versions 1.18 and 1.17 of go:

// v1.18.x
go build -o ./_local/build github.com/golang/mock/mockgen 
// v1.17.x
go1.17 build -o ./_local/build_117 github.com/golang/mock/mockgen

And both outputted a binary file in the specified location without error.

Can you enable the workflow to run on this, or do you spot anything else I need to do first?

Update: I also downloaded go1.16 and it also builds. I am looking into the issue with the embedded generic interface type you mentioned though.

Update2: Pushed an update that I think solves the issue for parsing in/out params that reference generic types through embedded iface (and even pointers to them - see generic.go Bar::Twenty|TwentyOne

@bradleygore
Copy link
Author

I think everything has been addressed and this is ready to run through CI tests 😄

Is there a way to run the test suite locally? I see in the internal/tests/generics/go.mod file it's doing this: replace github.com/golang/mock => ../../../.. but that doesn't seem to be working for me locally - if I open that directory and try to run the go:generate script, it tries to use my globally-installed mockgen binary in the GOBIN directory. I guess I can just build from source and use that binary, but was hoping this replace directive would make it so I don't have to. Thoughts?

@sodul
Copy link
Contributor

sodul commented Aug 12, 2022

@bradleygore I just did this a few hours ago. Since the code is expected to build with Go 1.15 I did the following from the root of the repository:

docker run -it -v $(pwd):/go/src/mock:cached golang:1.15
cd src/mock/mockgen
go install ./...
# confirm mockgen was installed:
mockgen -h
go generate ./...
go test ./...
echo $?

The only dependency is for you to have docker running. My laptop is not backward compatible with Go 1.15 (M1 CPU) so the docker approach worked just fine, and at native speed since it used the ARM image.

See https://github.com/golang/mock/blob/main/.github/workflows/test.yaml for how it runs the tests.

@bradleygore
Copy link
Author

Hey @codyoss - I am able to run the CI workflow locally using https://github.com/nektos/act tool. Everything is now passing here, so hoping it will in CI as well 😄

@bradleygore
Copy link
Author

Just checking in to see if anything further needed from me on this PR. It doesn't run the workflow without approval for each run it seems. As noted, I did run the workflow locally and everything passed there, so I'm hopeful it will here as well.

@bradleygore
Copy link
Author

Hey @codyoss - is there anything further needed from me on this?

@eduardsmetanin
Copy link

@sodul @codyoss Could you please review this pull request?

@sodul
Copy link
Contributor

sodul commented Sep 20, 2022

@eduardsmetanin I'm not a maintainer, it all in the hands of @codyoss :-).

@clsx524
Copy link

clsx524 commented Sep 22, 2022

Hi, thank you for doing this PR. It's very useful. I was trying it out in my own fork and found something, not sure the root cause of it.

Let's say I have an interface like

type Base[E any] interface {
	FindAll(ctx context.Context, filter interface{}) ([]*E, error)
	FindOne(ctx context.Context, filter interface{}) (*E, error)
}

and I instantiate it in another interface like

type RealThing interface {
	Base[SomeStruct]
}

The mock file for RealThing interface has correct mock for FindOne method which replaces E with SomeStruct but the FindAll still uses E type in mocks.

Is this a real issue or I did something wrong?

@codyoss
Copy link
Member

codyoss commented Oct 7, 2022

Just getting back to this today sorry for the delay. I am looking over this and #669 as they are both doing roughly the same thing with different impls

@bradleygore
Copy link
Author

@clsx524 - thanks for finding an unhandled case! I reworked the parsing of the generic in/out parameters on methods to be much more complete, and added new test cases for these. Please try again and let me know what you think 😄

@clsx524
Copy link

clsx524 commented Oct 16, 2022

@clsx524 - thanks for finding an unhandled case! I reworked the parsing of the generic in/out parameters on methods to be much more complete, and added new test cases for these. Please try again and let me know what you think smile

@bradleygore Thanks for your work! I had a try and your change definitely fixed the issue I mentioned. I noticed it probably has some issue to handle vararg syntax in method like

FindAll(ctx context.Context, filter interface{}, options ...Option[T]) ([]*E, error)

Option would be some random interface. Could you help double check this scenario?

@bradleygore
Copy link
Author

@clsx524 - nice find! I just updated it to handle that. I hope that it is handling all of the scenarios now 😅

@bradleygore
Copy link
Author

bradleygore commented Oct 17, 2022

Just getting back to this today sorry for the delay. I am looking over this and #669 as they are both doing roughly the same thing with different impls

@codyoss - yeah, I didn't realize that someone else was also tackling this. Here are some main differences I can see:

  • I've done some simplification in parse.go, whereas the alternative adds new types and funcs to that file
  • my PR tries to keep all generic parsing in the generic_go... file so it's encapsulated in that one file
  • alternative PR swaps out generic types in real-time while parsing the interface, whereas I do it kind of at the end after parsing the interface

Not saying any of these are right/wrong/better/worse - just in reviewing these PRs these are the core differentiators I see.

If the other PR gets merged in, I would still advocate for checking out some of the cleanup I've done in parse.go in this PR as I think it will be beneficial for future contributors 😄

@clsx524
Copy link

clsx524 commented Oct 18, 2022

@clsx524 - nice find! I just updated it to handle that. I hope that it is handling all of the scenarios now 😅

Thanks for the update. I can confirm it fixed the issues I mentioned.

@davisford
Copy link

Hey @codyoss can you re-review this one? It's been out here a while, and we need a resolution for this to be able to start using generics.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants