diff --git a/entity/build.go b/entity/build.go index c6253502..039b182a 100644 --- a/entity/build.go +++ b/entity/build.go @@ -7,34 +7,28 @@ const ( // BuildStatusUnknown is the unreachable state. It is set by default when the structure is initialized. It should never be seen in the system. BuildStatusUnknown BuildStatus = "" - // BuildStatusQueued indicates the build has been scheduled but not yet started. - BuildStatusQueued BuildStatus = "queued" + // BuildStatusAccepted indicates the build has been accepted by the CI provider. + BuildStatusAccepted BuildStatus = "accepted" - // BuildStatusRunning indicates the build is currently executing. - BuildStatusRunning BuildStatus = "running" - - // BuildStatusPassed indicates the build completed successfully. + // BuildStatusSucceeded indicates the build completed successfully. // This is a terminal state. - BuildStatusPassed BuildStatus = "passed" + BuildStatusSucceeded BuildStatus = "succeeded" // BuildStatusFailed indicates the build completed with failures. // This is a terminal state. BuildStatusFailed BuildStatus = "failed" - // BuildStatusCancelled indicates the build was cancelled before completion. + // BuildStatusCancelled indicates the build was cancelled by SubmitQueue. // This is a terminal state. + // Note: If the build system cancels a build for external reasons (e.g., timeout, resource limits), + // this should be reported as BuildStatusFailed, not BuildStatusCancelled. BuildStatusCancelled BuildStatus = "cancelled" - - // BuildStatusBlocked indicates the build is waiting for manual approval or unblocking. - // Some CI systems (like BuildKite) support manual approval steps. - BuildStatusBlocked BuildStatus = "blocked" ) -// IsTerminal returns true if the build state represents a final state (passed, failed, or cancelled). +// IsTerminal returns true if the build state represents a final state (succeeded, failed, or cancelled). // Terminal states indicate the build has finished and will not change state again. -// Note: BuildStatusBlocked is NOT terminal as blocked builds can be unblocked and continue execution. func (s BuildStatus) IsTerminal() bool { - return s == BuildStatusPassed || s == BuildStatusFailed || s == BuildStatusCancelled + return s == BuildStatusSucceeded || s == BuildStatusFailed || s == BuildStatusCancelled } @@ -61,3 +55,30 @@ type Build struct { // Status represents the state of the build lifecycle this build is in. Status BuildStatus } + +// ChangeAction defines the action to perform on a change submitted to the build system. +type ChangeAction string + +const ( + // ChangeActionUnknown is the sentinel value for uninitialized actions. + ChangeActionUnknown ChangeAction = "" + // ChangeActionApply applies the change to the target branch. + ChangeActionApply ChangeAction = "apply" + // ChangeActionValidate applies the change first, and then validates the change by running respective validation/test suites. + ChangeActionValidate ChangeAction = "validate" +) + +// BuildChange represents a code change to be processed by the build system. +// This is used by BuildManager to specify what changes to build and what action to perform. +type BuildChange struct { + // Change is the code change to process. + // This references the same Change entity used in Request, containing the source provider + // and list of change IDs (e.g., PR numbers, diff IDs). + Change Change + // Action specifies what operation to perform on this change. + Action ChangeAction +} + +// BuildMetadata contains additional metadata about a build returned by the build system. +// The specific keys and values are implementation-defined. +type BuildMetadata map[string]string diff --git a/entity/build_test.go b/entity/build_test.go index cf0aa0ac..93c71f0e 100644 --- a/entity/build_test.go +++ b/entity/build_test.go @@ -13,8 +13,8 @@ func TestBuildStatus_IsTerminal(t *testing.T) { expected bool }{ { - name: "passed is terminal", - status: BuildStatusPassed, + name: "succeeded is terminal", + status: BuildStatusSucceeded, expected: true, }, { @@ -28,30 +28,64 @@ func TestBuildStatus_IsTerminal(t *testing.T) { expected: true, }, { - name: "queued is not terminal", - status: BuildStatusQueued, + name: "accepted is not terminal", + status: BuildStatusAccepted, expected: false, }, { - name: "running is not terminal", - status: BuildStatusRunning, + name: "unknown is not terminal", + status: BuildStatusUnknown, expected: false, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.status.IsTerminal()) + }) + } +} + +func TestBuildChange_Creation(t *testing.T) { + tests := []struct { + name string + change BuildChange + wantID string + wantAction ChangeAction + }{ { - name: "blocked is not terminal", - status: BuildStatusBlocked, - expected: false, + name: "apply action", + change: BuildChange{ + ChangeID: "PR-42", + Action: ChangeActionApply, + }, + wantID: "PR-42", + wantAction: ChangeActionApply, }, { - name: "unknown is not terminal", - status: BuildStatusUnknown, - expected: false, + name: "validate action", + change: BuildChange{ + ChangeID: "D12345", + Action: ChangeActionValidate, + }, + wantID: "D12345", + wantAction: ChangeActionValidate, + }, + { + name: "unknown action", + change: BuildChange{ + ChangeID: "123", + Action: ChangeActionUnknown, + }, + wantID: "123", + wantAction: ChangeActionUnknown, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, tt.status.IsTerminal()) + assert.Equal(t, tt.wantID, tt.change.ChangeID) + assert.Equal(t, tt.wantAction, tt.change.Action) }) } } diff --git a/extension/build/BUILD.bazel b/extension/build/BUILD.bazel new file mode 100644 index 00000000..b5f597ae --- /dev/null +++ b/extension/build/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "build", + srcs = [ + "build_manager.go", + "errors.go", + ], + importpath = "github.com/uber/submitqueue/extension/build", + visibility = ["//visibility:public"], + deps = [ + "//entity", + ], +) diff --git a/extension/build/README.md b/extension/build/README.md new file mode 100644 index 00000000..8ce239be --- /dev/null +++ b/extension/build/README.md @@ -0,0 +1,347 @@ +# BuildManager Extension + +Vendor-agnostic interface for managing builds with external CI/CD providers. + +## Overview + +The BuildManager extension provides a clean abstraction for integrating with CI/CD systems like BuildKite, Jenkins, and others. It allows the Orchestrator service to schedule builds, poll their status, and cancel running builds without being coupled to any specific CI provider. + +## Interfaces + +### BuildManager + +Main interface for interacting with CI providers. + +```go +type BuildManager interface { + // Schedule submits a list of changes to the CI provider for processing + Schedule( + ctx context.Context, + queueName string, + changes []entity.BuildChange, + ) (string, error) + + // Poll retrieves the current status of a build from the CI provider + Poll(ctx context.Context, buildID string) (entity.BuildStatus, entity.BuildMetadata, error) + + // CancelBuild requests cancellation of a build (asynchronous operation) + CancelBuild(ctx context.Context, buildID string) error + + // Close gracefully shuts down the build manager + Close() error +} +``` + +**Thread-safety**: All implementations must be thread-safe and support concurrent operations. + +**Implementation Design**: Implementations are long-lived singletons (one per build provider) initialized at service startup, similar to Storage and other extension components. They should manage connection pooling, caching, and other resources for the lifetime of the service. + +## Types + +### BuildChange + +Represents a code change to be processed by the build system. + +```go +type BuildChange struct { + // Change is the code change to process. + // This references the same Change entity used in Request, containing the source provider + // and list of change IDs (e.g., PR numbers, diff IDs). + Change Change + // Action specifies what operation to perform on this change + Action ChangeAction +} +``` + +### ChangeAction + +Defines the action to perform on a change. + +```go +type ChangeAction string + +const ( + ChangeActionUnknown ChangeAction = "" // Sentinel value + ChangeActionApply ChangeAction = "apply" // Apply the change to the target branch + ChangeActionValidate ChangeAction = "validate" // Applies the change first, and then validates the change by running respective validation/test suites +) +``` + +### Schedule Parameters + +**queueName** (string, required): Name of the queue processing these changes. Used to look up job configuration from queue config. + +**changes** ([]entity.BuildChange, required): List of changes to process. Each change includes: +- **Change**: The code change entity containing source provider and list of change IDs (e.g., "D12345" for Phabricator, "42" for GitHub PR) +- **Action**: What to do with the change (validate or apply) + +Order of changes may be significant for dependencies. + +### Build ID Format + +Build IDs returned by `Schedule` should use a URI-like format: `"provider://id"` + +**Examples**: +- `"buildkite://uber/submitqueue-ci/123"` +- `"jenkins://456"` +- `"mock://1"` + +This format allows the implementation to encode both the provider name and provider-specific build identifier in a single string. + +### Build Status Enum + +```go +type BuildStatus string + +const ( + BuildStatusUnknown BuildStatus = "" // Sentinel value + BuildStatusAccepted BuildStatus = "accepted" // Accepted by CI provider + BuildStatusSucceeded BuildStatus = "succeeded" // Completed successfully (terminal) + BuildStatusFailed BuildStatus = "failed" // Completed with failures (terminal) + BuildStatusCancelled BuildStatus = "cancelled" // Cancelled by SubmitQueue (terminal) + // Note: Build system cancellations should be reported as BuildStatusFailed +) + +// IsTerminal returns true for succeeded/failed/cancelled states +func (s BuildStatus) IsTerminal() bool +``` + +**Build Lifecycle**: `accepted` → `succeeded`/`failed`/`cancelled` + +### Build Metadata + +The `Poll` method returns `entity.BuildMetadata` containing additional metadata about the build. The specific keys and values are implementation-defined, but common examples include: + +**Common metadata keys:** +- `build_url` - Direct link to the build in the CI provider's UI +- `commit_sha` - Git commit SHA being tested +- `duration_ms` - Build duration in milliseconds +- `started_at` - Build start timestamp +- `finished_at` - Build completion timestamp +- `error_message` - Error details for failed builds + +Implementations may include additional provider-specific metadata. Consumers should handle missing keys gracefully. + +## Error Handling + +The extension defines sentinel errors following the SubmitQueue pattern: + +- **`ErrBuildNotFound`** - Build doesn't exist or was deleted +- **`ErrInvalidRequest`** - Request validation failed + +Each error has helper functions: +- `Is{Error}(err)` - Check if error is of specific type +- `Wrap{Error}(err)` - Wrap provider-specific errors + +Example: +```go +status, metadata, err := buildMgr.Poll(ctx, buildID) +if build.IsBuildNotFound(err) { + // Handle missing build +} +``` + +## Usage + +### Basic Workflow + +```go +// 1. Schedule a build with changes +changes := []entity.BuildChange{ + { + Change: entity.Change{Source: "github", IDs: []string{"123"}}, + Action: entity.ChangeActionApply, + }, + { + Change: entity.Change{Source: "github", IDs: []string{"124"}}, + Action: entity.ChangeActionValidate, + }, +} + +buildID, err := buildMgr.Schedule(ctx, "my-queue", changes) +if err != nil { + // Handle error +} + +// 2. Poll for build status +status, metadata, err := buildMgr.Poll(ctx, buildID) +if err != nil { + // Handle error +} + +// Access build metadata +buildURL := metadata["build_url"] +commitSHA := metadata["commit_sha"] + +// 3. Check if build is done +if status.IsTerminal() { + // Build finished: status is succeeded, failed, or cancelled +} +``` + +### GoMock Mocks + +For unit testing with gomock, use the generated mock: + +```go +import ( + "testing" + + "github.com/uber/submitqueue/entity" + "github.com/uber/submitqueue/extension/build/mock" + "go.uber.org/mock/gomock" +) + +func TestMyController(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBuildMgr := mock.NewMockBuildManager(ctrl) + + // Set up expectations + changes := []entity.BuildChange{ + { + Change: entity.Change{Source: "github", IDs: []string{"123"}}, + Action: entity.ChangeActionValidate, + }, + } + + mockBuildMgr.EXPECT(). + Schedule(gomock.Any(), "test-queue", changes). + Return("mock://1", nil) + + // Test your code that uses the mock +} +``` + +### Cancelling Builds + +Cancellation is **asynchronous** - the method initiates the cancellation request and returns immediately. + +**Important**: SubmitQueue sets `BuildStatusCancelled` immediately upon calling `CancelBuild`, without waiting for confirmation from the build system. The build status reflects SubmitQueue's decision to cancel, not the build system's actual state. + +```go +err := mgr.CancelBuild(ctx, buildID) +if build.IsBuildNotFound(err) { + // Build doesn't exist +} else if err != nil { + // Other error +} + +// SubmitQueue will have already marked the build as BuildStatusCancelled +// No need to poll for confirmation +``` + +## Implementing a New Provider + +To add support for a new CI provider: + +1. **Create provider directory**: `extension/build/{provider}/` + +2. **Implement BuildManager interface**: + ```go + package jenkins + + import "github.com/uber/submitqueue/extension/build" + + type Params struct { + // Provider-specific configuration + BaseURL string + Username string + APIToken string + QueueConfig QueueConfigStore // For looking up job names + Logger *zap.Logger + MetricsScope tally.Scope + } + + func NewBuildManager(params Params) (build.BuildManager, error) { + // Validate params + // Create HTTP client + // Return implementation + } + ``` + +3. **Map provider states to entity.BuildStatus enum**: + - Map provider's state values to the standard `entity.BuildStatus` constants + - Include mapping for `BuildStatusAccepted` if provider supports it + - Use `entity.BuildStatusUnknown` for unexpected states + +4. **Handle provider errors**: + - 404 errors → `build.WrapBuildNotFound()` + - Validation errors → `build.WrapInvalidRequest()` + +5. **Implement Schedule method**: + - Look up job name from queue config using `queueName` parameter + - Handle both `ChangeActionApply` and `ChangeActionValidate` actions + - Create appropriate builds/jobs for each change + - Return unique build ID + +6. **Implement CancelBuild**: + - Make asynchronous cancellation request to CI provider + - Return immediately after initiating request + +7. **Add tests**: + - Unit tests with mock HTTP server + - Validation tests for all required fields + - Error mapping tests + - Thread-safety tests + - Test both validate and apply actions + +8. **Update BUILD.bazel**: + ```bazel + go_library( + name = "jenkins", + srcs = ["jenkins.go"], + importpath = "github.com/uber/submitqueue/extension/build/jenkins", + visibility = ["//visibility:public"], + deps = [ + "//entity", + "//extension/build", + "//extension/queueconfig", + "@org_uber_go_zap//:zap", + "@com_github_uber_go_tally_v4//:tally", + ], + ) + ``` + +## Architecture + +### Extension Pattern + +Following the established SubmitQueue extension pattern: + +``` +extension/build/ +├── build_manager.go # Interface definition +├── errors.go # Sentinel errors +├── README.md # This file +├── BUILD.bazel # Bazel configuration +├── mock/ # Generated gomock mocks +│ ├── build_manager.go # Generated by mockgen +│ ├── build_manager_test.go +│ └── BUILD.bazel +└── {provider}/ # Provider implementations (future) + ├── {provider}.go + ├── {provider}_test.go + └── BUILD.bazel +``` + +### Design Principles + +1. **Vendor-agnostic**: Interface doesn't leak provider-specific details +2. **Change-focused**: Operates on individual changes with explicit actions +3. **Thread-safe**: All implementations support concurrent operations +4. **Error transparency**: Sentinel errors for common failure modes +5. **Long-lived singletons**: Implementations are initialized once per build provider at service startup +6. **Asynchronous cancellation**: CancelBuild initiates request and returns immediately + +## Future Enhancements + +Potential improvements not in the current implementation: + +- **Webhook support**: Accept push notifications from CI providers instead of polling +- **Build artifacts**: Track and retrieve build artifacts +- **Retry logic**: Automatic retry for transient failures +- **Batch operations**: Poll/cancel multiple builds at once +- **Streaming logs**: Real-time log streaming from builds +- **Build caching**: Cache build status to reduce API calls diff --git a/extension/build/build_manager.go b/extension/build/build_manager.go new file mode 100644 index 00000000..83bac312 --- /dev/null +++ b/extension/build/build_manager.go @@ -0,0 +1,72 @@ +package build + +//go:generate mockgen -source=build_manager.go -destination=mock/build_manager.go -package=mock + +import ( + "context" + + "github.com/uber/submitqueue/entity" +) + +// BuildManager is a vendor-agnostic interface for managing builds with external CI/CD providers. +// Implementations provide integration with specific CI systems (BuildKite, Jenkins, etc.) +// to schedule builds, poll their status, and cancel running builds. +// +// Implementations are long-lived singletons (one per build provider) initialized at service +// startup, similar to Storage and other extension components. They should manage connection +// pooling, caching, and other resources for the lifetime of the service. +// +// All implementations must be thread-safe and support concurrent operations. +type BuildManager interface { + // Schedule submits a list of changes to the CI provider for processing. + // Each change specifies an action (validate or apply) to perform. + // + // The implementation is responsible for: + // - Looking up the job name from the queue configuration + // - Creating appropriate builds/jobs for each change based on its action + // - Handling dependencies between changes (order may be significant) + // + // Parameters: + // - ctx: Request context for cancellation and timeouts + // - queueName: Name of the queue processing these changes. Used to look up job configuration. + // - changes: List of changes to process. Order may be significant for dependencies. + // + // Returns: + // - string: Unique build ID that can be used with Poll and CancelBuild methods + // - error: ErrInvalidRequest if validation fails + Schedule(ctx context.Context, queueName string, changes []entity.BuildChange) (string, error) + + // Poll retrieves the current status of a build from the CI provider. + // This is a synchronous call that queries the provider's API. + // + // Parameters: + // - buildID: Build ID string + // + // Returns: + // - BuildStatus: Current state of the build + // - BuildMetadata: Additional metadata about the build (e.g., build URL, commit SHA, duration) + // - error: ErrBuildNotFound if the build doesn't exist + Poll(ctx context.Context, buildID string) (entity.BuildStatus, entity.BuildMetadata, error) + + // CancelBuild requests cancellation of a build. + // + // This operation is asynchronous and does not wait for the cancellation to complete. + // The implementation should initiate the cancellation request with the CI provider + // and return immediately. + // + // SubmitQueue will mark the build as BuildStatusCancelled immediately without waiting + // for confirmation from the build system. + // + // Parameters: + // - buildID: Build ID string + // + // Returns: + // - error: ErrBuildNotFound if the build doesn't exist + CancelBuild(ctx context.Context, buildID string) error + + // Close gracefully shuts down the build manager. + // Implementations should close HTTP clients and clean up resources. + // After Close is called, all other methods should return errors. + // Close is idempotent and safe to call multiple times. + Close() error +} diff --git a/extension/build/errors.go b/extension/build/errors.go new file mode 100644 index 00000000..00727e37 --- /dev/null +++ b/extension/build/errors.go @@ -0,0 +1,41 @@ +package build + +import ( + "errors" + "fmt" +) + +// ErrBuildNotFound is returned when a build does not exist in the CI provider. +// This can occur if: +// - The build ID is invalid or malformed +// - The build was deleted from the provider +// - The build never existed +var ErrBuildNotFound = errors.New("build not found") + +// IsBuildNotFound returns true if any error in the error chain is ErrBuildNotFound. +func IsBuildNotFound(err error) bool { + return errors.Is(err, ErrBuildNotFound) +} + +// WrapBuildNotFound wraps ErrBuildNotFound with the original error from the build provider. +// This preserves the original error details while marking it as a "not found" error. +func WrapBuildNotFound(err error) error { + return fmt.Errorf("%w: %w", ErrBuildNotFound, err) +} + +// ErrInvalidRequest is returned when Schedule parameters fail validation. +// This can occur when: +// - queueName is empty or invalid +// - changes list is empty +// - changes contain invalid Change entities or Actions +var ErrInvalidRequest = errors.New("invalid request") + +// IsInvalidRequest returns true if any error in the error chain is ErrInvalidRequest. +func IsInvalidRequest(err error) bool { + return errors.Is(err, ErrInvalidRequest) +} + +// WrapInvalidRequest wraps ErrInvalidRequest with a descriptive error message. +func WrapInvalidRequest(err error) error { + return fmt.Errorf("%w: %w", ErrInvalidRequest, err) +} diff --git a/extension/build/mock/BUILD.bazel b/extension/build/mock/BUILD.bazel new file mode 100644 index 00000000..3de8b8ff --- /dev/null +++ b/extension/build/mock/BUILD.bazel @@ -0,0 +1,22 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "mock", + srcs = ["build_manager.go"], + importpath = "github.com/uber/submitqueue/extension/build/mock", + visibility = ["//visibility:public"], + deps = [ + "//entity", + "@org_uber_go_mock//gomock", + ], +) + +go_test( + name = "mock_test", + srcs = ["build_manager_test.go"], + embed = [":mock"], + deps = [ + "//entity", + "@org_uber_go_mock//gomock", + ], +) diff --git a/extension/build/mock/build_manager.go b/extension/build/mock/build_manager.go new file mode 100644 index 00000000..2f46269a --- /dev/null +++ b/extension/build/mock/build_manager.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: build_manager.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + entity "github.com/uber/submitqueue/entity" +) + +// MockBuildManager is a mock of BuildManager interface. +type MockBuildManager struct { + ctrl *gomock.Controller + recorder *MockBuildManagerMockRecorder +} + +// MockBuildManagerMockRecorder is the mock recorder for MockBuildManager. +type MockBuildManagerMockRecorder struct { + mock *MockBuildManager +} + +// NewMockBuildManager creates a new mock instance. +func NewMockBuildManager(ctrl *gomock.Controller) *MockBuildManager { + mock := &MockBuildManager{ctrl: ctrl} + mock.recorder = &MockBuildManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBuildManager) EXPECT() *MockBuildManagerMockRecorder { + return m.recorder +} + +// CancelBuild mocks base method. +func (m *MockBuildManager) CancelBuild(ctx context.Context, buildID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelBuild", ctx, buildID) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelBuild indicates an expected call of CancelBuild. +func (mr *MockBuildManagerMockRecorder) CancelBuild(ctx, buildID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelBuild", reflect.TypeOf((*MockBuildManager)(nil).CancelBuild), ctx, buildID) +} + +// Close mocks base method. +func (m *MockBuildManager) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockBuildManagerMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockBuildManager)(nil).Close)) +} + +// Poll mocks base method. +func (m *MockBuildManager) Poll(ctx context.Context, buildID string) (entity.BuildStatus, entity.BuildMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Poll", ctx, buildID) + ret0, _ := ret[0].(entity.BuildStatus) + ret1, _ := ret[1].(entity.BuildMetadata) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Poll indicates an expected call of Poll. +func (mr *MockBuildManagerMockRecorder) Poll(ctx, buildID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Poll", reflect.TypeOf((*MockBuildManager)(nil).Poll), ctx, buildID) +} + +// Schedule mocks base method. +func (m *MockBuildManager) Schedule(ctx context.Context, queueName string, changes []entity.BuildChange) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Schedule", ctx, queueName, changes) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Schedule indicates an expected call of Schedule. +func (mr *MockBuildManagerMockRecorder) Schedule(ctx, queueName, changes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schedule", reflect.TypeOf((*MockBuildManager)(nil).Schedule), ctx, queueName, changes) +} diff --git a/extension/build/mock/build_manager_test.go b/extension/build/mock/build_manager_test.go new file mode 100644 index 00000000..56cdb0bf --- /dev/null +++ b/extension/build/mock/build_manager_test.go @@ -0,0 +1,60 @@ +package mock + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "github.com/uber/submitqueue/entity" +) + +// TestMockBuildManager_Compilation verifies the mock compiles and basic setup works. +func TestMockBuildManager_Compilation(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBuildMgr := NewMockBuildManager(ctrl) + + // Test Schedule + expectedBuildID := "mock://test-build-123" + mockBuildMgr.EXPECT(). + Schedule(gomock.Any(), gomock.Any(), gomock.Any()). + Return(expectedBuildID, nil) + + queueName := "test-queue" + changes := []entity.BuildChange{ + {ChangeID: "D12345", Action: entity.ChangeActionApply}, + {ChangeID: "D12346", Action: entity.ChangeActionValidate}, + } + + buildID, err := mockBuildMgr.Schedule( + context.Background(), queueName, changes, + ) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if buildID != expectedBuildID { + t.Fatalf("expected build ID %v, got %v", expectedBuildID, buildID) + } + + // Test Poll + expectedMetadata := entity.BuildMetadata{ + "build_url": "https://ci.example.com/builds/123", + "commit_sha": "abc123", + } + mockBuildMgr.EXPECT(). + Poll(gomock.Any(), gomock.Any()). + Return(entity.BuildStatusSucceeded, expectedMetadata, nil) + + status, metadata, err := mockBuildMgr.Poll(context.Background(), buildID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if status != entity.BuildStatusSucceeded { + t.Fatalf("expected %v, got %v", entity.BuildStatusSucceeded, status) + } + if metadata["build_url"] != expectedMetadata["build_url"] { + t.Fatalf("expected metadata %v, got %v", expectedMetadata, metadata) + } +}