sliding-sync/tests-e2e/main_test.go

234 lines
7.0 KiB
Go
Raw Normal View History

package syncv3_test
import (
2022-07-26 11:39:19 +01:00
"encoding/json"
"fmt"
"os"
2022-07-26 11:39:19 +01:00
"reflect"
2023-08-02 10:30:29 +01:00
"sort"
"strings"
2022-07-26 11:39:19 +01:00
"sync/atomic"
"testing"
2022-07-26 11:39:19 +01:00
"time"
2023-10-11 12:23:46 +01:00
"github.com/matrix-org/complement/client"
"github.com/matrix-org/sliding-sync/sync3"
"github.com/matrix-org/sliding-sync/testutils/m"
2023-08-02 10:30:29 +01:00
"github.com/tidwall/gjson"
)
var (
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
proxyBaseURL = os.Getenv("SYNCV3_ADDR")
homeserverBaseURL = os.Getenv("SYNCV3_SERVER")
2022-07-26 11:39:19 +01:00
userCounter uint64
)
func TestMain(m *testing.M) {
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
if proxyBaseURL == "" {
fmt.Println("SYNCV3_ADDR must be set e.g 'http://localhost:8008'")
os.Exit(1)
}
fmt.Println("proxy located at", proxyBaseURL)
exitCode := m.Run()
os.Exit(exitCode)
}
2022-07-26 11:39:19 +01:00
func assertEventsEqual(t *testing.T, wantList []Event, gotList []json.RawMessage) {
t.Helper()
err := eventsEqual(wantList, gotList)
if err != nil {
t.Errorf(err.Error())
}
}
func eventsEqual(wantList []Event, gotList []json.RawMessage) error {
2022-07-26 11:39:19 +01:00
if len(wantList) != len(gotList) {
return fmt.Errorf("got %d events, want %d", len(gotList), len(wantList))
2022-07-26 11:39:19 +01:00
}
for i := 0; i < len(wantList); i++ {
want := wantList[i]
var got Event
if err := json.Unmarshal(gotList[i], &got); err != nil {
return fmt.Errorf("failed to unmarshal event %d: %s", i, err)
2022-07-26 11:39:19 +01:00
}
if want.ID != "" && got.ID != want.ID {
return fmt.Errorf("event %d ID mismatch: got %v want %v", i, got.ID, want.ID)
2022-07-26 11:39:19 +01:00
}
if want.Content != nil && !reflect.DeepEqual(got.Content, want.Content) {
return fmt.Errorf("event %d content mismatch: got %+v want %+v", i, got.Content, want.Content)
2022-07-26 11:39:19 +01:00
}
if want.Type != "" && want.Type != got.Type {
return fmt.Errorf("event %d Type mismatch: got %v want %v", i, got.Type, want.Type)
2022-07-26 11:39:19 +01:00
}
if want.StateKey != nil {
if got.StateKey == nil {
return fmt.Errorf("event %d StateKey mismatch: want %v got <nil>", i, *want.StateKey)
2022-07-26 11:39:19 +01:00
} else if *want.StateKey != *got.StateKey {
return fmt.Errorf("event %d StateKey mismatch: got %v want %v", i, *got.StateKey, *want.StateKey)
2022-07-26 11:39:19 +01:00
}
}
if want.Sender != "" && want.Sender != got.Sender {
return fmt.Errorf("event %d Sender mismatch: got %v want %v", i, got.Sender, want.Sender)
}
// loop each key on unsigned as unsigned also includes "age" which is unpredictable so cannot DeepEqual
if want.Unsigned != nil {
for k, v := range want.Unsigned {
got := got.Unsigned[k]
if !reflect.DeepEqual(got, v) {
return fmt.Errorf("event %d Unsigned.%s mismatch: got %v want %v", i, k, got, v)
}
}
}
}
return nil
}
// MatchRoomTimelineMostRecent builds a matcher which checks that the last `n` elements
// of `events` are the same as the last n elements of the room timeline. If either list
// contains fewer than `n` events, the match fails.
// Events are tested for equality using `eventsEqual`.
func MatchRoomTimelineMostRecent(n int, events []Event) m.RoomMatcher {
return func(r sync3.Room) error {
if len(events) < n {
return fmt.Errorf("list of wanted events has %d events, expected at least %d", len(events), n)
}
wantList := events[len(events)-n:]
if len(r.Timeline) < n {
return fmt.Errorf("timeline has %d events, expected at least %d", len(r.Timeline), n)
}
gotList := r.Timeline[len(r.Timeline)-n:]
return eventsEqual(wantList, gotList)
}
}
func MatchRoomTimeline(events []Event) m.RoomMatcher {
return func(r sync3.Room) error {
return eventsEqual(events, r.Timeline)
}
}
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
func MatchRoomTimelineContains(event Event) m.RoomMatcher {
return func(r sync3.Room) error {
var err error
for _, got := range r.Timeline {
if err = eventsEqual([]Event{event}, []json.RawMessage{got}); err == nil {
return nil
}
}
return err
}
}
func MatchRoomRequiredState(events []Event) m.RoomMatcher {
return func(r sync3.Room) error {
// allow any ordering for required state
for _, want := range events {
found := false
for _, got := range r.RequiredState {
if err := eventsEqual([]Event{want}, []json.RawMessage{got}); err == nil {
found = true
break
}
}
if !found {
return fmt.Errorf("required state want event %+v but did not find exact match", want)
}
2022-07-26 11:39:19 +01:00
}
return nil
2022-07-26 11:39:19 +01:00
}
}
func MatchRoomRequiredStateStrict(events []Event) m.RoomMatcher {
return func(r sync3.Room) error {
if len(r.RequiredState) != len(events) {
return fmt.Errorf("required state length mismatch, got %d want %d", len(r.RequiredState), len(events))
}
return MatchRoomRequiredState(events)(r)
}
}
func MatchRoomInviteState(events []Event, partial bool) m.RoomMatcher {
return func(r sync3.Room) error {
if !partial && len(r.InviteState) != len(events) {
return fmt.Errorf("MatchRoomInviteState: length mismatch, got %d want %d", len(r.InviteState), len(events))
}
// allow any ordering for state
for _, want := range events {
found := false
for _, got := range r.InviteState {
if err := eventsEqual([]Event{want}, []json.RawMessage{got}); err == nil {
found = true
break
}
}
if !found {
return fmt.Errorf("MatchRoomInviteState: want event %+v but it does not exist or failed to pass equality checks", want)
}
}
return nil
}
}
2023-08-02 10:30:29 +01:00
// MatchGlobalAccountData builds a matcher which asserts that the account data in a sync
// response matches the given `globals`, with any ordering.
// If there is no account data extension in the response, the match fails.
func MatchGlobalAccountData(globals []Event) m.RespMatcher {
// sort want list by type
sort.Slice(globals, func(i, j int) bool {
return globals[i].Type < globals[j].Type
})
return func(res *sync3.Response) error {
if res.Extensions.AccountData == nil {
return fmt.Errorf("MatchGlobalAccountData: no account_data extension")
}
if len(globals) != len(res.Extensions.AccountData.Global) {
return fmt.Errorf("MatchGlobalAccountData: got %v global account data, want %v", len(res.Extensions.AccountData.Global), len(globals))
}
// sort the got list by type
got := res.Extensions.AccountData.Global
sort.Slice(got, func(i, j int) bool {
return gjson.GetBytes(got[i], "type").Str < gjson.GetBytes(got[j], "type").Str
})
if err := eventsEqual(globals, got); err != nil {
return fmt.Errorf("MatchGlobalAccountData: %s", err)
}
return nil
}
}
2022-07-26 11:39:19 +01:00
func registerNewUser(t *testing.T) *CSAPI {
2023-05-24 18:00:52 +01:00
return registerNamedUser(t, "user")
}
func registerNamedUser(t *testing.T, localpartPrefix string) *CSAPI {
2022-07-26 11:39:19 +01:00
// create user
2023-05-24 18:00:52 +01:00
localpart := fmt.Sprintf("%s-%d-%d", localpartPrefix, time.Now().Unix(), atomic.AddUint64(&userCounter, 1))
2023-10-11 12:23:46 +01:00
httpClient := client.NewLoggedClient(t, "localhost", nil)
2022-07-26 11:39:19 +01:00
client := &CSAPI{
2023-10-11 12:23:46 +01:00
CSAPI: &client.CSAPI{
Client: httpClient,
BaseURL: homeserverBaseURL,
SyncUntilTimeout: 3 * time.Second,
},
2022-07-26 11:39:19 +01:00
}
client.UserID, client.AccessToken, client.DeviceID = client.RegisterUser(t, localpart, "password")
2023-09-11 20:16:12 +01:00
parts := strings.Split(client.UserID, ":")
client.Localpart = parts[0][1:]
client.Domain = strings.Split(client.UserID, ":")[1]
2022-07-26 11:39:19 +01:00
return client
}
func ptr(s string) *string {
return &s
}
func assertEqual(t *testing.T, msg string, got, want interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Fatalf("%s: got %v want %v", msg, got, want)
}
}