diff options
| -rw-r--r-- | internal/processing/timeline/home.go | 37 | ||||
| -rw-r--r-- | internal/processing/timeline/list.go | 47 | ||||
| -rw-r--r-- | internal/timeline/get.go | 12 | ||||
| -rw-r--r-- | internal/timeline/get_test.go | 617 | ||||
| -rw-r--r-- | internal/timeline/index.go | 8 | ||||
| -rw-r--r-- | internal/timeline/index_test.go | 92 | ||||
| -rw-r--r-- | internal/timeline/indexeditems.go | 5 | ||||
| -rw-r--r-- | internal/timeline/manager.go | 10 | ||||
| -rw-r--r-- | internal/timeline/manager_test.go | 134 | ||||
| -rw-r--r-- | internal/timeline/prepare.go | 4 | ||||
| -rw-r--r-- | internal/timeline/prune_test.go | 123 | ||||
| -rw-r--r-- | internal/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 index e65f12e17..807189495 100644 --- 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 index adad35197..830c874a9 100644 --- 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 index a93e2d1ed..bc238c276 100644 --- 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 index f99e58611..7b31ec977 100644 --- 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 index 2d556a3b2..993f7dc5d 100644 --- 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 index f62c0a9c6..a7eeebb6e 100644 --- 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 := >smodel.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 = >smodel.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 index ff36a66d2..c2c7a19b6 100644 --- 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 index b70e6cf82..95a40aca1 100644 --- 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 deleted file mode 100644 index 652708ccd..000000000 --- a/internal/timeline/manager_test.go +++ /dev/null @@ -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 index 8fbcb15a7..81d0ddc38 100644 --- 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 index d70e1eb91..f9f3ccc51 100644 --- 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 index fb1859fc4..2dc17e963 100644 --- 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()) +		} +	}  } | 
