diff --git a/Makefile b/Makefile index 5aeff768..11b4a083 100644 --- a/Makefile +++ b/Makefile @@ -364,7 +364,7 @@ local-stovepipe-stop: ## Stop the Stovepipe service mocks: ## Generate mock files using mockgen @echo "Generating mocks..." - @$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/mergechecker/... ./submitqueue/extension/pusher/... ./submitqueue/extension/scorer/... ./submitqueue/extension/conflict/... ./platform/consumer/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/... + @$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/mergechecker/... ./submitqueue/extension/pusher/... ./submitqueue/extension/scorer/... ./submitqueue/extension/conflict/... ./submitqueue/extension/speculation/enumerator/... ./submitqueue/extension/speculation/dependencylimit/... ./platform/consumer/... ./submitqueue/core/changeset/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/... @echo "Mocks generated successfully!" proto: ## Generate protobuf files from .proto definitions diff --git a/submitqueue/extension/speculation/dependencylimit/BUILD.bazel b/submitqueue/extension/speculation/dependencylimit/BUILD.bazel new file mode 100644 index 00000000..5abae99b --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["dependencylimit.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit", + visibility = ["//visibility:public"], +) diff --git a/submitqueue/extension/speculation/dependencylimit/README.md b/submitqueue/extension/speculation/dependencylimit/README.md new file mode 100644 index 00000000..0368f2c8 --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/README.md @@ -0,0 +1,17 @@ +# Speculation Dependency Limit + +Vendor-agnostic "how much" policy that bounds how many **active** (in-flight, non-terminal) dependencies a batch may speculate over. + +See the [Speculation RFC](../../../../doc/rfc/submitqueue/speculation.md) for the end-to-end design and how limits fit into the two-layer speculation model. + +## Dependency Limit + +Speculation splits into *decision seams* (what to build) and *limit policies* (how much to allow). The dependency limit is the first limit: it is the **eligibility gate** for speculation. A batch becomes eligible to enumerate only when its count of active dependencies is at or below the current limit; otherwise it waits. Nothing is dropped — as dependencies land they leave the active set, the count shrinks, and the batch is admitted. The gate applies even to the fully-stacked happy path, so a very long chain is not speculated in full at once. + +The value is **signal-driven**, not a fixed constant. Its primary input is the build system's available capacity, so a period of CI pressure can shrink how deep the queue speculates, but a policy may also weigh historical pass rates, cost budgets, time of day, or an experiment toggle. Because the value is dynamic, a change to the limit alone — not only a landing dependency or a DAG change — can newly admit a waiting batch. + +Unlike the selection and prioritization limits, the dependency limit is **not injected into a decision seam**. It gates eligibility *before* enumeration and needs active-dependency reconciliation, which is controller orchestration — so the controller holds it, consults it on every respeculate, and applies it, keeping the enumerator pure. + +## Factory + +A per-queue factory returns the limit policy for a queue, following the repo's extension contract. It is handed only the queue identity; the signals a policy weighs — a capacity feed, historical metrics, config — are injected at construction by the integrator in the wiring layer. Computing the limit itself takes no further inputs. diff --git a/submitqueue/extension/speculation/dependencylimit/dependencylimit.go b/submitqueue/extension/speculation/dependencylimit/dependencylimit.go new file mode 100644 index 00000000..b32d47f3 --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/dependencylimit.go @@ -0,0 +1,61 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 dependencylimit + +//go:generate mockgen -source=dependencylimit.go -destination=mock/dependencylimit_mock.go -package=mock + +import "context" + +// DependencyLimit is the "how much" policy that bounds how many active +// (in-flight, non-terminal) dependencies a batch may speculate over. +// +// It is the eligibility gate for speculation: a batch becomes eligible to +// enumerate only when its count of active dependencies is at or below the +// current limit; otherwise it waits, and is admitted later as predecessors land +// and leave the active set. The limit is a bound, not a trim — nothing is +// dropped from a batch's base. +// +// The value is dynamic: it may change between calls — not only when a +// dependency lands — so a change alone can newly admit a waiting batch, and the +// controller re-consults it on every respeculate rather than caching it. +// +// This limit is the exception among the speculation limits: it gates eligibility +// *before* enumeration and needs active-dependency reconciliation, which is +// controller orchestration — so the controller holds and applies it, rather than +// it being injected into a decision seam. The enumerator stays pure. +type DependencyLimit interface { + // Limit returns the current maximum number of active dependencies a batch + // may speculate over. The controller compares a batch's active-dependency + // count against this to decide eligibility. It takes no parameters; anything + // an implementation needs is injected at construction. + Limit(ctx context.Context) (int, error) +} + +// Config carries the per-queue identity handed to a Factory. The system knows +// only the queue name; everything a policy needs to compute the limit (a +// capacity feed, historical metrics, config) is injected at construction by the +// integrator. +type Config struct { + // QueueName identifies the queue this DependencyLimit serves. + QueueName string +} + +// Factory builds the DependencyLimit for a queue. Implementations are provided +// by integrators (and tests) and inject whatever signals they need at +// construction. +type Factory interface { + // For returns the DependencyLimit for the given queue. + For(cfg Config) (DependencyLimit, error) +} diff --git a/submitqueue/extension/speculation/dependencylimit/fake/BUILD.bazel b/submitqueue/extension/speculation/dependencylimit/fake/BUILD.bazel new file mode 100644 index 00000000..41a13937 --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/fake/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["fake.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit/fake", + visibility = ["//visibility:public"], + deps = ["//submitqueue/extension/speculation/dependencylimit:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["fake_test.go"], + embed = [":go_default_library"], + deps = [ + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/submitqueue/extension/speculation/dependencylimit/fake/fake.go b/submitqueue/extension/speculation/dependencylimit/fake/fake.go new file mode 100644 index 00000000..3045cfdd --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/fake/fake.go @@ -0,0 +1,52 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 fake provides a programmable dependencylimit.DependencyLimit for tests +// and examples. New sets the value returned by Limit; FailWith injects an error +// on every call. It is intended for examples and tests only, never production. +package fake + +import ( + "context" + + "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit" +) + +// DependencyLimit is a programmable dependencylimit.DependencyLimit. +type DependencyLimit struct { + limit int + err error +} + +// New returns a fake DependencyLimit whose Limit returns the given value. +func New(limit int) *DependencyLimit { + return &DependencyLimit{limit: limit} +} + +// FailWith makes every Limit call return err. +func (l *DependencyLimit) FailWith(err error) *DependencyLimit { + l.err = err + return l +} + +// Limit returns the configured value, or the injected error if FailWith was set. +func (l *DependencyLimit) Limit(_ context.Context) (int, error) { + if l.err != nil { + return 0, l.err + } + return l.limit, nil +} + +// ensure the fake satisfies the interface. +var _ dependencylimit.DependencyLimit = (*DependencyLimit)(nil) diff --git a/submitqueue/extension/speculation/dependencylimit/fake/fake_test.go b/submitqueue/extension/speculation/dependencylimit/fake/fake_test.go new file mode 100644 index 00000000..36bcb690 --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/fake/fake_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 fake + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLimit_ReturnsConfiguredValue(t *testing.T) { + got, err := New(3).Limit(context.Background()) + require.NoError(t, err) + assert.Equal(t, 3, got) +} + +func TestLimit_FailWith(t *testing.T) { + sentinel := errors.New("boom") + _, err := New(3).FailWith(sentinel).Limit(context.Background()) + require.ErrorIs(t, err, sentinel) +} diff --git a/submitqueue/extension/speculation/dependencylimit/mock/BUILD.bazel b/submitqueue/extension/speculation/dependencylimit/mock/BUILD.bazel new file mode 100644 index 00000000..3e928a64 --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/mock/BUILD.bazel @@ -0,0 +1,12 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["dependencylimit_mock.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit/mock", + visibility = ["//visibility:public"], + deps = [ + "//submitqueue/extension/speculation/dependencylimit:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", + ], +) diff --git a/submitqueue/extension/speculation/dependencylimit/mock/dependencylimit_mock.go b/submitqueue/extension/speculation/dependencylimit/mock/dependencylimit_mock.go new file mode 100644 index 00000000..52c743cd --- /dev/null +++ b/submitqueue/extension/speculation/dependencylimit/mock/dependencylimit_mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dependencylimit.go +// +// Generated by this command: +// +// mockgen -source=dependencylimit.go -destination=mock/dependencylimit_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + dependencylimit "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit" + gomock "go.uber.org/mock/gomock" +) + +// MockDependencyLimit is a mock of DependencyLimit interface. +type MockDependencyLimit struct { + ctrl *gomock.Controller + recorder *MockDependencyLimitMockRecorder + isgomock struct{} +} + +// MockDependencyLimitMockRecorder is the mock recorder for MockDependencyLimit. +type MockDependencyLimitMockRecorder struct { + mock *MockDependencyLimit +} + +// NewMockDependencyLimit creates a new mock instance. +func NewMockDependencyLimit(ctrl *gomock.Controller) *MockDependencyLimit { + mock := &MockDependencyLimit{ctrl: ctrl} + mock.recorder = &MockDependencyLimitMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDependencyLimit) EXPECT() *MockDependencyLimitMockRecorder { + return m.recorder +} + +// Limit mocks base method. +func (m *MockDependencyLimit) Limit(ctx context.Context) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Limit", ctx) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Limit indicates an expected call of Limit. +func (mr *MockDependencyLimitMockRecorder) Limit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Limit", reflect.TypeOf((*MockDependencyLimit)(nil).Limit), ctx) +} + +// MockFactory is a mock of Factory interface. +type MockFactory struct { + ctrl *gomock.Controller + recorder *MockFactoryMockRecorder + isgomock struct{} +} + +// MockFactoryMockRecorder is the mock recorder for MockFactory. +type MockFactoryMockRecorder struct { + mock *MockFactory +} + +// NewMockFactory creates a new mock instance. +func NewMockFactory(ctrl *gomock.Controller) *MockFactory { + mock := &MockFactory{ctrl: ctrl} + mock.recorder = &MockFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { + return m.recorder +} + +// For mocks base method. +func (m *MockFactory) For(cfg dependencylimit.Config) (dependencylimit.DependencyLimit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(dependencylimit.DependencyLimit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// For indicates an expected call of For. +func (mr *MockFactoryMockRecorder) For(cfg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "For", reflect.TypeOf((*MockFactory)(nil).For), cfg) +} diff --git a/submitqueue/extension/speculation/enumerator/BUILD.bazel b/submitqueue/extension/speculation/enumerator/BUILD.bazel new file mode 100644 index 00000000..ad5c3284 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["enumerator.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator", + visibility = ["//visibility:public"], + deps = ["//submitqueue/entity:go_default_library"], +) diff --git a/submitqueue/extension/speculation/enumerator/README.md b/submitqueue/extension/speculation/enumerator/README.md new file mode 100644 index 00000000..9dfe9121 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/README.md @@ -0,0 +1,19 @@ +# Speculation Tree Enumerator + +Vendor-agnostic interface for enumerating the **speculation tree** of a batch — the set of candidate speculation paths the orchestrator may build. + +See the [Speculation RFC](../../../../doc/rfc/submitqueue/speculation.md) for the end-to-end design and how enumeration fits into the orchestrator pipeline. + +## Enumerator + +An enumerator is deliberately **dumb** and purely **structural**: *given a batch and its active dependency batches, it mechanically lists the candidate paths.* It does **not** score paths — that is the [scorer](../scorer)'s job, which the controller re-runs on every respeculate — it does **not** decide which paths to build — that is the [selector](../selector)'s job — it does **not** set path status, and it does **not** decide how far back to speculate. The dependency limit is the controller's responsibility: the controller gates a batch on the limit and hands the enumerator exactly the active dependencies to speculate over, which it then enumerates over verbatim. + +Each candidate is a path: an assumed-good prefix of predecessor batches (the base) on top of which the batch under verification (the head) is built. The base maps directly onto the build stage's base changes and the head onto the changes being validated. + +Enumeration is **pure and deterministic**: the same batch and dependency list always produce the same tree. This lets the controller regenerate a tree whenever the dependency graph changes without tracking incremental state in the enumerator. Keeping enumeration tractable for a very wide dependency list is the enumerator's only real concern. + +The returned paths carry structure only — a Base/Head split, with `Score` and `Status` left unset. The controller stamps `Status` when it persists the tree and calls the scorer to fill `Score`; enumeration produces neither. + +## Factory + +A per-queue factory returns the enumerator for a queue, following the repo's extension contract. It is handed only the queue identity and nothing else; everything an implementation needs — including behavioral knobs like enumeration breadth — is injected at construction by the integrator in the wiring layer, which resolves per-queue settings through `queueconfig`. Enumeration itself stays config-free. diff --git a/submitqueue/extension/speculation/enumerator/enumerator.go b/submitqueue/extension/speculation/enumerator/enumerator.go new file mode 100644 index 00000000..a10b11b3 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/enumerator.go @@ -0,0 +1,63 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 enumerator + +//go:generate mockgen -source=enumerator.go -destination=mock/enumerator_mock.go -package=mock + +import ( + "context" + + "github.com/uber/submitqueue/submitqueue/entity" +) + +// Enumerator builds the speculation tree for a batch: the set of candidate +// speculation paths to consider. +// +// Enumeration answers "what futures are possible" for a batch. It is +// deliberately dumb and purely structural: it mechanically lists candidate +// Base/Head paths from the dependency batches it is handed and nothing else. It +// does not score paths — that is the scorer's job (see +// extension/speculation/scorer), which the controller re-runs on every +// respeculate — it does not decide which paths to build — that is the selector's +// job (see extension/speculation/selector) — it does not set path status, and it +// does not decide how far back to speculate: the controller gates on the +// dependency limit and hands Enumerate exactly the active dependencies to +// speculate over. +type Enumerator interface { + // Enumerate returns the speculation tree structure for the batch identified + // by batchID, given its active dependency batches in arrival order. Each + // returned path carries a Base/Head split only: Score and Status are left + // unset — the controller stamps Status on persist and calls the scorer to + // fill Score. + // + // Enumeration is pure and deterministic: the same (batchID, deps) always + // yields the same tree, so callers may regenerate safely. + Enumerate(ctx context.Context, batchID string, deps []entity.Batch) (entity.SpeculationTree, error) +} + +// Config carries the per-queue identity handed to a Factory. The system knows +// only the queue name; everything an implementation needs (including behavioral +// knobs such as speculation depth) is injected at construction by the integrator. +type Config struct { + // QueueName identifies the queue this Enumerator serves. + QueueName string +} + +// Factory builds the Enumerator for a queue. Implementations are provided by +// integrators (and tests) and inject whatever they need at construction. +type Factory interface { + // For returns the Enumerator for the given queue. + For(cfg Config) (Enumerator, error) +} diff --git a/submitqueue/extension/speculation/enumerator/fake/BUILD.bazel b/submitqueue/extension/speculation/enumerator/fake/BUILD.bazel new file mode 100644 index 00000000..14e92890 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/fake/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["fake.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator/fake", + visibility = ["//visibility:public"], + deps = [ + "//submitqueue/entity:go_default_library", + "//submitqueue/extension/speculation/enumerator:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["fake_test.go"], + embed = [":go_default_library"], + deps = [ + "//submitqueue/entity:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/submitqueue/extension/speculation/enumerator/fake/fake.go b/submitqueue/extension/speculation/enumerator/fake/fake.go new file mode 100644 index 00000000..3ed0f7a8 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/fake/fake.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 fake provides a programmable in-memory enumerator.Enumerator for tests +// and examples. Seed the tree returned for a batch with Set, keyed by batch ID; +// an unseeded batch enumerates to an empty tree carrying the batch's identity. +// FailWith injects an error on every call to exercise the error path. It is +// intended for examples and tests only, never production. +package fake + +import ( + "context" + + "github.com/uber/submitqueue/submitqueue/entity" + "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator" +) + +// Enumerator is a programmable in-memory enumerator.Enumerator. +type Enumerator struct { + trees map[string]entity.SpeculationTree + err error +} + +// New returns an empty fake Enumerator. Seed it with Set. +func New() *Enumerator { + return &Enumerator{trees: map[string]entity.SpeculationTree{}} +} + +// Set seeds the tree returned by Enumerate for the given batch ID. +func (e *Enumerator) Set(batchID string, tree entity.SpeculationTree) *Enumerator { + e.trees[batchID] = tree + return e +} + +// FailWith makes every Enumerate call return err. +func (e *Enumerator) FailWith(err error) *Enumerator { + e.err = err + return e +} + +// Enumerate returns the seeded tree for the batch. An unseeded batch returns an +// empty tree carrying the batch's identity. The deps argument is ignored. +func (e *Enumerator) Enumerate(_ context.Context, batchID string, _ []entity.Batch) (entity.SpeculationTree, error) { + if e.err != nil { + return entity.SpeculationTree{}, e.err + } + if tree, ok := e.trees[batchID]; ok { + return tree, nil + } + return entity.SpeculationTree{BatchID: batchID}, nil +} + +// ensure the fake satisfies the interface. +var _ enumerator.Enumerator = (*Enumerator)(nil) diff --git a/submitqueue/extension/speculation/enumerator/fake/fake_test.go b/submitqueue/extension/speculation/enumerator/fake/fake_test.go new file mode 100644 index 00000000..284eb658 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/fake/fake_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// 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 fake + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber/submitqueue/submitqueue/entity" +) + +func TestEnumerate_SeededTree(t *testing.T) { + tree := entity.SpeculationTree{ + BatchID: "q/batch/2", + Paths: []entity.SpeculationPathInfo{ + {Path: entity.SpeculationPath{Head: "q/batch/2"}}, + }, + } + e := New().Set("q/batch/2", tree) + + got, err := e.Enumerate(context.Background(), "q/batch/2", nil) + require.NoError(t, err) + assert.Equal(t, tree, got) +} + +func TestEnumerate_UnseededReturnsEmptyTreeWithID(t *testing.T) { + got, err := New().Enumerate(context.Background(), "q/batch/9", nil) + require.NoError(t, err) + assert.Equal(t, entity.SpeculationTree{BatchID: "q/batch/9"}, got) +} + +func TestEnumerate_FailWith(t *testing.T) { + sentinel := errors.New("boom") + _, err := New().FailWith(sentinel).Enumerate(context.Background(), "q/batch/1", nil) + require.ErrorIs(t, err, sentinel) +} diff --git a/submitqueue/extension/speculation/enumerator/mock/BUILD.bazel b/submitqueue/extension/speculation/enumerator/mock/BUILD.bazel new file mode 100644 index 00000000..c6fab414 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/mock/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["enumerator_mock.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator/mock", + visibility = ["//visibility:public"], + deps = [ + "//submitqueue/entity:go_default_library", + "//submitqueue/extension/speculation/enumerator:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", + ], +) diff --git a/submitqueue/extension/speculation/enumerator/mock/enumerator_mock.go b/submitqueue/extension/speculation/enumerator/mock/enumerator_mock.go new file mode 100644 index 00000000..6ad1cc75 --- /dev/null +++ b/submitqueue/extension/speculation/enumerator/mock/enumerator_mock.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: enumerator.go +// +// Generated by this command: +// +// mockgen -source=enumerator.go -destination=mock/enumerator_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "github.com/uber/submitqueue/submitqueue/entity" + enumerator "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator" + gomock "go.uber.org/mock/gomock" +) + +// MockEnumerator is a mock of Enumerator interface. +type MockEnumerator struct { + ctrl *gomock.Controller + recorder *MockEnumeratorMockRecorder + isgomock struct{} +} + +// MockEnumeratorMockRecorder is the mock recorder for MockEnumerator. +type MockEnumeratorMockRecorder struct { + mock *MockEnumerator +} + +// NewMockEnumerator creates a new mock instance. +func NewMockEnumerator(ctrl *gomock.Controller) *MockEnumerator { + mock := &MockEnumerator{ctrl: ctrl} + mock.recorder = &MockEnumeratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEnumerator) EXPECT() *MockEnumeratorMockRecorder { + return m.recorder +} + +// Enumerate mocks base method. +func (m *MockEnumerator) Enumerate(ctx context.Context, batchID string, deps []entity.Batch) (entity.SpeculationTree, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Enumerate", ctx, batchID, deps) + ret0, _ := ret[0].(entity.SpeculationTree) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Enumerate indicates an expected call of Enumerate. +func (mr *MockEnumeratorMockRecorder) Enumerate(ctx, batchID, deps any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enumerate", reflect.TypeOf((*MockEnumerator)(nil).Enumerate), ctx, batchID, deps) +} + +// MockFactory is a mock of Factory interface. +type MockFactory struct { + ctrl *gomock.Controller + recorder *MockFactoryMockRecorder + isgomock struct{} +} + +// MockFactoryMockRecorder is the mock recorder for MockFactory. +type MockFactoryMockRecorder struct { + mock *MockFactory +} + +// NewMockFactory creates a new mock instance. +func NewMockFactory(ctrl *gomock.Controller) *MockFactory { + mock := &MockFactory{ctrl: ctrl} + mock.recorder = &MockFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { + return m.recorder +} + +// For mocks base method. +func (m *MockFactory) For(cfg enumerator.Config) (enumerator.Enumerator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(enumerator.Enumerator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// For indicates an expected call of For. +func (mr *MockFactoryMockRecorder) For(cfg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "For", reflect.TypeOf((*MockFactory)(nil).For), cfg) +}