Skip to content

Commit 8726212

Browse files
devinbinnieagarciamontoroagnivade
authored
[MM-55728] Add user reporting load tests (#675)
* Add user reporting load tests * Oops * Update MM server * Update model * Update loadtest/control/simulcontroller/actions.go Co-authored-by: Alejandro García Montoro <[email protected]> * PR feedback * Lint * Add config, specify number of admin users * Fix test * Fix docs * Fix test * fix detecting admin user * add comment --------- Co-authored-by: Alejandro García Montoro <[email protected]> Co-authored-by: Agniva De Sarker <[email protected]>
1 parent 88df7c5 commit 8726212

File tree

10 files changed

+112
-12
lines changed

10 files changed

+112
-12
lines changed

api/agent.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,17 +365,26 @@ func NewControllerWrapper(config *loadtest.Config, controllerConfig interface{},
365365
return nil, err
366366
}
367367

368+
modAdmins := 0
369+
if config.UsersConfiguration.PercentOfUsersAreAdmin > 0 {
370+
modAdmins = int(1 / config.UsersConfiguration.PercentOfUsersAreAdmin)
371+
}
372+
368373
return func(id int, status chan<- control.UserStatus) (control.UserController, error) {
369374
id += userOffset
370375

371376
username := fmt.Sprintf("%s-%d", namePrefix, id)
372377
email := fmt.Sprintf("%s-%[email protected]", namePrefix, id)
373378
password := "testPass123$"
374379

375-
// If UsersFilePath was set, and we haven't yet consumed all of the credentials
376-
// provided there, ovewrite this user's credentials with the next available
377-
// user in that file
378-
if len(creds) > 0 && id < len(creds) {
380+
if modAdmins > 0 && id%modAdmins == 0 {
381+
username = ""
382+
email = config.ConnectionConfiguration.AdminEmail
383+
password = config.ConnectionConfiguration.AdminPassword
384+
} else if len(creds) > 0 && id < len(creds) {
385+
// If UsersFilePath was set, and we haven't yet consumed all of the credentials
386+
// provided there, ovewrite this user's credentials with the next available
387+
// user in that file
379388
username = creds[id].username
380389
email = creds[id].email
381390
password = creds[id].password

config/config.sample.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"InitialActiveUsers": 0,
5050
"UsersFilePath": "",
5151
"MaxActiveUsers": 2000,
52-
"AvgSessionsPerUser": 1
52+
"AvgSessionsPerUser": 1,
53+
"PercentOfUsersAreAdmin": 0.02
5354
},
5455
"LogSettings": {
5556
"EnableConsole": true,

docs/config/config.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ The path to the file which contains a list of user email and passwords that will
122122

123123
The maximum amount of concurrently active users the load-test agent will run.
124124

125+
### PercentOfUsersAreAdmin
126+
127+
*float*
128+
129+
The percentage of users generated that will be system admins.
130+
125131
## LogSettings
126132

127133
### EnableConsole

loadtest/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ type UsersConfiguration struct {
135135
MaxActiveUsers int `default:"2000" validate:"range:(0,]"`
136136
// The average number of sessions per user.
137137
AvgSessionsPerUser int `default:"1" validate:"range:[1,]"`
138+
// The percentage of users generated that will be system admins
139+
PercentOfUsersAreAdmin float64 `default:"0.02" validate:"range:[0,1]"`
138140
}
139141

140142
// Config holds information needed to create and initialize a new load-test

loadtest/control/simulcontroller/actions.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,3 +2073,48 @@ func (c *SimulController) openPermalink(u user.User) control.UserActionResponse
20732073

20742074
return control.UserActionResponse{Info: fmt.Sprintf("clicked permalink on post %s", postID)}
20752075
}
2076+
2077+
func (c *SimulController) generateUserReport(u user.User) control.UserActionResponse {
2078+
// We are detecting the sysadmin here, and not in the actions slice initialization,
2079+
// because the roles of a user aren't initialized until the user logs in.
2080+
// See https://github.com/mattermost/mattermost-load-test-ng/pull/675 for more context.
2081+
isAdmin, err := u.IsSysAdmin()
2082+
if err != nil {
2083+
return control.UserActionResponse{Err: control.NewUserError(err)}
2084+
}
2085+
if !isAdmin {
2086+
return control.UserActionResponse{Info: "User is not an admin. Skipping from generating user report."}
2087+
}
2088+
2089+
// Simulate scrolling through the entire list of users
2090+
// (should be similar to generating the complete report and exporting it)
2091+
pageSize := 50
2092+
lastColumnValue := ""
2093+
lastId := ""
2094+
totalUsers := 0
2095+
2096+
for {
2097+
report, err := u.GetUsersForReporting(&model.UserReportOptionsAPI{
2098+
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
2099+
SortColumn: "Username",
2100+
PageSize: pageSize,
2101+
LastSortColumnValue: lastColumnValue,
2102+
LastUserId: lastId,
2103+
},
2104+
})
2105+
2106+
if err != nil {
2107+
return control.UserActionResponse{Err: control.NewUserError(err)}
2108+
}
2109+
2110+
totalUsers += len(report)
2111+
if len(report) < pageSize {
2112+
break
2113+
}
2114+
2115+
lastColumnValue = report[len(report)-1].Username
2116+
lastId = report[len(report)-1].Id
2117+
}
2118+
2119+
return control.UserActionResponse{Info: fmt.Sprintf("generated user report for %d users", totalUsers)}
2120+
}

loadtest/control/simulcontroller/controller.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
func getActionList(c *SimulController) []userAction {
19-
return []userAction{
19+
actions := []userAction{
2020
{
2121
name: "SwitchChannel",
2222
run: switchChannel,
@@ -202,7 +202,14 @@ func getActionList(c *SimulController) []userAction {
202202
run: c.reconnectWebSocket,
203203
frequency: 0.144,
204204
},
205+
{
206+
name: "GenerateUserReport",
207+
run: c.generateUserReport,
208+
frequency: 0.0001,
209+
},
205210
}
211+
212+
return actions
206213
}
207214

208215
func getActionMap(actionList []userAction) map[string]userAction {

loadtest/control/simulcontroller/controller_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ func TestNew(t *testing.T) {
1717
require.NoError(t, err)
1818
require.NotNil(t, config)
1919

20-
c, err := New(1, &userentity.UserEntity{}, config, make(chan control.UserStatus))
20+
store, err := memstore.New(nil)
21+
require.NotNil(t, store)
22+
require.NoError(t, err)
23+
24+
user := userentity.New(userentity.Setup{Store: store}, userentity.Config{
25+
ServerURL: "http://localhost:8065",
26+
WebSocketURL: "ws://localhost:8065",
27+
})
28+
29+
c, err := New(1, user, config, make(chan control.UserStatus))
2130
require.Nil(t, err)
2231

2332
require.Equal(t, len(c.actionList), len(c.actionMap))
@@ -28,7 +37,16 @@ func TestSetRate(t *testing.T) {
2837
require.NoError(t, err)
2938
require.NotNil(t, config)
3039

31-
c, err := New(1, &userentity.UserEntity{}, config, make(chan control.UserStatus))
40+
store, err := memstore.New(nil)
41+
require.NotNil(t, store)
42+
require.NoError(t, err)
43+
44+
user := userentity.New(userentity.Setup{Store: store}, userentity.Config{
45+
ServerURL: "http://localhost:8065",
46+
WebSocketURL: "ws://localhost:8065",
47+
})
48+
49+
c, err := New(1, user, config, make(chan control.UserStatus))
3250
require.Nil(t, err)
3351
require.Equal(t, 1.0, c.rate)
3452

loadtest/user/user.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ type User interface {
9292
GetUsers(page, perPage int) ([]string, error)
9393
// GetUsersNotInChannel returns a list of user ids not in a given channel.
9494
GetUsersNotInChannel(teamId, channelId string, page, perPage int) ([]string, error)
95+
// GetUsersForReporting returns a list of users in a report format
96+
GetUsersForReporting(options *model.UserReportOptionsAPI) ([]*model.UserReport, error)
9597

9698
// SetProfileImage sets the profile image for the user.
9799
SetProfileImage(data []byte) error

loadtest/user/userentity/actions.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,15 @@ func (ue *UserEntity) GetChannelsAndChannelMembersGQL(teamID string, includeDele
15211521
return chCursor, cmCursor, nil
15221522
}
15231523

1524+
func (ue *UserEntity) GetUsersForReporting(options *model.UserReportOptionsAPI) ([]*model.UserReport, error) {
1525+
report, _, err := ue.client.GetUsersForReporting(context.Background(), options)
1526+
if err != nil {
1527+
return nil, err
1528+
}
1529+
1530+
return report, nil
1531+
}
1532+
15241533
func (ue *UserEntity) prepareRequest(method, url string, data io.Reader, headers map[string]string) (*http.Request, error) {
15251534
rq, err := http.NewRequest(method, url, data)
15261535
if err != nil {

loadtest/user/userentity/helper_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ type config struct {
4646
PercentGroupChannels float64 `default:"0.1" validate:"range:[0,1]"`
4747
}
4848
UsersConfiguration struct {
49-
UsersFilePath string
50-
InitialActiveUsers int `default:"0" validate:"range:[0,$MaxActiveUsers]"`
51-
MaxActiveUsers int `default:"2000" validate:"range:(0,]"`
52-
AvgSessionsPerUser int `default:"1" validate:"range:[1,]"`
49+
UsersFilePath string
50+
InitialActiveUsers int `default:"0" validate:"range:[0,$MaxActiveUsers]"`
51+
MaxActiveUsers int `default:"2000" validate:"range:(0,]"`
52+
AvgSessionsPerUser int `default:"1" validate:"range:[1,]"`
53+
PercentOfUsersAreAdmin float64 `default:"0.02" validate:"range:[0,1]"`
5354
}
5455
LogSettings logger.Settings
5556
}

0 commit comments

Comments
 (0)