From 5de0329d5b11473df60f7ab8041935023b5fb9c0 Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 25 Apr 2023 18:17:59 +0200 Subject: [PATCH] GO-1077: Refactor integration tests --- tests/basic_test.go | 33 +-- tests/blockbuilder/block_builder.go | 230 ++++++++++++++++++++ tests/blockbuilder/builder_test.go | 41 ++++ tests/events_test.go | 13 +- tests/migration_test.go | 7 + tests/session_test.go | 1 + tests/testing_test.go | 223 +++++++------------ tests/util_test.go | 317 ++++++++-------------------- 8 files changed, 468 insertions(+), 397 deletions(-) create mode 100644 tests/blockbuilder/block_builder.go create mode 100644 tests/blockbuilder/builder_test.go create mode 100644 tests/migration_test.go create mode 100644 tests/session_test.go diff --git a/tests/basic_test.go b/tests/basic_test.go index f88097dde..34a28cd77 100644 --- a/tests/basic_test.go +++ b/tests/basic_test.go @@ -9,11 +9,14 @@ import ( "github.com/anytypeio/go-anytype-middleware/pkg/lib/bundle" "github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model" "github.com/anytypeio/go-anytype-middleware/util/pbtypes" + + . "github.com/anytypeio/go-anytype-middleware/tests/blockbuilder" ) func (s *testSuite) TestBasic() { s.Run("open dashboard", func() { - resp := call(s, s.ObjectOpen, &pb.RpcObjectOpenRequest{ + cctx := s.newCallCtx(s.T()) + resp := call(cctx, s.ObjectOpen, &pb.RpcObjectOpenRequest{ ObjectId: s.acc.Info.HomeObjectId, }) @@ -25,13 +28,14 @@ func (s *testSuite) TestBasic() { s.NotZero(resp.ObjectView.Type) }) + cctx := s.newCallCtx(s.T()) s.Require().NotEmpty( - call(s, s.ObjectSearch, &pb.RpcObjectSearchRequest{ + call(cctx, s.ObjectSearch, &pb.RpcObjectSearchRequest{ Keys: []string{"id", "type", "name"}, }).Records, ) - call(s, s.ObjectSearchSubscribe, &pb.RpcObjectSearchSubscribeRequest{ + call(cctx, s.ObjectSearchSubscribe, &pb.RpcObjectSearchSubscribeRequest{ SubId: "recent", Filters: []*model.BlockContentDataviewFilter{ { @@ -43,7 +47,8 @@ func (s *testSuite) TestBasic() { }) s.Run("create and open an object", func() { - objId := call(s, s.BlockLinkCreateWithObject, &pb.RpcBlockLinkCreateWithObjectRequest{ + cctx := s.newCallCtx(s.T()) + objId := call(cctx, s.BlockLinkCreateWithObject, &pb.RpcBlockLinkCreateWithObjectRequest{ InternalFlags: []*model.InternalFlag{ { Value: model.InternalFlag_editorDeleteEmpty, @@ -59,15 +64,15 @@ func (s *testSuite) TestBasic() { }, }).TargetId - resp := call(s, s.ObjectOpen, &pb.RpcObjectOpenRequest{ + resp := call(cctx, s.ObjectOpen, &pb.RpcObjectOpenRequest{ ObjectId: objId, }) s.Require().NotNil(resp.ObjectView) - waitEvent(s, func(sa *pb.EventMessageValueOfSubscriptionAdd) { + waitEvent(cctx.t, s, func(sa *pb.EventMessageValueOfSubscriptionAdd) { s.Equal(sa.SubscriptionAdd.Id, objId) }) - waitEvent(s, func(sa *pb.EventMessageValueOfObjectDetailsSet) { + waitEvent(cctx.t, s, func(sa *pb.EventMessageValueOfObjectDetailsSet) { s.Equal(sa.ObjectDetailsSet.Id, objId) s.Contains(sa.ObjectDetailsSet.Details.Fields, bundle.RelationKeyLastOpenedDate.String()) }) @@ -96,7 +101,8 @@ func pageTemplate(children ...*Block) *Block { } func (s *testSuite) testOnNewObject(objectType bundle.TypeKey, fn func(objectID string), wantPage *Block) { - resp := call(s, s.ObjectCreate, &pb.RpcObjectCreateRequest{ + cctx := s.newCallCtx(s.T()) + resp := call(cctx, s.ObjectCreate, &pb.RpcObjectCreateRequest{ Details: &types.Struct{ Fields: map[string]*types.Value{ bundle.RelationKeyType.String(): pbtypes.String(objectType.BundledURL()), @@ -107,7 +113,7 @@ func (s *testSuite) testOnNewObject(objectType bundle.TypeKey, fn func(objectID fn(resp.ObjectId) - sresp := call(s, s.ObjectShow, &pb.RpcObjectShowRequest{ + sresp := call(cctx, s.ObjectShow, &pb.RpcObjectShowRequest{ ObjectId: resp.ObjectId, }) @@ -120,18 +126,19 @@ func (s *testSuite) TestEditor_CreateBlocks() { Text("Level 2")), )) + cctx := s.newCallCtx(s.T()) s.testOnNewObject(bundle.TypeKeyPage, func(objectID string) { - bresp := call(s, s.BlockCreate, &pb.RpcBlockCreateRequest{ + bresp := call(cctx, s.BlockCreate, &pb.RpcBlockCreateRequest{ ContextId: objectID, - Block: Text("Level 1", Color("red")).block, + Block: Text("Level 1", Color("red")).Block(), TargetId: "", Position: model.Block_Inner, }) s.NotEmpty(bresp.BlockId) - bresp2 := call(s, s.BlockCreate, &pb.RpcBlockCreateRequest{ + bresp2 := call(cctx, s.BlockCreate, &pb.RpcBlockCreateRequest{ ContextId: objectID, - Block: Text("Level 2").block, + Block: Text("Level 2").Block(), TargetId: bresp.BlockId, Position: model.Block_Inner, }) diff --git a/tests/blockbuilder/block_builder.go b/tests/blockbuilder/block_builder.go new file mode 100644 index 000000000..e4a9f293b --- /dev/null +++ b/tests/blockbuilder/block_builder.go @@ -0,0 +1,230 @@ +package blockbuilder + +import ( + "testing" + + "github.com/globalsign/mgo/bson" + "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/assert" + + "github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model" + "github.com/anytypeio/go-anytype-middleware/util/pbtypes" +) + +type options struct { + children []*Block + color string + restrictions *model.BlockRestrictions + textStyle model.BlockContentTextStyle + marks *model.BlockContentTextMarks + fields *types.Struct +} + +type Option func(*options) + +func Children(v ...*Block) Option { + return func(o *options) { + o.children = v + } +} + +func Restrictions(r model.BlockRestrictions) Option { + return func(o *options) { + o.restrictions = &r + } +} + +func Fields(v *types.Struct) Option { + return func(o *options) { + o.fields = v + } +} + +func Color(v string) Option { + return func(o *options) { + o.color = v + } +} + +func TextStyle(s model.BlockContentTextStyle) Option { + return func(o *options) { + o.textStyle = s + } +} + +func TextMarks(m model.BlockContentTextMarks) Option { + return func(o *options) { + o.marks = &m + } +} + +type Block struct { + block *model.Block + children []*Block +} + +func (b *Block) Block() *model.Block { + return b.block +} + +func (b *Block) Copy() *Block { + children := make([]*Block, 0, len(b.children)) + for _, c := range b.children { + children = append(children, c.Copy()) + } + bc := Block{ + block: pbtypes.CopyBlock(b.block), + children: children, + } + return &bc +} + +func (b *Block) Build() []*model.Block { + if b.block.Id == "" { + b.block.Id = bson.NewObjectId().Hex() + } + + var descendants []*model.Block + b.block.ChildrenIds = b.block.ChildrenIds[:0] + for _, c := range b.children { + descendants = append(descendants, c.Build()...) + b.block.ChildrenIds = append(b.block.ChildrenIds, c.block.Id) + } + + return append([]*model.Block{ + b.block, + }, descendants...) +} + +func mkBlock(b *model.Block, opts ...Option) *Block { + o := options{ + // Init children for easier equality check in tests + children: []*Block{}, + restrictions: &model.BlockRestrictions{}, + } + for _, apply := range opts { + apply(&o) + } + b.Restrictions = o.restrictions + b.Fields = o.fields + return &Block{ + block: b, + children: o.children, + } +} + +func Root(opts ...Option) *Block { + return mkBlock(&model.Block{ + Content: &model.BlockContentOfSmartblock{ + Smartblock: &model.BlockContentSmartblock{}, + }, + }, opts...) +} + +func Layout(style model.BlockContentLayoutStyle, opts ...Option) *Block { + return mkBlock(&model.Block{ + Content: &model.BlockContentOfLayout{ + Layout: &model.BlockContentLayout{Style: style}, + }, + }, opts...) +} + +func Header(opts ...Option) *Block { + return Layout(model.BlockContentLayout_Header, append(opts, Restrictions( + model.BlockRestrictions{ + Edit: true, + Remove: true, + Drag: true, + DropOn: true, + }))...) +} + +func FeaturedRelations(opts ...Option) *Block { + return mkBlock(&model.Block{ + Content: &model.BlockContentOfFeaturedRelations{ + FeaturedRelations: &model.BlockContentFeaturedRelations{}, + }, + }, append(opts, Restrictions(model.BlockRestrictions{ + Remove: true, + Drag: true, + DropOn: true, + }))...) +} + +func Text(s string, opts ...Option) *Block { + o := options{ + marks: &model.BlockContentTextMarks{}, + } + for _, apply := range opts { + apply(&o) + } + + return mkBlock(&model.Block{ + Content: &model.BlockContentOfText{ + Text: &model.BlockContentText{ + Text: s, + Style: o.textStyle, + Color: o.color, + Marks: o.marks, + }, + }, + }, opts...) +} + +func BuildAST(raw []*model.Block) *Block { + rawMap := make(map[string]*model.Block, len(raw)) + for _, b := range raw { + rawMap[b.Id] = b + } + + blocks := make(map[string]*Block, len(raw)) + for _, b := range raw { + blocks[b.Id] = &Block{ + block: b, + } + } + + isChildOf := map[string]string{} + + for _, b := range raw { + children := make([]*Block, 0, len(b.ChildrenIds)) + for _, id := range b.ChildrenIds { + isChildOf[id] = b.Id + v, ok := blocks[id] + if !ok { + continue + } + children = append(children, v) + } + + blocks[b.Id].children = children + } + + for _, b := range raw { + if _, ok := isChildOf[b.Id]; !ok { + return blocks[b.Id] + } + } + return nil +} + +func dropBlockIDs(b *Block) { + b.block.Id = "" + for i := range b.block.ChildrenIds { + b.block.ChildrenIds[i] = "" + } + + for _, c := range b.children { + dropBlockIDs(c) + } +} + +func AssertPagesEqual(t *testing.T, want, got []*model.Block) bool { + wantTree := BuildAST(want) + gotTree := BuildAST(got) + + dropBlockIDs(wantTree) + dropBlockIDs(gotTree) + + return assert.Equal(t, wantTree, gotTree) +} diff --git a/tests/blockbuilder/builder_test.go b/tests/blockbuilder/builder_test.go new file mode 100644 index 000000000..da4cd287e --- /dev/null +++ b/tests/blockbuilder/builder_test.go @@ -0,0 +1,41 @@ +package blockbuilder + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuilder(t *testing.T) { + makeTree := func() *Block { + return Text("kek", Children( + Text("level 2", Color("red")), + Text("level 2.1", Children( + Text("level 3.1"), Text("level 3.2"), Text("level 3.3"))), + )) + } + + b := makeTree() + blocks := b.Build() + + root := BuildAST(blocks) + + assert.Equal(t, b, root) + +} + +func TestTreesEquality(t *testing.T) { + makeTree := func() *Block { + root := Text("level 1", Children( + Text("level 2", Color("red")), + Text("level 2.1", Children( + Text("level 3.1"), Text("level 3.2"), Text("level 3.3"))), + )) + + return BuildAST(root.Build()) + } + a := makeTree() + b := makeTree() + + AssertPagesEqual(t, a.Build(), b.Build()) +} diff --git a/tests/events_test.go b/tests/events_test.go index 521b09f13..60dc57d9d 100644 --- a/tests/events_test.go +++ b/tests/events_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "sync" + "testing" "time" "github.com/anytypeio/go-anytype-middleware/pb" @@ -55,8 +56,12 @@ func startEventReceiver(ctx context.Context, c service.ClientCommandsClient, tok return er, nil } -func waitEvent[t pb.IsEventMessageValue](s *testSuite, fn func(x t)) { - er := s.events +type eventReceiverProvider interface { + EventReceiver() *eventReceiver +} + +func waitEvent[msgType pb.IsEventMessageValue](t *testing.T, provider eventReceiverProvider, fn func(x msgType)) { + er := provider.EventReceiver() ticker := time.NewTicker(10 * time.Millisecond) timeout := time.NewTimer(2 * time.Second) @@ -67,7 +72,7 @@ func waitEvent[t pb.IsEventMessageValue](s *testSuite, fn func(x t)) { if m == nil { continue } - if v, ok := m.Value.(t); ok { + if v, ok := m.Value.(msgType); ok { fn(v) er.events[i] = nil er.lock.Unlock() @@ -79,7 +84,7 @@ func waitEvent[t pb.IsEventMessageValue](s *testSuite, fn func(x t)) { select { case <-ticker.C: case <-timeout.C: - s.FailNow("wait event timeout") + t.Fatal("wait event timeout") } } } diff --git a/tests/migration_test.go b/tests/migration_test.go new file mode 100644 index 000000000..13a2c9073 --- /dev/null +++ b/tests/migration_test.go @@ -0,0 +1,7 @@ +package tests + +func (s *testSuite) TestMigration() { + // oldClient, err := newClient("31017") + + // s.Require().NoError(err) +} diff --git a/tests/session_test.go b/tests/session_test.go new file mode 100644 index 000000000..ca8701d29 --- /dev/null +++ b/tests/session_test.go @@ -0,0 +1 @@ +package tests diff --git a/tests/testing_test.go b/tests/testing_test.go index ad4f27a64..8f09d05ee 100644 --- a/tests/testing_test.go +++ b/tests/testing_test.go @@ -4,12 +4,7 @@ package tests import ( "context" - "fmt" "os" - "path/filepath" - "reflect" - "runtime" - "strings" "testing" "time" @@ -18,7 +13,6 @@ import ( "github.com/stretchr/testify/suite" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" "github.com/anytypeio/go-anytype-middleware/pb" "github.com/anytypeio/go-anytype-middleware/pb/service" @@ -30,96 +24,94 @@ const rootPath = "/var/anytype" type testSuite struct { suite.Suite - service.ClientCommandsClient - - ctx context.Context - acc *model.Account - events *eventReceiver + *testSession } func TestBasic(t *testing.T) { suite.Run(t, &testSuite{}) } -func (s *testSuite) Context() context.Context { - return s.ctx -} - -func cachedString(key string, rewriteCache bool, proc func() (string, error)) (string, bool, error) { - filename := filepath.Join(cacheDir, key) - raw, err := os.ReadFile(filename) - result := string(raw) - - if rewriteCache || os.IsNotExist(err) || result == "" { - res, err := proc() - if err != nil { - return "", false, fmt.Errorf("running proc for caching %s: %w", key, err) - } - err = os.WriteFile(filename, []byte(res), 0600) - if err != nil { - return "", false, fmt.Errorf("writing cache for %s: %w", key, err) - } - return res, false, nil - } - - return result, true, nil -} - -func (s *testSuite) recoverAccount() (accountID string) { - s.T().Log("recovering the account") - call(s, s.AccountRecover, &pb.RpcAccountRecoverRequest{}) - waitEvent(s, func(a *pb.EventMessageValueOfAccountShow) { +func (s *testSession) recoverAccount(t *testing.T) (accountID string) { + cctx := s.newCallCtx(t) + t.Log("recovering the account") + call(cctx, s.AccountRecover, &pb.RpcAccountRecoverRequest{}) + waitEvent(t, s, func(a *pb.EventMessageValueOfAccountShow) { accountID = a.AccountShow.Account.Id }) return accountID } func (s *testSuite) SetupSuite() { - s.ctx = context.Background() + port := os.Getenv("ANYTYPE_TEST_GRPC_PORT") + if port == "" { + s.FailNow("you must specify ANYTYPE_TEST_GRPC_PORT env variable") + } - c, err := newClient() - s.Require().NoError(err) + s.testSession = newTestSession(s.T(), port) +} + +type testSession struct { + service.ClientCommandsClient + + acc *model.Account + eventReceiver *eventReceiver + token string +} + +func (s *testSession) EventReceiver() *eventReceiver { + return s.eventReceiver +} + +func newTestSession(t *testing.T, port string) *testSession { + var s testSession + + c, err := newClient(port) + + require.NoError(t, err) s.ClientCommandsClient = c mnemonic, _, err := cachedString("mnemonic", false, func() (string, error) { - s.T().Log("creating new test account") - return s.accountCreate(), nil + t.Log("creating new test account") + return s.accountCreate(t), nil }) - s.Require().NoError(err) - s.T().Log("your mnemonic:", mnemonic) + require.NoError(t, err) + t.Log("your mnemonic:", mnemonic) - _ = call(s, s.WalletRecover, &pb.RpcWalletRecoverRequest{ + cctx := s.newCallCtx(t) + _ = call(cctx, s.WalletRecover, &pb.RpcWalletRecoverRequest{ Mnemonic: mnemonic, RootPath: rootPath, }) - s.events = s.setSessionCtx(mnemonic) + cctx, s.eventReceiver = s.openClientSession(t, mnemonic) accountID, _, err := cachedString("account_id", false, func() (string, error) { - return s.recoverAccount(), nil + return s.recoverAccount(t), nil }) - s.Require().NoError(err) - s.T().Log("your account ID:", accountID) + require.NoError(t, err) + t.Log("your account ID:", accountID) - resp, err := callReturnError(s, s.AccountSelect, &pb.RpcAccountSelectRequest{ + resp, err := callReturnError(cctx, s.AccountSelect, &pb.RpcAccountSelectRequest{ Id: accountID, RootPath: rootPath, }) if err != nil { - s.T().Log("can't select account, recovering...") + t.Log("can't select account, recovering...") accountID, _, err = cachedString("account_id", true, func() (string, error) { - return s.recoverAccount(), nil + return s.recoverAccount(t), nil }) - s.Require().NoError(err) - s.T().Log("freshly recovered account ID:", accountID) - resp, err = callReturnError(s, s.AccountSelect, &pb.RpcAccountSelectRequest{ + require.NoError(t, err) + t.Log("freshly recovered account ID:", accountID) + resp, err = callReturnError(cctx, s.AccountSelect, &pb.RpcAccountSelectRequest{ Id: accountID, RootPath: rootPath, }) - s.Require().NoError(err) + require.NoError(t, err) } s.acc = resp.Account + + return &s } func (s *testSuite) TearDownSuite() { @@ -127,131 +119,66 @@ func (s *testSuite) TearDownSuite() { if s.ClientCommandsClient == nil { return } - call(s, s.AccountStop, &pb.RpcAccountStopRequest{ + + s.stopAccount(s.T()) +} + +func (s *testSession) stopAccount(t *testing.T) { + cctx := s.newCallCtx(t) + call(cctx, s.AccountStop, &pb.RpcAccountStopRequest{ RemoveData: true, }) - call(s, s.WalletCloseSession, &pb.RpcWalletCloseSessionRequest{ - Token: s.events.token, + call(cctx, s.WalletCloseSession, &pb.RpcWalletCloseSessionRequest{ + Token: s.eventReceiver.token, }) } -func (s *testSuite) setSessionCtx(mnemonic string) *eventReceiver { - tok := call(s, s.WalletCreateSession, &pb.RpcWalletCreateSessionRequest{ +func (s *testSession) openClientSession(t *testing.T, mnemonic string) (callCtx, *eventReceiver) { + cctx := s.newCallCtx(t) + tok := call(cctx, s.WalletCreateSession, &pb.RpcWalletCreateSessionRequest{ Mnemonic: mnemonic, }).Token - s.ctx = metadata.AppendToOutgoingContext(s.ctx, "token", tok) + s.token = tok + cctx = s.newCallCtx(t) - events, err := startEventReceiver(s.ctx, s, tok) - s.Require().NoError(err) + events, err := startEventReceiver(cctx.newContext(), s, tok) + require.NoError(t, err) - return events + return cctx, events } -func (s *testSuite) accountCreate() string { - s.ctx = context.Background() +func (s *testSession) accountCreate(t *testing.T) string { + cctx := s.newCallCtx(t) - mnemonic := call(s, s.WalletCreate, &pb.RpcWalletCreateRequest{ + mnemonic := call(cctx, s.WalletCreate, &pb.RpcWalletCreateRequest{ RootPath: rootPath, }).Mnemonic - events := s.setSessionCtx(mnemonic) + cctx, events := s.openClientSession(t, mnemonic) - acc := call(s, s.AccountCreate, &pb.RpcAccountCreateRequest{ + acc := call(cctx, s.AccountCreate, &pb.RpcAccountCreateRequest{ Name: "John Doe", AlphaInviteCode: "elbrus", StorePath: rootPath, }) - t := s.T() require.NotNil(t, acc.Account) require.NotNil(t, acc.Account.Info) assert.NotEmpty(t, acc.Account.Id) - call(s, s.AccountStop, &pb.RpcAccountStopRequest{ + call(cctx, s.AccountStop, &pb.RpcAccountStopRequest{ RemoveData: true, }) - call(s, s.WalletCloseSession, &pb.RpcWalletCloseSessionRequest{ + call(cctx, s.WalletCloseSession, &pb.RpcWalletCloseSessionRequest{ Token: events.token, }) return mnemonic } -const cacheDir = ".cache" - -func getError(i interface{}) error { - v := reflect.ValueOf(i).Elem() - - for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - if f.Kind() != reflect.Pointer { - continue - } - el := f.Elem() - if !el.IsValid() { - continue - } - if strings.Contains(el.Type().Name(), "ResponseError") { - code := el.FieldByName("Code").Int() - desc := el.FieldByName("Description").String() - if code > 0 { - return fmt.Errorf("error code %d: %s", code, desc) - } - return nil - } - } - return nil -} - -type callCtx interface { - T() *testing.T - Context() context.Context -} - -func call[reqT, respT any]( - cctx callCtx, - method func(context.Context, reqT, ...grpc.CallOption) (respT, error), - req reqT, -) respT { - t := cctx.T() - resp, err := callReturnError(cctx, method, req) - require.NoError(t, err) - require.NotNil(t, resp) - return resp -} - -func callReturnError[reqT any, respT any]( - cctx callCtx, - method func(context.Context, reqT, ...grpc.CallOption) (respT, error), - req reqT, -) (respT, error) { - name := runtime.FuncForPC(reflect.ValueOf(method).Pointer()).Name() - name = name[strings.LastIndex(name, ".")+1:] - name = name[:strings.LastIndex(name, "-")] - t := cctx.T() - t.Logf("calling %s", name) - - var nilResp respT - - resp, err := method(cctx.Context(), req) - if err != nil { - return nilResp, err - } - err = getError(resp) - if err != nil { - return nilResp, err - } - require.NotNil(t, resp) - return resp, nil -} - -func newClient() (service.ClientCommandsClient, error) { - port := os.Getenv("ANYTYPE_TEST_GRPC_PORT") - if port == "" { - return nil, fmt.Errorf("you must specify ANYTYPE_TEST_GRPC_PORT env variable") - } +func newClient(port string) (service.ClientCommandsClient, error) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() conn, err := grpc.DialContext(ctx, ":"+port, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/tests/util_test.go b/tests/util_test.go index 3cefbb075..c40f54b55 100644 --- a/tests/util_test.go +++ b/tests/util_test.go @@ -1,260 +1,113 @@ package tests import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" "testing" - "github.com/globalsign/mgo/bson" - "github.com/gogo/protobuf/types" - "github.com/stretchr/testify/assert" - - "github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model" - "github.com/anytypeio/go-anytype-middleware/util/pbtypes" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" ) -type options struct { - children []*Block - color string - restrictions *model.BlockRestrictions - textStyle model.BlockContentTextStyle - marks *model.BlockContentTextMarks - fields *types.Struct -} +const cacheDir = ".cache" -type Option func(*options) +func cachedString(key string, rewriteCache bool, proc func() (string, error)) (string, bool, error) { + filename := filepath.Join(cacheDir, key) + raw, err := os.ReadFile(filename) + result := string(raw) -func Children(v ...*Block) Option { - return func(o *options) { - o.children = v - } -} - -func Restrictions(r model.BlockRestrictions) Option { - return func(o *options) { - o.restrictions = &r - } -} - -func Fields(v *types.Struct) Option { - return func(o *options) { - o.fields = v - } -} - -func Color(v string) Option { - return func(o *options) { - o.color = v - } -} - -func TextStyle(s model.BlockContentTextStyle) Option { - return func(o *options) { - o.textStyle = s - } -} - -func TextMarks(m model.BlockContentTextMarks) Option { - return func(o *options) { - o.marks = &m - } -} - -type Block struct { - block *model.Block - children []*Block -} - -func (b *Block) Copy() *Block { - children := make([]*Block, 0, len(b.children)) - for _, c := range b.children { - children = append(children, c.Copy()) - } - bc := Block{ - block: pbtypes.CopyBlock(b.block), - children: children, - } - return &bc -} - -func (b *Block) Build() []*model.Block { - if b.block.Id == "" { - b.block.Id = bson.NewObjectId().Hex() - } - - var descendants []*model.Block - b.block.ChildrenIds = b.block.ChildrenIds[:0] - for _, c := range b.children { - descendants = append(descendants, c.Build()...) - b.block.ChildrenIds = append(b.block.ChildrenIds, c.block.Id) - } - - return append([]*model.Block{ - b.block, - }, descendants...) -} - -func mkBlock(b *model.Block, opts ...Option) *Block { - o := options{ - // Init children for easier equality check in tests - children: []*Block{}, - restrictions: &model.BlockRestrictions{}, - } - for _, apply := range opts { - apply(&o) - } - b.Restrictions = o.restrictions - b.Fields = o.fields - return &Block{ - block: b, - children: o.children, - } -} - -func Root(opts ...Option) *Block { - return mkBlock(&model.Block{ - Content: &model.BlockContentOfSmartblock{ - Smartblock: &model.BlockContentSmartblock{}, - }, - }, opts...) -} - -func Layout(style model.BlockContentLayoutStyle, opts ...Option) *Block { - return mkBlock(&model.Block{ - Content: &model.BlockContentOfLayout{ - Layout: &model.BlockContentLayout{Style: style}, - }, - }, opts...) -} - -func Header(opts ...Option) *Block { - return Layout(model.BlockContentLayout_Header, append(opts, Restrictions( - model.BlockRestrictions{ - Edit: true, - Remove: true, - Drag: true, - DropOn: true, - }))...) -} - -func FeaturedRelations(opts ...Option) *Block { - return mkBlock(&model.Block{ - Content: &model.BlockContentOfFeaturedRelations{ - FeaturedRelations: &model.BlockContentFeaturedRelations{}, - }, - }, append(opts, Restrictions(model.BlockRestrictions{ - Remove: true, - Drag: true, - DropOn: true, - }))...) -} - -func Text(s string, opts ...Option) *Block { - o := options{ - marks: &model.BlockContentTextMarks{}, - } - for _, apply := range opts { - apply(&o) - } - - return mkBlock(&model.Block{ - Content: &model.BlockContentOfText{ - Text: &model.BlockContentText{ - Text: s, - Style: o.textStyle, - Color: o.color, - Marks: o.marks, - }, - }, - }, opts...) -} - -func BuildAST(raw []*model.Block) *Block { - rawMap := make(map[string]*model.Block, len(raw)) - for _, b := range raw { - rawMap[b.Id] = b - } - - blocks := make(map[string]*Block, len(raw)) - for _, b := range raw { - blocks[b.Id] = &Block{ - block: b, + if rewriteCache || os.IsNotExist(err) || result == "" { + res, err := proc() + if err != nil { + return "", false, fmt.Errorf("running proc for caching %s: %w", key, err) } + err = os.WriteFile(filename, []byte(res), 0600) + if err != nil { + return "", false, fmt.Errorf("writing cache for %s: %w", key, err) + } + return res, false, nil } - isChildOf := map[string]string{} + return result, true, nil +} - for _, b := range raw { - children := make([]*Block, 0, len(b.ChildrenIds)) - for _, id := range b.ChildrenIds { - isChildOf[id] = b.Id - v, ok := blocks[id] - if !ok { - continue +func getError(i interface{}) error { + v := reflect.ValueOf(i).Elem() + + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.Kind() != reflect.Pointer { + continue + } + el := f.Elem() + if !el.IsValid() { + continue + } + if strings.Contains(el.Type().Name(), "ResponseError") { + code := el.FieldByName("Code").Int() + desc := el.FieldByName("Description").String() + if code > 0 { + return fmt.Errorf("error code %d: %s", code, desc) } - children = append(children, v) - } - - blocks[b.Id].children = children - } - - for _, b := range raw { - if _, ok := isChildOf[b.Id]; !ok { - return blocks[b.Id] + return nil } } return nil } -func dropBlockIDs(b *Block) { - b.block.Id = "" - for i := range b.block.ChildrenIds { - b.block.ChildrenIds[i] = "" - } +type callCtx struct { + t *testing.T + token string +} - for _, c := range b.children { - dropBlockIDs(c) +func (c callCtx) newContext() context.Context { + return metadata.AppendToOutgoingContext(context.Background(), "token", c.token) +} + +func (s testSession) newCallCtx(t *testing.T) callCtx { + return callCtx{ + t: t, + token: s.token, } } -func AssertPagesEqual(t *testing.T, want, got []*model.Block) bool { - wantTree := BuildAST(want) - gotTree := BuildAST(got) - - dropBlockIDs(wantTree) - dropBlockIDs(gotTree) - - return assert.Equal(t, wantTree, gotTree) +func call[reqT, respT any]( + cctx callCtx, + method func(context.Context, reqT, ...grpc.CallOption) (respT, error), + req reqT, +) respT { + resp, err := callReturnError(cctx, method, req) + require.NoError(cctx.t, err) + require.NotNil(cctx.t, resp) + return resp } -func TestBuilder(t *testing.T) { - makeTree := func() *Block { - return Text("kek", Children( - Text("level 2", Color("red")), - Text("level 2.1", Children( - Text("level 3.1"), Text("level 3.2"), Text("level 3.3"))), - )) +func callReturnError[reqT any, respT any]( + cctx callCtx, + method func(context.Context, reqT, ...grpc.CallOption) (respT, error), + req reqT, +) (respT, error) { + name := runtime.FuncForPC(reflect.ValueOf(method).Pointer()).Name() + name = name[strings.LastIndex(name, ".")+1:] + name = name[:strings.LastIndex(name, "-")] + cctx.t.Logf("calling %s", name) + + var nilResp respT + + resp, err := method(cctx.newContext(), req) + if err != nil { + return nilResp, err } - - b := makeTree() - blocks := b.Build() - - root := BuildAST(blocks) - - assert.Equal(t, b, root) - -} - -func TestTreesEquality(t *testing.T) { - makeTree := func() *Block { - root := Text("level 1", Children( - Text("level 2", Color("red")), - Text("level 2.1", Children( - Text("level 3.1"), Text("level 3.2"), Text("level 3.3"))), - )) - - return BuildAST(root.Build()) + err = getError(resp) + if err != nil { + return nilResp, err } - a := makeTree() - b := makeTree() - - AssertPagesEqual(t, a.Build(), b.Build()) + require.NotNil(cctx.t, resp) + return resp, nil }