mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(cli): add --max-steps for step-bounded runs
runner.Options.MaxSteps already worked but was unreachable from the command line. A step budget is what makes two generators comparable: one making a model call per step and one drawing from a PRNG are not comparable per second. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
26b49b379a
commit
8388ac5d2a
4 files changed
+50
No files matched your search
@@ -27,6 +27,7 @@ type testOptions struct {
|
|||||||
iosAppPath string
|
iosAppPath string
|
||||||
androidAppPath string
|
androidAppPath string
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
|
maxSteps int
|
||||||
seed int64
|
seed int64
|
||||||
output string
|
output string
|
||||||
clearData bool
|
clearData bool
|
||||||
@@ -60,6 +61,7 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) {
|
|||||||
flagSet.StringVar(&options.iosAppPath, "ios-app-path", "", "path to the .app bundle for iOS clear-state reinstall (simulator: simctl; device: devicectl)")
|
flagSet.StringVar(&options.iosAppPath, "ios-app-path", "", "path to the .app bundle for iOS clear-state reinstall (simulator: simctl; device: devicectl)")
|
||||||
flagSet.StringVar(&options.androidAppPath, "android-app-path", "", "path to the .apk for Android clear-state reinstall; required to reset apps on OEM builds that deny `pm clear`")
|
flagSet.StringVar(&options.androidAppPath, "android-app-path", "", "path to the .apk for Android clear-state reinstall; required to reset apps on OEM builds that deny `pm clear`")
|
||||||
flagSet.DurationVar(&options.duration, "duration", 5*time.Minute, "total test duration")
|
flagSet.DurationVar(&options.duration, "duration", 5*time.Minute, "total test duration")
|
||||||
|
flagSet.IntVar(&options.maxSteps, "max-steps", 0, "stop after this many steps (0 = no cap; the duration deadline governs). A step budget is what makes two generators comparable, since one making a model call per step and one drawing from a PRNG are not comparable per second")
|
||||||
flagSet.Int64Var(&options.seed, "seed", 0, "RNG seed (0 = random)")
|
flagSet.Int64Var(&options.seed, "seed", 0, "RNG seed (0 = random)")
|
||||||
flagSet.StringVar(&options.output, "output", "./runs", "output directory for traces")
|
flagSet.StringVar(&options.output, "output", "./runs", "output directory for traces")
|
||||||
flagSet.BoolVar(&options.clearData, "clear-data", true, "clear app data before launching so each run starts from a fresh install; pass --clear-data=false to resume prior state")
|
flagSet.BoolVar(&options.clearData, "clear-data", true, "clear app data before launching so each run starts from a fresh install; pass --clear-data=false to resume prior state")
|
||||||
@@ -78,6 +80,9 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) {
|
|||||||
default:
|
default:
|
||||||
return testOptions{}, fmt.Errorf("unsupported platform: %q (android, ios, web)", options.platform)
|
return testOptions{}, fmt.Errorf("unsupported platform: %q (android, ios, web)", options.platform)
|
||||||
}
|
}
|
||||||
|
if options.maxSteps < 0 {
|
||||||
|
return testOptions{}, fmt.Errorf("--max-steps must not be negative: %d", options.maxSteps)
|
||||||
|
}
|
||||||
switch options.generator {
|
switch options.generator {
|
||||||
case "seeded", "llm":
|
case "seeded", "llm":
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -259,3 +259,41 @@ func TestRun_TestSubcommand_PipelineErrors(t *testing.T) {
|
|||||||
t.Errorf("expected bundle-resolution error for the missing spec, got %v", err)
|
t.Errorf("expected bundle-resolution error for the missing spec, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTestArgs_MaxStepsDefaultsToUncapped(t *testing.T) {
|
||||||
|
options, err := parseTestArgs([]string{
|
||||||
|
"--spec", "s.ts",
|
||||||
|
"--bundle-id", "com.example",
|
||||||
|
}, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if options.maxSteps != 0 {
|
||||||
|
t.Errorf("maxSteps default: got %d, want 0", options.maxSteps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestArgs_MaxSteps(t *testing.T) {
|
||||||
|
options, err := parseTestArgs([]string{
|
||||||
|
"--spec", "s.ts",
|
||||||
|
"--bundle-id", "com.example",
|
||||||
|
"--max-steps", "300",
|
||||||
|
}, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if options.maxSteps != 300 {
|
||||||
|
t.Errorf("maxSteps: got %d, want 300", options.maxSteps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestArgs_RejectsNegativeMaxSteps(t *testing.T) {
|
||||||
|
_, err := parseTestArgs([]string{
|
||||||
|
"--spec", "s.ts",
|
||||||
|
"--bundle-id", "com.example",
|
||||||
|
"--max-steps", "-1",
|
||||||
|
}, io.Discard)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error for a negative --max-steps")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
|
|||||||
IosAppPath: options.iosAppPath,
|
IosAppPath: options.iosAppPath,
|
||||||
AndroidAppPath: options.androidAppPath,
|
AndroidAppPath: options.androidAppPath,
|
||||||
Duration: options.duration,
|
Duration: options.duration,
|
||||||
|
MaxSteps: options.maxSteps,
|
||||||
Seed: options.seed,
|
Seed: options.seed,
|
||||||
Output: options.output,
|
Output: options.output,
|
||||||
ClearData: options.clearData,
|
ClearData: options.clearData,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Options struct {
|
|||||||
IosAppPath string
|
IosAppPath string
|
||||||
AndroidAppPath string
|
AndroidAppPath string
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
|
MaxSteps int
|
||||||
Seed int64
|
Seed int64
|
||||||
Output string
|
Output string
|
||||||
ClearData bool
|
ClearData bool
|
||||||
@@ -159,9 +160,14 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
}()
|
}()
|
||||||
fmt.Fprintf(stdout, "trace dir: %s\n", runDirectory)
|
fmt.Fprintf(stdout, "trace dir: %s\n", runDirectory)
|
||||||
|
|
||||||
|
if options.MaxSteps > 0 {
|
||||||
|
fmt.Fprintf(stdout, "running for %s or %d steps, whichever comes first (seed=%d)\n", options.Duration, options.MaxSteps, seed)
|
||||||
|
} else {
|
||||||
fmt.Fprintf(stdout, "running for %s (seed=%d)\n", options.Duration, seed)
|
fmt.Fprintf(stdout, "running for %s (seed=%d)\n", options.Duration, seed)
|
||||||
|
}
|
||||||
summary, err := runner.Run(ctx, runner.Options{
|
summary, err := runner.Run(ctx, runner.Options{
|
||||||
Duration: options.Duration,
|
Duration: options.Duration,
|
||||||
|
MaxSteps: options.MaxSteps,
|
||||||
IdleTimeout: 1 * time.Second,
|
IdleTimeout: 1 * time.Second,
|
||||||
BundleID: options.BundleID,
|
BundleID: options.BundleID,
|
||||||
Driver: activeDriver,
|
Driver: activeDriver,
|
||||||
|
|||||||
Reference in new issue
Block a user