blob: ce511cf43c48b43b24b573cd214cf8b66d108465 [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bbfacade
import (
"context"
"fmt"
"io/ioutil"
"testing"
"time"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/cv/api/recipe/v1"
bbfake "go.chromium.org/luci/cv/internal/buildbucket/fake"
"go.chromium.org/luci/cv/internal/tryjob"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/common/clock/testclock"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestParseStatusAndResult(t *testing.T) {
Convey("Parse Status and Result", t, func() {
const buildID = 12345
const buildSummary = "foo"
builder := &bbpb.BuilderID{
Project: "aProject",
Bucket: "aBucket",
Builder: "aBuilder",
}
createTime := testclock.TestRecentTimeUTC
b := bbfake.NewBuildConstructor().
WithHost("example.buildbucket.com").
WithID(buildID).
WithBuilderID(builder).
WithCreateTime(createTime).
WithStatus(bbpb.Status_SCHEDULED).
WithSummaryMarkdown(buildSummary).
Construct()
ctx := context.Background()
Convey("Returns an error", func() {
Convey("On an invalid build status", func() {
b.Status = bbpb.Status_ENDED_MASK
_, _, err := parseStatusAndResult(ctx, b)
So(err, ShouldErrLike, "unexpected buildbucket status")
})
})
Convey("Parses a valid build proto", func() {
Convey("For an ended build", func() {
startTime := createTime.Add(1 * time.Minute)
endTime := createTime.Add(2 * time.Minute)
b = bbfake.NewConstructorFromBuild(b).
WithStartTime(startTime).
WithEndTime(endTime).
WithUpdateTime(endTime).
Construct()
Convey("That succeeded", func() {
b = bbfake.NewConstructorFromBuild(b).WithStatus(bbpb.Status_SUCCESS).Construct()
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_ENDED)
So(result.Status, ShouldEqual, tryjob.Result_SUCCEEDED)
So(result, ShouldResembleProto, &tryjob.Result{
Status: tryjob.Result_SUCCEEDED,
CreateTime: timestamppb.New(createTime),
UpdateTime: timestamppb.New(endTime),
Backend: &tryjob.Result_Buildbucket_{
Buildbucket: &tryjob.Result_Buildbucket{
Id: buildID,
Builder: builder,
Status: bbpb.Status_SUCCESS,
SummaryMarkdown: buildSummary,
},
},
})
})
Convey("That timed out", func() {
b = bbfake.NewConstructorFromBuild(b).
WithStatus(bbpb.Status_FAILURE).
WithTimeout(true).
Construct()
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_ENDED)
So(result.GetStatus(), ShouldEqual, tryjob.Result_TIMEOUT)
})
Convey("That failed", func() {
Convey("Transiently", func() {
b = bbfake.NewConstructorFromBuild(b).WithStatus(bbpb.Status_INFRA_FAILURE).Construct()
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_ENDED)
So(result.GetStatus(), ShouldEqual, tryjob.Result_FAILED_TRANSIENTLY)
})
Convey("Permanently", func() {
b.Status = bbpb.Status_FAILURE
status, result, err := parseStatusAndResult(ctx, b)
So(status, ShouldEqual, tryjob.Status_ENDED)
So(err, ShouldBeNil)
So(result.GetStatus(), ShouldEqual, tryjob.Result_FAILED_PERMANENTLY)
})
})
Convey("That cancelled", func() {
b = bbfake.NewConstructorFromBuild(b).WithStatus(bbpb.Status_INFRA_FAILURE).Construct()
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_ENDED)
So(result.GetStatus(), ShouldEqual, tryjob.Result_FAILED_TRANSIENTLY)
})
})
Convey("For a pending build", func() {
Convey("That is still scheduled", func() {
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_TRIGGERED)
So(result.Status, ShouldEqual, tryjob.Result_UNKNOWN)
})
Convey("That is already running", func() {
b = bbfake.NewConstructorFromBuild(b).
WithStatus(bbpb.Status_STARTED).
WithStartTime(createTime.Add(1 * time.Minute)).
Construct()
status, result, err := parseStatusAndResult(ctx, b)
So(err, ShouldBeNil)
So(status, ShouldEqual, tryjob.Status_TRIGGERED)
So(result.Status, ShouldEqual, tryjob.Result_UNKNOWN)
})
})
})
})
}
func TestParseOutput(t *testing.T) {
ctx := context.Background()
Convey("parseOutput", t, func() {
Convey("Allow reuse", func() {
Convey("For full runs", func() {
result := parseBuildResult(ctx, loadTestBuild("reuse_full"))
So(result.output, ShouldResembleProto, &recipe.Output{
Reuse: []*recipe.Output_Reuse{{ModeRegexp: "FULL_RUN"}},
})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
Convey("For dry runs", func() {
result := parseBuildResult(ctx, loadTestBuild("reuse_dry"))
So(result.output, ShouldResembleProto, &recipe.Output{
Reuse: []*recipe.Output_Reuse{{ModeRegexp: "DRY_RUN"}},
})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
})
Convey("Triggered ids", func() {
Convey("Legacy property only", func() {
result := parseBuildResult(ctx, loadTestBuild("triggered_builds_legacy"))
So(result.output, ShouldResembleProto, &recipe.Output{
TriggeredBuildIds: []int64{8832715138311111281},
})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
Convey("Proto property only", func() {
result := parseBuildResult(ctx, loadTestBuild("triggered_builds_new"))
So(result.output, ShouldResembleProto, &recipe.Output{
TriggeredBuildIds: []int64{8832715138311111281},
})
So(result.isTransFailure, ShouldBeFalse)
})
Convey("Proto overrides legacy", func() {
// In this test, legacy has a different triggered build id, the
// id set in the protobuf property should be the one in the
// returned output.
result := parseBuildResult(ctx, loadTestBuild("triggered_builds_conflict"))
So(result.output, ShouldResembleProto, &recipe.Output{
TriggeredBuildIds: []int64{8832715138311111281},
})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
})
Convey("Do not retry", func() {
Convey("Legacy property only", func() {
result := parseBuildResult(ctx, loadTestBuild("retry_denied_legacy"))
So(result.output, ShouldResembleProto, &recipe.Output{Retry: recipe.Output_OUTPUT_RETRY_DENIED})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
Convey("Proto property only", func() {
result := parseBuildResult(ctx, loadTestBuild("retry_denied_new"))
So(result.output, ShouldResembleProto, &recipe.Output{Retry: recipe.Output_OUTPUT_RETRY_DENIED})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
Convey("Proto overrides legacy", func() {
// In this test, the protobuf-based property allows retry and
// the legacy property denies it.
// Test that the protobuf property overrides the legacy one.
result := parseBuildResult(ctx, loadTestBuild("retry_denied_conflict"))
So(result.output, ShouldResembleProto, &recipe.Output{Retry: recipe.Output_OUTPUT_RETRY_ALLOWED})
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
})
Convey("Transient failure", func() {
result := parseBuildResult(ctx, loadTestBuild("transient_failure"))
So(result.output, ShouldResembleProto, &recipe.Output{})
So(result.isTransFailure, ShouldBeTrue)
So(result.err, ShouldBeNil)
})
Convey("No properties", func() {
result := parseBuildResult(ctx, loadTestBuild("no_props"))
So(result.output, ShouldBeNil)
So(result.isTransFailure, ShouldBeFalse)
So(result.err, ShouldBeNil)
})
Convey("Bad data", func() {
result := parseBuildResult(ctx, loadTestBuild("bad_data"))
So(result.output, ShouldResembleProto, &recipe.Output{})
So(result.isTransFailure, ShouldBeFalse)
So(result.err.Errors, ShouldHaveLength, 3)
})
})
}
func loadTestBuild(fixtureBaseName string) *bbpb.Build {
data, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", fixtureBaseName))
if err != nil {
panic(err)
}
ret := &bbpb.Build{}
if err := protojson.Unmarshal(data, ret); err != nil {
panic(err)
}
return ret
}