Skip to content

fix(coderd): use stable sorting for insights and improve test coverage #9250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ffc7391
test(coderd): add extended template insights test suite
mafredri Aug 22, 2023
95ad848
add initial golden files
mafredri Aug 22, 2023
d4d58cf
add some more usage and verify golden
mafredri Aug 22, 2023
e4f901a
fixes
mafredri Aug 22, 2023
f333170
add an hour of app usage
mafredri Aug 22, 2023
6fb49a9
add 15 min of app usage -> active user on last day
mafredri Aug 22, 2023
e9a967e
add placeholder data
mafredri Aug 22, 2023
9879462
add identical usage in two templates for one user
mafredri Aug 22, 2023
fc1a98d
test app merging
mafredri Aug 22, 2023
f14ddf6
fix(codersdk): use url values for query params
mafredri Aug 22, 2023
8a60568
improve stability of template id
mafredri Aug 22, 2023
7d93d13
add sao paulo tz test (invalid interval times)
mafredri Aug 22, 2023
f9fdb47
fix start/end time tz returned in interval report
mafredri Aug 22, 2023
54638b0
cleanup
mafredri Aug 22, 2023
7b0ac4c
refactor template creation with stable IDs
mafredri Aug 23, 2023
1e7d860
add parameter test
mafredri Aug 23, 2023
9d96052
refactor
mafredri Aug 23, 2023
930d7ef
add parameter no data test
mafredri Aug 23, 2023
07cdc0b
add 1h usage to other user
mafredri Aug 23, 2023
73367d1
add usage outside of utc timeframe
mafredri Aug 23, 2023
c2a0496
add third user with ssh and web term usage
mafredri Aug 23, 2023
b738799
add third user app usage
mafredri Aug 23, 2023
79bdbf9
add webterminal app usage
mafredri Aug 23, 2023
ce2bb78
use stable sorting in Go and minor query tweaks and fakedb fixes
mafredri Aug 23, 2023
0896439
use github.com/google/go-cmp/cmp for diffs
mafredri Aug 23, 2023
5b4733a
restructure
mafredri Aug 23, 2023
3607a80
more fakedb fixes
mafredri Aug 23, 2023
688e1d0
fix fakedb logic bugs
mafredri Aug 23, 2023
8c294d0
fix commented code
mafredri Aug 23, 2023
decbdfe
fix test bug
mafredri Aug 24, 2023
870aff1
add test for tpl1 param4 option2
mafredri Aug 24, 2023
75a96d7
add outside timeframe tests for apps
mafredri Aug 24, 2023
e72e265
remove previous insights test in favor of new one
mafredri Aug 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor template creation with stable IDs
  • Loading branch information
mafredri committed Aug 23, 2023
commit 7b0ac4cc85786d6ec2db0aa15db070da723487d9
1 change: 1 addition & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion coderd/database/queries/insights.sql
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ SELECT
is_app,
SUM(seconds) AS usage_seconds
FROM app_stats_by_user_and_agent
GROUP BY access_method, slug_or_port, display_name, icon, is_app;
GROUP BY access_method, slug_or_port, display_name, icon, is_app
ORDER BY access_method, slug_or_port, display_name, icon, is_app;

-- name: GetTemplateDailyInsights :many
-- GetTemplateDailyInsights returns all daily intervals between start and end
Expand Down
164 changes: 84 additions & 80 deletions coderd/insights_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import (
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/coderd/batchstats"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
Expand Down Expand Up @@ -580,19 +582,6 @@ func TestTemplateInsights(t *testing.T) {
func TestTemplateInsights_Golden(t *testing.T) {
t.Parallel()

stabilizeReportForGoldenComparison := func(report *codersdk.TemplateInsightsResponse, toStableTemplateIDs func([]uuid.UUID)) {
toStableTemplateIDs(report.Report.TemplateIDs)
for _, param := range report.Report.ParametersUsage {
toStableTemplateIDs(param.TemplateIDs)
}
for _, app := range report.Report.AppsUsage {
toStableTemplateIDs(app.TemplateIDs)
}
for _, intervalReport := range report.IntervalReports {
toStableTemplateIDs(intervalReport.TemplateIDs)
}
}

// Prepare test data types.
type templateParameterOption struct {
name string
Expand All @@ -613,7 +602,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
apps []templateApp

// Filled later.
id uuid.UUID // Set to the created template ID.
id uuid.UUID
}
type buildParameter struct {
templateParameter *templateParameter
Expand Down Expand Up @@ -778,7 +767,17 @@ func TestTemplateInsights_Golden(t *testing.T) {
},
}

// Post-process workspaces.
// Post-process.
var stableIDs []uuid.UUID
newStableUUID := func() uuid.UUID {
stableIDs = append(stableIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableIDs)+1)))
stableID := stableIDs[len(stableIDs)-1]
return stableID
}

for _, template := range templates {
template.id = newStableUUID()
}
for _, user := range users {
for _, workspace := range user.workspaces {
workspace.user = user
Expand All @@ -792,14 +791,16 @@ func TestTemplateInsights_Golden(t *testing.T) {
return templates, users
}

prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, func([]uuid.UUID)) {
prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) *codersdk.Client {
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
opts := &coderdtest.Options{
db, pubsub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
Logger: &logger,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Hour, // Not relevant for this test.
}
client, _, coderdAPI := coderdtest.NewWithAPI(t, opts)
})
firstUser := coderdtest.CreateFirstUser(t, client)

// Prepare all test users.
Expand Down Expand Up @@ -926,11 +927,28 @@ func TestTemplateInsights_Golden(t *testing.T) {
},
}},
})
createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
require.Empty(t, createdTemplate.BuildTimeStats[codersdk.WorkspaceTransitionStart])
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)

template.id = createdTemplate.ID
// Create template, essentially a modified version of CreateTemplate
// where we can control the template ID.
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
createdTemplate := dbgen.Template(t, db, database.Template{
ID: template.id,
ActiveVersionID: version.ID,
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
},
})
err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{
ID: version.ID,
TemplateID: uuid.NullUUID{
UUID: createdTemplate.ID,
Valid: true,
},
})
require.NoError(t, err, "want no error updating template version")

// Create all workspaces and wait for them.
for _, createWorkspace := range createWorkspaces {
Expand All @@ -949,7 +967,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
// the database.
batcher, batcherCloser, err := batchstats.New(
ctx,
batchstats.WithStore(coderdAPI.Database),
batchstats.WithStore(db),
batchstats.WithLogger(logger.Named("batchstats")),
batchstats.WithInterval(time.Hour),
)
Expand Down Expand Up @@ -1000,80 +1018,57 @@ func TestTemplateInsights_Golden(t *testing.T) {
})
}
}
reporter := workspaceapps.NewStatsDBReporter(coderdAPI.Database, workspaceapps.DefaultStatsDBReporterBatchSize)
reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize)
//nolint:gocritic // This is a test.
err = reporter.Report(dbauthz.AsSystemRestricted(ctx), stats)
require.NoError(t, err, "want no error inserting app stats")

var stableTemplateIDs []uuid.UUID
stableTemplateIDMap := make(map[uuid.UUID]uuid.UUID)
toStableTemplateID := func(id uuid.UUID) uuid.UUID {
if stableID, ok := stableTemplateIDMap[id]; ok {
return stableID
}
stableTemplateIDs = append(stableTemplateIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableTemplateIDs)+1)))
stableID := stableTemplateIDs[len(stableTemplateIDs)-1]
stableTemplateIDMap[id] = stableID
return stableID
}
// Prime the map.
for _, template := range templates {
_ = toStableTemplateID(template.id)
}

return client, func(ids []uuid.UUID) {
for i, id := range ids {
ids[i] = toStableTemplateID(id)
}
slices.SortFunc(ids, func(a, b uuid.UUID) int {
return slice.Ascending(a.String(), b.String())
})
}
return client
}

// Time range for report, test data will be generated within and
// outside this range, but only data within the range should be
// included in the report.
lastNight := time.Now().UTC().Truncate(24 * time.Hour)
weekAgo := lastNight.AddDate(0, 0, -7)
frozenLastNight := time.Date(2023, 8, 22, 0, 0, 0, 0, time.UTC)
frozenWeekAgo := frozenLastNight.AddDate(0, 0, -7)

saoPaulo, err := time.LoadLocation("America/Sao_Paulo")
require.NoError(t, err)
weekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, weekAgo.Format(time.DateTime), saoPaulo)
frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo)
require.NoError(t, err)

makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
return map[*testWorkspace]testDataGen{
users[0].workspaces[0]: {
agentStats: []agentStat{
{ // One hour of usage.
startedAt: weekAgo,
endedAt: weekAgo.Add(time.Hour),
startedAt: frozenWeekAgo,
endedAt: frozenWeekAgo.Add(time.Hour),
sessionCountVSCode: 1,
sessionCountSSH: 1,
},
{ // 12 minutes of usage -> 15 minutes.
startedAt: weekAgo.AddDate(0, 0, 1),
endedAt: weekAgo.AddDate(0, 0, 1).Add(12 * time.Minute),
startedAt: frozenWeekAgo.AddDate(0, 0, 1),
endedAt: frozenWeekAgo.AddDate(0, 0, 1).Add(12 * time.Minute),
sessionCountSSH: 1,
},
{ // 2 minutes of usage -> 10 minutes because it crosses the 5 minute interval boundary.
startedAt: weekAgo.AddDate(0, 0, 2).Add(4 * time.Minute),
endedAt: weekAgo.AddDate(0, 0, 2).Add(6 * time.Minute),
startedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(4 * time.Minute),
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Minute),
sessionCountJetBrains: 1,
},
},
appUsage: []appUsage{
{ // One hour of usage.
app: users[0].workspaces[0].apps[0],
startedAt: weekAgo,
endedAt: weekAgo.Add(time.Hour),
startedAt: frozenWeekAgo,
endedAt: frozenWeekAgo.Add(time.Hour),
requests: 1,
},
{ // used an app on the last day, counts as active user, 12m -> 15m rounded.
app: users[0].workspaces[0].apps[2],
startedAt: weekAgo.AddDate(0, 0, 6),
endedAt: weekAgo.AddDate(0, 0, 6).Add(12 * time.Minute),
startedAt: frozenWeekAgo.AddDate(0, 0, 6),
endedAt: frozenWeekAgo.AddDate(0, 0, 6).Add(12 * time.Minute),
requests: 1,
},
},
Expand All @@ -1085,8 +1080,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
// as in first template. When selecting both templates
// this user and their app usage will only be counted
// once but the template ID will show up in the data.
startedAt: weekAgo,
endedAt: weekAgo.Add(time.Hour),
startedAt: frozenWeekAgo,
endedAt: frozenWeekAgo.Add(time.Hour),
sessionCountVSCode: 1,
sessionCountSSH: 1,
},
Expand All @@ -1105,8 +1100,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
// Different templates but identical apps, apps will be
// combined and usage will be summed.
app: users[0].workspaces[1].apps[0],
startedAt: weekAgo.AddDate(0, 0, 2),
endedAt: weekAgo.AddDate(0, 0, 2).Add(6 * time.Hour),
startedAt: frozenWeekAgo.AddDate(0, 0, 2),
endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Hour),
requests: 1,
},
},
Expand All @@ -1128,6 +1123,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
type testRequest struct {
name string
makeRequest func([]*testTemplate) codersdk.TemplateInsightsRequest
ignoreTimes bool
}
tests := []struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that we can convert-to-table/remove some other insights tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can delete at least one or two. But depending on if we rely primarily on dbgen here or not, we might want to keep some "whole tests" around.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted one test TestTemplateInsights, its test cases are now covered by the golden files and then some.

name string
Expand All @@ -1142,8 +1138,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
name: "week deployment wide",
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
StartTime: weekAgo,
EndTime: weekAgo.AddDate(0, 0, 7),
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand All @@ -1153,8 +1149,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
StartTime: weekAgo,
EndTime: weekAgo.AddDate(0, 0, 7),
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand All @@ -1164,8 +1160,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
TemplateIDs: []uuid.UUID{templates[0].id},
StartTime: weekAgo,
EndTime: weekAgo.AddDate(0, 0, 7),
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand All @@ -1175,8 +1171,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
TemplateIDs: []uuid.UUID{templates[1].id},
StartTime: weekAgo,
EndTime: weekAgo.AddDate(0, 0, 7),
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand All @@ -1186,8 +1182,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
TemplateIDs: []uuid.UUID{templates[2].id},
StartTime: weekAgo,
EndTime: weekAgo.AddDate(0, 0, 7),
StartTime: frozenWeekAgo,
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand All @@ -1198,8 +1194,8 @@ func TestTemplateInsights_Golden(t *testing.T) {
name: "week other timezone (São Paulo)",
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
return codersdk.TemplateInsightsRequest{
StartTime: weekAgoSaoPaulo,
EndTime: weekAgoSaoPaulo.AddDate(0, 0, 7),
StartTime: frozenWeekAgoSaoPaulo,
EndTime: frozenWeekAgoSaoPaulo.AddDate(0, 0, 7),
Interval: codersdk.InsightsReportIntervalDay,
}
},
Expand Down Expand Up @@ -1233,7 +1229,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
}
}

client, toStableTemplateIDs := prepare(t, templates, users, testData)
client := prepare(t, templates, users, testData)

for _, req := range tt.requests {
req := req
Expand All @@ -1245,7 +1241,15 @@ func TestTemplateInsights_Golden(t *testing.T) {
report, err := client.TemplateInsights(ctx, req.makeRequest(templates))
require.NoError(t, err, "want no error getting template insights")

stabilizeReportForGoldenComparison(&report, toStableTemplateIDs)
if req.ignoreTimes {
// Ignore times, we're only interested in the data.
report.Report.StartTime = time.Time{}
report.Report.EndTime = time.Time{}
for i := range report.IntervalReports {
report.IntervalReports[i].StartTime = time.Time{}
report.IntervalReports[i].EndTime = time.Time{}
}
}

partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_")
goldenFile := filepath.Join("testdata", "insights", partialName+".json.golden")
Expand Down