From 3982cef2558c4d98c0b7cb268643a7dafad59950 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sat, 5 Nov 2022 13:09:56 +0100 Subject: [PATCH 01/10] feat(configio): implement Builder --- configio/builder.go | 161 +++++++++++++++++++++++++++++++++++++ configio/builder_test.go | 148 ++++++++++++++++++++++++++++++++++ configio/representation.go | 4 + 3 files changed, 313 insertions(+) create mode 100644 configio/builder.go create mode 100644 configio/builder_test.go diff --git a/configio/builder.go b/configio/builder.go new file mode 100644 index 0000000..ad15021 --- /dev/null +++ b/configio/builder.go @@ -0,0 +1,161 @@ +package configio + +import ( + "io" + "net/http" + "net/url" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +type Builder struct { + // TODO: benchmark this vs []func(*benchttp.Runner) + modifier func(*benchttp.Runner) +} + +func (b *Builder) WriteJSON(in []byte) error { + return b.decodeAndWrite(in, FormatJSON) +} + +func (b *Builder) WriteYAML(in []byte) error { + return b.decodeAndWrite(in, FormatYAML) +} + +func (b *Builder) decodeAndWrite(in []byte, format Format) error { + repr := Representation{} + if err := DecoderOf(format, in).Decode(&repr); err != nil { + return err + } + if err := repr.validate(); err != nil { + return err + } + b.pipe(func(dst *benchttp.Runner) { + _ = repr.Into(dst) + }) + return nil +} + +func (b *Builder) Runner() benchttp.Runner { + runner := benchttp.Runner{} + b.into(&runner) + return runner +} + +func (b *Builder) Into(dst *benchttp.Runner) { + b.into(dst) +} + +func (b *Builder) into(dst *benchttp.Runner) { + if b.modifier == nil { + return + } + b.modifier(dst) +} + +// setters + +func (b *Builder) SetRequest(r *http.Request) { + b.pipe(func(runner *benchttp.Runner) { + runner.Request = r + }) +} + +func (b *Builder) SetRequestMethod(v string) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Method = v + }) +} + +func (b *Builder) SetRequestURL(v *url.URL) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.URL = v + }) +} + +func (b *Builder) SetRequestHeader(v http.Header) { + b.SetRequestHeaderFunc(func(_ http.Header) http.Header { + return v + }) +} + +func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Header = f(runner.Request.Header) + }) +} + +func (b *Builder) SetRequestBody(v io.ReadCloser) { + b.pipe(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Body = v + }) +} + +func (b *Builder) SetRequests(v int) { + b.pipe(func(runner *benchttp.Runner) { + runner.Requests = v + }) +} + +func (b *Builder) SetConcurrency(v int) { + b.pipe(func(runner *benchttp.Runner) { + runner.Concurrency = v + }) +} + +func (b *Builder) SetInterval(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.Interval = v + }) +} + +func (b *Builder) SetRequestTimeout(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.RequestTimeout = v + }) +} + +func (b *Builder) SetGlobalTimeout(v time.Duration) { + b.pipe(func(runner *benchttp.Runner) { + runner.GlobalTimeout = v + }) +} + +func (b *Builder) SetTests(v []benchttp.TestCase) { + b.pipe(func(runner *benchttp.Runner) { + runner.Tests = v + }) +} + +func (b *Builder) AddTests(v ...benchttp.TestCase) { + b.pipe(func(runner *benchttp.Runner) { + runner.Tests = append(runner.Tests, v...) + }) +} + +func (b *Builder) pipe(modifier func(runner *benchttp.Runner)) { + if modifier == nil { + panicInternal("Builder.pipe", "call with nil modifier") + } + if b.modifier == nil { + b.modifier = modifier + return + } + applyPreviousModifier := b.modifier + b.modifier = func(dst *benchttp.Runner) { + applyPreviousModifier(dst) + modifier(dst) + } +} diff --git a/configio/builder_test.go b/configio/builder_test.go new file mode 100644 index 0000000..6dc95ee --- /dev/null +++ b/configio/builder_test.go @@ -0,0 +1,148 @@ +package configio_test + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" + "github.com/benchttp/sdk/configio" +) + +func TestBuilder_WriteJSON(t *testing.T) { + in := []byte(`{"runner":{"requests": 5}}`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder{} + if err := b.WriteJSON(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_WriteYAML(t *testing.T) { + in := []byte(`runner: { requests: 5 }`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder{} + if err := b.WriteYAML(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_Set(t *testing.T) { + t.Run("basic fields", func(t *testing.T) { + want := benchttp.Runner{ + Requests: 5, + Concurrency: 2, + Interval: 10 * time.Millisecond, + RequestTimeout: 1 * time.Second, + GlobalTimeout: 10 * time.Second, + } + + b := configio.Builder{} + b.SetRequests(want.Requests) + b.SetConcurrency(-1) + b.SetConcurrency(want.Concurrency) + b.SetInterval(want.Interval) + b.SetRequestTimeout(want.RequestTimeout) + b.SetGlobalTimeout(want.GlobalTimeout) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request", func(t *testing.T) { + want := benchttp.Runner{ + Request: httptest.NewRequest("GET", "https://example.com", nil), + } + + b := configio.Builder{} + b.SetRequest(want.Request) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request fields", func(t *testing.T) { + want := benchttp.Runner{ + Request: &http.Request{ + Method: "PUT", + URL: mustParseRequestURI("https://example.com"), + Header: http.Header{ + "API_KEY": []string{"abc"}, + "Accept": []string{"text/html", "application/json"}, + }, + Body: readcloser("hello"), + }, + } + + b := configio.Builder{} + b.SetRequestMethod(want.Request.Method) + b.SetRequestURL(want.Request.URL) + b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) + b.SetRequestHeaderFunc(func(prev http.Header) http.Header { + prev.Add("Accept", "text/html") + prev.Add("Accept", "application/json") + return prev + }) + b.SetRequestBody(readcloser("hello")) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("test cases", func(t *testing.T) { + want := benchttp.Runner{ + Tests: []benchttp.TestCase{ + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LT", + Target: 100 * time.Millisecond, + }, + { + Name: "similar response times", + Field: "ResponseTimes.StdDev", + Predicate: "LTE", + Target: 20 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } + + b := configio.Builder{} + b.SetTests([]benchttp.TestCase{want.Tests[0]}) + b.AddTests(want.Tests[1:]...) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) +} + +// helpers + +func mustParseRequestURI(s string) *url.URL { + u, err := url.ParseRequestURI(s) + if err != nil { + panic("mustParseRequestURI: " + err.Error()) + } + return u +} + +func readcloser(s string) io.ReadCloser { + return io.NopCloser(strings.NewReader(s)) +} diff --git a/configio/representation.go b/configio/representation.go index c668f0d..e13f2db 100644 --- a/configio/representation.go +++ b/configio/representation.go @@ -49,6 +49,10 @@ type Representation struct { } `yaml:"tests" json:"tests"` } +func (repr Representation) validate() error { + return repr.Into(&benchttp.Runner{}) +} + // Into parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. From 9b8780039665a0a08ad4e6cbf01c60c275137e36 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 17:44:16 +0100 Subject: [PATCH 02/10] doc(configio): write ExampleBuilder example --- configio/example_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 configio/example_test.go diff --git a/configio/example_test.go b/configio/example_test.go new file mode 100644 index 0000000..4efe5b2 --- /dev/null +++ b/configio/example_test.go @@ -0,0 +1,40 @@ +package configio_test + +import ( + "fmt" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/configio" +) + +var jsonConfig = []byte( + `{"request": {"method": "GET", "url": "https://example.com"}}`, +) + +var yamlConfig = []byte( + `{request: {method: PUT}, runner: {requests: 42}}`, +) + +func ExampleBuilder() { + runner := benchttp.Runner{Requests: -1, Concurrency: 3} + + b := configio.Builder{} + _ = b.WriteJSON(jsonConfig) + _ = b.WriteYAML(yamlConfig) + b.SetInterval(100 * time.Millisecond) + + b.Into(&runner) + + // Output: + // PUT + // https://example.com + // 42 + // 3 + // 100ms + fmt.Println(runner.Request.Method) + fmt.Println(runner.Request.URL) + fmt.Println(runner.Requests) + fmt.Println(runner.Concurrency) + fmt.Println(runner.Interval) +} From 528dd50ab2bbbf410b8e6c3be3e387b02e5e4119 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:08:54 +0100 Subject: [PATCH 03/10] tmp: benchmark pipe vs append implementations for configio.Builder --- configio/builder_append.go | 151 +++++++++++++++++++++++++++++ configio/builder_append_test.go | 131 +++++++++++++++++++++++++ configio/builder_benchmark_test.go | 75 ++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 configio/builder_append.go create mode 100644 configio/builder_append_test.go create mode 100644 configio/builder_benchmark_test.go diff --git a/configio/builder_append.go b/configio/builder_append.go new file mode 100644 index 0000000..d067cd1 --- /dev/null +++ b/configio/builder_append.go @@ -0,0 +1,151 @@ +package configio + +import ( + "io" + "net/http" + "net/url" + "time" + + "github.com/benchttp/sdk/benchttp" +) + +type Builder_append struct { // nolint:revive + modifiers []func(*benchttp.Runner) +} + +func (b *Builder_append) WriteJSON(in []byte) error { + return b.decodeAndWrite(in, FormatJSON) +} + +func (b *Builder_append) WriteYAML(in []byte) error { + return b.decodeAndWrite(in, FormatYAML) +} + +func (b *Builder_append) decodeAndWrite(in []byte, format Format) error { + repr := Representation{} + if err := DecoderOf(format, in).Decode(&repr); err != nil { + return err + } + if err := repr.validate(); err != nil { + return err + } + b.append(func(dst *benchttp.Runner) { + _ = repr.Into(dst) + }) + return nil +} + +func (b *Builder_append) Runner() benchttp.Runner { + runner := benchttp.Runner{} + b.into(&runner) + return runner +} + +func (b *Builder_append) Into(dst *benchttp.Runner) { + b.into(dst) +} + +func (b *Builder_append) into(dst *benchttp.Runner) { + for _, modify := range b.modifiers { + modify(dst) + } +} + +// setters + +func (b *Builder_append) SetRequest(r *http.Request) { + b.append(func(runner *benchttp.Runner) { + runner.Request = r + }) +} + +func (b *Builder_append) SetRequestMethod(v string) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Method = v + }) +} + +func (b *Builder_append) SetRequestURL(v *url.URL) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.URL = v + }) +} + +func (b *Builder_append) SetRequestHeader(v http.Header) { + b.SetRequestHeaderFunc(func(_ http.Header) http.Header { + return v + }) +} + +func (b *Builder_append) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Header = f(runner.Request.Header) + }) +} + +func (b *Builder_append) SetRequestBody(v io.ReadCloser) { + b.append(func(runner *benchttp.Runner) { + if runner.Request == nil { + runner.Request = &http.Request{} + } + runner.Request.Body = v + }) +} + +func (b *Builder_append) SetRequests(v int) { + b.append(func(runner *benchttp.Runner) { + runner.Requests = v + }) +} + +func (b *Builder_append) SetConcurrency(v int) { + b.append(func(runner *benchttp.Runner) { + runner.Concurrency = v + }) +} + +func (b *Builder_append) SetInterval(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.Interval = v + }) +} + +func (b *Builder_append) SetRequestTimeout(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.RequestTimeout = v + }) +} + +func (b *Builder_append) SetGlobalTimeout(v time.Duration) { + b.append(func(runner *benchttp.Runner) { + runner.GlobalTimeout = v + }) +} + +func (b *Builder_append) SetTests(v []benchttp.TestCase) { + b.append(func(runner *benchttp.Runner) { + runner.Tests = v + }) +} + +func (b *Builder_append) AddTests(v ...benchttp.TestCase) { + b.append(func(runner *benchttp.Runner) { + runner.Tests = append(runner.Tests, v...) + }) +} + +func (b *Builder_append) append(modifier func(runner *benchttp.Runner)) { + if modifier == nil { + panicInternal("Builder.append", "call with nil modifier") + } + b.modifiers = append(b.modifiers, modifier) +} diff --git a/configio/builder_append_test.go b/configio/builder_append_test.go new file mode 100644 index 0000000..0a6d2ab --- /dev/null +++ b/configio/builder_append_test.go @@ -0,0 +1,131 @@ +package configio_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/benchttp/sdk/benchttp" + "github.com/benchttp/sdk/benchttptest" + "github.com/benchttp/sdk/configio" +) + +func TestBuilder_append_WriteJSON(t *testing.T) { + in := []byte(`{"runner":{"requests": 5}}`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder_append{} + if err := b.WriteJSON(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_append_WriteYAML(t *testing.T) { + in := []byte(`runner: { requests: 5 }`) + dest := benchttp.Runner{Requests: 0, Concurrency: 2} + want := benchttp.Runner{Requests: 5, Concurrency: 2} + + b := configio.Builder_append{} + if err := b.WriteYAML(in); err != nil { + t.Fatal(err) + } + b.Into(&dest) + + benchttptest.AssertEqualRunners(t, want, dest) +} + +func TestBuilder_append_Set(t *testing.T) { + t.Run("basic fields", func(t *testing.T) { + want := benchttp.Runner{ + Requests: 5, + Concurrency: 2, + Interval: 10 * time.Millisecond, + RequestTimeout: 1 * time.Second, + GlobalTimeout: 10 * time.Second, + } + + b := configio.Builder_append{} + b.SetRequests(want.Requests) + b.SetConcurrency(-1) + b.SetConcurrency(want.Concurrency) + b.SetInterval(want.Interval) + b.SetRequestTimeout(want.RequestTimeout) + b.SetGlobalTimeout(want.GlobalTimeout) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request", func(t *testing.T) { + want := benchttp.Runner{ + Request: httptest.NewRequest("GET", "https://example.com", nil), + } + + b := configio.Builder_append{} + b.SetRequest(want.Request) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("request fields", func(t *testing.T) { + want := benchttp.Runner{ + Request: &http.Request{ + Method: "PUT", + URL: mustParseRequestURI("https://example.com"), + Header: http.Header{ + "API_KEY": []string{"abc"}, + "Accept": []string{"text/html", "application/json"}, + }, + Body: readcloser("hello"), + }, + } + + b := configio.Builder_append{} + b.SetRequestMethod(want.Request.Method) + b.SetRequestURL(want.Request.URL) + b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) + b.SetRequestHeaderFunc(func(prev http.Header) http.Header { + prev.Add("Accept", "text/html") + prev.Add("Accept", "application/json") + return prev + }) + b.SetRequestBody(readcloser("hello")) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) + + t.Run("test cases", func(t *testing.T) { + want := benchttp.Runner{ + Tests: []benchttp.TestCase{ + { + Name: "maximum response time", + Field: "ResponseTimes.Max", + Predicate: "LT", + Target: 100 * time.Millisecond, + }, + { + Name: "similar response times", + Field: "ResponseTimes.StdDev", + Predicate: "LTE", + Target: 20 * time.Millisecond, + }, + { + Name: "100% availability", + Field: "RequestFailureCount", + Predicate: "EQ", + Target: 0, + }, + }, + } + + b := configio.Builder_append{} + b.SetTests([]benchttp.TestCase{want.Tests[0]}) + b.AddTests(want.Tests[1:]...) + + benchttptest.AssertEqualRunners(t, want, b.Runner()) + }) +} diff --git a/configio/builder_benchmark_test.go b/configio/builder_benchmark_test.go new file mode 100644 index 0000000..f4431c9 --- /dev/null +++ b/configio/builder_benchmark_test.go @@ -0,0 +1,75 @@ +package configio_test + +import ( + "strconv" + "testing" + + "github.com/benchttp/sdk/configio" +) + +/* +Output: + +Running tool: /Users/greg/sdk/go1.17.6/bin/go test -benchmem -run=^$ -bench ^(BenchmarkBuilder)$ github.com/benchttp/sdk/configio + +goos: darwin +goarch: amd64 +pkg: github.com/benchttp/sdk/configio +cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz +BenchmarkBuilder/setters/Builder_pipe-8 12966073 97.47 ns/op 40 B/op 2 allocs/op +BenchmarkBuilder/setters/Builder_append-8 21533343 81.65 ns/op 64 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_100-8 3044132 396.5 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_100-8 5413728 222.9 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_10000-8 25195 41284 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_10000-8 71463 16838 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_pipe_1000000-8 156 6993338 ns/op 96 B/op 1 allocs/op +BenchmarkBuilder/build/Builder_append_1000000-8 451 2730270 ns/op 96 B/op 1 allocs/op +*/ +func BenchmarkBuilder(b *testing.B) { + b.Run("setters", func(b *testing.B) { + b.Run("Builder_pipe", func(b *testing.B) { + builder := configio.Builder{} + for i := 0; i < b.N; i++ { + builder.SetConcurrency(100) + } + }) + b.Run("Builder_append", func(b *testing.B) { + builder := configio.Builder_append{} + for i := 0; i < b.N; i++ { + builder.SetConcurrency(100) + } + }) + }) + + b.Run("build", func(b *testing.B) { + for _, iter := range []int{100, 10_000, 1_000_000} { + b.Run("Builder_pipe_"+strconv.Itoa(iter), func(b *testing.B) { + builder := configio.Builder{} + setupBuilder(b, &builder, iter) + for i := 0; i < b.N; i++ { + _ = builder.Runner() + } + }) + b.Run("Builder_append_"+strconv.Itoa(iter), func(b *testing.B) { + builder := configio.Builder_append{} + setupBuilder(b, &builder, iter) + for i := 0; i < b.N; i++ { + _ = builder.Runner() + } + }) + } + }) +} + +func setupBuilder( + b *testing.B, + builder interface{ SetConcurrency(int) }, + iter int, +) { + b.Helper() + values := []int{-100, 100} + for i := 0; i < iter; i++ { + builder.SetConcurrency(values[i%2]) + } + b.ResetTimer() +} From b3baa391778bfa6d0c8d2c17241215a24c518bb5 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:09:29 +0100 Subject: [PATCH 04/10] Revert "tmp: benchmark pipe vs append implementations for configio.Builder" This reverts commit 731547e2925a323b1c568c3c440e0aab637c58fd. --- configio/builder_append.go | 151 ----------------------------- configio/builder_append_test.go | 131 ------------------------- configio/builder_benchmark_test.go | 75 -------------- 3 files changed, 357 deletions(-) delete mode 100644 configio/builder_append.go delete mode 100644 configio/builder_append_test.go delete mode 100644 configio/builder_benchmark_test.go diff --git a/configio/builder_append.go b/configio/builder_append.go deleted file mode 100644 index d067cd1..0000000 --- a/configio/builder_append.go +++ /dev/null @@ -1,151 +0,0 @@ -package configio - -import ( - "io" - "net/http" - "net/url" - "time" - - "github.com/benchttp/sdk/benchttp" -) - -type Builder_append struct { // nolint:revive - modifiers []func(*benchttp.Runner) -} - -func (b *Builder_append) WriteJSON(in []byte) error { - return b.decodeAndWrite(in, FormatJSON) -} - -func (b *Builder_append) WriteYAML(in []byte) error { - return b.decodeAndWrite(in, FormatYAML) -} - -func (b *Builder_append) decodeAndWrite(in []byte, format Format) error { - repr := Representation{} - if err := DecoderOf(format, in).Decode(&repr); err != nil { - return err - } - if err := repr.validate(); err != nil { - return err - } - b.append(func(dst *benchttp.Runner) { - _ = repr.Into(dst) - }) - return nil -} - -func (b *Builder_append) Runner() benchttp.Runner { - runner := benchttp.Runner{} - b.into(&runner) - return runner -} - -func (b *Builder_append) Into(dst *benchttp.Runner) { - b.into(dst) -} - -func (b *Builder_append) into(dst *benchttp.Runner) { - for _, modify := range b.modifiers { - modify(dst) - } -} - -// setters - -func (b *Builder_append) SetRequest(r *http.Request) { - b.append(func(runner *benchttp.Runner) { - runner.Request = r - }) -} - -func (b *Builder_append) SetRequestMethod(v string) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Method = v - }) -} - -func (b *Builder_append) SetRequestURL(v *url.URL) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.URL = v - }) -} - -func (b *Builder_append) SetRequestHeader(v http.Header) { - b.SetRequestHeaderFunc(func(_ http.Header) http.Header { - return v - }) -} - -func (b *Builder_append) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Header = f(runner.Request.Header) - }) -} - -func (b *Builder_append) SetRequestBody(v io.ReadCloser) { - b.append(func(runner *benchttp.Runner) { - if runner.Request == nil { - runner.Request = &http.Request{} - } - runner.Request.Body = v - }) -} - -func (b *Builder_append) SetRequests(v int) { - b.append(func(runner *benchttp.Runner) { - runner.Requests = v - }) -} - -func (b *Builder_append) SetConcurrency(v int) { - b.append(func(runner *benchttp.Runner) { - runner.Concurrency = v - }) -} - -func (b *Builder_append) SetInterval(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.Interval = v - }) -} - -func (b *Builder_append) SetRequestTimeout(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.RequestTimeout = v - }) -} - -func (b *Builder_append) SetGlobalTimeout(v time.Duration) { - b.append(func(runner *benchttp.Runner) { - runner.GlobalTimeout = v - }) -} - -func (b *Builder_append) SetTests(v []benchttp.TestCase) { - b.append(func(runner *benchttp.Runner) { - runner.Tests = v - }) -} - -func (b *Builder_append) AddTests(v ...benchttp.TestCase) { - b.append(func(runner *benchttp.Runner) { - runner.Tests = append(runner.Tests, v...) - }) -} - -func (b *Builder_append) append(modifier func(runner *benchttp.Runner)) { - if modifier == nil { - panicInternal("Builder.append", "call with nil modifier") - } - b.modifiers = append(b.modifiers, modifier) -} diff --git a/configio/builder_append_test.go b/configio/builder_append_test.go deleted file mode 100644 index 0a6d2ab..0000000 --- a/configio/builder_append_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package configio_test - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/benchttp/sdk/benchttp" - "github.com/benchttp/sdk/benchttptest" - "github.com/benchttp/sdk/configio" -) - -func TestBuilder_append_WriteJSON(t *testing.T) { - in := []byte(`{"runner":{"requests": 5}}`) - dest := benchttp.Runner{Requests: 0, Concurrency: 2} - want := benchttp.Runner{Requests: 5, Concurrency: 2} - - b := configio.Builder_append{} - if err := b.WriteJSON(in); err != nil { - t.Fatal(err) - } - b.Into(&dest) - - benchttptest.AssertEqualRunners(t, want, dest) -} - -func TestBuilder_append_WriteYAML(t *testing.T) { - in := []byte(`runner: { requests: 5 }`) - dest := benchttp.Runner{Requests: 0, Concurrency: 2} - want := benchttp.Runner{Requests: 5, Concurrency: 2} - - b := configio.Builder_append{} - if err := b.WriteYAML(in); err != nil { - t.Fatal(err) - } - b.Into(&dest) - - benchttptest.AssertEqualRunners(t, want, dest) -} - -func TestBuilder_append_Set(t *testing.T) { - t.Run("basic fields", func(t *testing.T) { - want := benchttp.Runner{ - Requests: 5, - Concurrency: 2, - Interval: 10 * time.Millisecond, - RequestTimeout: 1 * time.Second, - GlobalTimeout: 10 * time.Second, - } - - b := configio.Builder_append{} - b.SetRequests(want.Requests) - b.SetConcurrency(-1) - b.SetConcurrency(want.Concurrency) - b.SetInterval(want.Interval) - b.SetRequestTimeout(want.RequestTimeout) - b.SetGlobalTimeout(want.GlobalTimeout) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("request", func(t *testing.T) { - want := benchttp.Runner{ - Request: httptest.NewRequest("GET", "https://example.com", nil), - } - - b := configio.Builder_append{} - b.SetRequest(want.Request) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("request fields", func(t *testing.T) { - want := benchttp.Runner{ - Request: &http.Request{ - Method: "PUT", - URL: mustParseRequestURI("https://example.com"), - Header: http.Header{ - "API_KEY": []string{"abc"}, - "Accept": []string{"text/html", "application/json"}, - }, - Body: readcloser("hello"), - }, - } - - b := configio.Builder_append{} - b.SetRequestMethod(want.Request.Method) - b.SetRequestURL(want.Request.URL) - b.SetRequestHeader(http.Header{"API_KEY": []string{"abc"}}) - b.SetRequestHeaderFunc(func(prev http.Header) http.Header { - prev.Add("Accept", "text/html") - prev.Add("Accept", "application/json") - return prev - }) - b.SetRequestBody(readcloser("hello")) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) - - t.Run("test cases", func(t *testing.T) { - want := benchttp.Runner{ - Tests: []benchttp.TestCase{ - { - Name: "maximum response time", - Field: "ResponseTimes.Max", - Predicate: "LT", - Target: 100 * time.Millisecond, - }, - { - Name: "similar response times", - Field: "ResponseTimes.StdDev", - Predicate: "LTE", - Target: 20 * time.Millisecond, - }, - { - Name: "100% availability", - Field: "RequestFailureCount", - Predicate: "EQ", - Target: 0, - }, - }, - } - - b := configio.Builder_append{} - b.SetTests([]benchttp.TestCase{want.Tests[0]}) - b.AddTests(want.Tests[1:]...) - - benchttptest.AssertEqualRunners(t, want, b.Runner()) - }) -} diff --git a/configio/builder_benchmark_test.go b/configio/builder_benchmark_test.go deleted file mode 100644 index f4431c9..0000000 --- a/configio/builder_benchmark_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package configio_test - -import ( - "strconv" - "testing" - - "github.com/benchttp/sdk/configio" -) - -/* -Output: - -Running tool: /Users/greg/sdk/go1.17.6/bin/go test -benchmem -run=^$ -bench ^(BenchmarkBuilder)$ github.com/benchttp/sdk/configio - -goos: darwin -goarch: amd64 -pkg: github.com/benchttp/sdk/configio -cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz -BenchmarkBuilder/setters/Builder_pipe-8 12966073 97.47 ns/op 40 B/op 2 allocs/op -BenchmarkBuilder/setters/Builder_append-8 21533343 81.65 ns/op 64 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_100-8 3044132 396.5 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_100-8 5413728 222.9 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_10000-8 25195 41284 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_10000-8 71463 16838 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_pipe_1000000-8 156 6993338 ns/op 96 B/op 1 allocs/op -BenchmarkBuilder/build/Builder_append_1000000-8 451 2730270 ns/op 96 B/op 1 allocs/op -*/ -func BenchmarkBuilder(b *testing.B) { - b.Run("setters", func(b *testing.B) { - b.Run("Builder_pipe", func(b *testing.B) { - builder := configio.Builder{} - for i := 0; i < b.N; i++ { - builder.SetConcurrency(100) - } - }) - b.Run("Builder_append", func(b *testing.B) { - builder := configio.Builder_append{} - for i := 0; i < b.N; i++ { - builder.SetConcurrency(100) - } - }) - }) - - b.Run("build", func(b *testing.B) { - for _, iter := range []int{100, 10_000, 1_000_000} { - b.Run("Builder_pipe_"+strconv.Itoa(iter), func(b *testing.B) { - builder := configio.Builder{} - setupBuilder(b, &builder, iter) - for i := 0; i < b.N; i++ { - _ = builder.Runner() - } - }) - b.Run("Builder_append_"+strconv.Itoa(iter), func(b *testing.B) { - builder := configio.Builder_append{} - setupBuilder(b, &builder, iter) - for i := 0; i < b.N; i++ { - _ = builder.Runner() - } - }) - } - }) -} - -func setupBuilder( - b *testing.B, - builder interface{ SetConcurrency(int) }, - iter int, -) { - b.Helper() - values := []int{-100, 100} - for i := 0; i < iter; i++ { - builder.SetConcurrency(values[i%2]) - } - b.ResetTimer() -} From a849d2ed01f1eb1979b29ccbf67dc0ea355acd37 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 19:42:44 +0100 Subject: [PATCH 05/10] perf(configio): use an array of modifiers for Builder Previous benchmarks showed 2.5x better performance over piped modifier. --- configio/builder.go | 48 ++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index ad15021..b306f4f 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -10,8 +10,7 @@ import ( ) type Builder struct { - // TODO: benchmark this vs []func(*benchttp.Runner) - modifier func(*benchttp.Runner) + modifiers []func(*benchttp.Runner) } func (b *Builder) WriteJSON(in []byte) error { @@ -30,7 +29,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { if err := repr.validate(); err != nil { return err } - b.pipe(func(dst *benchttp.Runner) { + b.append(func(dst *benchttp.Runner) { _ = repr.Into(dst) }) return nil @@ -47,22 +46,21 @@ func (b *Builder) Into(dst *benchttp.Runner) { } func (b *Builder) into(dst *benchttp.Runner) { - if b.modifier == nil { - return + for _, modify := range b.modifiers { + modify(dst) } - b.modifier(dst) } // setters func (b *Builder) SetRequest(r *http.Request) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Request = r }) } func (b *Builder) SetRequestMethod(v string) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -71,7 +69,7 @@ func (b *Builder) SetRequestMethod(v string) { } func (b *Builder) SetRequestURL(v *url.URL) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -86,7 +84,7 @@ func (b *Builder) SetRequestHeader(v http.Header) { } func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -95,7 +93,7 @@ func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { } func (b *Builder) SetRequestBody(v io.ReadCloser) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { if runner.Request == nil { runner.Request = &http.Request{} } @@ -104,58 +102,50 @@ func (b *Builder) SetRequestBody(v io.ReadCloser) { } func (b *Builder) SetRequests(v int) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Requests = v }) } func (b *Builder) SetConcurrency(v int) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Concurrency = v }) } func (b *Builder) SetInterval(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Interval = v }) } func (b *Builder) SetRequestTimeout(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.RequestTimeout = v }) } func (b *Builder) SetGlobalTimeout(v time.Duration) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.GlobalTimeout = v }) } func (b *Builder) SetTests(v []benchttp.TestCase) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Tests = v }) } func (b *Builder) AddTests(v ...benchttp.TestCase) { - b.pipe(func(runner *benchttp.Runner) { + b.append(func(runner *benchttp.Runner) { runner.Tests = append(runner.Tests, v...) }) } -func (b *Builder) pipe(modifier func(runner *benchttp.Runner)) { +func (b *Builder) append(modifier func(runner *benchttp.Runner)) { if modifier == nil { - panicInternal("Builder.pipe", "call with nil modifier") - } - if b.modifier == nil { - b.modifier = modifier - return - } - applyPreviousModifier := b.modifier - b.modifier = func(dst *benchttp.Runner) { - applyPreviousModifier(dst) - modifier(dst) + panicInternal("Builder.append", "call with nil modifier") } + b.modifiers = append(b.modifiers, modifier) } From 54d5ecc68537a0aa81cef78b296b68a924911b5a Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 20:12:54 +0100 Subject: [PATCH 06/10] refactor(configio): Builder renamings - Builder.Into -> Builder.Mutate - Builder.modifiers -> Builder.mutations --- configio/builder.go | 16 ++++++---------- configio/builder_test.go | 4 ++-- configio/example_test.go | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index b306f4f..a541896 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -10,7 +10,7 @@ import ( ) type Builder struct { - modifiers []func(*benchttp.Runner) + mutations []func(*benchttp.Runner) } func (b *Builder) WriteJSON(in []byte) error { @@ -37,17 +37,13 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { func (b *Builder) Runner() benchttp.Runner { runner := benchttp.Runner{} - b.into(&runner) + b.Mutate(&runner) return runner } -func (b *Builder) Into(dst *benchttp.Runner) { - b.into(dst) -} - -func (b *Builder) into(dst *benchttp.Runner) { - for _, modify := range b.modifiers { - modify(dst) +func (b *Builder) Mutate(dst *benchttp.Runner) { + for _, mutate := range b.mutations { + mutate(dst) } } @@ -147,5 +143,5 @@ func (b *Builder) append(modifier func(runner *benchttp.Runner)) { if modifier == nil { panicInternal("Builder.append", "call with nil modifier") } - b.modifiers = append(b.modifiers, modifier) + b.mutations = append(b.mutations, modifier) } diff --git a/configio/builder_test.go b/configio/builder_test.go index 6dc95ee..4123882 100644 --- a/configio/builder_test.go +++ b/configio/builder_test.go @@ -23,7 +23,7 @@ func TestBuilder_WriteJSON(t *testing.T) { if err := b.WriteJSON(in); err != nil { t.Fatal(err) } - b.Into(&dest) + b.Mutate(&dest) benchttptest.AssertEqualRunners(t, want, dest) } @@ -37,7 +37,7 @@ func TestBuilder_WriteYAML(t *testing.T) { if err := b.WriteYAML(in); err != nil { t.Fatal(err) } - b.Into(&dest) + b.Mutate(&dest) benchttptest.AssertEqualRunners(t, want, dest) } diff --git a/configio/example_test.go b/configio/example_test.go index 4efe5b2..b2f86dc 100644 --- a/configio/example_test.go +++ b/configio/example_test.go @@ -24,7 +24,7 @@ func ExampleBuilder() { _ = b.WriteYAML(yamlConfig) b.SetInterval(100 * time.Millisecond) - b.Into(&runner) + b.Mutate(&runner) // Output: // PUT From 0637323a444aa2d9c1ea6fe0e10af439314449bf Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 20:44:56 +0100 Subject: [PATCH 07/10] doc(configio): add documenting comments for Builder --- configio/builder.go | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/configio/builder.go b/configio/builder.go index a541896..92efe91 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -9,14 +9,25 @@ import ( "github.com/benchttp/sdk/benchttp" ) +// A Builder is used to incrementally build a benchttp.Runner +// using Set and Write methods. +// The zero value is ready to use. type Builder struct { mutations []func(*benchttp.Runner) } +// WriteJSON decodes the input bytes as a JSON benchttp configuration +// and appends the resulting modifier to the builder. +// It returns any encountered during the decoding process or if the +// decoded configuration is invalid. func (b *Builder) WriteJSON(in []byte) error { return b.decodeAndWrite(in, FormatJSON) } +// WriteYAML decodes the input bytes as a YAML benchttp configuration +// and appends the resulting modifier to the builder. +// It returns any encountered during the decoding process or if the +// decoded configuration is invalid. func (b *Builder) WriteYAML(in []byte) error { return b.decodeAndWrite(in, FormatYAML) } @@ -26,21 +37,29 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { if err := DecoderOf(format, in).Decode(&repr); err != nil { return err } + // early check for invalid configuration if err := repr.validate(); err != nil { return err } b.append(func(dst *benchttp.Runner) { - _ = repr.Into(dst) + // err is already checked via repr.validate(), so nil is expected. + if err := repr.Into(dst); err != nil { + panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) + } }) return nil } +// Runner successively applies the Builder's mutations +// to a zero benchttp.Runner and returns it. func (b *Builder) Runner() benchttp.Runner { runner := benchttp.Runner{} b.Mutate(&runner) return runner } +// Mutate successively applies the Builder's mutations +// to the benchttp.Runner value pointed to by dst. func (b *Builder) Mutate(dst *benchttp.Runner) { for _, mutate := range b.mutations { mutate(dst) @@ -49,12 +68,14 @@ func (b *Builder) Mutate(dst *benchttp.Runner) { // setters +// SetRequest adds a mutation that sets a runner's request to r. func (b *Builder) SetRequest(r *http.Request) { b.append(func(runner *benchttp.Runner) { runner.Request = r }) } +// SetRequestMethod adds a mutation that sets a runner's request method to v. func (b *Builder) SetRequestMethod(v string) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -64,6 +85,7 @@ func (b *Builder) SetRequestMethod(v string) { }) } +// SetRequestURL adds a mutation that sets a runner's request URL to v. func (b *Builder) SetRequestURL(v *url.URL) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -73,12 +95,15 @@ func (b *Builder) SetRequestURL(v *url.URL) { }) } +// SetRequestHeader adds a mutation that sets a runner's request header to v. func (b *Builder) SetRequestHeader(v http.Header) { b.SetRequestHeaderFunc(func(_ http.Header) http.Header { return v }) } +// SetRequestHeaderFunc adds a mutation that sets a runner's request header +// to the result of calling f with its current request header. func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -88,6 +113,7 @@ func (b *Builder) SetRequestHeaderFunc(f func(prev http.Header) http.Header) { }) } +// SetRequestBody adds a mutation that sets a runner's request body to v. func (b *Builder) SetRequestBody(v io.ReadCloser) { b.append(func(runner *benchttp.Runner) { if runner.Request == nil { @@ -97,42 +123,56 @@ func (b *Builder) SetRequestBody(v io.ReadCloser) { }) } +// SetRequests adds a mutation that sets a runner's +// Requests field to v. func (b *Builder) SetRequests(v int) { b.append(func(runner *benchttp.Runner) { runner.Requests = v }) } +// SetConcurrency adds a mutation that sets a runner's +// Concurrency field to v. func (b *Builder) SetConcurrency(v int) { b.append(func(runner *benchttp.Runner) { runner.Concurrency = v }) } +// SetInterval adds a mutation that sets a runner's +// Interval field to v. func (b *Builder) SetInterval(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.Interval = v }) } +// SetRequestTimeout adds a mutation that sets a runner's +// RequestTimeout field to v. func (b *Builder) SetRequestTimeout(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.RequestTimeout = v }) } +// SetGlobalTimeout adds a mutation that sets a runner's +// GlobalTimeout field to v. func (b *Builder) SetGlobalTimeout(v time.Duration) { b.append(func(runner *benchttp.Runner) { runner.GlobalTimeout = v }) } +// SetTests adds a mutation that sets a runner's +// Tests field to v. func (b *Builder) SetTests(v []benchttp.TestCase) { b.append(func(runner *benchttp.Runner) { runner.Tests = v }) } +// SetTests adds a mutation that appends the given benchttp.TestCases +// to a runner's Tests field. func (b *Builder) AddTests(v ...benchttp.TestCase) { b.append(func(runner *benchttp.Runner) { runner.Tests = append(runner.Tests, v...) From 313f8c4623f4329d9a3a5d5d37ff856cf6f41deb Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 22:06:30 +0100 Subject: [PATCH 08/10] feat(configio): make Representation private - configio.Representation -> configio.representation - make all representation methods private --- configio/builder.go | 4 ++-- configio/decoder.go | 2 +- configio/file.go | 4 ++-- configio/json.go | 8 ++++---- configio/representation.go | 22 +++++++++++----------- configio/yaml.go | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index 92efe91..ffd3a15 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -33,7 +33,7 @@ func (b *Builder) WriteYAML(in []byte) error { } func (b *Builder) decodeAndWrite(in []byte, format Format) error { - repr := Representation{} + repr := representation{} if err := DecoderOf(format, in).Decode(&repr); err != nil { return err } @@ -43,7 +43,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { } b.append(func(dst *benchttp.Runner) { // err is already checked via repr.validate(), so nil is expected. - if err := repr.Into(dst); err != nil { + if err := repr.parseAndMutate(dst); err != nil { panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) } }) diff --git a/configio/decoder.go b/configio/decoder.go index 0014f70..4045ec5 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -8,7 +8,7 @@ import ( ) type Decoder interface { - Decode(dst *Representation) error + Decode(dst *representation) error DecodeRunner(dst *benchttp.Runner) error } diff --git a/configio/file.go b/configio/file.go index f0aff7d..d2fb01c 100644 --- a/configio/file.go +++ b/configio/file.go @@ -49,7 +49,7 @@ func UnmarshalFile(filename string, dst *benchttp.Runner) error { type file struct { prev *file path string - repr Representation + repr representation } // decodeAll reads f.path as a file and decodes it into f.repr. @@ -122,7 +122,7 @@ func (f file) seen(p string) bool { // reprs returns a slice of Representation, starting with the receiver // and ending with the last child. func (f file) reprs() representations { - reprs := []Representation{f.repr} + reprs := []representation{f.repr} if f.prev != nil { reprs = append(reprs, f.prev.reprs()...) } diff --git a/configio/json.go b/configio/json.go index debd94e..de31c1e 100644 --- a/configio/json.go +++ b/configio/json.go @@ -13,7 +13,7 @@ import ( // UnmarshalJSON parses the JSON-encoded data and stores the result // in the Representation pointed to by dst. -func UnmarshalJSON(in []byte, dst *Representation) error { +func UnmarshalJSON(in []byte, dst *representation) error { dec := NewJSONDecoder(bytes.NewReader(in)) return dec.Decode(dst) } @@ -34,7 +34,7 @@ func NewJSONDecoder(r io.Reader) JSONDecoder { // Decode reads the next JSON-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d JSONDecoder) Decode(dst *Representation) error { +func (d JSONDecoder) Decode(dst *representation) error { decoder := json.NewDecoder(d.r) decoder.DisallowUnknownFields() return d.handleError(decoder.Decode(dst)) @@ -43,11 +43,11 @@ func (d JSONDecoder) Decode(dst *Representation) error { // Decode reads the next JSON-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d JSONDecoder) DecodeRunner(dst *benchttp.Runner) error { - repr := Representation{} + repr := representation{} if err := d.Decode(&repr); err != nil { return err } - return repr.Into(dst) + return repr.parseAndMutate(dst) } // handleError handles an error from package json, diff --git a/configio/representation.go b/configio/representation.go index e13f2db..6097283 100644 --- a/configio/representation.go +++ b/configio/representation.go @@ -14,12 +14,12 @@ import ( "github.com/benchttp/sdk/internal/errorutil" ) -// Representation is a raw data model for formatted runner config (json, yaml). +// representation is a raw data model for formatted runner config (json, yaml). // It serves as a receiver for unmarshaling processes and for that reason // its types are kept simple (certain types are incompatible with certain // unmarshalers). // It exposes a method Unmarshal to convert its values into a runner.Config. -type Representation struct { +type representation struct { Extends *string `yaml:"extends" json:"extends"` Request struct { @@ -49,14 +49,14 @@ type Representation struct { } `yaml:"tests" json:"tests"` } -func (repr Representation) validate() error { - return repr.Into(&benchttp.Runner{}) +func (repr representation) validate() error { + return repr.parseAndMutate(&benchttp.Runner{}) } -// Into parses the Representation receiver as a benchttp.Runner +// parseAndMutate parses the Representation receiver as a benchttp.Runner // and stores any non-nil field value into the corresponding field // of dst. -func (repr Representation) Into(dst *benchttp.Runner) error { +func (repr representation) parseAndMutate(dst *benchttp.Runner) error { if err := repr.parseRequestInto(dst); err != nil { return err } @@ -66,7 +66,7 @@ func (repr Representation) Into(dst *benchttp.Runner) error { return repr.parseTestsInto(dst) } -func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { +func (repr representation) parseRequestInto(dst *benchttp.Runner) error { if dst.Request == nil { dst.Request = &http.Request{} } @@ -103,7 +103,7 @@ func (repr Representation) parseRequestInto(dst *benchttp.Runner) error { return nil } -func (repr Representation) parseRunnerInto(dst *benchttp.Runner) error { +func (repr representation) parseRunnerInto(dst *benchttp.Runner) error { if requests := repr.Runner.Requests; requests != nil { dst.Requests = *requests } @@ -139,7 +139,7 @@ func (repr Representation) parseRunnerInto(dst *benchttp.Runner) error { return nil } -func (repr Representation) parseTestsInto(dst *benchttp.Runner) error { +func (repr representation) parseTestsInto(dst *benchttp.Runner) error { testSuite := repr.Tests if len(testSuite) == 0 { return nil @@ -254,7 +254,7 @@ func requireConfigFields(fields map[string]interface{}) error { return nil } -type representations []Representation +type representations []representation // mergeInto successively parses the given representations into dst. // @@ -265,7 +265,7 @@ func (reprs representations) mergeInto(dst *benchttp.Runner) error { } for _, repr := range reprs { - if err := repr.Into(dst); err != nil { + if err := repr.parseAndMutate(dst); err != nil { return errorutil.WithDetails(ErrFileParse, err) } } diff --git a/configio/yaml.go b/configio/yaml.go index d1130c3..7b13b90 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -14,7 +14,7 @@ import ( // UnmarshalYAML parses the YAML-encoded data and stores the result // in the Representation pointed to by dst. -func UnmarshalYAML(in []byte, dst *Representation) error { +func UnmarshalYAML(in []byte, dst *representation) error { dec := NewYAMLDecoder(bytes.NewReader(in)) return dec.Decode(dst) } @@ -35,7 +35,7 @@ func NewYAMLDecoder(r io.Reader) YAMLDecoder { // Decode reads the next YAML-encoded value from its input // and stores it in the Representation pointed to by dst. -func (d YAMLDecoder) Decode(dst *Representation) error { +func (d YAMLDecoder) Decode(dst *representation) error { decoder := yaml.NewDecoder(d.r) decoder.KnownFields(true) return d.handleError(decoder.Decode(dst)) @@ -44,11 +44,11 @@ func (d YAMLDecoder) Decode(dst *Representation) error { // Decode reads the next YAML-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. func (d YAMLDecoder) DecodeRunner(dst *benchttp.Runner) error { - repr := Representation{} + repr := representation{} if err := d.Decode(&repr); err != nil { return err } - return repr.Into(dst) + return repr.parseAndMutate(dst) } // handleError handles a raw yaml decoder.Decode error, filters it, From 39fb4a61b356f38eba5fb3c13fd9c92ba88914c3 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Sun, 6 Nov 2022 22:25:49 +0100 Subject: [PATCH 09/10] feat(configio): remove public references to representation - remove Decode methods from interface Decoder and its implementations - rename DecodeRunner methods to Decode - remove UnmarshalXXXX functions - rename UnmarshalXXXXRunner functions to UnmarshalXXXX --- configio/builder.go | 2 +- configio/decoder.go | 18 +++++++++++++----- configio/file.go | 2 +- configio/json.go | 39 +++++++++++++++++---------------------- configio/json_test.go | 4 ++-- configio/yaml.go | 39 +++++++++++++++++---------------------- configio/yaml_test.go | 2 +- 7 files changed, 52 insertions(+), 54 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index ffd3a15..bb7d08e 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -34,7 +34,7 @@ func (b *Builder) WriteYAML(in []byte) error { func (b *Builder) decodeAndWrite(in []byte, format Format) error { repr := representation{} - if err := DecoderOf(format, in).Decode(&repr); err != nil { + if err := decoderOf(format, in).decodeRepr(&repr); err != nil { return err } // early check for invalid configuration diff --git a/configio/decoder.go b/configio/decoder.go index 4045ec5..41ca421 100644 --- a/configio/decoder.go +++ b/configio/decoder.go @@ -7,11 +7,6 @@ import ( "github.com/benchttp/sdk/benchttp" ) -type Decoder interface { - Decode(dst *representation) error - DecodeRunner(dst *benchttp.Runner) error -} - type Format string const ( @@ -19,9 +14,22 @@ const ( FormatYAML Format = "yaml" ) +type Decoder interface { + Decode(dst *benchttp.Runner) error +} + // DecoderOf returns the appropriate Decoder for the given Format. // It panics if the format is not a Format declared in configio. func DecoderOf(format Format, in []byte) Decoder { + return decoderOf(format, in) +} + +type decoder interface { + Decoder + decodeRepr(dst *representation) error +} + +func decoderOf(format Format, in []byte) decoder { r := bytes.NewReader(in) switch format { case FormatYAML: diff --git a/configio/file.go b/configio/file.go index d2fb01c..8a73d1e 100644 --- a/configio/file.go +++ b/configio/file.go @@ -88,7 +88,7 @@ func (f *file) decode() (err error) { return err } - if err := DecoderOf(ext, b).Decode(&f.repr); err != nil { + if err := decoderOf(ext, b).decodeRepr(&f.repr); err != nil { return errorutil.WithDetails(ErrFileParse, f.path, err) } diff --git a/configio/json.go b/configio/json.go index de31c1e..9d83a11 100644 --- a/configio/json.go +++ b/configio/json.go @@ -11,45 +11,40 @@ import ( "github.com/benchttp/sdk/benchttp" ) -// UnmarshalJSON parses the JSON-encoded data and stores the result -// in the Representation pointed to by dst. -func UnmarshalJSON(in []byte, dst *representation) error { - dec := NewJSONDecoder(bytes.NewReader(in)) - return dec.Decode(dst) -} +// JSONDecoder implements Decoder +type JSONDecoder struct{ r io.Reader } + +var _ decoder = (*JSONDecoder)(nil) -// UnmarshalJSONRunner parses the JSON-encoded data and stores the result +// UnmarshalJSON parses the JSON-encoded data and stores the result // in the benchttp.Runner pointed to by dst. -func UnmarshalJSONRunner(in []byte, dst *benchttp.Runner) error { +func UnmarshalJSON(in []byte, dst *benchttp.Runner) error { dec := NewJSONDecoder(bytes.NewReader(in)) - return dec.DecodeRunner(dst) + return dec.Decode(dst) } -// JSONDecoder implements Decoder -type JSONDecoder struct{ r io.Reader } - func NewJSONDecoder(r io.Reader) JSONDecoder { return JSONDecoder{r: r} } -// Decode reads the next JSON-encoded value from its input -// and stores it in the Representation pointed to by dst. -func (d JSONDecoder) Decode(dst *representation) error { - decoder := json.NewDecoder(d.r) - decoder.DisallowUnknownFields() - return d.handleError(decoder.Decode(dst)) -} - // Decode reads the next JSON-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. -func (d JSONDecoder) DecodeRunner(dst *benchttp.Runner) error { +func (d JSONDecoder) Decode(dst *benchttp.Runner) error { repr := representation{} - if err := d.Decode(&repr); err != nil { + if err := d.decodeRepr(&repr); err != nil { return err } return repr.parseAndMutate(dst) } +// decodeRepr reads the next JSON-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d JSONDecoder) decodeRepr(dst *representation) error { + decoder := json.NewDecoder(d.r) + decoder.DisallowUnknownFields() + return d.handleError(decoder.Decode(dst)) +} + // handleError handles an error from package json, // transforms it into a user-friendly standardized format // and returns the resulting error. diff --git a/configio/json_test.go b/configio/json_test.go index c04bba2..d21a98b 100644 --- a/configio/json_test.go +++ b/configio/json_test.go @@ -57,7 +57,7 @@ func TestMarshalJSON(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { gotRunner := benchttp.DefaultRunner() - gotError := configio.UnmarshalJSONRunner(tc.input, &gotRunner) + gotError := configio.UnmarshalJSON(tc.input, &gotRunner) if !tc.isValidRunner(benchttp.DefaultRunner(), gotRunner) { t.Errorf("unexpected runner:\n%+v", gotRunner) @@ -103,7 +103,7 @@ func TestJSONDecoder(t *testing.T) { runner := benchttp.Runner{} decoder := configio.NewJSONDecoder(bytes.NewReader(tc.in)) - gotErr := decoder.DecodeRunner(&runner) + gotErr := decoder.Decode(&runner) if tc.exp == "" { if gotErr != nil { diff --git a/configio/yaml.go b/configio/yaml.go index 7b13b90..9bc604b 100644 --- a/configio/yaml.go +++ b/configio/yaml.go @@ -12,45 +12,40 @@ import ( "github.com/benchttp/sdk/benchttp" ) -// UnmarshalYAML parses the YAML-encoded data and stores the result -// in the Representation pointed to by dst. -func UnmarshalYAML(in []byte, dst *representation) error { - dec := NewYAMLDecoder(bytes.NewReader(in)) - return dec.Decode(dst) -} +// YAMLDecoder implements Decoder +type YAMLDecoder struct{ r io.Reader } + +var _ decoder = (*YAMLDecoder)(nil) -// UnmarshalYAMLRunner parses the YAML-encoded data and stores the result +// UnmarshalYAML parses the YAML-encoded data and stores the result // in the benchttp.Runner pointed to by dst. -func UnmarshalYAMLRunner(in []byte, dst *benchttp.Runner) error { +func UnmarshalYAML(in []byte, dst *benchttp.Runner) error { dec := NewYAMLDecoder(bytes.NewReader(in)) - return dec.DecodeRunner(dst) + return dec.Decode(dst) } -// YAMLDecoder implements Decoder -type YAMLDecoder struct{ r io.Reader } - func NewYAMLDecoder(r io.Reader) YAMLDecoder { return YAMLDecoder{r: r} } -// Decode reads the next YAML-encoded value from its input -// and stores it in the Representation pointed to by dst. -func (d YAMLDecoder) Decode(dst *representation) error { - decoder := yaml.NewDecoder(d.r) - decoder.KnownFields(true) - return d.handleError(decoder.Decode(dst)) -} - // Decode reads the next YAML-encoded value from its input // and stores it in the benchttp.Runner pointed to by dst. -func (d YAMLDecoder) DecodeRunner(dst *benchttp.Runner) error { +func (d YAMLDecoder) Decode(dst *benchttp.Runner) error { repr := representation{} - if err := d.Decode(&repr); err != nil { + if err := d.decodeRepr(&repr); err != nil { return err } return repr.parseAndMutate(dst) } +// decodeRepr reads the next YAML-encoded value from its input +// and stores it in the Representation pointed to by dst. +func (d YAMLDecoder) decodeRepr(dst *representation) error { + decoder := yaml.NewDecoder(d.r) + decoder.KnownFields(true) + return d.handleError(decoder.Decode(dst)) +} + // handleError handles a raw yaml decoder.Decode error, filters it, // and return the resulting error. func (d YAMLDecoder) handleError(err error) error { diff --git a/configio/yaml_test.go b/configio/yaml_test.go index 2f191cf..4bff966 100644 --- a/configio/yaml_test.go +++ b/configio/yaml_test.go @@ -69,7 +69,7 @@ func TestYAMLDecoder(t *testing.T) { runner := benchttp.Runner{} decoder := configio.NewYAMLDecoder(bytes.NewReader(tc.in)) - gotErr := decoder.DecodeRunner(&runner) + gotErr := decoder.Decode(&runner) if tc.expErr == nil { if gotErr != nil { From da00b0bf8da66ab52f147224dddd59d46a12c7c1 Mon Sep 17 00:00:00 2001 From: Gregory Albouy Date: Tue, 28 Mar 2023 23:49:03 +0200 Subject: [PATCH 10/10] refactor(configio): rename builder methods Write -> Decode --- configio/builder.go | 16 ++++++++-------- configio/builder_test.go | 4 ++-- configio/example_test.go | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/configio/builder.go b/configio/builder.go index bb7d08e..400ba95 100644 --- a/configio/builder.go +++ b/configio/builder.go @@ -16,23 +16,23 @@ type Builder struct { mutations []func(*benchttp.Runner) } -// WriteJSON decodes the input bytes as a JSON benchttp configuration +// DecodeJSON decodes the input bytes as a JSON benchttp configuration // and appends the resulting modifier to the builder. // It returns any encountered during the decoding process or if the // decoded configuration is invalid. -func (b *Builder) WriteJSON(in []byte) error { - return b.decodeAndWrite(in, FormatJSON) +func (b *Builder) DecodeJSON(in []byte) error { + return b.decode(in, FormatJSON) } -// WriteYAML decodes the input bytes as a YAML benchttp configuration +// DecodeYAML decodes the input bytes as a YAML benchttp configuration // and appends the resulting modifier to the builder. // It returns any encountered during the decoding process or if the // decoded configuration is invalid. -func (b *Builder) WriteYAML(in []byte) error { - return b.decodeAndWrite(in, FormatYAML) +func (b *Builder) DecodeYAML(in []byte) error { + return b.decode(in, FormatYAML) } -func (b *Builder) decodeAndWrite(in []byte, format Format) error { +func (b *Builder) decode(in []byte, format Format) error { repr := representation{} if err := decoderOf(format, in).decodeRepr(&repr); err != nil { return err @@ -44,7 +44,7 @@ func (b *Builder) decodeAndWrite(in []byte, format Format) error { b.append(func(dst *benchttp.Runner) { // err is already checked via repr.validate(), so nil is expected. if err := repr.parseAndMutate(dst); err != nil { - panicInternal("Builder.decodeAndWrite", "unexpected error: "+err.Error()) + panicInternal("Builder.decode", "unexpected error: "+err.Error()) } }) return nil diff --git a/configio/builder_test.go b/configio/builder_test.go index 4123882..4665d0d 100644 --- a/configio/builder_test.go +++ b/configio/builder_test.go @@ -20,7 +20,7 @@ func TestBuilder_WriteJSON(t *testing.T) { want := benchttp.Runner{Requests: 5, Concurrency: 2} b := configio.Builder{} - if err := b.WriteJSON(in); err != nil { + if err := b.DecodeJSON(in); err != nil { t.Fatal(err) } b.Mutate(&dest) @@ -34,7 +34,7 @@ func TestBuilder_WriteYAML(t *testing.T) { want := benchttp.Runner{Requests: 5, Concurrency: 2} b := configio.Builder{} - if err := b.WriteYAML(in); err != nil { + if err := b.DecodeYAML(in); err != nil { t.Fatal(err) } b.Mutate(&dest) diff --git a/configio/example_test.go b/configio/example_test.go index b2f86dc..e9f9f61 100644 --- a/configio/example_test.go +++ b/configio/example_test.go @@ -20,8 +20,8 @@ func ExampleBuilder() { runner := benchttp.Runner{Requests: -1, Concurrency: 3} b := configio.Builder{} - _ = b.WriteJSON(jsonConfig) - _ = b.WriteYAML(yamlConfig) + _ = b.DecodeJSON(jsonConfig) + _ = b.DecodeYAML(yamlConfig) b.SetInterval(100 * time.Millisecond) b.Mutate(&runner)