blob: ec8a6f7096ffbc89eb3fafa1a8d5f74c18a83086 [file] [log] [blame]
package buildbucket_taskbackend
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
buildbucket_mocks "go.skia.org/infra/go/buildbucket/mocks"
"go.skia.org/infra/go/firestore"
"go.skia.org/infra/go/now"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/task_scheduler/go/db"
"go.skia.org/infra/task_scheduler/go/mocks"
"go.skia.org/infra/task_scheduler/go/types"
"google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
fakeBuildbucketPubsubTopic = "fake-bb-pubsub"
fakeBuildbucketTarget = "skia://fake-scheduler"
fakeBuildbucketToken = "fake-token"
fakeBuildIdStr = "12345"
fakeJobName = "fake-job-name"
fakeTaskSchedulerHost = "https://fake-scheduler"
fakeProject = "fake-project"
fakeRepo = "https://fake.git"
fakeGerritHost = "fake-project-review.googlesource.com"
fakeGerritChange = 6789
fakeGerritPatchset = 3
)
var (
// This is a var instead of a constant so that we can take its address.
fakeBuildIdInt int64 = 12345
fakeCreateTime = firestore.FixTimestamp(time.Unix(1702395110, 0)) // Arbitrary timestamp.
)
func setup(t *testing.T) (context.Context, *TaskBackend, *mocks.JobDB, *buildbucket_mocks.BuildBucketInterface) {
ctx := now.TimeTravelingContext(fakeCreateTime.Add(time.Minute))
bb := &buildbucket_mocks.BuildBucketInterface{}
projectRepoMapping := map[string]string{
fakeProject: fakeRepo,
}
db := &mocks.JobDB{}
tb := NewTaskBackend(fakeBuildbucketTarget, fakeTaskSchedulerHost, projectRepoMapping, db, bb)
t.Cleanup(func() {
db.AssertExpectations(t)
bb.AssertExpectations(t)
})
return ctx, tb, db, bb
}
func fakeBuild() *buildbucketpb.Build {
return &buildbucketpb.Build{
Id: fakeBuildIdInt,
Builder: &buildbucketpb.BuilderID{
Project: fakeProject,
Builder: fakeJobName,
},
CreateTime: timestamppb.New(fakeCreateTime),
Input: &buildbucketpb.Build_Input{
GerritChanges: []*buildbucketpb.GerritChange{
{
Project: fakeProject,
Host: fakeGerritHost,
Change: fakeGerritChange,
Patchset: fakeGerritPatchset,
},
},
},
}
}
func fakeRunTaskRequest() *buildbucketpb.RunTaskRequest {
return &buildbucketpb.RunTaskRequest{
BuildId: fakeBuildIdStr,
PubsubTopic: fakeBuildbucketPubsubTopic,
Secrets: &buildbucketpb.BuildSecrets{
StartBuildToken: fakeBuildbucketToken,
},
Target: fakeBuildbucketTarget,
}
}
func makeJob(ctx context.Context) *types.Job {
return &types.Job{
Name: fakeJobName,
BuildbucketBuildId: fakeBuildIdInt,
BuildbucketPubSubTopic: fakeBuildbucketPubsubTopic,
BuildbucketToken: fakeBuildbucketToken,
Requested: fakeCreateTime,
Created: firestore.FixTimestamp(now.Now(ctx)),
RepoState: types.RepoState{
Patch: types.Patch{
Server: "https://" + fakeGerritHost,
Issue: strconv.FormatInt(fakeGerritChange, 10),
PatchRepo: fakeRepo,
Patchset: strconv.FormatInt(fakeGerritPatchset, 10),
},
Repo: fakeRepo,
Revision: "",
},
Status: types.JOB_STATUS_REQUESTED,
}
}
func TestTaskBackend_RunTask_Success(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(fakeBuild(), nil)
expectedJob := makeJob(ctx)
d.On("PutJob", testutils.AnyContext, expectedJob).Return(nil)
resp, err := tb.RunTask(ctx, fakeRunTaskRequest())
require.NoError(t, err)
require.Equal(t, &buildbucketpb.RunTaskResponse{
Task: JobToBuildbucketTask(ctx, expectedJob, fakeBuildbucketTarget, fakeTaskSchedulerHost),
}, resp)
}
func TestTaskBackend_RunTask_WrongTarget(t *testing.T) {
ctx, tb, _, _ := setup(t)
req := fakeRunTaskRequest()
req.Target = "bogus target"
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "incorrect target for this scheduler")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_InvalidID(t *testing.T) {
ctx, tb, _, _ := setup(t)
req := fakeRunTaskRequest()
req.BuildId = "not parseable as int64"
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "invalid build ID")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_Duplicate(t *testing.T) {
ctx, tb, d, _ := setup(t)
expectedJob := makeJob(ctx)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return([]*types.Job{expectedJob}, nil)
resp, err := tb.RunTask(ctx, fakeRunTaskRequest())
require.NoError(t, err)
require.Equal(t, &buildbucketpb.RunTaskResponse{
Task: JobToBuildbucketTask(ctx, expectedJob, fakeBuildbucketTarget, fakeTaskSchedulerHost),
}, resp)
}
func TestTaskBackend_RunTask_MultipleDuplicates_UseFirst(t *testing.T) {
ctx, tb, d, _ := setup(t)
expectedJob := makeJob(ctx)
otherJob := makeJob(ctx)
otherJob.Id = "not this one"
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return([]*types.Job{expectedJob, otherJob}, nil)
resp, err := tb.RunTask(ctx, fakeRunTaskRequest())
require.NoError(t, err)
require.Equal(t, &buildbucketpb.RunTaskResponse{
Task: JobToBuildbucketTask(ctx, expectedJob, fakeBuildbucketTarget, fakeTaskSchedulerHost),
}, resp)
}
func TestTaskBackend_RunTask_FailedSearchingJobs(t *testing.T) {
ctx, tb, d, _ := setup(t)
req := fakeRunTaskRequest()
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, errors.New("can't find the jobs!"))
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "failed looking for duplicate jobs")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_GetBuildFailed(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(nil, errors.New("can't find the build!"))
req := fakeRunTaskRequest()
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "failed to retrieve build")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_NoBuilder(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
build := fakeBuild()
build.Builder = nil
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(build, nil)
req := fakeRunTaskRequest()
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "builder isn't set")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_NoGerritChanges(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
build := fakeBuild()
build.Input.GerritChanges = nil
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(build, nil)
req := fakeRunTaskRequest()
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "input should have exactly one GerritChanges")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_UnknownPatchProject(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
build := fakeBuild()
build.Input.GerritChanges[0].Project = "bogus project"
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(build, nil)
req := fakeRunTaskRequest()
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "unknown patch project")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_FailedDBInsert(t *testing.T) {
ctx, tb, d, bb := setup(t)
d.On("SearchJobs", testutils.AnyContext, &db.JobSearchParams{
BuildbucketBuildID: &fakeBuildIdInt,
}).Return(nil, nil)
bb.On("GetBuild", testutils.AnyContext, fakeBuildIdInt).Return(fakeBuild(), nil)
expectedJob := makeJob(ctx)
d.On("PutJob", testutils.AnyContext, expectedJob).Return(errors.New("db failed"))
resp, err := tb.RunTask(ctx, fakeRunTaskRequest())
require.ErrorContains(t, err, "failed to insert Job into the DB")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_MissingSecrets(t *testing.T) {
ctx, tb, _, _ := setup(t)
req := fakeRunTaskRequest()
req.Secrets = nil
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "secrets not set on request")
require.Nil(t, resp)
}
func TestTaskBackend_RunTask_MissingStartBuildToken(t *testing.T) {
ctx, tb, _, _ := setup(t)
req := fakeRunTaskRequest()
req.Secrets.StartBuildToken = ""
resp, err := tb.RunTask(ctx, req)
require.ErrorContains(t, err, "missing StartBuildToken")
require.Nil(t, resp)
}
func TestTaskBackend_FetchTasks_Success(t *testing.T) {
ctx, tb, d, _ := setup(t)
job := makeJob(ctx)
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(job, nil)
resp, err := tb.FetchTasks(ctx, &buildbucketpb.FetchTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.FetchTasksResponse{
Responses: []*buildbucketpb.FetchTasksResponse_Response{
{
Response: &buildbucketpb.FetchTasksResponse_Response_Task{
Task: JobToBuildbucketTask(ctx, job, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
},
},
}, resp)
}
func TestTaskBackend_FetchTasks_WrongTarget(t *testing.T) {
ctx, tb, d, _ := setup(t)
job := makeJob(ctx)
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(job, nil)
resp, err := tb.FetchTasks(ctx, &buildbucketpb.FetchTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
{
Target: "bogus target",
Id: "fail-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.FetchTasksResponse{
Responses: []*buildbucketpb.FetchTasksResponse_Response{
{
Response: &buildbucketpb.FetchTasksResponse_Response_Task{
Task: JobToBuildbucketTask(ctx, job, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
},
{
Response: &buildbucketpb.FetchTasksResponse_Response_Error{
Error: &status.Status{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("incorrect target for this scheduler; expected %s", fakeBuildbucketTarget),
},
},
},
},
}, resp)
}
func TestTaskBackend_FetchTasks_DBError(t *testing.T) {
ctx, tb, d, _ := setup(t)
job := makeJob(ctx)
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(job, nil)
d.On("GetJobById", testutils.AnyContext, "fail-job-id").Return(nil, errors.New("DB error"))
resp, err := tb.FetchTasks(ctx, &buildbucketpb.FetchTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
{
Target: fakeBuildbucketTarget,
Id: "fail-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.FetchTasksResponse{
Responses: []*buildbucketpb.FetchTasksResponse_Response{
{
Response: &buildbucketpb.FetchTasksResponse_Response_Task{
Task: JobToBuildbucketTask(ctx, job, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
},
{
Response: &buildbucketpb.FetchTasksResponse_Response_Error{
Error: &status.Status{
Code: http.StatusInternalServerError,
Message: "DB error",
},
},
},
},
}, resp)
}
func TestTaskBackend_FetchTasks_NotFound(t *testing.T) {
ctx, tb, d, _ := setup(t)
job := makeJob(ctx)
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(job, nil)
d.On("GetJobById", testutils.AnyContext, "fail-job-id").Return(nil, nil)
resp, err := tb.FetchTasks(ctx, &buildbucketpb.FetchTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
{
Target: fakeBuildbucketTarget,
Id: "fail-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.FetchTasksResponse{
Responses: []*buildbucketpb.FetchTasksResponse_Response{
{
Response: &buildbucketpb.FetchTasksResponse_Response_Task{
Task: JobToBuildbucketTask(ctx, job, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
},
{
Response: &buildbucketpb.FetchTasksResponse_Response_Error{
Error: &status.Status{
Code: http.StatusNotFound,
Message: "unknown task",
},
},
},
},
}, resp)
}
func TestTaskBackend_CancelTasks_Success(t *testing.T) {
ctx, tb, d, _ := setup(t)
beforeJob := makeJob(ctx)
afterJob := beforeJob.Copy()
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(beforeJob, nil)
afterJob.Status = types.JOB_STATUS_CANCELED
afterJob.StatusDetails = "Canceled by Buildbucket"
d.On("PutJobs", testutils.AnyContext, []*types.Job{afterJob}).Return(nil)
resp, err := tb.CancelTasks(ctx, &buildbucketpb.CancelTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.CancelTasksResponse{
Tasks: []*buildbucketpb.Task{
JobToBuildbucketTask(ctx, afterJob, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
}, resp)
}
func TestTaskBackend_CancelTasks_WrongTarget(t *testing.T) {
ctx, tb, _, _ := setup(t)
resp, err := tb.CancelTasks(ctx, &buildbucketpb.CancelTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: "bogus-target",
Id: "my-job-id",
},
},
})
require.ErrorContains(t, err, "incorrect target for this scheduler")
require.Nil(t, resp)
}
func TestTaskBackend_CancelTasks_FailedDBRetrieve(t *testing.T) {
ctx, tb, d, _ := setup(t)
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(nil, nil)
resp, err := tb.CancelTasks(ctx, &buildbucketpb.CancelTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
},
})
require.ErrorContains(t, err, "unknown job")
require.Nil(t, resp)
}
func TestTaskBackend_CancelTasks_FailedDBInsert(t *testing.T) {
ctx, tb, d, _ := setup(t)
beforeJob := makeJob(ctx)
afterJob := beforeJob.Copy()
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(beforeJob, nil)
afterJob.Status = types.JOB_STATUS_CANCELED
afterJob.StatusDetails = "Canceled by Buildbucket"
d.On("PutJobs", testutils.AnyContext, []*types.Job{afterJob}).Return(errors.New("DB error"))
resp, err := tb.CancelTasks(ctx, &buildbucketpb.CancelTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
},
})
require.ErrorContains(t, err, "DB error")
require.Nil(t, resp)
}
func TestTaskBackend_CancelTasks_NoUpdates(t *testing.T) {
ctx, tb, d, _ := setup(t)
job := makeJob(ctx)
job.Status = types.JOB_STATUS_CANCELED
job.StatusDetails = "Canceled by Buildbucket"
d.On("GetJobById", testutils.AnyContext, "my-job-id").Return(job, nil)
resp, err := tb.CancelTasks(ctx, &buildbucketpb.CancelTasksRequest{
TaskIds: []*buildbucketpb.TaskID{
{
Target: fakeBuildbucketTarget,
Id: "my-job-id",
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.CancelTasksResponse{
Tasks: []*buildbucketpb.Task{
JobToBuildbucketTask(ctx, job, fakeBuildbucketTarget, fakeTaskSchedulerHost),
},
}, resp)
}
func TestTaskBackend_ValidateConfigs_Success(t *testing.T) {
ctx, tb, _, _ := setup(t)
resp, err := tb.ValidateConfigs(ctx, &buildbucketpb.ValidateConfigsRequest{
Configs: []*buildbucketpb.ValidateConfigsRequest_ConfigContext{
{
Target: fakeBuildbucketTarget,
ConfigJson: nil,
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.ValidateConfigsResponse{
ConfigErrors: nil,
}, resp)
}
func TestTaskBackend_ValidateConfigs_WrongTarget(t *testing.T) {
ctx, tb, _, _ := setup(t)
resp, err := tb.ValidateConfigs(ctx, &buildbucketpb.ValidateConfigsRequest{
Configs: []*buildbucketpb.ValidateConfigsRequest_ConfigContext{
{
Target: "bogus-target",
ConfigJson: nil,
},
},
})
require.NoError(t, err)
require.Equal(t, &buildbucketpb.ValidateConfigsResponse{
ConfigErrors: []*buildbucketpb.ValidateConfigsResponse_ErrorDetail{
{
Index: 0,
Error: fmt.Sprintf("incorrect target for this scheduler; expected %s", fakeBuildbucketTarget),
},
},
}, resp)
}