blob: f83930d7f2ee342301b85187dcdec3fb50c8bd11 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"testing"
"time"
"github.com/golang/protobuf/jsonpb"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/common/data/stringset"
ds "go.chromium.org/luci/gae/service/datastore"
"infra/qscheduler/qslib/tutils"
admin "infra/tricium/api/admin/v1"
tricium "infra/tricium/api/v1"
"infra/tricium/appengine/common/gerrit"
"infra/tricium/appengine/common/track"
"infra/tricium/appengine/common/triciumtest"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestWorkerDoneRequest(t *testing.T) {
Convey("Worker done request with successful worker", t, func() {
ctx := triciumtest.Context()
simple := "Simple"
simpleUbuntu := "Simple_Ubuntu"
workflowProvider := &mockWorkflowProvider{
Workflow: &admin.Workflow{
Workers: []*admin.Worker{
{
Name: simpleUbuntu,
Needs: tricium.Data_FILES,
Provides: tricium.Data_RESULTS,
},
},
},
}
// Add pending workflow run.
request := &track.AnalyzeRequest{}
request.GitRef = "refs/changes/88/508788/7"
So(ds.Put(ctx, request), ShouldBeNil)
requestKey := ds.KeyForObj(ctx, request)
run := &track.WorkflowRun{ID: 1, Parent: requestKey}
So(ds.Put(ctx, run), ShouldBeNil)
runKey := ds.KeyForObj(ctx, run)
So(ds.Put(ctx, &track.WorkflowRunResult{
ID: 1,
Parent: runKey,
State: tricium.State_PENDING,
}), ShouldBeNil)
// Mark workflow as launched.
So(workflowLaunched(ctx, &admin.WorkflowLaunchedRequest{
RunId: request.ID,
}, workflowProvider), ShouldBeNil)
// Mark worker as launched.
So(workerLaunched(ctx, &admin.WorkerLaunchedRequest{
RunId: request.ID,
Worker: simpleUbuntu,
}), ShouldBeNil)
// Mark worker as done.
So(workerDone(ctx, &admin.WorkerDoneRequest{
RunId: request.ID,
Worker: simpleUbuntu,
Provides: tricium.Data_RESULTS,
State: tricium.State_SUCCESS,
BuildbucketOutput: `{"comments": [{"message": "foo"}, {"message": "bar"}]}`,
}), ShouldBeNil)
functionKey := ds.NewKey(ctx, "FunctionRun", simple, 0, runKey)
Convey("Marks worker as done", func() {
workerKey := ds.NewKey(ctx, "WorkerRun", simpleUbuntu, 0, functionKey)
wr := &track.WorkerRunResult{ID: 1, Parent: workerKey}
So(ds.Get(ctx, wr), ShouldBeNil)
So(wr.State, ShouldEqual, tricium.State_SUCCESS)
})
Convey("Marks function as done and adds no comments", func() {
fr := &track.FunctionRunResult{ID: 1, Parent: functionKey}
So(ds.Get(ctx, fr), ShouldBeNil)
So(fr.State, ShouldEqual, tricium.State_SUCCESS)
})
Convey("Marks workflow as done and adds comments", func() {
wr := &track.WorkflowRunResult{ID: 1, Parent: runKey}
So(ds.Get(ctx, wr), ShouldBeNil)
So(wr.State, ShouldEqual, tricium.State_SUCCESS)
So(wr.NumComments, ShouldEqual, 2)
})
Convey("Marks request as done", func() {
ar := &track.AnalyzeRequestResult{ID: 1, Parent: requestKey}
So(ds.Get(ctx, ar), ShouldBeNil)
So(ar.State, ShouldEqual, tricium.State_SUCCESS)
})
})
}
func TestRecipeWorkerDoneRequest(t *testing.T) {
Convey("Worker done request with successful worker", t, func() {
ctx := triciumtest.Context()
simple := "Simple"
simpleUbuntu := "Simple_Ubuntu"
workflowProvider := &mockWorkflowProvider{
Workflow: &admin.Workflow{
Workers: []*admin.Worker{
{
Name: simpleUbuntu,
Provides: tricium.Data_RESULTS,
},
},
},
}
// Add pending workflow run.
request := &track.AnalyzeRequest{}
request.GitRef = "refs/changes/88/508788/7"
So(ds.Put(ctx, request), ShouldBeNil)
requestKey := ds.KeyForObj(ctx, request)
run := &track.WorkflowRun{ID: 1, Parent: requestKey}
So(ds.Put(ctx, run), ShouldBeNil)
runKey := ds.KeyForObj(ctx, run)
So(ds.Put(ctx, &track.WorkflowRunResult{
ID: 1,
Parent: runKey,
State: tricium.State_PENDING,
}), ShouldBeNil)
// Mark workflow as launched.
So(workflowLaunched(ctx, &admin.WorkflowLaunchedRequest{
RunId: request.ID,
}, workflowProvider), ShouldBeNil)
// Mark worker as launched.
So(workerLaunched(ctx, &admin.WorkerLaunchedRequest{
RunId: request.ID,
Worker: simpleUbuntu,
}), ShouldBeNil)
// Mark worker as done.
So(workerDone(ctx, &admin.WorkerDoneRequest{
RunId: request.ID,
Worker: simpleUbuntu,
Provides: tricium.Data_GIT_FILE_DETAILS,
State: tricium.State_SUCCESS,
BuildbucketOutput: `{"comments": []}`,
}), ShouldBeNil)
functionKey := ds.NewKey(ctx, "FunctionRun", simple, 0, runKey)
Convey("Marks worker as done", func() {
workerKey := ds.NewKey(ctx, "WorkerRun", simpleUbuntu, 0, functionKey)
wr := &track.WorkerRunResult{ID: 1, Parent: workerKey}
So(ds.Get(ctx, wr), ShouldBeNil)
So(wr.State, ShouldEqual, tricium.State_SUCCESS)
})
})
}
func TestAbortedWorkerDoneRequest(t *testing.T) {
Convey("Worker done request with an aborted worker", t, func() {
// This test is similar to the case above, except that one of
// the workers is aborted, so the function is considered
// failed, and thus the workflow run is failed.
ctx := triciumtest.Context()
simple := "Simple"
simpleUbuntu := "Simple_Ubuntu"
workflowProvider := &mockWorkflowProvider{
Workflow: &admin.Workflow{
Workers: []*admin.Worker{
{
Name: simpleUbuntu,
Needs: tricium.Data_GIT_FILE_DETAILS,
Provides: tricium.Data_RESULTS,
},
},
},
}
// Add pending run entry.
request := &track.AnalyzeRequest{}
request.GitRef = "refs/changes/88/508788/7"
So(ds.Put(ctx, request), ShouldBeNil)
requestKey := ds.KeyForObj(ctx, request)
run := &track.WorkflowRun{ID: 1, Parent: requestKey}
So(ds.Put(ctx, run), ShouldBeNil)
runKey := ds.KeyForObj(ctx, run)
So(ds.Put(ctx, &track.WorkflowRunResult{
ID: 1,
Parent: runKey,
State: tricium.State_PENDING,
}), ShouldBeNil)
// Mark workflow as launched.
So(workflowLaunched(ctx, &admin.WorkflowLaunchedRequest{
RunId: request.ID,
}, workflowProvider), ShouldBeNil)
// Mark worker as launched.
So(workerLaunched(ctx, &admin.WorkerLaunchedRequest{
RunId: request.ID,
Worker: simpleUbuntu,
}), ShouldBeNil)
// Mark worker as aborted.
So(workerDone(ctx, &admin.WorkerDoneRequest{
RunId: request.ID,
Worker: simpleUbuntu,
State: tricium.State_ABORTED,
BuildbucketOutput: `{"comments": []}`,
}), ShouldBeNil)
functionKey := ds.NewKey(ctx, "FunctionRun", simple, 0, runKey)
Convey("WorkerRun is marked as aborted", func() {
workerKey := ds.NewKey(ctx, "WorkerRun", simpleUbuntu, 0, functionKey)
wr := &track.WorkerRunResult{ID: 1, Parent: workerKey}
So(ds.Get(ctx, wr), ShouldBeNil)
So(wr.State, ShouldEqual, tricium.State_ABORTED)
})
Convey("FunctionRun is failed, with no comments", func() {
fr := &track.FunctionRunResult{ID: 1, Parent: functionKey}
So(ds.Get(ctx, fr), ShouldBeNil)
So(fr.State, ShouldEqual, tricium.State_FAILURE)
})
Convey("WorkflowRun is marked as failed", func() {
wr := &track.WorkflowRunResult{ID: 1, Parent: runKey}
So(ds.Get(ctx, wr), ShouldBeNil)
So(wr.State, ShouldEqual, tricium.State_FAILURE)
})
Convey("AnalyzeRequest is marked as failed", func() {
ar := &track.AnalyzeRequestResult{ID: 1, Parent: requestKey}
So(ds.Get(ctx, ar), ShouldBeNil)
So(ar.State, ShouldEqual, tricium.State_FAILURE)
})
})
}
func TestValidateWorkerDoneRequestRequest(t *testing.T) {
Convey("Request with all parts is valid", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
RunId: 1234,
Worker: "MyLint_Ubuntu",
Provides: tricium.Data_RESULTS,
State: tricium.State_SUCCESS,
BuildbucketOutput: `{"comments": []}`,
}), ShouldBeNil)
})
Convey("Specifying provides and state is optional", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
RunId: 1234,
Worker: "MyLint_Ubuntu",
BuildbucketOutput: `{"comments": []}`,
}), ShouldBeNil)
})
Convey("Request with no run ID is invalid", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
Worker: "MyLint_Ubuntu",
BuildbucketOutput: `{"comments": []}`,
}), ShouldNotBeNil)
})
Convey("Request with no worker name invalid", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
RunId: 1234,
BuildbucketOutput: `{"comments": []}`,
}), ShouldNotBeNil)
})
Convey("Request with no output is valid", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
RunId: 1234,
Worker: "MyLint_Ubuntu",
}), ShouldBeNil)
})
Convey("Providing just buildbucket output is OK", t, func() {
So(validateWorkerDoneRequest(&admin.WorkerDoneRequest{
RunId: 1234,
Worker: "MyLint_Ubuntu",
BuildbucketOutput: `{"comments": []}`,
}), ShouldBeNil)
})
}
func TestCreateCommentSelection(t *testing.T) {
changedLines := gerrit.ChangedLinesInfo{
"dir/file.txt": {2, 5, 6},
}
mock := &gerrit.MockRestAPI{ChangedLines: changedLines}
ctx := triciumtest.Context()
req := track.AnalyzeRequest{
GerritProject: "my-project",
GerritChange: "my-project~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
}
Convey("createCommentSelection keeps unchanged lines if comment's ShowOnUnchangedLines is true", t, func() {
commentJSON, err := (&jsonpb.Marshaler{}).MarshalToString(&tricium.Data_Comment{
Message: "Not in Change",
Path: "dir/file.txt",
StartLine: 10,
EndLine: 11,
ShowOnUnchangedLines: true,
})
comments := []*track.Comment{
{
Comment: []byte(commentJSON),
},
}
selections := []*track.CommentSelection{
{
ID: 1,
Parent: nil,
Included: true,
},
}
result, err := createCommentSelections(ctx, mock, &req, comments)
So(result, ShouldResemble, selections)
So(err, ShouldBeNil)
})
Convey("createCommentSelection discards unchanged lines if comment's ShowOnUnchangedLines is false", t, func() {
commentJSON, err := (&jsonpb.Marshaler{}).MarshalToString(&tricium.Data_Comment{
Message: "Not in Change",
Path: "dir/file.txt",
StartLine: 10,
EndLine: 11,
ShowOnUnchangedLines: false,
})
comments := []*track.Comment{
{
Comment: []byte(commentJSON),
},
}
selections := []*track.CommentSelection{
{
ID: 1,
Parent: nil,
Included: false,
},
}
result, err := createCommentSelections(ctx, mock, &req, comments)
So(result, ShouldResemble, selections)
So(err, ShouldBeNil)
})
}
func TestCreateAnalysisResults(t *testing.T) {
Convey("Default objects", t, func() {
wres := track.WorkerRunResult{}
areq := track.AnalyzeRequest{}
ares := track.AnalyzeRequestResult{}
comments := []*track.Comment{}
selections := []*track.CommentSelection{}
areq.GitRef = "refs/changes/88/508788/102"
result, err := createAnalysisResults(&wres, &areq, &ares, comments, selections)
So(err, ShouldBeNil)
So(result, ShouldNotBeNil)
So(result.RevisionNumber, ShouldEqual, 102)
})
Convey("GitRef required", t, func() {
wres := track.WorkerRunResult{}
areq := track.AnalyzeRequest{}
ares := track.AnalyzeRequestResult{}
comments := []*track.Comment{}
selections := []*track.CommentSelection{}
result, err := createAnalysisResults(&wres, &areq, &ares, comments, selections)
So(err, ShouldNotBeNil)
So(result, ShouldBeNil)
})
Convey("All values", t, func() {
wres := track.WorkerRunResult{}
areq := track.AnalyzeRequest{}
areq.GerritHost = "http://my-gerrit-review.com/my-project"
areq.Project = "my-project"
areq.GerritChange = "my-project~master~I8473b95934b5732ac55d26311a706c9c2bde9940"
areq.GitURL = "http://the-git-url.com/my-project"
areq.GitRef = "refs/changes/88/508788/7"
areq.Received = time.Now()
areq.Files = []tricium.Data_File{
{Path: "dir/file.txt"},
{Path: "dir/file2.txt"},
}
ares := track.AnalyzeRequestResult{}
deletedFileCommentJSON, err := (&jsonpb.Marshaler{}).MarshalToString(&tricium.Data_Comment{
Category: "L",
Message: "Line too long",
Path: "dir/deleted_file.txt",
})
inChangeCommentJSON, err := (&jsonpb.Marshaler{}).MarshalToString(&tricium.Data_Comment{
Category: "L",
Message: "Line too short",
Path: "dir/file.txt",
StartLine: 2,
EndLine: 3,
})
So(err, ShouldBeNil)
comments := []*track.Comment{
{
UUID: "1234",
Parent: nil,
Platforms: tricium.PlatformBitPosToMask(tricium.Platform_ANY),
Analyzer: "analyzerName",
Category: "analyzerName/categoryName",
Comment: []byte(deletedFileCommentJSON),
CreationTime: time.Now(),
},
{
UUID: "1234",
Parent: nil,
Platforms: tricium.PlatformBitPosToMask(tricium.Platform_IOS) | tricium.PlatformBitPosToMask(tricium.Platform_WINDOWS),
Analyzer: "analyzerName",
Category: "analyzerName/categoryName",
Comment: []byte(inChangeCommentJSON),
CreationTime: time.Now(),
},
{
UUID: "1234",
Parent: nil,
Platforms: tricium.PlatformBitPosToMask(tricium.Platform_OSX),
Analyzer: "notSelected",
Category: "notSelected/categoryName",
Comment: []byte(inChangeCommentJSON),
CreationTime: time.Now(),
},
}
selections := []*track.CommentSelection{
{
ID: 1,
Parent: nil,
Included: true,
},
{
ID: 1,
Parent: nil,
Included: true,
},
{
ID: 1,
Parent: nil,
Included: false,
},
}
result, err := createAnalysisResults(&wres, &areq, &ares, comments, selections)
So(err, ShouldBeNil)
So(result, ShouldNotBeNil)
So(result.GerritRevision.Host, ShouldEqual, areq.GerritHost)
So(result.GerritRevision.Project, ShouldEqual, areq.Project)
So(result.GerritRevision.Change, ShouldEqual, areq.GerritChange)
So(result.GerritRevision.GitUrl, ShouldEqual, areq.GitURL)
So(result.GerritRevision.GitRef, ShouldEqual, areq.GitRef)
So(result.RevisionNumber, ShouldEqual, 7)
So(tutils.Timestamp(result.RequestedTime), ShouldEqual, areq.Received)
So(len(result.Files), ShouldEqual, len(areq.Files))
for i := 0; i < len(result.Files); i++ {
So(result.Files[i], ShouldResembleProto, &areq.Files[i])
}
So(len(result.Comments), ShouldEqual, len(comments))
for i, gcomment := range result.Comments {
tcomment := tricium.Data_Comment{}
err := jsonpb.UnmarshalString(string(comments[i].Comment), &tcomment)
So(err, ShouldBeNil)
So(&tcomment, ShouldResembleProto, gcomment.Comment)
So(gcomment.Analyzer, ShouldEqual, comments[i].Analyzer)
So(gcomment.CreatedTime, ShouldResembleProto, tutils.TimestampProto(comments[i].CreationTime))
platforms, _ := tricium.GetPlatforms(comments[i].Platforms)
So(gcomment.Platforms, ShouldResemble, platforms)
So(gcomment.Selected, ShouldEqual, selections[i].Included)
}
})
}
func TestCommentFetchingFunctions(t *testing.T) {
Convey("Test Environment", t, func() {
ctx := triciumtest.Context()
// Add a request with no Gerrit details; it will not be fetched.
So(ds.Put(ctx, &track.AnalyzeRequest{ID: 11}), ShouldBeNil)
// Add two requests for the same CL.
So(ds.Put(ctx, &track.AnalyzeRequest{
ID: 22,
GitRef: "refs/changes/99/99/1",
GerritHost: "example.com",
GerritChange: "p~master~I2222",
}), ShouldBeNil)
So(ds.Put(ctx, &track.AnalyzeRequest{
ID: 23,
GitRef: "refs/changes/99/99/2",
GerritHost: "example.com",
GerritChange: "p~master~I2222",
}), ShouldBeNil)
// And one more request with the same change ID but different host.
So(ds.Put(ctx, &track.AnalyzeRequest{
ID: 33,
GitRef: "refs/changes/99/99/1",
GerritHost: "other.test",
GerritChange: "p~master~I2222",
}), ShouldBeNil)
Convey("A non-existent change has no runs", func() {
keys, err := fetchRequestKeysByChange(ctx, "none.test", "none~m~Iabcd")
So(len(keys), ShouldEqual, 0)
So(err, ShouldBeNil)
})
Convey("No runs match if there are no Gerrit details", func() {
keys, err := fetchRequestKeysByChange(ctx, "none.test", "")
So(keys, ShouldBeNil)
So(err, ShouldNotBeNil)
})
Convey("Two keys are fetched for a change with two runs", func() {
keys, err := fetchRequestKeysByChange(ctx, "example.com", "p~master~I2222")
So(len(keys), ShouldEqual, 2)
So(err, ShouldBeNil)
})
Convey("One key is fetched for a change with one runs", func() {
keys, err := fetchRequestKeysByChange(ctx, "other.test", "p~master~I2222")
So(len(keys), ShouldEqual, 1)
So(err, ShouldBeNil)
})
// In addition to the runs, add some comments and comment feedback.
// In this example, run 22 has comments, some of which have not useful feedback.
// Run 23 (same CL, different run) has one comment with not useful feedback.
run22WorkerKey := ds.MakeKey(
ctx, "AnalyzeRequest", 22, "WorkflowRun", 1,
"FunctionRun", "Foo", "WorkerRun", "Foo_UBUNTU")
c1 := &track.Comment{Parent: run22WorkerKey, Category: "Foo/C1"}
So(ds.Put(ctx, c1), ShouldBeNil)
c1Key := ds.KeyForObj(ctx, c1)
So(ds.Put(ctx, &track.CommentFeedback{Parent: c1Key, ID: 1, NotUsefulReports: 1}), ShouldBeNil)
c2 := &track.Comment{Parent: run22WorkerKey, Category: "Foo/C2"}
So(ds.Put(ctx, c2), ShouldBeNil)
c2Key := ds.KeyForObj(ctx, c2)
So(ds.Put(ctx, &track.CommentFeedback{Parent: c2Key, ID: 1, NotUsefulReports: 2}), ShouldBeNil)
c3 := &track.Comment{Parent: run22WorkerKey, Category: "Foo/C3"}
So(ds.Put(ctx, c3), ShouldBeNil)
c3Key := ds.KeyForObj(ctx, c3)
So(ds.Put(ctx, &track.CommentFeedback{Parent: c3Key, ID: 1, NotUsefulReports: 0}), ShouldBeNil)
run23WorkerKey := ds.MakeKey(
ctx, "AnalyzeRequest", 23, "WorkflowRun", 1,
"FunctionRun", "Foo", "WorkerRun", "Foo_UBUNTU")
c4 := &track.Comment{Parent: run23WorkerKey, Category: "Foo/C4"}
So(ds.Put(ctx, c4), ShouldBeNil)
c4Key := ds.KeyForObj(ctx, c4)
So(ds.Put(ctx, &track.CommentFeedback{Parent: c4Key, ID: 1, NotUsefulReports: 1}), ShouldBeNil)
Convey("No CommentFeedback keys fetched for empty input", func() {
keys, err := fetchAllCommentFeedback(ctx, nil)
So(len(keys), ShouldEqual, 0)
So(err, ShouldBeNil)
})
Convey("No CommentFeedback keys fetched for run with no comments", func() {
keys, err := fetchAllCommentFeedback(ctx, []*ds.Key{ds.MakeKey(ctx, "AnalyzeRequest", 33)})
So(len(keys), ShouldEqual, 0)
So(err, ShouldBeNil)
})
Convey("Two CommentFeedback keys fetched for run with two comments", func() {
keys, err := fetchAllCommentFeedback(ctx, []*ds.Key{ds.MakeKey(ctx, "AnalyzeRequest", 22)})
// Comments c1 and c2 have "not useful" feedback.
So(len(keys), ShouldEqual, 2)
So(err, ShouldBeNil)
})
Convey("One CommentFeedback key fetched for run with one comment", func() {
keys, err := fetchAllCommentFeedback(ctx, []*ds.Key{ds.MakeKey(ctx, "AnalyzeRequest", 23)})
So(len(keys), ShouldEqual, 1)
So(err, ShouldBeNil)
})
Convey("Three CommentFeedback keys for both of those runs together", func() {
keys, err := fetchAllCommentFeedback(ctx, []*ds.Key{
ds.MakeKey(ctx, "AnalyzeRequest", 22),
ds.MakeKey(ctx, "AnalyzeRequest", 23),
})
So(len(keys), ShouldEqual, 3)
So(err, ShouldBeNil)
})
Convey("suppressedCategories returns all not useful categories for all patchsets", func() {
categories := suppressedCategories(ctx, "example.com", "p~master~I2222")
So(categories, ShouldResemble, stringset.NewFromSlice("Foo/C1", "Foo/C2", "Foo/C4"))
})
Convey("suppressedCategories returns an empty set for nonexistent CLs", func() {
categories := suppressedCategories(ctx, "example.com", "p~master~I999")
So(categories, ShouldBeEmpty)
})
})
}