Skip to content

Commit cdcb128

Browse files
committed
test(stovepipe): add build-slow marker to the fake build runner
## Summary ### Why? The fake build runner never reports a non-terminal status — `Status` decides purely from the build id and always returns succeeded, failed, or an error. That means no integration or e2e stack can reach `buildsignal`'s reschedule branch, so the entire poll loop is exercised only by unit tests with mocked publishers. A defect in the reschedule path is invisible above the unit layer. ### What? Adds a `build-slow` marker. `Trigger` encodes a ready-at wall-clock instant into the build id (`fake-build-slow-<readyAtMs>-<suffix>`) and `Status` reports `running` until that instant passes, then `succeeded`. Encoding the deadline in the id preserves the fake's stateless, decide-purely-from-the-id property, so `Trigger` and `Status` can still live in different processes. `SlowBuildDurationMs` is a package var so tests can tune it. Also widens `marker()` to end a token at `/` as well as `&` and `#`. The fake SourceControl builds head URIs as `git://<queue>/HEAD`, so a marker injected through a queue name sits mid-URI and would otherwise be read as `build-slow/HEAD`. Existing markers sit at the end of a URI and are unaffected. No pipeline behaviour changes; this only makes a previously unreachable path reachable in tests. ## Test Plan ✅ `bazel test //stovepipe/...` — covers `build-slow` reporting running then succeeded, the no-deadline fallback, and `marker()` against trailing path segments, query separators, and end-of-URI.
1 parent e00c689 commit cdcb128

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

stovepipe/extension/buildrunner/fake/fake.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
// Package fake provides a buildrunner.BuildRunner whose outcome is driven by the
1616
// triggered head URI. With no marker every build immediately succeeds, behaving
17-
// as a best-case stub for local-stack/e2e wiring. Failures are injected by
17+
// as a best-case stub for local-stack/e2e wiring. Other behaviors are injected by
1818
// embedding a marker token in headURI of the form "buildrunner-fake=<token>":
1919
//
2020
// buildrunner-fake=trigger-error -> Trigger returns a non-nil error
2121
// buildrunner-fake=build-fail -> Status reports BuildStatusFailed
2222
// buildrunner-fake=build-error -> Status returns a non-nil error
23+
// buildrunner-fake=build-slow -> Status reports BuildStatusRunning until
24+
// SlowBuildDurationMs after Trigger, then succeeds
2325
//
2426
// The runner is stateless: Trigger encodes the desired terminal outcome into the
2527
// returned BuildID, and Status decides the result purely from the BuildID it is
@@ -35,12 +37,20 @@ import (
3537
"crypto/rand"
3638
"encoding/hex"
3739
"fmt"
40+
"strconv"
3841
"strings"
42+
"time"
3943

4044
"github.com/uber/submitqueue/stovepipe/entity"
4145
"github.com/uber/submitqueue/stovepipe/extension/buildrunner"
4246
)
4347

48+
// SlowBuildDurationMs is how long a build-slow build reports BuildStatusRunning
49+
// before succeeding. A var (not a const) so tests can shorten it; the local stack
50+
// always uses the default. It must be long enough for the caller's poll loop to
51+
// observe at least one non-terminal status.
52+
var SlowBuildDurationMs int64 = 3000
53+
4454
// markerPrefix introduces a marker token in headURI: "buildrunner-fake=<token>".
4555
const markerPrefix = "buildrunner-fake="
4656

@@ -49,6 +59,7 @@ const (
4959
tokenTriggerError = "trigger-error"
5060
tokenFail = "build-fail"
5161
tokenError = "build-error"
62+
tokenSlow = "build-slow"
5263
)
5364

5465
// outcomeOK is the BuildID outcome segment for a build that should succeed.
@@ -80,6 +91,10 @@ func (r runner) Trigger(_ context.Context, _, headURI string, _ entity.BuildMeta
8091
outcome = tokenFail
8192
case tokenError:
8293
outcome = tokenError
94+
case tokenSlow:
95+
// A slow build carries the wall-clock instant it becomes terminal, so Status
96+
// stays stateless: any instance, in any process, decodes the same deadline.
97+
outcome = fmt.Sprintf("%s-%d", tokenSlow, time.Now().UnixMilli()+SlowBuildDurationMs)
8398
}
8499

85100
// Encode the outcome in the id (e.g. "fake-build-fail-a1b2c3d4") so Status is
@@ -101,6 +116,11 @@ func (r runner) Status(_ context.Context, buildID entity.BuildID) (entity.BuildS
101116
return entity.BuildStatusUnknown, nil, fmt.Errorf("fake: marked build error")
102117
case strings.Contains(buildID.ID, tokenFail):
103118
return entity.BuildStatusFailed, nil, nil
119+
case strings.Contains(buildID.ID, tokenSlow):
120+
if readyAt, ok := slowReadyAt(buildID.ID); ok && time.Now().UnixMilli() < readyAt {
121+
return entity.BuildStatusRunning, nil, nil
122+
}
123+
return entity.BuildStatusSucceeded, nil, nil
104124
default:
105125
return entity.BuildStatusSucceeded, nil, nil
106126
}
@@ -112,19 +132,38 @@ func (r runner) Cancel(_ context.Context, _ entity.BuildID) error {
112132
}
113133

114134
// marker returns the marker token embedded in uri, or "" if none is present.
115-
// The token ends at the first "&" or "#" delimiter, so a marker may sit among
116-
// other query parameters or a fragment.
135+
// The token ends at the first "&", "#", or "/" delimiter, so a marker may sit
136+
// among other query parameters, before a fragment, or ahead of a further path
137+
// segment (as when a URI is built as "git://<queue>/HEAD" and the marker rides
138+
// in on the queue name).
117139
func marker(uri string) string {
118140
_, rest, found := strings.Cut(uri, markerPrefix)
119141
if !found {
120142
return ""
121143
}
122-
if i := strings.IndexAny(rest, "&#"); i >= 0 {
144+
if i := strings.IndexAny(rest, "&#/"); i >= 0 {
123145
rest = rest[:i]
124146
}
125147
return rest
126148
}
127149

150+
// slowReadyAt extracts the epoch-millisecond instant at which a build-slow build
151+
// becomes terminal, which Trigger encodes into the id as
152+
// "fake-build-slow-<readyAtMs>-<suffix>". It reports false when the id carries no
153+
// parsable deadline, in which case the caller treats the build as already terminal.
154+
func slowReadyAt(id string) (int64, bool) {
155+
_, rest, found := strings.Cut(id, tokenSlow+"-")
156+
if !found {
157+
return 0, false
158+
}
159+
digits, _, _ := strings.Cut(rest, "-")
160+
readyAt, err := strconv.ParseInt(digits, 10, 64)
161+
if err != nil {
162+
return 0, false
163+
}
164+
return readyAt, true
165+
}
166+
128167
// randomSuffix returns a short random hex string used to keep fake BuildIDs
129168
// globally unique. Hex digits never spell the outcome marker tokens, so the
130169
// suffix cannot interfere with Status decoding the outcome via substring match.

stovepipe/extension/buildrunner/fake/fake_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ package fake
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"testing"
21+
"time"
2022

2123
"github.com/stretchr/testify/assert"
2224
"github.com/stretchr/testify/require"
@@ -110,3 +112,56 @@ func TestCancel_NoOp(t *testing.T) {
110112
err := New().Cancel(context.Background(), entity.BuildID{ID: "anything"})
111113
assert.NoError(t, err)
112114
}
115+
116+
// TestStatus_BuildSlowReportsRunningThenSucceeds covers the one marker that yields a
117+
// non-terminal status, which is what makes a caller's poll loop reachable in an
118+
// integration or e2e stack.
119+
func TestStatus_BuildSlowReportsRunningThenSucceeds(t *testing.T) {
120+
original := SlowBuildDurationMs
121+
SlowBuildDurationMs = 30_000
122+
defer func() { SlowBuildDurationMs = original }()
123+
124+
id, err := New().Trigger(context.Background(), "", "git://repo/ref/deadbeef?buildrunner-fake=build-slow", nil)
125+
require.NoError(t, err)
126+
127+
status, _, err := New().Status(context.Background(), id)
128+
require.NoError(t, err)
129+
assert.Equal(t, entity.BuildStatusRunning, status)
130+
131+
// An id whose deadline has already passed reports the terminal outcome. Encoding
132+
// the deadline in the id is what keeps Status stateless across instances.
133+
elapsed := entity.BuildID{ID: fmt.Sprintf("fake-build-slow-%d-abcd1234", time.Now().UnixMilli()-1)}
134+
status, _, err = New().Status(context.Background(), elapsed)
135+
require.NoError(t, err)
136+
assert.Equal(t, entity.BuildStatusSucceeded, status)
137+
}
138+
139+
// TestStatus_BuildSlowWithoutDeadlineSucceeds pins the fallback: an id carrying the
140+
// marker but no parsable deadline is treated as already terminal rather than polling
141+
// forever.
142+
func TestStatus_BuildSlowWithoutDeadlineSucceeds(t *testing.T) {
143+
status, _, err := New().Status(context.Background(), entity.BuildID{ID: "fake-build-slow-nodeadline"})
144+
require.NoError(t, err)
145+
assert.Equal(t, entity.BuildStatusSucceeded, status)
146+
}
147+
148+
// TestMarker_StopsAtPathSegment covers a marker that arrives mid-URI rather than at the
149+
// end, as it does when the head URI is built as "git://<queue>/HEAD" and the marker
150+
// rides in on the queue name.
151+
func TestMarker_StopsAtPathSegment(t *testing.T) {
152+
tests := []struct {
153+
name string
154+
uri string
155+
want string
156+
}{
157+
{name: "trailing path segment", uri: "git://repo/main?buildrunner-fake=build-slow/HEAD", want: "build-slow"},
158+
{name: "end of uri", uri: "git://repo/ref/deadbeef?buildrunner-fake=build-fail", want: "build-fail"},
159+
{name: "query separator", uri: "git://repo/ref?buildrunner-fake=build-fail&other=1", want: "build-fail"},
160+
{name: "no marker", uri: "git://repo/ref/deadbeef", want: ""},
161+
}
162+
for _, tt := range tests {
163+
t.Run(tt.name, func(t *testing.T) {
164+
assert.Equal(t, tt.want, marker(tt.uri))
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)