Skip to content

Commit a35191e

Browse files
committed
feat(sqsim): add scenario DSL and registry
1 parent 585c953 commit a35191e

29 files changed

Lines changed: 1797 additions & 0 deletions

sqsim/BUILD.bazel

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = [
6+
"behavior.go",
7+
"buildrunner.go",
8+
"fault.go",
9+
"land.go",
10+
"merger.go",
11+
"scenario.go",
12+
"types.go",
13+
"validation.go",
14+
],
15+
importpath = "github.com/uber/submitqueue/sqsim",
16+
visibility = ["//visibility:public"],
17+
deps = ["//sqsim/entity:go_default_library"],
18+
)
19+
20+
go_test(
21+
name = "go_default_test",
22+
srcs = [
23+
"behavior_test.go",
24+
"buildrunner_test.go",
25+
"fault_test.go",
26+
"land_test.go",
27+
"merger_test.go",
28+
"scenario_test.go",
29+
"validation_test.go",
30+
],
31+
embed = [":go_default_library"],
32+
deps = [
33+
"@com_github_stretchr_testify//assert:go_default_library",
34+
"@com_github_stretchr_testify//require:go_default_library",
35+
],
36+
)

sqsim/behavior.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2026 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sqsim
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/uber/submitqueue/sqsim/entity"
21+
)
22+
23+
// BehaviorBuilder constructs the external behavior attached to one or more Lands.
24+
type BehaviorBuilder struct {
25+
buildRunner *BuildRunnerBehaviorBuilder
26+
mergeConflictCheck *MergeConflictCheckBehaviorBuilder
27+
merge *MergeBehaviorBuilder
28+
}
29+
30+
// NewBehavior returns an empty behavior builder.
31+
func NewBehavior() *BehaviorBuilder {
32+
return &BehaviorBuilder{}
33+
}
34+
35+
// BuildRunner sets the build behavior.
36+
func (b *BehaviorBuilder) BuildRunner(behavior *BuildRunnerBehaviorBuilder) *BehaviorBuilder {
37+
b.buildRunner = behavior
38+
return b
39+
}
40+
41+
// MergeConflictCheck sets the dry-run mergeability behavior.
42+
func (b *BehaviorBuilder) MergeConflictCheck(behavior *MergeConflictCheckBehaviorBuilder) *BehaviorBuilder {
43+
b.mergeConflictCheck = behavior
44+
return b
45+
}
46+
47+
// Merge sets the committing merge behavior.
48+
func (b *BehaviorBuilder) Merge(behavior *MergeBehaviorBuilder) *BehaviorBuilder {
49+
b.merge = behavior
50+
return b
51+
}
52+
53+
func (b *BehaviorBuilder) build() (entity.Behavior, error) {
54+
if b == nil {
55+
return entity.Behavior{}, fmt.Errorf("behavior builder is nil")
56+
}
57+
if b.buildRunner == nil {
58+
return entity.Behavior{}, fmt.Errorf("build runner behavior is required")
59+
}
60+
if b.mergeConflictCheck == nil {
61+
return entity.Behavior{}, fmt.Errorf("merge conflict check behavior is required")
62+
}
63+
if b.merge == nil {
64+
return entity.Behavior{}, fmt.Errorf("merge behavior is required")
65+
}
66+
67+
buildRunner, err := b.buildRunner.build()
68+
if err != nil {
69+
return entity.Behavior{}, fmt.Errorf("build runner: %w", err)
70+
}
71+
mergeConflictCheck, err := b.mergeConflictCheck.build()
72+
if err != nil {
73+
return entity.Behavior{}, fmt.Errorf("merge conflict check: %w", err)
74+
}
75+
merge, err := b.merge.build()
76+
if err != nil {
77+
return entity.Behavior{}, fmt.Errorf("merge: %w", err)
78+
}
79+
return entity.Behavior{
80+
BuildRunner: buildRunner,
81+
MergeConflictCheck: mergeConflictCheck,
82+
Merge: merge,
83+
}, nil
84+
}
85+
86+
func cloneBehavior(behavior entity.Behavior) entity.Behavior {
87+
triggers := make([]entity.Invocation[entity.BuildTriggerOutcome], len(behavior.BuildRunner.Triggers))
88+
for i, trigger := range behavior.BuildRunner.Triggers {
89+
trigger.Outcome.Build.Timeline = append([]entity.BuildStatusPoint(nil), trigger.Outcome.Build.Timeline...)
90+
trigger.Outcome.Build.StatusFaults = append([]entity.FaultOnCall(nil), trigger.Outcome.Build.StatusFaults...)
91+
triggers[i] = trigger
92+
}
93+
behavior.BuildRunner.Triggers = triggers
94+
behavior.MergeConflictCheck.Invocations = append([]entity.Invocation[entity.MergeConflictCheckOutcome](nil), behavior.MergeConflictCheck.Invocations...)
95+
behavior.Merge.Invocations = append([]entity.Invocation[entity.MergeOutcome](nil), behavior.Merge.Invocations...)
96+
return behavior
97+
}

sqsim/behavior_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2026 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sqsim
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
func TestBehaviorBuilderRequiresEveryExternalOperation(t *testing.T) {
25+
behavior := NewBehavior().
26+
BuildRunner(SuccessfulBuildRunner()).
27+
MergeConflictCheck(SuccessfulMergeConflictCheck())
28+
29+
_, err := NewScenario().
30+
Timeout(time.Minute).
31+
Land(NewLand("l1").Queue("sqsim").Behavior(behavior).Expect(RequestLanded)).
32+
Build()
33+
require.Error(t, err)
34+
}

sqsim/buildrunner.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) 2026 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sqsim
16+
17+
import (
18+
"time"
19+
20+
"github.com/uber/submitqueue/sqsim/entity"
21+
)
22+
23+
// BuildRunnerBehaviorBuilder constructs BuildRunner behavior.
24+
type BuildRunnerBehaviorBuilder struct {
25+
triggers []entity.Invocation[entity.BuildTriggerOutcome]
26+
}
27+
28+
// NewBuildRunnerBehavior returns an empty BuildRunner behavior builder.
29+
func NewBuildRunnerBehavior() *BuildRunnerBehaviorBuilder {
30+
return &BuildRunnerBehaviorBuilder{}
31+
}
32+
33+
// Trigger appends Trigger invocations.
34+
func (b *BuildRunnerBehaviorBuilder) Trigger(invocations ...entity.Invocation[entity.BuildTriggerOutcome]) *BuildRunnerBehaviorBuilder {
35+
b.triggers = append(b.triggers, invocations...)
36+
return b
37+
}
38+
39+
// StatusFaultOnCall adds a Status fault to the most recent successful Trigger outcome.
40+
func (b *BuildRunnerBehaviorBuilder) StatusFaultOnCall(call int, fault Fault) *BuildRunnerBehaviorBuilder {
41+
if len(b.triggers) == 0 {
42+
return b
43+
}
44+
last := &b.triggers[len(b.triggers)-1]
45+
last.Outcome.Build.StatusFaults = append(last.Outcome.Build.StatusFaults, entity.FaultOnCall{
46+
Call: call,
47+
Fault: fault,
48+
})
49+
return b
50+
}
51+
52+
func (b *BuildRunnerBehaviorBuilder) build() (entity.BuildRunnerBehavior, error) {
53+
triggers := append([]entity.Invocation[entity.BuildTriggerOutcome](nil), b.triggers...)
54+
for i := range triggers {
55+
triggers[i].Outcome.Build.Timeline = append([]entity.BuildStatusPoint(nil), triggers[i].Outcome.Build.Timeline...)
56+
triggers[i].Outcome.Build.StatusFaults = append([]entity.FaultOnCall(nil), triggers[i].Outcome.Build.StatusFaults...)
57+
}
58+
return entity.BuildRunnerBehavior{Triggers: triggers}, nil
59+
}
60+
61+
// StatusAt returns a build status timeline point.
62+
func StatusAt(after time.Duration, status BuildStatus) BuildStatusPoint {
63+
return entity.BuildStatusPoint{AfterMs: after.Milliseconds(), Status: status}
64+
}
65+
66+
// BuildCreated returns a successful Trigger invocation with a build timeline.
67+
func BuildCreated(timeline ...BuildStatusPoint) entity.Invocation[entity.BuildTriggerOutcome] {
68+
return entity.Invocation[entity.BuildTriggerOutcome]{
69+
Outcome: entity.BuildTriggerOutcome{
70+
Build: entity.BuildExecution{Timeline: append([]entity.BuildStatusPoint(nil), timeline...)},
71+
},
72+
}
73+
}
74+
75+
// BuildCreatedWithFault returns a Trigger invocation that creates a build and then faults.
76+
func BuildCreatedWithFault(fault Fault, timeline ...BuildStatusPoint) entity.Invocation[entity.BuildTriggerOutcome] {
77+
invocation := BuildCreated(timeline...)
78+
invocation.Fault = fault
79+
return invocation
80+
}
81+
82+
// BuildTriggerFault returns a Trigger invocation that fails without creating a build.
83+
func BuildTriggerFault(fault Fault) entity.Invocation[entity.BuildTriggerOutcome] {
84+
return entity.Invocation[entity.BuildTriggerOutcome]{Fault: fault}
85+
}
86+
87+
// SuccessfulBuildRunner returns a build behavior that succeeds immediately.
88+
func SuccessfulBuildRunner() *BuildRunnerBehaviorBuilder {
89+
return NewBuildRunnerBehavior().Trigger(BuildCreated(StatusAt(0, BuildSucceeded)))
90+
}
91+
92+
// BuildSucceededAfter returns a build behavior that succeeds after the given duration.
93+
func BuildSucceededAfter(after time.Duration) *BuildRunnerBehaviorBuilder {
94+
timeline := []BuildStatusPoint{StatusAt(0, BuildAccepted)}
95+
if after > 0 {
96+
timeline = append(timeline, StatusAt(after, BuildSucceeded))
97+
} else {
98+
timeline[0] = StatusAt(0, BuildSucceeded)
99+
}
100+
return NewBuildRunnerBehavior().Trigger(BuildCreated(timeline...))
101+
}
102+
103+
// BuildFailedAfter returns a build behavior that fails after the given duration.
104+
func BuildFailedAfter(after time.Duration) *BuildRunnerBehaviorBuilder {
105+
timeline := []BuildStatusPoint{StatusAt(0, BuildAccepted)}
106+
if after > 0 {
107+
timeline = append(timeline, StatusAt(after, BuildFailed))
108+
} else {
109+
timeline[0] = StatusAt(0, BuildFailed)
110+
}
111+
return NewBuildRunnerBehavior().Trigger(BuildCreated(timeline...))
112+
}

sqsim/buildrunner_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2026 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sqsim
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestBuildRunnerBuilderAddsStatusFault(t *testing.T) {
26+
builder := NewBuildRunnerBehavior().
27+
Trigger(BuildCreated(
28+
StatusAt(0, BuildAccepted),
29+
StatusAt(time.Second, BuildSucceeded),
30+
)).
31+
StatusFaultOnCall(2, RetryableErrorBeforeSideEffect())
32+
33+
behavior, err := builder.build()
34+
require.NoError(t, err)
35+
require.Len(t, behavior.Triggers, 1)
36+
require.Len(t, behavior.Triggers[0].Outcome.Build.StatusFaults, 1)
37+
assert.Equal(t, 2, behavior.Triggers[0].Outcome.Build.StatusFaults[0].Call)
38+
}

sqsim/entity/BUILD.bazel

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = [
6+
"behavior.go",
7+
"scenario.go",
8+
],
9+
importpath = "github.com/uber/submitqueue/sqsim/entity",
10+
visibility = ["//visibility:public"],
11+
)
12+
13+
go_test(
14+
name = "go_default_test",
15+
srcs = [
16+
"behavior_test.go",
17+
"scenario_test.go",
18+
],
19+
embed = [":go_default_library"],
20+
deps = [
21+
"@com_github_stretchr_testify//assert:go_default_library",
22+
"@com_github_stretchr_testify//require:go_default_library",
23+
],
24+
)

0 commit comments

Comments
 (0)