| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "sort" |
| "testing" |
| "time" |
| |
| "github.com/golang/mock/gomock" |
| "github.com/golang/protobuf/jsonpb" |
| "github.com/golang/protobuf/proto" |
| structpb "github.com/golang/protobuf/ptypes/struct" |
| . "github.com/smartystreets/goconvey/convey" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/metadata" |
| "google.golang.org/grpc/status" |
| |
| "go.chromium.org/luci/buildbucket" |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "go.chromium.org/luci/common/clock" |
| "go.chromium.org/luci/common/clock/testclock" |
| "go.chromium.org/luci/common/errors" |
| luciproto "go.chromium.org/luci/common/proto" |
| . "go.chromium.org/luci/common/testing/assertions" |
| "go.chromium.org/luci/logdog/common/types" |
| "go.chromium.org/luci/lucictx" |
| annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto" |
| ) |
| |
| func newAnn(stepNames ...string) *annopb.Step { |
| ann := &annopb.Step{ |
| Substep: make([]*annopb.Step_Substep, len(stepNames)), |
| } |
| for i, n := range stepNames { |
| ann.Substep[i] = &annopb.Step_Substep{ |
| Substep: &annopb.Step_Substep_Step{ |
| Step: &annopb.Step{Name: n}, |
| }, |
| } |
| } |
| return ann |
| } |
| |
| func newAnnBytes(stepNames ...string) []byte { |
| ret, err := proto.Marshal(newAnn(stepNames...)) |
| if err != nil { |
| panic(err) |
| } |
| return ret |
| } |
| |
| func TestBuildUpdater(t *testing.T) { |
| t.Parallel() |
| |
| Convey(`buildUpdater`, t, func(c C) { |
| ctrl := gomock.NewController(t) |
| defer ctrl.Finish() |
| client := buildbucketpb.NewMockBuildsClient(ctrl) |
| |
| // Ensure tests don't hang. |
| ctx, cancel := context.WithTimeout(context.Background(), time.Second) |
| |
| ctx, clk := testclock.UseTime(ctx, testclock.TestRecentTimeUTC) |
| |
| bu := &buildUpdater{ |
| buildID: 42, |
| buildToken: "build token", |
| annAddr: &types.StreamAddr{ |
| Host: "logdog.example.com", |
| Project: "chromium", |
| Path: "prefix/+/annotations", |
| }, |
| client: client, |
| annotations: make(chan []byte), |
| } |
| |
| Convey("build token is sent", func() { |
| updateBuild := func(ctx context.Context, req *buildbucketpb.UpdateBuildRequest, opts ...grpc.CallOption) (*buildbucketpb.Build, error) { |
| md, ok := metadata.FromOutgoingContext(ctx) |
| c.So(ok, ShouldBeTrue) |
| c.So(md.Get(buildbucket.BuildTokenHeader), ShouldResemble, []string{"build token"}) |
| res := &buildbucketpb.Build{} |
| return res, nil |
| } |
| client.EXPECT(). |
| UpdateBuild(gomock.Any(), gomock.Any()). |
| AnyTimes(). |
| DoAndReturn(updateBuild) |
| |
| err := bu.UpdateBuild(ctx, &buildbucketpb.UpdateBuildRequest{}) |
| So(err, ShouldBeNil) |
| }) |
| |
| Convey(`run`, func() { |
| update := func(ctx context.Context, annBytes []byte) error { |
| return nil |
| } |
| |
| errC := make(chan error) |
| done := make(chan struct{}) |
| start := func() { |
| go func() { |
| errC <- bu.run(ctx, done, update) |
| }() |
| } |
| |
| run := func(err1, err2 error) error { |
| update = func(ctx context.Context, annBytes []byte) error { |
| if string(annBytes) == "1" { |
| return err1 |
| } |
| return err2 |
| } |
| start() |
| bu.AnnotationUpdated([]byte("1")) |
| bu.AnnotationUpdated([]byte("2")) |
| cancel() |
| return <-errC |
| } |
| |
| Convey("two successful requests", func() { |
| So(run(nil, nil), ShouldBeNil) |
| }) |
| |
| Convey("first failed, second succeeded", func() { |
| So(run(fmt.Errorf("transient"), nil), ShouldBeNil) |
| }) |
| |
| Convey("first succeeded, second failed", func() { |
| So(run(nil, fmt.Errorf("fatal")), ShouldErrLike, "fatal") |
| }) |
| |
| Convey("minDistance", func() { |
| var sleepDuration time.Duration |
| open := true |
| clk.SetTimerCallback(func(d time.Duration, t clock.Timer) { |
| if testclock.HasTags(t, "update-build-distance") { |
| sleepDuration += d |
| clk.Add(d) |
| |
| if open { |
| close(done) |
| open = false |
| } |
| |
| } |
| }) |
| |
| start() |
| bu.AnnotationUpdated([]byte("1")) |
| So(<-errC, ShouldBeNil) |
| So(sleepDuration, ShouldBeGreaterThanOrEqualTo, time.Second) |
| }) |
| |
| Convey("errSleep", func() { |
| attempt := 0 |
| clk.SetTimerCallback(func(d time.Duration, t clock.Timer) { |
| switch { |
| case testclock.HasTags(t, "update-build-distance"): |
| clk.Add(d) |
| case testclock.HasTags(t, "update-build-error"): |
| clk.Add(d) |
| attempt++ |
| if attempt == 4 { |
| bu.AnnotationUpdated([]byte("2")) |
| } |
| } |
| }) |
| |
| update = func(ctx context.Context, annBytes []byte) error { |
| if string(annBytes) == "1" { |
| return fmt.Errorf("err") |
| } |
| |
| close(done) |
| return nil |
| } |
| |
| start() |
| bu.AnnotationUpdated([]byte("1")) |
| So(<-errC, ShouldBeNil) |
| }) |
| |
| Convey("first is fatal, second never occurs", func() { |
| fatal := status.Error(codes.InvalidArgument, "too large") |
| calls := 0 |
| update = func(ctx context.Context, annBytes []byte) error { |
| calls++ |
| return fatal |
| } |
| start() |
| bu.AnnotationUpdated([]byte("1")) |
| cancel() |
| So(errors.Unwrap(<-errC), ShouldEqual, fatal) |
| So(calls, ShouldEqual, 1) |
| }) |
| |
| Convey("done is closed", func() { |
| start() |
| bu.AnnotationUpdated([]byte("1")) |
| close(done) |
| So(<-errC, ShouldBeNil) |
| }) |
| }) |
| |
| Convey("ParseAnnotations", func() { |
| ann := &annopb.Step{} |
| err := luciproto.UnmarshalTextML(` |
| substep: < |
| step: < |
| name: "bot_update" |
| status: SUCCESS |
| started: < seconds: 1400000000 > |
| ended: < seconds: 1400001000 > |
| property: < |
| name: "$recipe_engine/buildbucket/output_gitiles_commit" |
| value: <<END |
| { |
| "host": "chrome-internal.googlesource.com", |
| "project": "chromeos/manifest-internal", |
| "ref": "refs/heads/master", |
| "id": "91401dc270212d98734ab894bd90609b882aa458", |
| "position": 2 |
| } |
| END |
| > |
| > |
| > |
| substep: < |
| step: < |
| name: "compile" |
| status: RUNNING |
| started: < seconds: 1400001000 > |
| property: < |
| name: "foo" |
| value: "\"bar\"" |
| > |
| > |
| > |
| `, ann) |
| So(err, ShouldBeNil) |
| So(ann.Substep, ShouldHaveLength, 2) |
| |
| expected := &buildbucketpb.UpdateBuildRequest{} |
| err = jsonpb.UnmarshalString(`{ |
| "build": { |
| "id": 42, |
| "steps": [ |
| { |
| "name": "bot_update", |
| "status": "SUCCESS", |
| "startTime": "2014-05-13T16:53:20.0Z", |
| "endTime": "2014-05-13T17:10:00.0Z" |
| |
| }, |
| { |
| "name": "compile", |
| "status": "STARTED", |
| "startTime": "2014-05-13T17:10:00.0Z" |
| } |
| ], |
| "output": { |
| "properties": {"foo": "bar"}, |
| "gitilesCommit": { |
| "host": "chrome-internal.googlesource.com", |
| "project": "chromeos/manifest-internal", |
| "ref": "refs/heads/master", |
| "id": "91401dc270212d98734ab894bd90609b882aa458", |
| "position": 2 |
| } |
| } |
| }, |
| "updateMask": { |
| "paths": [ |
| "build.steps", |
| "build.output.properties", |
| "build.output.gitiles_commit" |
| ] |
| } |
| }`, expected) |
| So(err, ShouldBeNil) |
| |
| actual, err := bu.ParseAnnotations(ctx, ann) |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, expected) |
| }) |
| |
| Convey(`test addtional tags`, func(c C) { |
| ann := &annopb.Step{} |
| err := luciproto.UnmarshalTextML(` |
| substep: < |
| step: < |
| name: "buildbucket.add_tags_to_current_build" |
| status: SUCCESS |
| started: < seconds: 1400000000 > |
| ended: < seconds: 1400001000 > |
| property: < |
| name: "$recipe_engine/buildbucket/runtime-tags" |
| value:<<END |
| { |
| "k1": ["v1"], |
| "k2": ["v2"] |
| } |
| END |
| > |
| > |
| > |
| `, ann) |
| So(err, ShouldBeNil) |
| So(ann.Substep, ShouldHaveLength, 1) |
| |
| expected := &buildbucketpb.UpdateBuildRequest{} |
| err = jsonpb.UnmarshalString(`{ |
| "build": { |
| "id": 42, |
| "steps": [ |
| { |
| "name": "buildbucket.add_tags_to_current_build", |
| "status": "SUCCESS", |
| "startTime": "2014-05-13T16:53:20.0Z", |
| "endTime": "2014-05-13T17:10:00.0Z" |
| |
| } |
| ], |
| "output": { |
| "properties": {} |
| }, |
| "tags": [ |
| { |
| "key": "k1", |
| "value": "v1" |
| }, |
| { |
| "key": "k2", |
| "value": "v2" |
| } |
| ] |
| }, |
| "updateMask": { |
| "paths": [ |
| "build.steps", |
| "build.output.properties", |
| "build.tags" |
| ] |
| } |
| }`, expected) |
| So(err, ShouldBeNil) |
| |
| actual, err := bu.ParseAnnotations(ctx, ann) |
| So(err, ShouldBeNil) |
| |
| // Because the order is undeterministic when iterating the dict during |
| // the process of converting to Tags proto in ParseAnnotations func. |
| sortTagsFunc := func(tags []*buildbucketpb.StringPair) { |
| sort.Slice(tags, func(i, j int) bool { |
| if tags[i].Key != tags[j].Key { |
| return tags[i].Key < tags[j].Key |
| } |
| return tags[i].Value < tags[j].Value |
| }) |
| } |
| sortTagsFunc(actual.Build.Tags) |
| sortTagsFunc(expected.Build.Tags) |
| |
| So(actual, ShouldResembleProto, expected) |
| }) |
| }) |
| } |
| |
| func TestReadBuildSecrets(t *testing.T) { |
| t.Parallel() |
| |
| Convey("readBuildSecrets", t, func() { |
| ctx := context.Background() |
| ctx = lucictx.SetSwarming(ctx, nil) |
| |
| Convey("empty", func() { |
| secrets, err := readBuildSecrets(ctx) |
| So(err, ShouldBeNil) |
| So(secrets, ShouldResemble, &buildbucketpb.BuildSecrets{}) |
| }) |
| |
| Convey("build token", func() { |
| secretBytes, err := proto.Marshal(&buildbucketpb.BuildSecrets{ |
| BuildToken: "build token", |
| }) |
| So(err, ShouldBeNil) |
| |
| ctx = lucictx.SetSwarming(ctx, &lucictx.Swarming{SecretBytes: secretBytes}) |
| |
| secrets, err := readBuildSecrets(ctx) |
| So(err, ShouldBeNil) |
| So(string(secrets.BuildToken), ShouldEqual, "build token") |
| }) |
| }) |
| } |
| |
| func TestOutputCommitFromLegacyProperties(t *testing.T) { |
| t.Parallel() |
| |
| parse := func(propJSON string) (*buildbucketpb.GitilesCommit, error) { |
| propStruct := &structpb.Struct{} |
| err := jsonpb.UnmarshalString(propJSON, propStruct) |
| So(err, ShouldBeNil) |
| |
| return outputCommitFromLegacyProperties(propStruct) |
| } |
| |
| Convey("TestOutputCommitFromLegacyProperties", t, func() { |
| Convey("no properties", func() { |
| actual, err := parse(`{}`) |
| So(err, ShouldBeNil) |
| So(actual, ShouldBeNil) |
| }) |
| |
| Convey("got_revision id", func() { |
| actual, err := parse(`{ |
| "$recipe_engine/buildbucket": { |
| "build": { |
| "input": { |
| "gitilesCommit": { |
| "host": "chromium.googlesource.com", |
| "project": "chromium/src", |
| "id": "deadbeef" |
| } |
| } |
| } |
| }, |
| "got_revision": "e57f4e87022d765b45e741e478a8351d9789bc37" |
| }`) |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, &buildbucketpb.GitilesCommit{ |
| Host: "chromium.googlesource.com", |
| Project: "chromium/src", |
| Ref: "refs/heads/master", |
| Id: "e57f4e87022d765b45e741e478a8351d9789bc37", |
| }) |
| }) |
| |
| Convey("got_revision non-default ref", func() { |
| actual, err := parse(`{ |
| "$recipe_engine/buildbucket": { |
| "build": { |
| "input": { |
| "gitilesCommit": { |
| "host": "chromium.googlesource.com", |
| "project": "chromium/src", |
| "ref": "refs/heads/x" |
| } |
| } |
| } |
| }, |
| "got_revision": "e57f4e87022d765b45e741e478a8351d9789bc37" |
| }`) |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, &buildbucketpb.GitilesCommit{ |
| Host: "chromium.googlesource.com", |
| Project: "chromium/src", |
| Ref: "refs/heads/x", |
| Id: "e57f4e87022d765b45e741e478a8351d9789bc37", |
| }) |
| }) |
| |
| Convey("got_revision id tryjob", func() { |
| actual, err := parse(`{ |
| "$recipe_engine/buildbucket": { |
| "build": { |
| "input": { |
| "gerritChanges": [{ |
| "host": "chromium.googlesource.com", |
| "project": "chromium/src", |
| "change": 1, |
| "patchset": 2 |
| }] |
| } |
| } |
| }, |
| "got_revision": "e57f4e87022d765b45e741e478a8351d9789bc37" |
| }`) |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, &buildbucketpb.GitilesCommit{ |
| Host: "chromium.googlesource.com", |
| Project: "chromium/src", |
| Ref: "refs/heads/master", |
| Id: "e57f4e87022d765b45e741e478a8351d9789bc37", |
| }) |
| }) |
| |
| Convey("got_revision ref", func() { |
| actual, err := parse(`{ |
| "$recipe_engine/buildbucket": { |
| "build": { |
| "input": { |
| "gitilesCommit": { |
| "host": "chromium.googlesource.com", |
| "project": "chromium/src" |
| } |
| } |
| } |
| }, |
| "got_revision": "refs/heads/master" |
| }`) |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, &buildbucketpb.GitilesCommit{ |
| Host: "chromium.googlesource.com", |
| Project: "chromium/src", |
| Ref: "refs/heads/master", |
| }) |
| }) |
| |
| Convey("got_revision_cp", func() { |
| actual, err := parse(`{ |
| "$recipe_engine/buildbucket": { |
| "build": { |
| "input": { |
| "gitilesCommit": { |
| "host": "chromium.googlesource.com", |
| "project": "chromium/src" |
| } |
| } |
| } |
| }, |
| "got_revision": "e57f4e87022d765b45e741e478a8351d9789bc37", |
| "got_revision_cp": "refs/heads/master@{#673406}" |
| }`) |
| |
| So(err, ShouldBeNil) |
| So(actual, ShouldResembleProto, &buildbucketpb.GitilesCommit{ |
| Host: "chromium.googlesource.com", |
| Project: "chromium/src", |
| Ref: "refs/heads/master", |
| Id: "e57f4e87022d765b45e741e478a8351d9789bc37", |
| Position: 673406, |
| }) |
| }) |
| }) |
| } |