gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 97bc2e713a6e5b2c25869b93f5232560d7110170
parent 186e849dbf58f7400673e8f57c1d0036be1327ad
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date:   Sun,  4 Jun 2023 18:55:30 +0200

[chore] tidy + test timelines a bit better (#1865)

* [chore] tidy + test timelines a bit better

* thanks linter
Diffstat:
Minternal/processing/timeline/home.go | 37+++++++++++++++++++++++--------------
Minternal/processing/timeline/list.go | 47+++++++++++++++++++++++++++++------------------
Minternal/timeline/get.go | 12+++++++-----
Minternal/timeline/get_test.go | 617+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Minternal/timeline/index.go | 8++++----
Minternal/timeline/index_test.go | 92+++++++++++++++++++++++--------------------------------------------------------
Minternal/timeline/indexeditems.go | 5+++--
Minternal/timeline/manager.go | 10++++++++--
Dinternal/timeline/manager_test.go | 134-------------------------------------------------------------------------------
Minternal/timeline/prepare.go | 4++--
Minternal/timeline/prune_test.go | 123+++++++++++++++++++++++++++++++++++--------------------------------------------
Minternal/timeline/timeline_test.go | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
12 files changed, 683 insertions(+), 477 deletions(-)

diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go @@ -20,7 +20,6 @@ package timeline import ( "context" "errors" - "fmt" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -38,14 +37,19 @@ import ( func HomeTimelineGrab(state *state.State) timeline.GrabFunction { return func(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int) ([]timeline.Timelineable, bool, error) { statuses, err := state.DB.GetHomeTimeline(ctx, accountID, maxID, sinceID, minID, limit, false) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, true, nil // we just don't have enough statuses left in the db so return stop = true - } - return nil, false, fmt.Errorf("HomeTimelineGrab: error getting statuses from db: %w", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("error getting statuses from db: %w", err) + return nil, false, err + } + + count := len(statuses) + if count == 0 { + // We just don't have enough statuses + // left in the db so return stop = true. + return nil, true, nil } - items := make([]timeline.Timelineable, len(statuses)) + items := make([]timeline.Timelineable, count) for i, s := range statuses { items[i] = s } @@ -59,17 +63,20 @@ func HomeTimelineFilter(state *state.State, filter *visibility.Filter) timeline. return func(ctx context.Context, accountID string, item timeline.Timelineable) (shouldIndex bool, err error) { status, ok := item.(*gtsmodel.Status) if !ok { - return false, errors.New("HomeTimelineFilter: could not convert item to *gtsmodel.Status") + err = gtserror.New("could not convert item to *gtsmodel.Status") + return false, err } requestingAccount, err := state.DB.GetAccountByID(ctx, accountID) if err != nil { - return false, fmt.Errorf("HomeTimelineFilter: error getting account with id %s: %w", accountID, err) + err = gtserror.Newf("error getting account with id %s: %w", accountID, err) + return false, err } timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status) if err != nil { - return false, fmt.Errorf("HomeTimelineFilter: error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err) + err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err) + return false, err } return timelineable, nil @@ -81,12 +88,14 @@ func HomeTimelineStatusPrepare(state *state.State, tc typeutils.TypeConverter) t return func(ctx context.Context, accountID string, itemID string) (timeline.Preparable, error) { status, err := state.DB.GetStatusByID(ctx, itemID) if err != nil { - return nil, fmt.Errorf("StatusPrepare: error getting status with id %s: %w", itemID, err) + err = gtserror.Newf("error getting status with id %s: %w", itemID, err) + return nil, err } requestingAccount, err := state.DB.GetAccountByID(ctx, accountID) if err != nil { - return nil, fmt.Errorf("StatusPrepare: error getting account with id %s: %w", accountID, err) + err = gtserror.Newf("error getting account with id %s: %w", accountID, err) + return nil, err } return tc.StatusToAPIStatus(ctx, status, requestingAccount) @@ -95,8 +104,8 @@ func HomeTimelineStatusPrepare(state *state.State, tc typeutils.TypeConverter) t func (p *Processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) { statuses, err := p.state.Timelines.Home.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) - if err != nil { - err = fmt.Errorf("HomeTimelineGet: error getting statuses: %w", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("error getting statuses: %w", err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go @@ -20,7 +20,6 @@ package timeline import ( "context" "errors" - "fmt" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -38,14 +37,19 @@ import ( func ListTimelineGrab(state *state.State) timeline.GrabFunction { return func(ctx context.Context, listID string, maxID string, sinceID string, minID string, limit int) ([]timeline.Timelineable, bool, error) { statuses, err := state.DB.GetListTimeline(ctx, listID, maxID, sinceID, minID, limit) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, true, nil // we just don't have enough statuses left in the db so return stop = true - } - return nil, false, fmt.Errorf("ListTimelineGrab: error getting statuses from db: %w", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("error getting statuses from db: %w", err) + return nil, false, err + } + + count := len(statuses) + if count == 0 { + // We just don't have enough statuses + // left in the db so return stop = true. + return nil, true, nil } - items := make([]timeline.Timelineable, len(statuses)) + items := make([]timeline.Timelineable, count) for i, s := range statuses { items[i] = s } @@ -54,27 +58,31 @@ func ListTimelineGrab(state *state.State) timeline.GrabFunction { } } -// HomeTimelineFilter returns a function that satisfies FilterFunction for list timelines. +// ListTimelineFilter returns a function that satisfies FilterFunction for list timelines. func ListTimelineFilter(state *state.State, filter *visibility.Filter) timeline.FilterFunction { return func(ctx context.Context, listID string, item timeline.Timelineable) (shouldIndex bool, err error) { status, ok := item.(*gtsmodel.Status) if !ok { - return false, errors.New("ListTimelineFilter: could not convert item to *gtsmodel.Status") + err = gtserror.New("could not convert item to *gtsmodel.Status") + return false, err } list, err := state.DB.GetListByID(ctx, listID) if err != nil { - return false, fmt.Errorf("ListTimelineFilter: error getting list with id %s: %w", listID, err) + err = gtserror.Newf("error getting list with id %s: %w", listID, err) + return false, err } requestingAccount, err := state.DB.GetAccountByID(ctx, list.AccountID) if err != nil { - return false, fmt.Errorf("ListTimelineFilter: error getting account with id %s: %w", list.AccountID, err) + err = gtserror.Newf("error getting account with id %s: %w", list.AccountID, err) + return false, err } timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status) if err != nil { - return false, fmt.Errorf("ListTimelineFilter: error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err) + err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err) + return false, err } return timelineable, nil @@ -86,17 +94,20 @@ func ListTimelineStatusPrepare(state *state.State, tc typeutils.TypeConverter) t return func(ctx context.Context, listID string, itemID string) (timeline.Preparable, error) { status, err := state.DB.GetStatusByID(ctx, itemID) if err != nil { - return nil, fmt.Errorf("ListTimelineStatusPrepare: error getting status with id %s: %w", itemID, err) + err = gtserror.Newf("error getting status with id %s: %w", itemID, err) + return nil, err } list, err := state.DB.GetListByID(ctx, listID) if err != nil { - return nil, fmt.Errorf("ListTimelineStatusPrepare: error getting list with id %s: %w", listID, err) + err = gtserror.Newf("error getting list with id %s: %w", listID, err) + return nil, err } requestingAccount, err := state.DB.GetAccountByID(ctx, list.AccountID) if err != nil { - return nil, fmt.Errorf("ListTimelineStatusPrepare: error getting account with id %s: %w", list.AccountID, err) + err = gtserror.Newf("error getting account with id %s: %w", list.AccountID, err) + return nil, err } return tc.StatusToAPIStatus(ctx, status, requestingAccount) @@ -114,13 +125,13 @@ func (p *Processor) ListTimelineGet(ctx context.Context, authed *oauth.Auth, lis } if list.AccountID != authed.Account.ID { - err = fmt.Errorf("list with id %s does not belong to account %s", list.ID, authed.Account.ID) + err = gtserror.Newf("list with id %s does not belong to account %s", list.ID, authed.Account.ID) return nil, gtserror.NewErrorNotFound(err) } statuses, err := p.state.Timelines.List.GetTimeline(ctx, listID, maxID, sinceID, minID, limit, false) - if err != nil { - err = fmt.Errorf("ListTimelineGet: error getting statuses: %w", err) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("error getting statuses: %w", err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/timeline/get.go b/internal/timeline/get.go @@ -21,11 +21,11 @@ import ( "container/list" "context" "errors" - "fmt" "time" "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" ) @@ -147,7 +147,7 @@ func (t *timeline) Get(ctx context.Context, amount int, maxID string, sinceID st } default: - err = errors.New("Get: switch statement exhausted with no results") + err = gtserror.New("switch statement exhausted with no results") } return items, err @@ -255,7 +255,8 @@ func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID stri return true, nil } // We've got a proper db error. - return false, fmt.Errorf("getXBetweenIDs: db error while trying to prepare %s: %w", entry.itemID, err) + err = gtserror.Newf("db error while trying to prepare %s: %w", entry.itemID, err) + return false, err } entry.prepared = prepared } @@ -349,7 +350,8 @@ func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID stri continue } // We've got a proper db error. - return nil, fmt.Errorf("getXBetweenIDs: db error while trying to prepare %s: %w", entry.itemID, err) + err = gtserror.Newf("db error while trying to prepare %s: %w", entry.itemID, err) + return nil, err } entry.prepared = prepared } @@ -396,7 +398,7 @@ func (t *timeline) prepareNextQuery(amount int, maxID string, sinceID string, mi case maxID == "" && minID != "": err = t.prepareXBetweenIDs(ctx, amount, id.Highest, minID, false) default: - err = errors.New("Get: switch statement exhausted with no results") + err = gtserror.New("switch statement exhausted with no results") } if err != nil { diff --git a/internal/timeline/get_test.go b/internal/timeline/get_test.go @@ -19,85 +19,19 @@ package timeline_test import ( "context" - "sort" + "sync" "testing" - "time" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/id" - tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline" "github.com/superseriousbusiness/gotosocial/internal/timeline" - "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/gotosocial/testrig" ) type GetTestSuite struct { TimelineStandardTestSuite } -func (suite *GetTestSuite) SetupSuite() { - suite.testAccounts = testrig.NewTestAccounts() - suite.testStatuses = testrig.NewTestStatuses() -} - -func (suite *GetTestSuite) SetupTest() { - suite.state.Caches.Init() - - testrig.InitTestConfig() - testrig.InitTestLog() - - suite.db = testrig.NewTestDB(&suite.state) - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.filter = visibility.NewFilter(&suite.state) - - testrig.StandardDBSetup(suite.db, nil) - - // Take local_account_1 as the timeline owner, it - // doesn't really matter too much for these tests. - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), - ) - - // Put testrig statuses in a determinate order - // since we can't trust a map to keep order. - statuses := []*gtsmodel.Status{} - for _, s := range suite.testStatuses { - statuses = append(statuses, s) - } - - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].ID > statuses[j].ID - }) - - // Statuses are now highest -> lowest. - suite.highestStatusID = statuses[0].ID - suite.lowestStatusID = statuses[len(statuses)-1].ID - if suite.highestStatusID < suite.lowestStatusID { - suite.FailNow("", "statuses weren't ordered properly by sort") - } - - // Put all test statuses into the timeline; we don't - // need to be fussy about who sees what for these tests. - for _, s := range statuses { - _, err := tl.IndexAndPrepareOne(context.Background(), s.GetID(), s.BoostOfID, s.AccountID, s.BoostOfAccountID) - if err != nil { - suite.FailNow(err.Error()) - } - } - - suite.timeline = tl -} - -func (suite *GetTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} - func (suite *GetTestSuite) checkStatuses(statuses []timeline.Preparable, maxID string, minID string, expectedLength int) { if l := len(statuses); l != expectedLength { suite.FailNow("", "expected %d statuses in slice, got %d", expectedLength, l) @@ -127,79 +61,168 @@ func (suite *GetTestSuite) checkStatuses(statuses []timeline.Preparable, maxID s } } +func (suite *GetTestSuite) emptyAccountFollows(ctx context.Context, accountID string) { + // Get all of account's follows. + follows, err := suite.state.DB.GetAccountFollows( + gtscontext.SetBarebones(ctx), + accountID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + // Remove each follow. + for _, follow := range follows { + if err := suite.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil { + suite.FailNow(err.Error()) + } + } + + // Ensure no follows left. + follows, err = suite.state.DB.GetAccountFollows( + gtscontext.SetBarebones(ctx), + accountID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + if len(follows) != 0 { + suite.FailNow("follows should be empty") + } +} + +func (suite *GetTestSuite) emptyAccountStatuses(ctx context.Context, accountID string) { + // Get all of account's statuses. + statuses, err := suite.state.DB.GetAccountStatuses( + ctx, + accountID, + 9999, + false, + false, + id.Highest, + id.Lowest, + false, + false, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + // Remove each status. + for _, status := range statuses { + if err := suite.state.DB.DeleteStatusByID(ctx, status.ID); err != nil { + suite.FailNow(err.Error()) + } + } +} + func (suite *GetTestSuite) TestGetNewTimelinePageDown() { - // Take a fresh timeline for this test. - // This tests whether indexing works - // properly against uninitialized timelines. - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 5 + local = false ) // Get 5 from the top. - statuses, err := tl.Get(context.Background(), 5, "", "", "", true) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } suite.checkStatuses(statuses, id.Highest, id.Lowest, 5) // Get 5 from next maxID. - nextMaxID := statuses[len(statuses)-1].GetID() - statuses, err = tl.Get(context.Background(), 5, nextMaxID, "", "", false) + maxID = statuses[len(statuses)-1].GetID() + statuses, err = suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, nextMaxID, id.Lowest, 5) + suite.checkStatuses(statuses, maxID, id.Lowest, 5) } func (suite *GetTestSuite) TestGetNewTimelinePageUp() { - // Take a fresh timeline for this test. - // This tests whether indexing works - // properly against uninitialized timelines. - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = id.Lowest + limit = 5 + local = false ) // Get 5 from the back. - statuses, err := tl.Get(context.Background(), 5, "", "", id.Lowest, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, id.Highest, id.Lowest, 5) + suite.checkStatuses(statuses, id.Highest, minID, 5) - // Page upwards. - nextMinID := statuses[len(statuses)-1].GetID() - statuses, err = tl.Get(context.Background(), 5, "", "", nextMinID, false) + // Page up from next minID. + minID = statuses[0].GetID() + statuses, err = suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } - suite.checkStatuses(statuses, id.Highest, nextMinID, 5) + suite.checkStatuses(statuses, id.Highest, minID, 5) } func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() { - // Take a fresh timeline for this test. - // This tests whether indexing works - // properly against uninitialized timelines. - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 100 + local = false ) // Get 100 from the top. - statuses, err := tl.Get(context.Background(), 100, id.Highest, "", "", false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -207,29 +230,120 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() { } func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() { - // Take a fresh timeline for this test. - // This tests whether indexing works - // properly against uninitialized timelines. - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = id.Lowest + limit = 100 + local = false ) // Get 100 from the back. - statuses, err := tl.Get(context.Background(), 100, "", "", id.Lowest, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } suite.checkStatuses(statuses, id.Highest, id.Lowest, 16) } +func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 10 + local = false + ) + + suite.emptyAccountFollows(ctx, testAccount.ID) + + // Try to get 10 from the top of the timeline. + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.checkStatuses(statuses, id.Highest, id.Lowest, 5) + + for _, s := range statuses { + if s.GetAccountID() != testAccount.ID { + suite.FailNow("timeline with no follows should only contain posts by timeline owner account") + } + } +} + +func (suite *GetTestSuite) TestGetNewTimelineNoFollowingNoStatuses() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 5 + local = false + ) + + suite.emptyAccountFollows(ctx, testAccount.ID) + suite.emptyAccountStatuses(ctx, testAccount.ID) + + // Try to get 5 from the top of the timeline. + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.checkStatuses(statuses, id.Highest, id.Lowest, 0) +} + func (suite *GetTestSuite) TestGetNoParams() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 10 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Get 10 statuses from the top (no params). - statuses, err := suite.timeline.Get(context.Background(), 10, "", "", "", false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -241,10 +355,28 @@ func (suite *GetTestSuite) TestGetNoParams() { } func (suite *GetTestSuite) TestGetMaxID() { - // Ask for 10 with a max ID somewhere in the middle of the stack. - maxID := "01F8MHBQCBTDKN6X5VHGMMN4MA" + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + sinceID = "" + minID = "" + limit = 10 + local = false + ) - statuses, err := suite.timeline.Get(context.Background(), 10, maxID, "", "", false) + suite.fillTimeline(testAccount.ID) + + // Ask for 10 with a max ID somewhere in the middle of the stack. + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -254,9 +386,28 @@ func (suite *GetTestSuite) TestGetMaxID() { } func (suite *GetTestSuite) TestGetSinceID() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + minID = "" + limit = 10 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 10 with a since ID somewhere in the middle of the stack. - sinceID := "01F8MHBQCBTDKN6X5VHGMMN4MA" - statuses, err := suite.timeline.Get(context.Background(), 10, "", sinceID, "", false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -269,9 +420,28 @@ func (suite *GetTestSuite) TestGetSinceID() { } func (suite *GetTestSuite) TestGetSinceIDOneOnly() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + minID = "" + limit = 1 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 1 with a since ID somewhere in the middle of the stack. - sinceID := "01F8MHBQCBTDKN6X5VHGMMN4MA" - statuses, err := suite.timeline.Get(context.Background(), 1, "", sinceID, "", false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -284,9 +454,28 @@ func (suite *GetTestSuite) TestGetSinceIDOneOnly() { } func (suite *GetTestSuite) TestGetMinID() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + limit = 5 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 5 with a min ID somewhere in the middle of the stack. - minID := "01F8MHBQCBTDKN6X5VHGMMN4MA" - statuses, err := suite.timeline.Get(context.Background(), 5, "", "", minID, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -299,9 +488,28 @@ func (suite *GetTestSuite) TestGetMinID() { } func (suite *GetTestSuite) TestGetMinIDOneOnly() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + limit = 1 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 1 with a min ID somewhere in the middle of the stack. - minID := "01F8MHBQCBTDKN6X5VHGMMN4MA" - statuses, err := suite.timeline.Get(context.Background(), 1, "", "", minID, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -314,9 +522,28 @@ func (suite *GetTestSuite) TestGetMinIDOneOnly() { } func (suite *GetTestSuite) TestGetMinIDFromLowestInTestrig() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = suite.lowestStatusID + limit = 1 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 1 with minID equal to the lowest status in the testrig. - minID := suite.lowestStatusID - statuses, err := suite.timeline.Get(context.Background(), 1, "", "", minID, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -329,9 +556,28 @@ func (suite *GetTestSuite) TestGetMinIDFromLowestInTestrig() { } func (suite *GetTestSuite) TestGetMinIDFromLowestPossible() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = id.Lowest + limit = 1 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 1 with the lowest possible min ID. - minID := id.Lowest - statuses, err := suite.timeline.Get(context.Background(), 1, "", "", minID, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -344,11 +590,28 @@ func (suite *GetTestSuite) TestGetMinIDFromLowestPossible() { } func (suite *GetTestSuite) TestGetBetweenID() { - // Ask for 10 between these two IDs - maxID := "01F8MHCP5P2NWYQ416SBA0XSEV" - minID := "01F8MHBQCBTDKN6X5VHGMMN4MA" + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = "01F8MHCP5P2NWYQ416SBA0XSEV" + sinceID = "" + minID = "01F8MHBQCBTDKN6X5VHGMMN4MA" + limit = 10 + local = false + ) - statuses, err := suite.timeline.Get(context.Background(), 10, maxID, "", minID, false) + suite.fillTimeline(testAccount.ID) + + // Ask for 10 between these two IDs + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -358,12 +621,29 @@ func (suite *GetTestSuite) TestGetBetweenID() { } func (suite *GetTestSuite) TestGetBetweenIDImpossible() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + maxID = id.Lowest + sinceID = "" + minID = id.Highest + limit = 10 + local = false + ) + + suite.fillTimeline(testAccount.ID) + // Ask for 10 between these two IDs which present // an impossible query. - maxID := id.Lowest - minID := id.Highest - - statuses, err := suite.timeline.Get(context.Background(), 10, maxID, "", minID, false) + statuses, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ) if err != nil { suite.FailNow(err.Error()) } @@ -372,18 +652,49 @@ func (suite *GetTestSuite) TestGetBetweenIDImpossible() { suite.checkStatuses(statuses, maxID, minID, 0) } -func (suite *GetTestSuite) TestLastGot() { - // LastGot should be zero - suite.Zero(suite.timeline.LastGot()) +func (suite *GetTestSuite) TestGetTimelinesAsync() { + var ( + ctx = context.Background() + accountToNuke = suite.testAccounts["local_account_1"] + maxID = "" + sinceID = "" + minID = "" + limit = 5 + local = false + multiplier = 5 + ) - // Get some from the top - _, err := suite.timeline.Get(context.Background(), 10, "", "", "", false) - if err != nil { - suite.FailNow(err.Error()) + // Nuke one account's statuses and follows, + // as though the account had just been created. + suite.emptyAccountFollows(ctx, accountToNuke.ID) + suite.emptyAccountStatuses(ctx, accountToNuke.ID) + + // Get 5 statuses from each timeline in + // our testrig at the same time, five times. + wg := new(sync.WaitGroup) + wg.Add(len(suite.testAccounts) * multiplier) + + for i := 0; i < multiplier; i++ { + go func() { + for _, testAccount := range suite.testAccounts { + if _, err := suite.state.Timelines.Home.GetTimeline( + ctx, + testAccount.ID, + maxID, + sinceID, + minID, + limit, + local, + ); err != nil { + suite.FailNow(err.Error()) + } + + wg.Done() + } + }() } - // LastGot should be updated - suite.WithinDuration(time.Now(), suite.timeline.LastGot(), 1*time.Second) + wg.Wait() // Wait until all get calls have returned. } func TestGetTestSuite(t *testing.T) { diff --git a/internal/timeline/index.go b/internal/timeline/index.go @@ -21,10 +21,10 @@ import ( "container/list" "context" "errors" - "fmt" "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" ) @@ -136,7 +136,7 @@ func (t *timeline) indexXBetweenIDs(ctx context.Context, amount int, behindID st } if _, err := t.items.insertIndexed(ctx, entry); err != nil { - return fmt.Errorf("error inserting entry with itemID %s into index: %w", entry.itemID, err) + return gtserror.Newf("error inserting entry with itemID %s into index: %w", entry.itemID, err) } } @@ -237,7 +237,7 @@ func (t *timeline) IndexAndPrepareOne(ctx context.Context, statusID string, boos } if inserted, err := t.items.insertIndexed(ctx, postIndexEntry); err != nil { - return false, fmt.Errorf("IndexAndPrepareOne: error inserting indexed: %w", err) + return false, gtserror.Newf("error inserting indexed: %w", err) } else if !inserted { // Entry wasn't inserted, so // don't bother preparing it. @@ -246,7 +246,7 @@ func (t *timeline) IndexAndPrepareOne(ctx context.Context, statusID string, boos preparable, err := t.prepareFunction(ctx, t.timelineID, statusID) if err != nil { - return true, fmt.Errorf("IndexAndPrepareOne: error preparing: %w", err) + return true, gtserror.Newf("error preparing: %w", err) } postIndexEntry.prepared = preparable diff --git a/internal/timeline/index_test.go b/internal/timeline/index_test.go @@ -24,103 +24,65 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline" - "github.com/superseriousbusiness/gotosocial/internal/timeline" - "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/gotosocial/testrig" ) type IndexTestSuite struct { TimelineStandardTestSuite } -func (suite *IndexTestSuite) SetupSuite() { - suite.testAccounts = testrig.NewTestAccounts() - suite.testStatuses = testrig.NewTestStatuses() -} - -func (suite *IndexTestSuite) SetupTest() { - suite.state.Caches.Init() - - testrig.InitTestLog() - testrig.InitTestConfig() - - suite.db = testrig.NewTestDB(&suite.state) - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.filter = visibility.NewFilter(&suite.state) - - testrig.StandardDBSetup(suite.db, nil) - - // let's take local_account_1 as the timeline owner, and start with an empty timeline - suite.timeline = timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), +func (suite *IndexTestSuite) TestOldestIndexedItemIDEmpty() { + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID ) -} -func (suite *IndexTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} - -func (suite *IndexTestSuite) TestOldestIndexedItemIDEmpty() { // the oldest indexed post should be an empty string since there's nothing indexed yet - postID := suite.timeline.OldestIndexedItemID() + postID := suite.state.Timelines.Home.GetOldestIndexedID(ctx, testAccountID) suite.Empty(postID) // indexLength should be 0 - indexLength := suite.timeline.Len() - suite.Equal(0, indexLength) + suite.Zero(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func (suite *IndexTestSuite) TestIndexAlreadyIndexed() { - testStatus := suite.testStatuses["local_account_1_status_1"] + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + testStatus = suite.testStatuses["local_account_1_status_1"] + ) // index one post -- it should be indexed - indexed, err := suite.timeline.IndexAndPrepareOne(context.Background(), testStatus.ID, testStatus.BoostOfID, testStatus.AccountID, testStatus.BoostOfAccountID) + indexed, err := suite.state.Timelines.Home.IngestOne(ctx, testAccountID, testStatus) suite.NoError(err) suite.True(indexed) // try to index the same post again -- it should not be indexed - indexed, err = suite.timeline.IndexAndPrepareOne(context.Background(), testStatus.ID, testStatus.BoostOfID, testStatus.AccountID, testStatus.BoostOfAccountID) - suite.NoError(err) - suite.False(indexed) -} - -func (suite *IndexTestSuite) TestIndexAndPrepareAlreadyIndexedAndPrepared() { - testStatus := suite.testStatuses["local_account_1_status_1"] - - // index and prepare one post -- it should be indexed - indexed, err := suite.timeline.IndexAndPrepareOne(context.Background(), testStatus.ID, testStatus.BoostOfID, testStatus.AccountID, testStatus.BoostOfAccountID) - suite.NoError(err) - suite.True(indexed) - - // try to index and prepare the same post again -- it should not be indexed - indexed, err = suite.timeline.IndexAndPrepareOne(context.Background(), testStatus.ID, testStatus.BoostOfID, testStatus.AccountID, testStatus.BoostOfAccountID) + indexed, err = suite.state.Timelines.Home.IngestOne(ctx, testAccountID, testStatus) suite.NoError(err) suite.False(indexed) } func (suite *IndexTestSuite) TestIndexBoostOfAlreadyIndexed() { - testStatus := suite.testStatuses["local_account_1_status_1"] - boostOfTestStatus := &gtsmodel.Status{ - CreatedAt: time.Now(), - ID: "01FD4TA6G2Z6M7W8NJQ3K5WXYD", - BoostOfID: testStatus.ID, - AccountID: "01FD4TAY1C0NGEJVE9CCCX7QKS", - BoostOfAccountID: testStatus.AccountID, - } + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + testStatus = suite.testStatuses["local_account_1_status_1"] + boostOfTestStatus = &gtsmodel.Status{ + CreatedAt: time.Now(), + ID: "01FD4TA6G2Z6M7W8NJQ3K5WXYD", + BoostOfID: testStatus.ID, + AccountID: "01FD4TAY1C0NGEJVE9CCCX7QKS", + BoostOfAccountID: testStatus.AccountID, + } + ) // index one post -- it should be indexed - indexed, err := suite.timeline.IndexAndPrepareOne(context.Background(), testStatus.ID, testStatus.BoostOfID, testStatus.AccountID, testStatus.BoostOfAccountID) + indexed, err := suite.state.Timelines.Home.IngestOne(ctx, testAccountID, testStatus) suite.NoError(err) suite.True(indexed) // try to index the a boost of that post -- it should not be indexed - indexed, err = suite.timeline.IndexAndPrepareOne(context.Background(), boostOfTestStatus.ID, boostOfTestStatus.BoostOfID, boostOfTestStatus.AccountID, boostOfTestStatus.BoostOfAccountID) + indexed, err = suite.state.Timelines.Home.IngestOne(ctx, testAccountID, boostOfTestStatus) suite.NoError(err) suite.False(indexed) } diff --git a/internal/timeline/indexeditems.go b/internal/timeline/indexeditems.go @@ -20,7 +20,8 @@ package timeline import ( "container/list" "context" - "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) type indexedItems struct { @@ -84,7 +85,7 @@ func (i *indexedItems) insertIndexed(ctx context.Context, newEntry *indexedItems currentEntry.boostOfAccountID, currentPosition, ); err != nil { - return false, fmt.Errorf("insertIndexed: error calling skipInsert: %w", err) + return false, gtserror.Newf("error calling skipInsert: %w", err) } else if skip { // We don't need to insert this at all, // so we can safely bail. diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go @@ -19,7 +19,6 @@ package timeline import ( "context" - "fmt" "sync" "time" @@ -76,6 +75,9 @@ type Manager interface { // WipeStatusesFromAccountID removes all items by the given accountID from the given timeline. WipeItemsFromAccountID(ctx context.Context, timelineID string, accountID string) error + // Prune manually triggers a prune operation for the given timelineID. + Prune(ctx context.Context, timelineID string, desiredPreparedItemsLength int, desiredIndexedItemsLength int) (int, error) + // Start starts hourly cleanup jobs for this timeline manager. Start() error @@ -191,7 +193,7 @@ func (m *manager) WipeItemFromAllTimelines(ctx context.Context, itemID string) e }) if len(errors) > 0 { - return fmt.Errorf("WipeItemFromAllTimelines: one or more errors wiping status %s: %w", itemID, errors.Combine()) + return gtserror.Newf("one or more errors wiping status %s: %w", itemID, errors.Combine()) } return nil @@ -202,6 +204,10 @@ func (m *manager) WipeItemsFromAccountID(ctx context.Context, timelineID string, return err } +func (m *manager) Prune(ctx context.Context, timelineID string, desiredPreparedItemsLength int, desiredIndexedItemsLength int) (int, error) { + return m.getOrCreateTimeline(ctx, timelineID).Prune(desiredPreparedItemsLength, desiredIndexedItemsLength), nil +} + // getOrCreateTimeline returns a timeline with the given id, // creating a new timeline with that id if necessary. func (m *manager) getOrCreateTimeline(ctx context.Context, timelineID string) Timeline { diff --git a/internal/timeline/manager_test.go b/internal/timeline/manager_test.go @@ -1,134 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package timeline_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" - tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline" - "github.com/superseriousbusiness/gotosocial/internal/timeline" - "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type ManagerTestSuite struct { - TimelineStandardTestSuite -} - -func (suite *ManagerTestSuite) SetupSuite() { - suite.testAccounts = testrig.NewTestAccounts() - suite.testStatuses = testrig.NewTestStatuses() -} - -func (suite *ManagerTestSuite) SetupTest() { - suite.state.Caches.Init() - - testrig.InitTestLog() - testrig.InitTestConfig() - - suite.db = testrig.NewTestDB(&suite.state) - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.filter = visibility.NewFilter(&suite.state) - - testrig.StandardDBSetup(suite.db, nil) - - manager := timeline.NewManager( - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), - ) - suite.manager = manager -} - -func (suite *ManagerTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} - -func (suite *ManagerTestSuite) TestManagerIntegration() { - ctx := context.Background() - - testAccount := suite.testAccounts["local_account_1"] - - // should start at 0 - indexedLen := suite.manager.GetIndexedLength(ctx, testAccount.ID) - suite.Equal(0, indexedLen) - - // oldestIndexed should be empty string since there's nothing indexed - oldestIndexed := suite.manager.GetOldestIndexedID(ctx, testAccount.ID) - suite.Empty(oldestIndexed) - - // get hometimeline - statuses, err := suite.manager.GetTimeline(ctx, testAccount.ID, "", "", "", 20, false) - suite.NoError(err) - suite.Len(statuses, 16) - - // now wipe the last status from all timelines, as though it had been deleted by the owner - err = suite.manager.WipeItemFromAllTimelines(ctx, "01F8MH75CBF9JFX4ZAD54N0W0R") - suite.NoError(err) - - // timeline should be shorter - indexedLen = suite.manager.GetIndexedLength(ctx, testAccount.ID) - suite.Equal(15, indexedLen) - - // oldest should now be different - oldestIndexed = suite.manager.GetOldestIndexedID(ctx, testAccount.ID) - suite.Equal("01F8MH82FYRXD2RC6108DAJ5HB", oldestIndexed) - - // delete the new oldest status specifically from this timeline, as though local_account_1 had muted or blocked it - removed, err := suite.manager.Remove(ctx, testAccount.ID, "01F8MH82FYRXD2RC6108DAJ5HB") - suite.NoError(err) - suite.Equal(1, removed) // 1 status should be removed - - // timeline should be shorter - indexedLen = suite.manager.GetIndexedLength(ctx, testAccount.ID) - suite.Equal(14, indexedLen) - - // oldest should now be different - oldestIndexed = suite.manager.GetOldestIndexedID(ctx, testAccount.ID) - suite.Equal("01F8MHAAY43M6RJ473VQFCVH37", oldestIndexed) - - // now remove all entries by local_account_2 from the timeline - err = suite.manager.WipeItemsFromAccountID(ctx, testAccount.ID, suite.testAccounts["local_account_2"].ID) - suite.NoError(err) - - // timeline should be shorter - indexedLen = suite.manager.GetIndexedLength(ctx, testAccount.ID) - suite.Equal(7, indexedLen) - - // ingest and prepare another one into the timeline - status := suite.testStatuses["local_account_2_status_1"] - ingested, err := suite.manager.IngestOne(ctx, testAccount.ID, status) - suite.NoError(err) - suite.True(ingested) - - // timeline should be longer now - indexedLen = suite.manager.GetIndexedLength(ctx, testAccount.ID) - suite.Equal(8, indexedLen) - - // try to ingest same status again - ingested, err = suite.manager.IngestOne(ctx, testAccount.ID, status) - suite.NoError(err) - suite.False(ingested) // should be false since it's a duplicate -} - -func TestManagerTestSuite(t *testing.T) { - suite.Run(t, new(ManagerTestSuite)) -} diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go @@ -21,10 +21,10 @@ import ( "container/list" "context" "errors" - "fmt" "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" ) @@ -129,7 +129,7 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID t.items.data.Remove(e) } // We've got a proper db error. - return fmt.Errorf("prepareXBetweenIDs: db error while trying to prepare %s: %w", entry.itemID, err) + return gtserror.Newf("db error while trying to prepare %s: %w", entry.itemID, err) } entry.prepared = prepared } diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go @@ -19,98 +19,83 @@ package timeline_test import ( "context" - "sort" "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline" - "github.com/superseriousbusiness/gotosocial/internal/timeline" - "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/gotosocial/testrig" ) type PruneTestSuite struct { TimelineStandardTestSuite } -func (suite *PruneTestSuite) SetupSuite() { - suite.testAccounts = testrig.NewTestAccounts() - suite.testStatuses = testrig.NewTestStatuses() -} - -func (suite *PruneTestSuite) SetupTest() { - suite.state.Caches.Init() - - testrig.InitTestLog() - testrig.InitTestConfig() - - suite.db = testrig.NewTestDB(&suite.state) - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.filter = visibility.NewFilter(&suite.state) - - testrig.StandardDBSetup(suite.db, nil) - - // let's take local_account_1 as the timeline owner - tl := timeline.NewTimeline( - context.Background(), - suite.testAccounts["local_account_1"].ID, - tlprocessor.HomeTimelineGrab(&suite.state), - tlprocessor.HomeTimelineFilter(&suite.state, suite.filter), - tlprocessor.HomeTimelineStatusPrepare(&suite.state, suite.tc), - tlprocessor.SkipInsert(), +func (suite *PruneTestSuite) TestPrune() { + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + desiredPreparedItemsLength = 5 + desiredIndexedItemsLength = 5 ) - // put the status IDs in a determinate order since we can't trust a map to keep its order - statuses := []*gtsmodel.Status{} - for _, s := range suite.testStatuses { - statuses = append(statuses, s) - } - sort.Slice(statuses, func(i, j int) bool { - return statuses[i].ID > statuses[j].ID - }) - - // prepare the timeline by just shoving all test statuses in it -- let's not be fussy about who sees what - for _, s := range statuses { - _, err := tl.IndexAndPrepareOne(context.Background(), s.GetID(), s.BoostOfID, s.AccountID, s.BoostOfAccountID) - if err != nil { - suite.FailNow(err.Error()) - } - } - - suite.timeline = tl -} - -func (suite *PruneTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} + suite.fillTimeline(testAccountID) -func (suite *PruneTestSuite) TestPrune() { - // prune down to 5 prepared + 5 indexed - suite.Equal(12, suite.timeline.Prune(5, 5)) - suite.Equal(5, suite.timeline.Len()) + pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) + suite.NoError(err) + suite.Equal(12, pruned) + suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func (suite *PruneTestSuite) TestPruneTwice() { - // prune down to 5 prepared + 10 indexed - suite.Equal(12, suite.timeline.Prune(5, 10)) - suite.Equal(10, suite.timeline.Len()) + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + desiredPreparedItemsLength = 5 + desiredIndexedItemsLength = 5 + ) + + suite.fillTimeline(testAccountID) + + pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) + suite.NoError(err) + suite.Equal(12, pruned) + suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) // Prune same again, nothing should be pruned this time. - suite.Zero(suite.timeline.Prune(5, 10)) - suite.Equal(10, suite.timeline.Len()) + pruned, err = suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) + suite.NoError(err) + suite.Equal(0, pruned) + suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func (suite *PruneTestSuite) TestPruneTo0() { - // prune down to 0 prepared + 0 indexed - suite.Equal(17, suite.timeline.Prune(0, 0)) - suite.Equal(0, suite.timeline.Len()) + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + desiredPreparedItemsLength = 0 + desiredIndexedItemsLength = 0 + ) + + suite.fillTimeline(testAccountID) + + pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) + suite.NoError(err) + suite.Equal(17, pruned) + suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() { - // prune to 99999, this should result in no entries being pruned - suite.Equal(0, suite.timeline.Prune(99999, 99999)) - suite.Equal(17, suite.timeline.Len()) + var ( + ctx = context.Background() + testAccountID = suite.testAccounts["local_account_1"].ID + desiredPreparedItemsLength = 9999999 + desiredIndexedItemsLength = 9999999 + ) + + suite.fillTimeline(testAccountID) + + pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength) + suite.NoError(err) + suite.Equal(0, pruned) + suite.Equal(17, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func TestPruneTestSuite(t *testing.T) { diff --git a/internal/timeline/timeline_test.go b/internal/timeline/timeline_test.go @@ -18,27 +18,80 @@ package timeline_test import ( + "context" + "sort" + "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/timeline" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" + "github.com/superseriousbusiness/gotosocial/testrig" ) type TimelineStandardTestSuite struct { suite.Suite - db db.DB - state state.State - tc typeutils.TypeConverter - filter *visibility.Filter + state *state.State testAccounts map[string]*gtsmodel.Account testStatuses map[string]*gtsmodel.Status highestStatusID string lowestStatusID string +} + +func (suite *TimelineStandardTestSuite) SetupSuite() { + suite.testAccounts = testrig.NewTestAccounts() + suite.testStatuses = testrig.NewTestStatuses() +} + +func (suite *TimelineStandardTestSuite) SetupTest() { + suite.state = new(state.State) + + suite.state.Caches.Init() + testrig.StartWorkers(suite.state) + + testrig.InitTestConfig() + testrig.InitTestLog() + + suite.state.DB = testrig.NewTestDB(suite.state) + + testrig.StartTimelines( + suite.state, + visibility.NewFilter(suite.state), + testrig.NewTestTypeConverter(suite.state.DB), + ) + + testrig.StandardDBSetup(suite.state.DB, nil) +} + +func (suite *TimelineStandardTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.state.DB) + testrig.StopWorkers(suite.state) +} + +func (suite *TimelineStandardTestSuite) fillTimeline(timelineID string) { + // Put testrig statuses in a determinate order + // since we can't trust a map to keep order. + statuses := []*gtsmodel.Status{} + for _, s := range suite.testStatuses { + statuses = append(statuses, s) + } + + sort.Slice(statuses, func(i, j int) bool { + return statuses[i].ID > statuses[j].ID + }) + + // Statuses are now highest -> lowest. + suite.highestStatusID = statuses[0].ID + suite.lowestStatusID = statuses[len(statuses)-1].ID + if suite.highestStatusID < suite.lowestStatusID { + suite.FailNow("", "statuses weren't ordered properly by sort") + } - timeline timeline.Timeline - manager timeline.Manager + // Put all test statuses into the timeline; we don't + // need to be fussy about who sees what for these tests. + for _, status := range statuses { + if _, err := suite.state.Timelines.Home.IngestOne(context.Background(), timelineID, status); err != nil { + suite.FailNow(err.Error()) + } + } }