diff --git a/cmd/relui/main.go b/cmd/relui/main.go index fced950835..21c52dbaae 100644 --- a/cmd/relui/main.go +++ b/cmd/relui/main.go @@ -6,6 +6,7 @@ package main import ( + "flag" "io/ioutil" "log" "net/http" @@ -16,8 +17,13 @@ import ( reluipb "golang.org/x/build/cmd/relui/protos" ) +var ( + devDataDir = flag.String("dev-data-directory", defaultDevDataDir(), "Development-only directory to use for storage of application state.") +) + func main() { - s := &server{store: &memoryStore{}, configs: loadWorkflowConfig("./workflows")} + flag.Parse() + s := &server{store: newFileStore(*devDataDir), configs: loadWorkflowConfig("./workflows")} http.Handle("/workflows/create", http.HandlerFunc(s.createWorkflowHandler)) http.Handle("/workflows/new", http.HandlerFunc(s.newWorkflowHandler)) http.Handle("/", fileServerHandler(relativeFile("./static"), http.HandlerFunc(s.homeHandler))) @@ -54,3 +60,12 @@ func loadWorkflowConfig(dir string) []*reluipb.Workflow { } return ws } + +// defaultDevDataDir returns a directory suitable for storage of data when developing relui on most platforms. +func defaultDevDataDir() string { + c, err := os.UserConfigDir() + if err != nil { + c = os.TempDir() + } + return filepath.Join(c, "go-build", "relui") +} diff --git a/cmd/relui/protos/relui.pb.go b/cmd/relui/protos/relui.pb.go index 039aaa6a44..6cca73abcf 100644 --- a/cmd/relui/protos/relui.pb.go +++ b/cmd/relui/protos/relui.pb.go @@ -195,6 +195,47 @@ func (m *BuildableTask) GetTaskType() string { return "" } +// LocalStorage is the persisted data of relui. It is used in development mode for saving application state. +type LocalStorage struct { + // workflows are a list of user-created workflows. + Workflows []*Workflow `protobuf:"bytes,1,rep,name=workflows,proto3" json:"workflows,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LocalStorage) Reset() { *m = LocalStorage{} } +func (m *LocalStorage) String() string { return proto.CompactTextString(m) } +func (*LocalStorage) ProtoMessage() {} +func (*LocalStorage) Descriptor() ([]byte, []int) { + return fileDescriptor_6de8859f82adce0a, []int{2} +} + +func (m *LocalStorage) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LocalStorage.Unmarshal(m, b) +} +func (m *LocalStorage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LocalStorage.Marshal(b, m, deterministic) +} +func (m *LocalStorage) XXX_Merge(src proto.Message) { + xxx_messageInfo_LocalStorage.Merge(m, src) +} +func (m *LocalStorage) XXX_Size() int { + return xxx_messageInfo_LocalStorage.Size(m) +} +func (m *LocalStorage) XXX_DiscardUnknown() { + xxx_messageInfo_LocalStorage.DiscardUnknown(m) +} + +var xxx_messageInfo_LocalStorage proto.InternalMessageInfo + +func (m *LocalStorage) GetWorkflows() []*Workflow { + if m != nil { + return m.Workflows + } + return nil +} + type GitSource struct { Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` Ref string `protobuf:"bytes,2,opt,name=ref,proto3" json:"ref,omitempty"` @@ -207,7 +248,7 @@ func (m *GitSource) Reset() { *m = GitSource{} } func (m *GitSource) String() string { return proto.CompactTextString(m) } func (*GitSource) ProtoMessage() {} func (*GitSource) Descriptor() ([]byte, []int) { - return fileDescriptor_6de8859f82adce0a, []int{2} + return fileDescriptor_6de8859f82adce0a, []int{3} } func (m *GitSource) XXX_Unmarshal(b []byte) error { @@ -247,35 +288,38 @@ func init() { proto.RegisterType((*Workflow)(nil), "protos.Workflow") proto.RegisterMapType((map[string]string)(nil), "protos.Workflow.ParamsEntry") proto.RegisterType((*BuildableTask)(nil), "protos.BuildableTask") + proto.RegisterType((*LocalStorage)(nil), "protos.LocalStorage") proto.RegisterType((*GitSource)(nil), "protos.GitSource") } func init() { proto.RegisterFile("relui.proto", fileDescriptor_6de8859f82adce0a) } var fileDescriptor_6de8859f82adce0a = []byte{ - // 384 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x4d, 0x6f, 0xda, 0x40, - 0x10, 0xed, 0x1a, 0xb0, 0xf0, 0xb8, 0xa5, 0x74, 0xdb, 0xaa, 0x56, 0x3f, 0x24, 0xca, 0xc9, 0xe2, - 0x40, 0x2b, 0xda, 0x43, 0x9b, 0x43, 0x24, 0x27, 0x41, 0x39, 0x20, 0x41, 0x64, 0x1b, 0x71, 0xb4, - 0xd6, 0xb0, 0x20, 0xcb, 0x8b, 0x6d, 0xed, 0xae, 0x13, 0xf9, 0x57, 0xe6, 0x2f, 0xe4, 0xa7, 0x44, - 0xbb, 0xd8, 0x09, 0x41, 0x39, 0xf9, 0xed, 0x7b, 0x33, 0x6f, 0xde, 0x8c, 0x0c, 0x36, 0xa7, 0xac, - 0x4c, 0xc6, 0x05, 0xcf, 0x65, 0x8e, 0x4d, 0xfd, 0x11, 0xc3, 0x7b, 0x04, 0xdd, 0x55, 0xce, 0xd3, - 0x2d, 0xcb, 0xef, 0x30, 0x86, 0x76, 0x46, 0xf6, 0xd4, 0x41, 0x03, 0xe4, 0x5a, 0xbe, 0xc6, 0xf8, - 0x1c, 0xde, 0xc7, 0x65, 0xc2, 0x36, 0x24, 0x66, 0x34, 0x92, 0x44, 0xa4, 0xc2, 0x31, 0x06, 0x2d, - 0xd7, 0x9e, 0x7c, 0x3e, 0x38, 0x89, 0xf1, 0x45, 0x23, 0x87, 0x44, 0xa4, 0x7e, 0x2f, 0x3e, 0x7e, - 0x0a, 0xfc, 0x17, 0xcc, 0x82, 0x70, 0xb2, 0x17, 0x4e, 0x4b, 0xb7, 0x7d, 0x6f, 0xda, 0x9a, 0xa9, - 0xe3, 0x1b, 0x2d, 0x4f, 0x33, 0xc9, 0x2b, 0xbf, 0xae, 0xfd, 0xfa, 0x1f, 0xec, 0x23, 0x1a, 0xf7, - 0xa1, 0x95, 0xd2, 0xaa, 0xce, 0xa5, 0x20, 0xfe, 0x04, 0x9d, 0x5b, 0xc2, 0x4a, 0xea, 0x18, 0x9a, - 0x3b, 0x3c, 0xce, 0x8c, 0x7f, 0x68, 0xf8, 0x80, 0xe0, 0xdd, 0x8b, 0x48, 0xaf, 0xae, 0xf5, 0x03, - 0x60, 0x43, 0x0b, 0x9a, 0x6d, 0x44, 0x94, 0x67, 0xb5, 0x89, 0x55, 0x33, 0x8b, 0x0c, 0x8f, 0xc0, - 0x14, 0x92, 0xc8, 0x52, 0xa5, 0x46, 0x6e, 0x6f, 0x82, 0x9b, 0xd4, 0xca, 0x30, 0xd0, 0x8a, 0x5f, - 0x57, 0xe0, 0x9f, 0xf0, 0x96, 0x70, 0x99, 0x6c, 0xc9, 0x5a, 0x46, 0x25, 0x67, 0x4e, 0x5b, 0x9b, - 0xd9, 0x0d, 0xb7, 0xe4, 0x0c, 0xff, 0x06, 0xd8, 0x25, 0x32, 0x12, 0x79, 0xc9, 0xd7, 0xd4, 0xe9, - 0x0c, 0x90, 0x6b, 0x4f, 0x3e, 0x34, 0x96, 0xd7, 0x89, 0x0c, 0xb4, 0xe0, 0x5b, 0xbb, 0x06, 0xe2, - 0x6f, 0x60, 0xa9, 0x63, 0x47, 0xb2, 0x2a, 0xa8, 0x63, 0x6a, 0xc7, 0xae, 0x22, 0xc2, 0xaa, 0xa0, - 0xc3, 0x5f, 0x60, 0x3d, 0x35, 0xa9, 0xdb, 0xa8, 0xa9, 0xf5, 0x6d, 0x4a, 0xce, 0x14, 0xc3, 0xe9, - 0xb6, 0x5e, 0x4a, 0xc1, 0xd1, 0x0a, 0xe0, 0x39, 0x38, 0xfe, 0x02, 0x1f, 0x43, 0x2f, 0x98, 0x45, - 0x41, 0xe8, 0x85, 0xcb, 0x20, 0x5a, 0xce, 0x67, 0xf3, 0xc5, 0x6a, 0xde, 0x7f, 0x73, 0x2a, 0x5c, - 0xfa, 0x53, 0x2f, 0x9c, 0x5e, 0xf5, 0xd1, 0xa9, 0x10, 0x84, 0x9e, 0xaf, 0x04, 0x23, 0x3e, 0xfc, - 0x46, 0x7f, 0x1e, 0x03, 0x00, 0x00, 0xff, 0xff, 0x73, 0x67, 0x81, 0x13, 0x5c, 0x02, 0x00, 0x00, + // 411 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x4f, 0x6f, 0xd3, 0x30, + 0x1c, 0xc5, 0xed, 0x56, 0x2d, 0xbf, 0x8c, 0x51, 0x0c, 0x08, 0x8b, 0x3f, 0x52, 0xe9, 0x29, 0xda, + 0x21, 0xa0, 0xc2, 0x01, 0x38, 0x4c, 0x0a, 0x50, 0x71, 0x18, 0xea, 0x90, 0x93, 0xaa, 0xc7, 0xc8, + 0x69, 0xdd, 0x2a, 0x8a, 0x17, 0x47, 0xb6, 0xc3, 0x94, 0x4f, 0xc9, 0x57, 0xe0, 0xa3, 0x20, 0xbb, + 0x0e, 0x1b, 0xd5, 0x4e, 0x79, 0x7e, 0xef, 0xe7, 0xe7, 0xf7, 0x7b, 0x0a, 0x84, 0x8a, 0x8b, 0xb6, + 0x8c, 0x1b, 0x25, 0x8d, 0xc4, 0x23, 0xf7, 0xd1, 0xd3, 0xdf, 0x08, 0x4e, 0x56, 0x52, 0x55, 0x5b, + 0x21, 0x6f, 0x30, 0x86, 0xa3, 0x9a, 0x5d, 0x73, 0x82, 0x26, 0x28, 0x0a, 0xa8, 0xc3, 0xf8, 0x02, + 0x1e, 0x15, 0x6d, 0x29, 0x36, 0xac, 0x10, 0x3c, 0x37, 0x4c, 0x57, 0x9a, 0x0c, 0x26, 0xc3, 0x28, + 0x9c, 0x3d, 0xdb, 0x3b, 0xe9, 0xf8, 0x4b, 0x2f, 0x67, 0x4c, 0x57, 0xf4, 0xac, 0xb8, 0x7b, 0xd4, + 0xf8, 0x03, 0x8c, 0x1a, 0xa6, 0xd8, 0xb5, 0x26, 0x43, 0x77, 0xed, 0x55, 0x7f, 0xad, 0x7f, 0x35, + 0xfe, 0xe9, 0xe4, 0x79, 0x6d, 0x54, 0x47, 0xfd, 0xec, 0x8b, 0x4f, 0x10, 0xde, 0xa1, 0xf1, 0x18, + 0x86, 0x15, 0xef, 0x7c, 0x2e, 0x0b, 0xf1, 0x53, 0x38, 0xfe, 0xc5, 0x44, 0xcb, 0xc9, 0xc0, 0x71, + 0xfb, 0xc3, 0xe7, 0xc1, 0x47, 0x34, 0xfd, 0x83, 0xe0, 0xe1, 0x7f, 0x91, 0xee, 0x5d, 0xeb, 0x35, + 0xc0, 0x86, 0x37, 0xbc, 0xde, 0xe8, 0x5c, 0xd6, 0xde, 0x24, 0xf0, 0xcc, 0x55, 0x8d, 0xcf, 0x61, + 0xa4, 0x0d, 0x33, 0xad, 0x4d, 0x8d, 0xa2, 0xb3, 0x19, 0xee, 0x53, 0x5b, 0xc3, 0xd4, 0x29, 0xd4, + 0x4f, 0xe0, 0x37, 0x70, 0xca, 0x94, 0x29, 0xb7, 0x6c, 0x6d, 0xf2, 0x56, 0x09, 0x72, 0xe4, 0xcc, + 0xc2, 0x9e, 0x5b, 0x2a, 0x81, 0xdf, 0x01, 0xec, 0x4a, 0x93, 0x6b, 0xd9, 0xaa, 0x35, 0x27, 0xc7, + 0x13, 0x14, 0x85, 0xb3, 0xc7, 0xbd, 0xe5, 0xf7, 0xd2, 0xa4, 0x4e, 0xa0, 0xc1, 0xae, 0x87, 0xf8, + 0x25, 0x04, 0xb6, 0xec, 0xdc, 0x74, 0x0d, 0x27, 0x23, 0xe7, 0x78, 0x62, 0x89, 0xac, 0x6b, 0xf8, + 0xf4, 0x02, 0x4e, 0x7f, 0xc8, 0x35, 0x13, 0xa9, 0x91, 0x8a, 0xed, 0x38, 0x8e, 0x21, 0xb8, 0xf1, + 0x6d, 0x6a, 0x82, 0x5c, 0xcd, 0xe3, 0xc3, 0x9a, 0xe9, 0xed, 0xc8, 0xf4, 0x2d, 0x04, 0xff, 0x1e, + 0xb5, 0xdd, 0xda, 0xd4, 0xbe, 0xdb, 0x56, 0x09, 0xcb, 0x28, 0xbe, 0xf5, 0xa5, 0x58, 0x78, 0xbe, + 0x02, 0xb8, 0x5d, 0x1c, 0x3f, 0x87, 0x27, 0x59, 0x92, 0x5e, 0xe6, 0x69, 0x96, 0x64, 0xcb, 0x34, + 0x5f, 0x2e, 0x2e, 0x17, 0x57, 0xab, 0xc5, 0xf8, 0xc1, 0xa1, 0xf0, 0x95, 0xce, 0x93, 0x6c, 0xfe, + 0x6d, 0x8c, 0x0e, 0x85, 0x34, 0x4b, 0xa8, 0x15, 0x06, 0xc5, 0xfe, 0x37, 0x7c, 0xff, 0x37, 0x00, + 0x00, 0xff, 0xff, 0xfa, 0x0a, 0x66, 0x8f, 0x9c, 0x02, 0x00, 0x00, } diff --git a/cmd/relui/protos/relui.proto b/cmd/relui/protos/relui.proto index fa1be06aed..6060dfadc4 100644 --- a/cmd/relui/protos/relui.proto +++ b/cmd/relui/protos/relui.proto @@ -41,6 +41,12 @@ message BuildableTask { string task_type = 6; } +// LocalStorage is the persisted data of relui. It is used in development mode for saving application state. +message LocalStorage { + // workflows are a list of user-created workflows. + repeated Workflow workflows = 1; +} + message GitSource { string url = 1; string ref = 2; diff --git a/cmd/relui/store.go b/cmd/relui/store.go index 09fd362428..a861058690 100644 --- a/cmd/relui/store.go +++ b/cmd/relui/store.go @@ -5,38 +5,81 @@ package main import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" "sync" + "github.com/golang/protobuf/proto" reluipb "golang.org/x/build/cmd/relui/protos" ) // store is a persistence adapter for saving data. type store interface { - GetWorkflows() []*reluipb.Workflow + Workflows() []*reluipb.Workflow AddWorkflow(workflow *reluipb.Workflow) error } -var _ store = (*memoryStore)(nil) +var _ store = (*fileStore)(nil) -// memoryStore is a non-durable implementation of store that keeps everything in memory. -type memoryStore struct { - mut sync.Mutex - workflows []*reluipb.Workflow +// newFileStore initializes a fileStore ready for use. +// +// If dir is set to an empty string (""), no data will be saved to disk. +func newFileStore(dir string) *fileStore { + return &fileStore{ + persistDir: dir, + ls: new(reluipb.LocalStorage), + } +} + +// fileStoreName is the name of the data file used by fileStore for persistence. +const fileStoreName = "local_storage.textpb" + +// fileStore is a non-durable implementation of store that keeps everything in memory. +type fileStore struct { + mu sync.Mutex + ls *reluipb.LocalStorage + + // persistDir is a path to a directory for saving application data in textproto format. + persistDir string } -// AddWorkflow adds a workflow to the store. -func (m *memoryStore) AddWorkflow(w *reluipb.Workflow) error { - m.mut.Lock() - defer m.mut.Unlock() - m.workflows = append(m.workflows, w) +// AddWorkflow adds a workflow to the store, persisting changes to disk. +func (f *fileStore) AddWorkflow(w *reluipb.Workflow) error { + f.mu.Lock() + f.ls.Workflows = append(f.ls.Workflows, w) + f.mu.Unlock() + if err := f.persist(); err != nil { + return err + } return nil } -// GetWorkflows returns all workflows stored. -// -// TODO(golang.org/issue/40279) - clone workflows if they're ever mutated. -func (m *memoryStore) GetWorkflows() []*reluipb.Workflow { - m.mut.Lock() - defer m.mut.Unlock() - return m.workflows +// Workflows returns all workflows stored. +func (f *fileStore) Workflows() []*reluipb.Workflow { + return f.localStorage().GetWorkflows() +} + +// localStorage returns a deep copy of data stored in fileStore. +func (f *fileStore) localStorage() *reluipb.LocalStorage { + f.mu.Lock() + defer f.mu.Unlock() + return proto.Clone(f.ls).(*reluipb.LocalStorage) +} + +// persist saves fileStore state to persistDir/fileStoreName. +func (f *fileStore) persist() error { + if f.persistDir == "" { + return nil + } + if err := os.MkdirAll(f.persistDir, 0755); err != nil { + return fmt.Errorf("os.MkDirAll(%q, %v) = %w", f.persistDir, 0755, err) + } + dst := filepath.Join(f.persistDir, fileStoreName) + data := []byte(proto.MarshalTextString(f.localStorage())) + if err := ioutil.WriteFile(dst, data, 0644); err != nil { + return fmt.Errorf("ioutil.WriteFile(%q, _, %v) = %w", dst, 0644, err) + } + return nil } diff --git a/cmd/relui/store_test.go b/cmd/relui/store_test.go new file mode 100644 index 0000000000..79843db6d7 --- /dev/null +++ b/cmd/relui/store_test.go @@ -0,0 +1,52 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" + reluipb "golang.org/x/build/cmd/relui/protos" +) + +func TestFileStorePersist(t *testing.T) { + dir, err := ioutil.TempDir("", "memory-store-test") + if err != nil { + t.Fatalf("ioutil.TempDir(%q, %q) = _, %v", "", "memory-store-test", err) + } + defer os.RemoveAll(dir) + want := &reluipb.LocalStorage{ + Workflows: []*reluipb.Workflow{ + { + Name: "Persist Test", + BuildableTasks: []*reluipb.BuildableTask{{Name: "Persist Test Task"}}, + }, + }, + } + fs := newFileStore(filepath.Join(dir, "relui")) + fs.ls = want + + err = fs.persist() + if err != nil { + t.Fatalf("fs.Persist() = %v, wanted no error", err) + } + + b, err := ioutil.ReadFile(filepath.Join(dir, "relui", fileStoreName)) + if err != nil { + t.Fatalf("ioutil.ReadFile(%q) = _, %v, wanted no error", filepath.Join(dir, "relui", fileStoreName), err) + } + got := new(reluipb.LocalStorage) + err = proto.UnmarshalText(string(b), got) + if err != nil { + t.Fatalf("proto.UnmarshalText(_) = %v, wanted no error", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("reluipb.LocalStorage mismatch (-want, +got):\n%s", diff) + } +} diff --git a/cmd/relui/web.go b/cmd/relui/web.go index fddde4cdec..a20d2fdefa 100644 --- a/cmd/relui/web.go +++ b/cmd/relui/web.go @@ -64,7 +64,7 @@ type homeResponse struct { // homeHandler renders the homepage. func (s *server) homeHandler(w http.ResponseWriter, _ *http.Request) { out := bytes.Buffer{} - if err := homeTmpl.Execute(&out, homeResponse{Workflows: s.store.GetWorkflows()}); err != nil { + if err := homeTmpl.Execute(&out, homeResponse{Workflows: s.store.Workflows()}); err != nil { log.Printf("homeHandler: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -106,6 +106,7 @@ func (s *server) createWorkflowHandler(w http.ResponseWriter, r *http.Request) { } wf.Params["GitObject"] = ref if err := s.store.AddWorkflow(wf); err != nil { + log.Printf("Error adding workflow: s.store.AddWorkflow(%v) = %v", wf, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go index 311a30b7b4..ab1ae59597 100644 --- a/cmd/relui/web_test.go +++ b/cmd/relui/web_test.go @@ -83,7 +83,7 @@ func TestServerHomeHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() - s := &server{store: &memoryStore{}} + s := &server{store: newFileStore("")} s.homeHandler(w, req) resp := w.Result() @@ -96,7 +96,7 @@ func TestServerNewWorkflowHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/workflows/new", nil) w := httptest.NewRecorder() - s := &server{store: &memoryStore{}} + s := &server{store: newFileStore("")} s.newWorkflowHandler(w, req) resp := w.Result() @@ -134,7 +134,7 @@ func TestServerCreateWorkflowHandler(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() - s := &server{store: &memoryStore{}, configs: config} + s := &server{store: newFileStore(""), configs: config} s.createWorkflowHandler(w, req) resp := w.Result() @@ -146,16 +146,16 @@ func TestServerCreateWorkflowHandler(t *testing.T) { t.Errorf("resp.Header.Get(%q) = %q, wanted %q", k, resp.Header.Get(k), v) } } - if len(s.store.GetWorkflows()) != 1 && c.wantParams != nil { - t.Fatalf("len(s.store.GetWorkflows()) = %d, wanted %d", len(s.store.GetWorkflows()), 1) - } else if len(s.store.GetWorkflows()) != 0 && c.wantParams == nil { - t.Fatalf("len(s.store.GetWorkflows()) = %d, wanted %d", len(s.store.GetWorkflows()), 0) + if len(s.store.Workflows()) != 1 && c.wantParams != nil { + t.Fatalf("len(s.store.Workflows()) = %d, wanted %d", len(s.store.Workflows()), 1) + } else if len(s.store.Workflows()) != 0 && c.wantParams == nil { + t.Fatalf("len(s.store.Workflows()) = %d, wanted %d", len(s.store.Workflows()), 0) } if c.wantParams == nil { return } - if diff := cmp.Diff(c.wantParams, s.store.GetWorkflows()[0].GetParams()); diff != "" { - t.Errorf("s.Store.GetWorkflows()[0].Params() mismatch (-want, +got):\n%s", diff) + if diff := cmp.Diff(c.wantParams, s.store.Workflows()[0].GetParams()); diff != "" { + t.Errorf("s.Store.Workflows()[0].Params() mismatch (-want, +got):\n%s", diff) } }) }