diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-01-23 15:25:16 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-01-24 07:02:54 +0000 |
| commit | 55a4296bd1fe1c873e4de2d099ab561e5fca592e (patch) | |
| tree | b8e7752b90800c8052c5b63a1dd931d0b95421cc /pkg | |
| parent | 8534bc8373e09185ab613e4b39eb1ee6bf6d180c (diff) | |
pkg/aflow: add DoWhile loop action
DoWhile represents "do { body } while (cond)" loop.
See added test for an example.
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/aflow/loop.go | 98 | ||||
| -rw-r--r-- | pkg/aflow/loop_test.go | 96 | ||||
| -rw-r--r-- | pkg/aflow/testdata/TestDoWhile.trajectory.json | 191 |
3 files changed, 385 insertions, 0 deletions
diff --git a/pkg/aflow/loop.go b/pkg/aflow/loop.go new file mode 100644 index 000000000..e074d4217 --- /dev/null +++ b/pkg/aflow/loop.go @@ -0,0 +1,98 @@ +// Copyright 2026 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package aflow + +import ( + "fmt" + "maps" + "reflect" + + "github.com/google/syzkaller/pkg/aflow/trajectory" +) + +// DoWhile represents "do { body } while (cond)" loop. +type DoWhile struct { + // Dody of the loop. + Do Action + // Exit condition. It should be a string state variable. + // The loop exists when the variable is empty. + While string + + loopVars map[string]reflect.Type +} + +func (dw *DoWhile) execute(ctx *Context) error { + span := &trajectory.Span{ + Type: trajectory.SpanLoop, + } + if err := ctx.startSpan(span); err != nil { + return err + } + err := dw.loop(ctx) + if err := ctx.finishSpan(span, err); err != nil { + return err + } + return nil +} + +func (dw *DoWhile) loop(ctx *Context) error { + for name, typ := range dw.loopVars { + if _, ok := ctx.state[name]; ok { + return fmt.Errorf("loop var %q is already defined", name) + } + ctx.state[name] = reflect.Zero(typ).Interface() + } + const maxIters = 100 + for iter := 0; iter < maxIters; iter++ { + span := &trajectory.Span{ + Type: trajectory.SpanLoopIteration, + Name: fmt.Sprint(iter), + } + if err := ctx.startSpan(span); err != nil { + return err + } + err := dw.Do.execute(ctx) + if err := ctx.finishSpan(span, err); err != nil { + return err + } + if ctx.state[dw.While].(string) == "" { + return nil + } + } + return fmt.Errorf("DoWhile loop is going in cycles for %v iterations", maxIters) +} + +func (dw *DoWhile) verify(ctx *verifyContext) { + // Verification of loops is a bit tricky. + // Normally we require each variable to be defined before use, but loops violate + // the assumption. An action in a loop body may want to use a variable produced + // by a subsequent action in the body on the previous iteration (otherwise there + // is no way to provide feedback from one iteration to the next iteration). + // But on the first iteration that variable is not defined yet. To resolve this, + // we split verification into 2 parts: first, all body actions provide outputs, + // and we collect all provided outputs in loopVars; second, we verify their inputs + // (with all outputs from the whole body already defined). Later, during execution + // we will define all loopVars to zero values before starting the loop body. + inputs, outputs := ctx.inputs, ctx.outputs + defer func() { + ctx.inputs, ctx.outputs = inputs, outputs + }() + if outputs { + ctx.inputs, ctx.outputs = false, true + origState := maps.Clone(ctx.state) + dw.Do.verify(ctx) + dw.loopVars = make(map[string]reflect.Type) + for name, desc := range ctx.state { + if origState[name] == nil { + dw.loopVars[name] = desc.typ + } + } + } + if inputs { + ctx.inputs, ctx.outputs = true, false + dw.Do.verify(ctx) + ctx.requireNotEmpty("DoWhile", "While", dw.While) + ctx.requireInput("DoWhile", dw.While, reflect.TypeFor[string]()) + } +} diff --git a/pkg/aflow/loop_test.go b/pkg/aflow/loop_test.go new file mode 100644 index 000000000..e68191c2b --- /dev/null +++ b/pkg/aflow/loop_test.go @@ -0,0 +1,96 @@ +// Copyright 2026 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package aflow + +import ( + "testing" +) + +func TestDoWhile(t *testing.T) { + type inputs struct { + Bug string + } + type outputs struct { + Diff string + } + type patchArgs struct { + Bug string + Diff string + TestError string + } + type patchResults struct { + Patch string + } + type testArgs struct { + Patch string + } + type testResults struct { + Diff string + TestError string + } + iter := 0 + testFlow[inputs, outputs](t, map[string]any{"Bug": "bug"}, map[string]any{"Diff": "diff"}, + &DoWhile{ + Do: Pipeline( + NewFuncAction("patch-generator", func(ctx *Context, args patchArgs) (patchResults, error) { + iter++ + if iter <= 2 { + return patchResults{"bad"}, nil + } + return patchResults{"good"}, nil + }), + NewFuncAction("patch-tester", func(ctx *Context, args testArgs) (testResults, error) { + if args.Patch == "bad" { + return testResults{TestError: "error"}, nil + } + return testResults{Diff: "diff"}, nil + }), + ), + While: "TestError", + }, + nil, + ) +} + +func TestDoWhileErrors(t *testing.T) { + testRegistrationError[struct{}, struct{}](t, + "flow test: action body: no input Missing, available inputs: []", + Pipeline( + &DoWhile{ + Do: NewFuncAction("body", func(ctx *Context, args struct { + Missing string + }) (struct{}, error) { + return struct{}{}, nil + }), + While: "Condition", + }, + )) + + testRegistrationError[struct{ Input string }, struct{}](t, + "flow test: action DoWhile: While must not be empty", + Pipeline( + &DoWhile{ + Do: NewFuncAction("body", func(ctx *Context, args struct { + Input string + }) (struct{}, error) { + return struct{}{}, nil + }), + }, + )) + + type output struct { + Output1 string + Output2 string + } + testRegistrationError[struct{}, struct{}](t, + "flow test: action body: output Output2 is unused", + Pipeline( + &DoWhile{ + Do: NewFuncAction("body", func(ctx *Context, args struct{}) (output, error) { + return output{}, nil + }), + While: "Output1", + }, + )) +} diff --git a/pkg/aflow/testdata/TestDoWhile.trajectory.json b/pkg/aflow/testdata/TestDoWhile.trajectory.json new file mode 100644 index 000000000..6375d2f95 --- /dev/null +++ b/pkg/aflow/testdata/TestDoWhile.trajectory.json @@ -0,0 +1,191 @@ +[ + { + "Seq": 0, + "Nesting": 0, + "Type": "flow", + "Name": "test", + "Started": "0001-01-01T00:00:01Z" + }, + { + "Seq": 1, + "Nesting": 1, + "Type": "loop", + "Name": "", + "Started": "0001-01-01T00:00:02Z" + }, + { + "Seq": 2, + "Nesting": 2, + "Type": "iteration", + "Name": "0", + "Started": "0001-01-01T00:00:03Z" + }, + { + "Seq": 3, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:04Z" + }, + { + "Seq": 3, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:04Z", + "Finished": "0001-01-01T00:00:05Z", + "Results": { + "Patch": "bad" + } + }, + { + "Seq": 4, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:06Z" + }, + { + "Seq": 4, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:06Z", + "Finished": "0001-01-01T00:00:07Z", + "Results": { + "Diff": "", + "TestError": "error" + } + }, + { + "Seq": 2, + "Nesting": 2, + "Type": "iteration", + "Name": "0", + "Started": "0001-01-01T00:00:03Z", + "Finished": "0001-01-01T00:00:08Z" + }, + { + "Seq": 5, + "Nesting": 2, + "Type": "iteration", + "Name": "1", + "Started": "0001-01-01T00:00:09Z" + }, + { + "Seq": 6, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:10Z" + }, + { + "Seq": 6, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:10Z", + "Finished": "0001-01-01T00:00:11Z", + "Results": { + "Patch": "bad" + } + }, + { + "Seq": 7, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:12Z" + }, + { + "Seq": 7, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:12Z", + "Finished": "0001-01-01T00:00:13Z", + "Results": { + "Diff": "", + "TestError": "error" + } + }, + { + "Seq": 5, + "Nesting": 2, + "Type": "iteration", + "Name": "1", + "Started": "0001-01-01T00:00:09Z", + "Finished": "0001-01-01T00:00:14Z" + }, + { + "Seq": 8, + "Nesting": 2, + "Type": "iteration", + "Name": "2", + "Started": "0001-01-01T00:00:15Z" + }, + { + "Seq": 9, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:16Z" + }, + { + "Seq": 9, + "Nesting": 3, + "Type": "action", + "Name": "patch-generator", + "Started": "0001-01-01T00:00:16Z", + "Finished": "0001-01-01T00:00:17Z", + "Results": { + "Patch": "good" + } + }, + { + "Seq": 10, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:18Z" + }, + { + "Seq": 10, + "Nesting": 3, + "Type": "action", + "Name": "patch-tester", + "Started": "0001-01-01T00:00:18Z", + "Finished": "0001-01-01T00:00:19Z", + "Results": { + "Diff": "diff", + "TestError": "" + } + }, + { + "Seq": 8, + "Nesting": 2, + "Type": "iteration", + "Name": "2", + "Started": "0001-01-01T00:00:15Z", + "Finished": "0001-01-01T00:00:20Z" + }, + { + "Seq": 1, + "Nesting": 1, + "Type": "loop", + "Name": "", + "Started": "0001-01-01T00:00:02Z", + "Finished": "0001-01-01T00:00:21Z" + }, + { + "Seq": 0, + "Nesting": 0, + "Type": "flow", + "Name": "test", + "Started": "0001-01-01T00:00:01Z", + "Finished": "0001-01-01T00:00:22Z", + "Results": { + "Diff": "diff" + } + } +]
\ No newline at end of file |
