Skip to content

Commit e7922f7

Browse files
committed
pkg/aflow: add helper for tool testing
Add simple codeeditor tests to test testing.
1 parent cf894e0 commit e7922f7

File tree

6 files changed

+171
-7
lines changed

6 files changed

+171
-7
lines changed

pkg/aflow/flow.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,7 @@ func registerOne[Inputs, Outputs any](all map[string]*Flow, flow *Flow) error {
8787
if all[flow.Name] != nil {
8888
return fmt.Errorf("flow %v is already registered", flow.Name)
8989
}
90-
ctx := &verifyContext{
91-
inputs: true,
92-
outputs: true,
93-
state: make(map[string]*varState),
94-
models: make(map[string]bool),
95-
}
90+
ctx := newVerifyContext()
9691
provideOutputs[Inputs](ctx, "flow inputs")
9792
flow.Root.verify(ctx)
9893
requireInputs[Outputs](ctx, "flow outputs")

pkg/aflow/func_tool.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ package aflow
55

66
import (
77
"fmt"
8+
"reflect"
9+
"testing"
810

11+
"github.com/stretchr/testify/require"
912
"google.golang.org/genai"
1013
)
1114

@@ -77,3 +80,19 @@ func (t *funcTool[State, Args, Results]) verify(ctx *verifyContext) {
7780
requireSchema[Results](ctx, t.Name, "Results")
7881
requireInputs[State](ctx, t.Name)
7982
}
83+
84+
func (*funcTool[State, Args, Results]) checkTestTypes(t *testing.T, ctx *verifyContext, state, args, results any) (
85+
map[string]any, map[string]any, map[string]any) {
86+
require.Equal(t, reflect.TypeFor[State](), reflect.TypeOf(state))
87+
require.Equal(t, reflect.TypeFor[Args](), reflect.TypeOf(args))
88+
require.Equal(t, reflect.TypeFor[Results](), reflect.TypeOf(results))
89+
provideOutputs[State](ctx, "state")
90+
return convertToMap(state.(State)), convertToMap(args.(Args)), convertToMap(results.(Results))
91+
}
92+
93+
func (*funcTool[State, Args, Results]) checkFuzzTypes(t *testing.T, state, args any) (
94+
map[string]any, map[string]any) {
95+
require.Equal(t, reflect.TypeFor[State](), reflect.TypeOf(state))
96+
require.Equal(t, reflect.TypeFor[Args](), reflect.TypeOf(args))
97+
return convertToMap(state.(State)), convertToMap(args.(Args))
98+
}

pkg/aflow/test_tool.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aflow
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestTool(t *testing.T, tool Tool, initState, initArgs, wantResults any, wantError string) {
13+
type toolTester interface {
14+
checkTestTypes(t *testing.T, ctx *verifyContext, state, args, results any) (
15+
map[string]any, map[string]any, map[string]any)
16+
}
17+
vctx := newVerifyContext()
18+
state, args, results := tool.(toolTester).checkTestTypes(t, vctx, initState, initArgs, wantResults)
19+
tool.verify(vctx)
20+
if err := vctx.finalize(); err != nil {
21+
t.Fatal(err)
22+
}
23+
// Just ensure it does not crash.
24+
_ = tool.declaration()
25+
// We don't init all fields, init more, if necessary.
26+
ctx := &Context{
27+
state: state,
28+
}
29+
defer ctx.close()
30+
gotResults, err := tool.execute(ctx, args)
31+
gotError := ""
32+
if err != nil {
33+
gotError = err.Error()
34+
}
35+
require.Equal(t, wantError, gotError)
36+
require.Equal(t, results, gotResults)
37+
}
38+
39+
func FuzzTool(t *testing.T, tool Tool, initState, initArgs any) (map[string]any, error) {
40+
type toolFuzzer interface {
41+
checkFuzzTypes(t *testing.T, state, args any) (map[string]any, map[string]any)
42+
}
43+
state, args := tool.(toolFuzzer).checkFuzzTypes(t, initState, initArgs)
44+
ctx := &Context{
45+
state: state,
46+
}
47+
defer ctx.close()
48+
return tool.execute(ctx, args)
49+
}

pkg/aflow/tool/codeeditor/codeeditor.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
package codeeditor
55

66
import (
7+
"path/filepath"
8+
"strings"
9+
710
"github.com/google/syzkaller/pkg/aflow"
11+
"github.com/google/syzkaller/pkg/osutil"
812
)
913

1014
var Tool = aflow.NewFuncTool("codeeditor", codeeditor, `
@@ -26,7 +30,16 @@ type args struct {
2630
}
2731

2832
func codeeditor(ctx *aflow.Context, state state, args args) (struct{}, error) {
29-
// TODO: check that the SourceFile is not escaping.
33+
if strings.Contains(filepath.Clean(args.SourceFile), "..") {
34+
return struct{}{}, aflow.BadCallError("SourceFile %q is outside of the source tree", args.SourceFile)
35+
}
36+
file := filepath.Join(state.KernelScratchSrc, args.SourceFile)
37+
if !osutil.IsExist(file) {
38+
return struct{}{}, aflow.BadCallError("SourceFile %q does not exist", args.SourceFile)
39+
}
40+
if strings.TrimSpace(args.CurrentCode) == "" {
41+
return struct{}{}, aflow.BadCallError("CurrentCode snippet is empty")
42+
}
3043
// If SourceFile is incorrect, or CurrentCode is not matched, return aflow.BadCallError
3144
// with an explanation. Say that it needs to increase context if CurrentCode is not matched.
3245
// Try to do as fuzzy match for CurrentCode as possible (strip line numbers,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package codeeditor
5+
6+
import (
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/google/syzkaller/pkg/aflow"
11+
"github.com/google/syzkaller/pkg/osutil"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCodeeditorEscapingPath(t *testing.T) {
16+
aflow.TestTool(t, Tool,
17+
state{
18+
KernelScratchSrc: "whatever",
19+
},
20+
args{
21+
SourceFile: "../../passwd",
22+
},
23+
struct{}{},
24+
`SourceFile "../../passwd" is outside of the source tree`,
25+
)
26+
}
27+
28+
func TestCodeeditorMissingPath(t *testing.T) {
29+
aflow.TestTool(t, Tool,
30+
state{
31+
KernelScratchSrc: t.TempDir(),
32+
},
33+
args{
34+
SourceFile: "missing-file",
35+
},
36+
struct{}{},
37+
`SourceFile "missing-file" does not exist`,
38+
)
39+
}
40+
41+
func TestCodeeditorEmptyCurrentCode(t *testing.T) {
42+
dir := writeTestFile(t, "foo", "data")
43+
aflow.TestTool(t, Tool,
44+
state{
45+
KernelScratchSrc: dir,
46+
},
47+
args{
48+
SourceFile: "foo",
49+
},
50+
struct{}{},
51+
`CurrentCode snippet is empty`,
52+
)
53+
}
54+
55+
func writeTestFile(t *testing.T, filename, data string) string {
56+
dir := t.TempDir()
57+
if err := osutil.WriteFile(filepath.Join(dir, filename), []byte(data)); err != nil {
58+
t.Fatal(err)
59+
}
60+
return dir
61+
}
62+
63+
func Fuzz(f *testing.F) {
64+
dir := f.TempDir()
65+
const filename = "src.c"
66+
fullFilename := filepath.Join(dir, filename)
67+
f.Fuzz(func(t *testing.T, fileData []byte, curCode, newCode string) {
68+
require.NoError(t, osutil.WriteFile(fullFilename, fileData))
69+
aflow.FuzzTool(t, Tool,
70+
state{
71+
KernelScratchSrc: dir,
72+
},
73+
args{
74+
SourceFile: filename,
75+
CurrentCode: curCode,
76+
NewCode: newCode,
77+
})
78+
})
79+
}

pkg/aflow/verify.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ type varState struct {
2424
used bool
2525
}
2626

27+
func newVerifyContext() *verifyContext {
28+
return &verifyContext{
29+
inputs: true,
30+
outputs: true,
31+
state: make(map[string]*varState),
32+
models: make(map[string]bool),
33+
}
34+
}
35+
2736
func (ctx *verifyContext) errorf(who, msg string, args ...any) {
2837
noteError(&ctx.err, fmt.Sprintf("action %v: %v", who, msg), args...)
2938
}

0 commit comments

Comments
 (0)