feat(cli): --allow-no-generator-actions

The flag the dead-run refusal names, wired through to the pipeline. The
property-free flag goes back to meaning what it says.
This commit is contained in:
pj committed 2026-08-18 20:10:29 +05:30
1 parent 93614a48b9
commit 74cae01a5c
3 files changed
+72 -37

No files matched your search

+21 -19
View File
@@ -20,24 +20,25 @@ import (
var Version = "dev" var Version = "dev"
type testOptions struct { type testOptions struct {
spec string spec string
bundleID string bundleID string
platform string platform string
avd string avd string
device string device string
iosDevice string iosDevice string
iosAppPath string iosAppPath string
androidAppPath string androidAppPath string
duration time.Duration duration time.Duration
maxSteps int maxSteps int
arm string arm string
seed int64 seed int64
output string output string
clearData bool clearData bool
generator string generator string
labelSource string labelSource string
exitOnViolation bool exitOnViolation bool
allowNoProperties bool allowNoProperties bool
allowNoGeneratorActions bool
} }
const topUsage = `sanderling is a property-based UI fuzzer for mobile apps. const topUsage = `sanderling is a property-based UI fuzzer for mobile apps.
@@ -75,7 +76,8 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) {
flagSet.StringVar(&options.generator, "generator", "seeded", "action generator: seeded (weighted random) or llm (model picks from the same candidate set; requires generator = llm() in the spec)") flagSet.StringVar(&options.generator, "generator", "seeded", "action generator: seeded (weighted random) or llm (model picks from the same candidate set; requires generator = llm() in the spec)")
flagSet.StringVar(&options.labelSource, "label-source", "visible-text", "how candidates are named to the llm generator: visible-text (what a user reads) or resource-id (the identifier the app assigned). The seeded generator picks by index and ignores this") flagSet.StringVar(&options.labelSource, "label-source", "visible-text", "how candidates are named to the llm generator: visible-text (what a user reads) or resource-id (the identifier the app assigned). The seeded generator picks by index and ignores this")
flagSet.BoolVar(&options.exitOnViolation, "exit-on-violation", false, "stop the run at the first property violation and exit 2, so CI can tell a found bug (2) from a broken harness (1)") flagSet.BoolVar(&options.exitOnViolation, "exit-on-violation", false, "stop the run at the first property violation and exit 2, so CI can tell a found bug (2) from a broken harness (1)")
flagSet.BoolVar(&options.allowNoProperties, "allow-no-properties", false, "run a spec that registers no properties. Such a run judges nothing and can only report no violations, so it is refused by default; pass this when the run measures what the spec extracts or where the generator reaches") flagSet.BoolVar(&options.allowNoProperties, "allow-no-properties", false, "run a spec that registers no properties. Such a run judges nothing and can only report no violations, so it is refused by default; pass this when the run measures what the spec extracts")
flagSet.BoolVar(&options.allowNoGeneratorActions, "allow-no-generator-actions", false, "finish a run the action generator never drove. Such a run judged whatever screen the spec's setup left it on and explored nothing, so it is refused by default; pass this when the run measures where the generator reaches and reaching nothing is the measurement")
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return testOptions{}, err return testOptions{}, err
} }
+32
View File
@@ -425,6 +425,38 @@ func TestParseTestArgs_AllowNoPropertiesReachesThePipeline(t *testing.T) {
} }
} }
// The exploration sweeps ask for a run the generator never drove, and they ask
// for that alone. Riding on --allow-no-properties made one flag name two
// unrelated waivers, so a sweep that wanted the property-free one silently lost
// the dead-run detector as well.
func TestParseTestArgs_AllowNoGeneratorActionsIsItsOwnFlag(t *testing.T) {
base := []string{"--spec", "s.ts", "--bundle-id", "com.example"}
options, err := parseTestArgs(base, io.Discard)
if err != nil {
t.Fatal(err)
}
if pipelineOptions(options).AllowNoGeneratorActions {
t.Error("allowNoGeneratorActions default: got true, want false")
}
options, err = parseTestArgs(append(base, "--allow-no-generator-actions"), io.Discard)
if err != nil {
t.Fatal(err)
}
if !pipelineOptions(options).AllowNoGeneratorActions {
t.Error("--allow-no-generator-actions never reached the pipeline options")
}
options, err = parseTestArgs(append(base, "--allow-no-properties"), io.Discard)
if err != nil {
t.Fatal(err)
}
if pipelineOptions(options).AllowNoGeneratorActions {
t.Error("--allow-no-properties still waives the dead-run refusal it does not name")
}
}
// TestExitCode_SeparatesFoundBugsFromBrokenHarnesses pins the three statuses CI // TestExitCode_SeparatesFoundBugsFromBrokenHarnesses pins the three statuses CI
// reads: 0 clean, 2 the run found violations, 1 everything else. A workflow // reads: 0 clean, 2 the run found violations, 1 everything else. A workflow
// that asserts "the known bug is still found" is only meaningful while 2 and 1 // that asserts "the known bug is still found" is only meaningful while 2 and 1
+19 -18
View File
@@ -16,23 +16,24 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
// and records another, which is worth being able to test on its own. // and records another, which is worth being able to test on its own.
func pipelineOptions(options testOptions) testrun.Options { func pipelineOptions(options testOptions) testrun.Options {
return testrun.Options{ return testrun.Options{
Spec: options.spec, Spec: options.spec,
AllowNoProperties: options.allowNoProperties, AllowNoProperties: options.allowNoProperties,
BundleID: options.bundleID, AllowNoGeneratorActions: options.allowNoGeneratorActions,
Platform: options.platform, BundleID: options.bundleID,
AVD: options.avd, Platform: options.platform,
Device: options.device, AVD: options.avd,
IosDevice: options.iosDevice, Device: options.device,
IosAppPath: options.iosAppPath, IosDevice: options.iosDevice,
AndroidAppPath: options.androidAppPath, IosAppPath: options.iosAppPath,
Duration: options.duration, AndroidAppPath: options.androidAppPath,
MaxSteps: options.maxSteps, Duration: options.duration,
Seed: options.seed, MaxSteps: options.maxSteps,
Output: options.output, Seed: options.seed,
ClearData: options.clearData, Output: options.output,
Generator: options.generator, ClearData: options.clearData,
LabelSource: options.labelSource, Generator: options.generator,
Arm: options.arm, LabelSource: options.labelSource,
ExitOnViolation: options.exitOnViolation, Arm: options.arm,
ExitOnViolation: options.exitOnViolation,
} }
} }