diff --git a/internal/wasm/sdk/internal/wasm/pool.go b/internal/wasm/sdk/internal/wasm/pool.go index 16b3fa0536..460fa31484 100644 --- a/internal/wasm/sdk/internal/wasm/pool.go +++ b/internal/wasm/sdk/internal/wasm/pool.go @@ -196,7 +196,6 @@ func (p *Pool) SetPolicyData(policy []byte, data []byte) error { if bytes.Equal(policy, currentPolicy) && bytes.Equal(data, currentData) { return nil - } err := p.setPolicyData(policy, data) diff --git a/internal/wasm/sdk/internal/wasm/util.go b/internal/wasm/sdk/internal/wasm/util.go index f4b16de9df..74cfef3f25 100644 --- a/internal/wasm/sdk/internal/wasm/util.go +++ b/internal/wasm/sdk/internal/wasm/util.go @@ -4,12 +4,13 @@ package wasm -const wasmPageSize = 65535 +// PageSize represents the WASM page size in bytes. +const PageSize = 65535 // Pages converts a byte size to Pages, rounding up as necessary. func Pages(n uint32) uint32 { - pages := n / wasmPageSize - if pages*wasmPageSize == n { + pages := n / PageSize + if pages*PageSize == n { return pages } diff --git a/internal/wasm/sdk/internal/wasm/vm.go b/internal/wasm/sdk/internal/wasm/vm.go index 084a79f25a..ff7f12ef39 100644 --- a/internal/wasm/sdk/internal/wasm/vm.go +++ b/internal/wasm/sdk/internal/wasm/vm.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "strconv" "time" @@ -259,7 +258,7 @@ func (i *VM) Eval(ctx context.Context, entrypoint int32, input *interface{}, met if e := recover(); e != nil { switch e := e.(type) { case abortError: - err = errors.New(e.message) + err = fmt.Errorf(e.message) case builtinError: err = e.err if _, ok := err.(topdown.Halt); !ok { diff --git a/internal/wasm/sdk/opa/config.go b/internal/wasm/sdk/opa/config.go index e75d1cee02..152c1c56db 100644 --- a/internal/wasm/sdk/opa/config.go +++ b/internal/wasm/sdk/opa/config.go @@ -64,7 +64,7 @@ func (o *OPA) WithDataJSON(data interface{}) *OPA { // WithMemoryLimits configures the memory limits (in bytes) for a single policy // evaluation. func (o *OPA) WithMemoryLimits(min, max uint32) *OPA { - if min < 2*65535 { + if min < 2*wasm.PageSize { o.configErr = fmt.Errorf("too low minimum memory limit: %w", errors.ErrInvalidConfig) return o } diff --git a/internal/wasm/sdk/opa/opa.go b/internal/wasm/sdk/opa/opa.go index ca715ee629..fd826c7960 100644 --- a/internal/wasm/sdk/opa/opa.go +++ b/internal/wasm/sdk/opa/opa.go @@ -182,7 +182,7 @@ func (o *OPA) Eval(ctx context.Context, opts EvalOpts) (*Result, error) { return nil, fmt.Errorf("%v: %w", err, errors.ErrInternal) } - return &Result{result}, nil + return &Result{Result: result}, nil } // Close waits until all the pending evaluations complete and then diff --git a/internal/wasm/sdk/opa/opa_bench_test.go b/internal/wasm/sdk/opa/opa_bench_test.go new file mode 100644 index 0000000000..302c92bccc --- /dev/null +++ b/internal/wasm/sdk/opa/opa_bench_test.go @@ -0,0 +1,194 @@ +package opa_test + +import ( + "context" + "fmt" + "testing" + + "github.com/open-policy-agent/opa/internal/wasm/sdk/internal/wasm" + "github.com/open-policy-agent/opa/internal/wasm/sdk/opa" + "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/util/test" +) + +func BenchmarkWasmRego(b *testing.B) { + policy := compileRegoToWasm("a = true", "data.p.a = x", false) + instance, _ := opa.New(). + WithPolicyBytes(policy). + WithMemoryLimits(131070, 2*131070). // TODO: For some reason unlimited memory slows down the eval_ctx_new(). + WithPoolSize(1). + Init() + + b.ReportAllocs() + b.ResetTimer() + + ctx := context.Background() + var input interface{} = make(map[string]interface{}) + + for i := 0; i < b.N; i++ { + if _, err := instance.Eval(ctx, opa.EvalOpts{Input: &input}); err != nil { + panic(err) + } + } +} + +func BenchmarkGoRego(b *testing.B) { + pq := compileRego(`package p + +a = true`, "data.p.a = x") + + b.ReportAllocs() + b.ResetTimer() + + ctx := context.Background() + input := make(map[string]interface{}) + + for i := 0; i < b.N; i++ { + if _, err := pq.Eval(ctx, rego.EvalInput(input)); err != nil { + panic(err) + } + } +} + +func BenchmarkWASMArrayIteration(b *testing.B) { + sizes := []int{10, 100, 1000, 10000} + for _, n := range sizes { + b.Run(fmt.Sprint(n), func(b *testing.B) { + benchmarkIteration(b, test.ArrayIterationBenchmarkModule(n)) + }) + } +} + +func BenchmarkWASMSetIteration(b *testing.B) { + sizes := []int{10, 100, 1000, 10000} + for _, n := range sizes { + b.Run(fmt.Sprint(n), func(b *testing.B) { + benchmarkIteration(b, test.SetIterationBenchmarkModule(n)) + }) + } +} + +func BenchmarkWASMObjectIteration(b *testing.B) { + sizes := []int{10, 100, 1000, 10000} + for _, n := range sizes { + b.Run(fmt.Sprint(n), func(b *testing.B) { + benchmarkIteration(b, test.ObjectIterationBenchmarkModule(n)) + }) + } +} + +var r *opa.Result + +func benchmarkIteration(b *testing.B, module string) { + query := "data.test.main = x" + policy := compileRegoToWasm(module, query, false) + + instance, err := opa.New(). + WithPolicyBytes(policy). + WithMemoryLimits(2*wasm.PageSize, 47*wasm.PageSize). + WithPoolSize(1). + Init() + if err != nil { + b.Fatalf("init sdk: %v", err) + } + + b.ResetTimer() + ctx := context.Background() + var input interface{} = make(map[string]interface{}) + + for i := 0; i < b.N; i++ { + r, err = instance.Eval(ctx, opa.EvalOpts{Input: &input}) + if err != nil { + b.Fatalf("Unexpected query error: %v", err) + } + if string(r.Result) != `{{"x":true}}` { + b.Errorf("unexpected result: %s", string(r.Result)) + } + } +} + +func BenchmarkWASMLargeJSON(b *testing.B) { + for _, kv := range []struct{ key, val int }{ + {10, 10}, + {10, 100}, + {10, 1000}, + {10, 10000}, + {100, 100}, + {100, 1000}, + } { + b.Run(fmt.Sprintf("%dx%d", kv.key, kv.val), func(b *testing.B) { + ctx := context.Background() + data := test.GenerateJSONBenchmarkData(kv.key, kv.val) + + // Read data.values N times inside query. + query := "data.keys[_] = x; data.values = y" + policy := compileRegoToWasm("", query, false) + + instance, err := opa.New(). + WithPolicyBytes(policy). + WithDataJSON(data). + WithMemoryLimits(200*wasm.PageSize, 600*wasm.PageSize). // This is rather much + WithPoolSize(1). + Init() + if err != nil { + b.Fatalf("init sdk: %v", err) + } + + b.ResetTimer() + var input interface{} = make(map[string]interface{}) + + for i := 0; i < b.N; i++ { + r, err = instance.Eval(ctx, opa.EvalOpts{Input: &input}) + if err != nil { + b.Fatalf("Unexpected query error: %v", err) + } + } + }) + } +} + +func BenchmarkWASMVirtualDocs(b *testing.B) { + for _, kv := range []struct{ total, hit int }{ + {1, 1}, + {10, 1}, + {100, 1}, + {1000, 1}, + {10, 10}, + {100, 10}, + {1000, 10}, + {100, 100}, + {1000, 100}, + {1000, 1000}, + } { + b.Run(fmt.Sprintf("total=%d/hit=%d", kv.total, kv.hit), func(b *testing.B) { + runVirtualDocsBenchmark(b, kv.total, kv.hit) + }) + } +} + +func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) { + ctx := context.Background() + module, input := test.GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules) + query := "data.a.b.c.allow = x" + + policy := compileRegoToWasm(module, query, false) + + instance, err := opa.New(). + WithPolicyBytes(policy). + WithMemoryLimits(8*wasm.PageSize, 8*wasm.PageSize). + WithPoolSize(1). + Init() + if err != nil { + b.Fatalf("init sdk: %v", err) + } + + b.ResetTimer() + var inp interface{} = input + + for i := 0; i < b.N; i++ { + r, err = instance.Eval(ctx, opa.EvalOpts{Input: &inp}) + if err != nil { + b.Fatalf("Unexpected query error: %v", err) + } + } +} diff --git a/internal/wasm/sdk/opa/opa_test.go b/internal/wasm/sdk/opa/opa_test.go index a09c16b1d9..5638a41e93 100644 --- a/internal/wasm/sdk/opa/opa_test.go +++ b/internal/wasm/sdk/opa/opa_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strings" "testing" "github.com/open-policy-agent/opa/ast" @@ -17,6 +18,9 @@ import ( "github.com/open-policy-agent/opa/util" ) +// control dumping in this file +const dump = true + func TestOPA(t *testing.T) { type Eval struct { NewPolicy string @@ -220,7 +224,7 @@ a = "c" { input > 2 }`, for _, test := range tests { t.Run(test.Description, func(t *testing.T) { - policy := compileRegoToWasm(test.Policy, test.Query) + policy := compileRegoToWasm(test.Policy, test.Query, dump) data := []byte(test.Data) if len(data) == 0 { data = nil @@ -240,14 +244,14 @@ a = "c" { input > 2 }`, for _, eval := range test.Evals { switch { case eval.NewPolicy != "" && eval.NewData != "": - policy := compileRegoToWasm(eval.NewPolicy, test.Query) + policy := compileRegoToWasm(eval.NewPolicy, test.Query, dump) data := parseJSON(eval.NewData) if err := instance.SetPolicyData(policy, data); err != nil { t.Errorf(err.Error()) } case eval.NewPolicy != "": - policy := compileRegoToWasm(eval.NewPolicy, test.Query) + policy := compileRegoToWasm(eval.NewPolicy, test.Query, dump) if err := instance.SetPolicy(policy); err != nil { t.Errorf(err.Error()) } @@ -349,51 +353,22 @@ func TestNamedEntrypoint(t *testing.T) { } } -func BenchmarkWasmRego(b *testing.B) { - policy := compileRegoToWasm("a = true", "data.p.a = x") - instance, _ := opa.New(). - WithPolicyBytes(policy). - WithMemoryLimits(131070, 2*131070). // TODO: For some reason unlimited memory slows down the eval_ctx_new(). - WithPoolSize(1). - Init() - - b.ReportAllocs() - b.ResetTimer() - - ctx := context.Background() - var input interface{} = make(map[string]interface{}) - - for i := 0; i < b.N; i++ { - if _, err := instance.Eval(ctx, opa.EvalOpts{Input: &input}); err != nil { - panic(err) - } +// compileRegoToWasm is shared with the benchmarking functions in opa_bench_test.go; +// those function use helpers shared with topdown_bench_test.go, and they all use +// `package test` -- whereas the callers in this file don't provide the package at +// all and assume it'll be `p`. +func compileRegoToWasm(module string, query string, dump bool) []byte { + if !strings.HasPrefix(module, "package") { + module = fmt.Sprintf("package p\n%s", module) } -} - -func BenchmarkGoRego(b *testing.B) { - pq := compileRego(`package p - -a = true`, "data.p.a = x") - - b.ReportAllocs() - b.ResetTimer() - - input := make(map[string]interface{}) - - for i := 0; i < b.N; i++ { - if _, err := pq.Eval(context.Background(), rego.EvalInput(input)); err != nil { - panic(err) - } - } -} - -func compileRegoToWasm(policy string, query string) []byte { - module := fmt.Sprintf("package p\n%s", policy) - cr, err := rego.New( + opts := []func(*rego.Rego){ rego.Query(query), rego.Module("module.rego", module), - rego.Dump(os.Stderr), - ).Compile(context.Background(), rego.CompilePartial(false)) + } + if dump { + opts = append(opts, rego.Dump(os.Stderr)) + } + cr, err := rego.New(opts...).Compile(context.Background(), rego.CompilePartial(false)) if err != nil { panic(err) } diff --git a/rego/rego.go b/rego/rego.go index 36969e4dff..93308bd760 100644 --- a/rego/rego.go +++ b/rego/rego.go @@ -1257,7 +1257,7 @@ func (r *Rego) Compile(ctx context.Context, opts ...CompileOption) (*CompileResu } } else { var err error - // If creating a new transacation it should be closed before calling the + // If creating a new transaction it should be closed before calling the // planner to avoid holding open the transaction longer than needed. // // TODO(tsandall): in future, planner could make use of store, in which diff --git a/topdown/topdown_bench_test.go b/topdown/topdown_bench_test.go index 2469140288..245b49057d 100644 --- a/topdown/topdown_bench_test.go +++ b/topdown/topdown_bench_test.go @@ -16,14 +16,14 @@ import ( "github.com/open-policy-agent/opa/metrics" "github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/storage/inmem" - "github.com/open-policy-agent/opa/util" + "github.com/open-policy-agent/opa/util/test" ) func BenchmarkArrayIteration(b *testing.B) { sizes := []int{10, 100, 1000, 10000} for _, n := range sizes { b.Run(fmt.Sprint(n), func(b *testing.B) { - benchmarkIteration(b, getArrayIterationBenchmarkModule(n)) + benchmarkIteration(b, test.ArrayIterationBenchmarkModule(n)) }) } } @@ -32,7 +32,7 @@ func BenchmarkSetIteration(b *testing.B) { sizes := []int{10, 100, 1000, 10000} for _, n := range sizes { b.Run(fmt.Sprint(n), func(b *testing.B) { - benchmarkIteration(b, getSetIterationBenchmarkModule(n)) + benchmarkIteration(b, test.SetIterationBenchmarkModule(n)) }) } } @@ -41,7 +41,7 @@ func BenchmarkObjectIteration(b *testing.B) { sizes := []int{10, 100, 1000, 10000} for _, n := range sizes { b.Run(fmt.Sprint(n), func(b *testing.B) { - benchmarkIteration(b, getObjectIterationBenchmarkModule(n)) + benchmarkIteration(b, test.ObjectIterationBenchmarkModule(n)) }) } } @@ -65,32 +65,8 @@ func benchmarkIteration(b *testing.B, module string) { } } -func getArrayIterationBenchmarkModule(n int) string { - return fmt.Sprintf(`package test - - fixture = [ x | x := numbers.range(1, %d)[_] ] - - main { fixture[i] }`, n) -} - -func getSetIterationBenchmarkModule(n int) string { - return fmt.Sprintf(`package test - - fixture = { x | x := numbers.range(1, %d)[_] } - - main { fixture[i] }`, n) -} - -func getObjectIterationBenchmarkModule(n int) string { - return fmt.Sprintf(`package test - - fixture = { x: x | x := numbers.range(1, %d)[_] } - - main { fixture[i] }`, n) -} - func BenchmarkLargeJSON(b *testing.B) { - data := generateLargeJSONBenchmarkData() + data := test.GenerateLargeJSONBenchmarkData() ctx := context.Background() store := inmem.NewFromObject(data) compiler := ast.NewCompiler() @@ -128,26 +104,6 @@ func BenchmarkLargeJSON(b *testing.B) { } } -func generateLargeJSONBenchmarkData() map[string]interface{} { - - // create array of null values that can be iterated over - keys := make([]interface{}, 100) - for i := range keys { - keys[i] = nil - } - - // create large JSON object value (100,000 entries is about 2MB on disk) - values := map[string]interface{}{} - for i := 0; i < 100*1000; i++ { - values[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i) - } - - return map[string]interface{}{ - "keys": keys, - "values": values, - } -} - func BenchmarkConcurrency1(b *testing.B) { benchmarkConcurrency(b, getParams(1, 0)) } @@ -174,10 +130,10 @@ func BenchmarkConcurrency8Writers(b *testing.B) { func benchmarkConcurrency(b *testing.B, params []storage.TransactionParams) { - mod, data := generateConcurrencyBenchmarkData() + mod, data := test.GenerateConcurrencyBenchmarkData() ctx := context.Background() store := inmem.NewFromObject(data) - mods := map[string]*ast.Module{"module": mod} + mods := map[string]*ast.Module{"module": ast.MustParseModule(mod)} compiler := ast.NewCompiler() if compiler.Compile(mods); compiler.Failed() { @@ -226,53 +182,6 @@ func getParams(nReaders, nWriters int) (sl []storage.TransactionParams) { return sl } -func generateConcurrencyBenchmarkData() (*ast.Module, map[string]interface{}) { - obj := util.MustUnmarshalJSON([]byte(` - { - "objs": [ - { - "attr1": "get", - "path": "/foo/bar", - "user": "bob" - }, - { - "attr1": "set", - "path": "/foo/bar/baz", - "user": "alice" - }, - { - "attr1": "get", - "path": "/foo", - "groups": [ - "admin", - "eng" - ] - }, - { - "path": "/foo/bar", - "user": "alice" - } - ] - } - `)) - - mod := `package test - - import data.objs - - p { - objs[i].attr1 = "get" - objs[i].groups[j] = "eng" - } - - p { - objs[i].user = "alice" - } - ` - - return ast.MustParseModule(mod), obj.(map[string]interface{}) -} - func BenchmarkVirtualDocs1x1(b *testing.B) { runVirtualDocsBenchmark(b, 1, 1) } @@ -315,24 +224,25 @@ func BenchmarkVirtualDocs1000x1000(b *testing.B) { func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) { - mod, input := generateVirtualDocsBenchmarkData(numTotalRules, numHitRules) + mod, inp := test.GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules) ctx := context.Background() compiler := ast.NewCompiler() - mods := map[string]*ast.Module{"module": mod} + mods := map[string]*ast.Module{"module": ast.MustParseModule(mod)} + input := ast.NewTerm(ast.MustInterfaceToValue(inp)) store := inmem.New() txn := storage.NewTransactionOrDie(ctx, store) if compiler.Compile(mods); compiler.Failed() { b.Fatalf("Unexpected compiler error: %v", compiler.Errors) } - body := ast.MustParseBody("data.a.b.c.allow = x") + query := ast.MustParseBody("data.a.b.c.allow = x") b.ResetTimer() for i := 0; i < b.N; i++ { b.StopTimer() - query := NewQuery(body). + query := NewQuery(query). WithCompiler(compiler). WithStore(store). WithTransaction(txn). @@ -345,84 +255,11 @@ func runVirtualDocsBenchmark(b *testing.B, numTotalRules, numHitRules int) { b.Fatalf("Unexpected topdown query error: %v", err) } if len(rs) != 1 || !rs[0][ast.Var("x")].Equal(ast.BooleanTerm(true)) { - b.Fatalf("Unexpecfted undefined/extra/bad result: %v", rs) + b.Fatalf("Unexpected undefined/extra/bad result: %v", rs) } } } -func generateVirtualDocsBenchmarkData(numTotalRules, numHitRules int) (*ast.Module, *ast.Term) { - - hitRule := ` - allow { - input.method = "POST" - input.path = ["accounts", account_id] - input.user_id = account_id - } - ` - - missRule := ` - allow { - input.method = "GET" - input.path = ["salaries", account_id] - input.user_id = account_id - } - ` - - testModuleTmpl := ` - package a.b.c - - {{range .MissRules }} - {{ . }} - {{end}} - - {{range .HitRules }} - {{ . }} - {{end}} - ` - - tmpl, err := template.New("Test").Parse(testModuleTmpl) - if err != nil { - panic(err) - } - - var buf bytes.Buffer - - var missRules []string - - if numTotalRules > numHitRules { - missRules = make([]string, numTotalRules-numHitRules) - for i := range missRules { - missRules[i] = missRule - } - } - - hitRules := make([]string, numHitRules) - for i := range hitRules { - hitRules[i] = hitRule - } - - params := struct { - MissRules []string - HitRules []string - }{ - MissRules: missRules, - HitRules: hitRules, - } - - err = tmpl.Execute(&buf, params) - if err != nil { - panic(err) - } - - input := ast.MustParseTerm(`{ - "path": ["accounts", "alice"], - "method": "POST", - "user_id": "alice" - }`) - - return ast.MustParseModule(buf.String()), input -} - func BenchmarkPartialEval(b *testing.B) { sizes := []int{1, 10, 100, 1000} for _, n := range sizes { diff --git a/util/test/benchmark.go b/util/test/benchmark.go new file mode 100644 index 0000000000..c232f7562f --- /dev/null +++ b/util/test/benchmark.go @@ -0,0 +1,198 @@ +package test + +// This file collects some helpers for generating data used in +// benchmarks, +// - topdown/topdown_bench_test.go + +import ( + "bytes" + "encoding/json" + "fmt" + "text/template" +) + +// ArrayIterationBenchmarkModule returns a module that iterates an array +// with `n` elements +func ArrayIterationBenchmarkModule(n int) string { + return fmt.Sprintf(`package test + + fixture = [ x | x := numbers.range(1, %d)[_] ] + + main { fixture[i] }`, n) +} + +// SetIterationBenchmarkModule returns a module that iterates a set +// with `n` elements +func SetIterationBenchmarkModule(n int) string { + return fmt.Sprintf(`package test + + fixture = { x | x := numbers.range(1, %d)[_] } + + main { fixture[i] }`, n) +} + +// ObjectIterationBenchmarkModule returns a module that iterates an object +// with `n` key/val pairs +func ObjectIterationBenchmarkModule(n int) string { + return fmt.Sprintf(`package test + + fixture = { x: x | x := numbers.range(1, %d)[_] } + + main { fixture[i] }`, n) +} + +// GenerateLargeJSONBenchmarkData returns a map of 100 keys and 100.000 key/value +// pairs. +func GenerateLargeJSONBenchmarkData() map[string]interface{} { + return GenerateJSONBenchmarkData(100, 100*1000) +} + +// GenerateJSONBenchmarkData returns a map of `k` keys and `v` key/value pairs. +func GenerateJSONBenchmarkData(k, v int) map[string]interface{} { + + // create array of null values that can be iterated over + keys := make([]interface{}, k) + for i := range keys { + keys[i] = nil + } + + // create large JSON object value (100,000 entries is about 2MB on disk) + values := map[string]interface{}{} + for i := 0; i < v; i++ { + values[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i) + } + + return map[string]interface{}{ + "keys": keys, + "values": values, + } +} + +// GenerateConcurrencyBenchmarkData returns a module and data; the module +// checks some input parameters against that data in a simple API authz +// scheme. +func GenerateConcurrencyBenchmarkData() (string, map[string]interface{}) { + obj := []byte(` + { + "objs": [ + { + "attr1": "get", + "path": "/foo/bar", + "user": "bob" + }, + { + "attr1": "set", + "path": "/foo/bar/baz", + "user": "alice" + }, + { + "attr1": "get", + "path": "/foo", + "groups": [ + "admin", + "eng" + ] + }, + { + "path": "/foo/bar", + "user": "alice" + } + ] + } + `) + + var data map[string]interface{} + if err := json.Unmarshal(obj, &data); err != nil { + panic(err) + } + mod := `package test + + import data.objs + + p { + objs[i].attr1 = "get" + objs[i].groups[j] = "eng" + } + + p { + objs[i].user = "alice" + } + ` + + return mod, data +} + +// GenerateVirtualDocsBenchmarkData generates a module and input; the +// numTotalRules and numHitRules create as many rules in the module to +// match/miss the returned input. +func GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules int) (string, map[string]interface{}) { + + hitRule := ` + allow { + input.method = "POST" + input.path = ["accounts", account_id] + input.user_id = account_id + } + ` + + missRule := ` + allow { + input.method = "GET" + input.path = ["salaries", account_id] + input.user_id = account_id + } + ` + + testModuleTmpl := `package a.b.c + + {{range .MissRules }} + {{ . }} + {{end}} + + {{range .HitRules }} + {{ . }} + {{end}} + ` + + tmpl, err := template.New("Test").Parse(testModuleTmpl) + if err != nil { + panic(err) + } + + var buf bytes.Buffer + + var missRules []string + + if numTotalRules > numHitRules { + missRules = make([]string, numTotalRules-numHitRules) + for i := range missRules { + missRules[i] = missRule + } + } + + hitRules := make([]string, numHitRules) + for i := range hitRules { + hitRules[i] = hitRule + } + + params := struct { + MissRules []string + HitRules []string + }{ + MissRules: missRules, + HitRules: hitRules, + } + + err = tmpl.Execute(&buf, params) + if err != nil { + panic(err) + } + + input := map[string]interface{}{ + "path": []interface{}{"accounts", "alice"}, + "method": "POST", + "user_id": "alice", + } + + return buf.String(), input +}