Repository: codesenberg/bombardier Branch: master Commit: 2d495aca5b23 Files: 50 Total size: 143.3 KB Directory structure: gitextract_k5a5acgj/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .semaphore/ │ └── semaphore.yml ├── LICENSE ├── README.md ├── args_parser.go ├── args_parser_test.go ├── bombardier.go ├── bombardier_performance_test.go ├── bombardier_test.go ├── build.py ├── client_cert.go ├── client_cert_test.go ├── clients.go ├── clients_test.go ├── cmd/ │ └── utils/ │ └── simplebenchserver/ │ ├── doc.go │ └── main.go ├── common.go ├── completion_barriers.go ├── completion_barriers_test.go ├── config.go ├── config_test.go ├── dialer.go ├── doc.go ├── docs/ │ └── CONTRIBUTING.md ├── error_map.go ├── error_map_test.go ├── flags.go ├── flags_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── headers.go ├── headers_test.go ├── internal/ │ └── test_info.go ├── limiter.go ├── limiter_barrier_test.go ├── limiter_test.go ├── proxy_reader.go ├── rateestimator.go ├── rateestimator_test.go ├── template/ │ └── doc.go ├── templates.go ├── testbody.txt ├── testclient.cert ├── testclient.key ├── testserver.cert └── testserver.key ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ This is an example of what a **bug report** can look like. Please, feel free to also provide any other information relevant to the issue. ### What version of bombardier are you using? Hash of the commit, like [00d7965d6cae34c62042abb0f6c45c45b870dcf3](https://github.com/codesenberg/bombardier/commit/00d7965d6cae34c62042abb0f6c45c45b870dcf3) in case you've built _bombardier_ yourself or version obtained by ``` bombardier --version ``` in case you are using binaries. ### What operating system and processor architecture are you using (if relevant)? Examples are `windows/amd64`, `linux/amd64`, `darwin/amd64`, etc. ### What did you do? Describe steps that can be used to reproduce the error. ### What you expected to happen? ### What actually happened? ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Before submitting a pull request be sure to check the code with [gometalinter](https://github.com/alecthomas/gometalinter). This can save a considerable amount of time during code review. Generally, try to follow this format when writing commit messages: ``` : , updates #, closes #. If applicable.> ``` Examples of such commit messages can be found in the commit log of this project or [in this section](https://golang.org/doc/contribute.html#commit_changes) of Go's Contribution Guidelines, from which this format was adopted. The pull request itself can contain a short description of changes made, questions or provide some other information, etc. ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof cover.* bombardier bombardier-* ================================================ FILE: .semaphore/semaphore.yml ================================================ version: v1.0 name: codesenberg/bombardier agent: machine: type: e1-standard-2 os_image: ubuntu2004 blocks: - name: Test task: prologue: commands: - checkout - go install gotest.tools/gotestsum@latest jobs: - name: Test go 1.21 commands: - sem-version go 1.21 - gotestsum --junitfile report.xml ./... - name: Test go 1.22 commands: - sem-version go 1.22 - gotestsum --junitfile report.xml ./... epilogue: always: commands: - '[[ -f report.xml ]] && test-results publish report.xml' after_pipeline: task: jobs: - name: Publish test results commands: - test-results gen-pipeline-report ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Максим Федосеев Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # bombardier [![Build Status](https://codesenberg.semaphoreci.com/badges/bombardier/branches/master.svg?key=249c678c-eb2a-441e-8128-1bdcfb9aaca6)](https://codesenberg.semaphoreci.com/projects/bombardier) [![Go Report Card](https://goreportcard.com/badge/github.com/codesenberg/bombardier)](https://goreportcard.com/report/github.com/codesenberg/bombardier) [![GoDoc](https://godoc.org/github.com/codesenberg/bombardier?status.svg)](http://godoc.org/github.com/codesenberg/bombardier) ![Logo](https://raw.githubusercontent.com/codesenberg/bombardier/master/img/logo.png) bombardier is a HTTP(S) benchmarking tool. It is written in Go programming language and uses excellent [fasthttp](https://github.com/valyala/fasthttp) instead of Go's default http library, because of its lightning fast performance. With `bombardier v1.1` and higher you can now use `net/http` client if you need to test HTTP/2.x services or want to use a more RFC-compliant HTTP client. ## Installation You can grab binaries in the [releases](https://github.com/codesenberg/bombardier/releases) section. Alternatively, to get latest and greatest run: Go 1.18+: `go install github.com/codesenberg/bombardier@latest` ## Usage ``` bombardier [] ``` For a more detailed information about flags consult [GoDoc](http://godoc.org/github.com/codesenberg/bombardier). ## Known issues AFAIK, it's impossible to pass Host header correctly with `fasthttp`, you can use `net/http`(`--http1`/`--http2` flags) to workaround this issue. ## Examples Example of running `bombardier` against [this server](https://godoc.org/github.com/codesenberg/bombardier/cmd/utils/simplebenchserver): ``` > bombardier -c 125 -n 10000000 http://localhost:8080 Bombarding http://localhost:8080 with 10000000 requests using 125 connections 10000000 / 10000000 [============================================] 100.00% 37s Done! Statistics Avg Stdev Max Reqs/sec 264560.00 10733.06 268434 Latency 471.00us 522.34us 51.00ms HTTP codes: 1xx - 0, 2xx - 10000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 Throughput: 292.92MB/s ``` Or, against a realworld server(with latency distribution): ``` > bombardier -c 200 -d 10s -l http://ya.ru Bombarding http://ya.ru for 10s using 200 connections [=========================================================================] 10s Done! Statistics Avg Stdev Max Reqs/sec 6607.00 524.56 7109 Latency 29.86ms 5.36ms 305.02ms Latency Distribution 50% 28.00ms 75% 32.00ms 90% 34.00ms 99% 48.00ms HTTP codes: 1xx - 0, 2xx - 0, 3xx - 66561, 4xx - 0, 5xx - 0 others - 5 Errors: dialing to the given TCP address timed out - 5 Throughput: 3.06MB/s ``` ================================================ FILE: args_parser.go ================================================ package main import ( "fmt" "runtime" "strconv" "strings" "time" "github.com/alecthomas/kingpin" "github.com/goware/urlx" ) type argsParser interface { parse([]string) (config, error) } type kingpinParser struct { app *kingpin.Application url string numReqs *nullableUint64 duration *nullableDuration headers *headersList numConns uint64 timeout time.Duration latencies bool insecure bool disableKeepAlives bool method string body string bodyFilePath string stream bool certPath string keyPath string rate *nullableUint64 clientType clientTyp printSpec *nullableString noPrint bool formatSpec string } func newKingpinParser() argsParser { kparser := &kingpinParser{ numReqs: new(nullableUint64), duration: new(nullableDuration), headers: new(headersList), numConns: defaultNumberOfConns, timeout: defaultTimeout, latencies: false, method: "GET", body: "", bodyFilePath: "", stream: false, certPath: "", keyPath: "", insecure: false, url: "", rate: new(nullableUint64), clientType: fhttp, printSpec: new(nullableString), noPrint: false, formatSpec: "plain-text", } app := kingpin.New("", "Fast cross-platform HTTP benchmarking tool"). Version("bombardier version " + version + " " + runtime.GOOS + "/" + runtime.GOARCH) app.Flag("connections", "Maximum number of concurrent connections"). Short('c'). PlaceHolder(strconv.FormatUint(defaultNumberOfConns, decBase)). Uint64Var(&kparser.numConns) app.Flag("timeout", "Socket/request timeout"). PlaceHolder(defaultTimeout.String()). Short('t'). DurationVar(&kparser.timeout) app.Flag("latencies", "Print latency statistics"). Short('l'). BoolVar(&kparser.latencies) app.Flag("method", "Request method"). PlaceHolder("GET"). Short('m'). StringVar(&kparser.method) app.Flag("body", "Request body"). Default(""). Short('b'). StringVar(&kparser.body) app.Flag("body-file", "File to use as request body"). Default(""). Short('f'). StringVar(&kparser.bodyFilePath) app.Flag("stream", "Specify whether to stream body using "+ "chunked transfer encoding or to serve it from memory"). Short('s'). BoolVar(&kparser.stream) app.Flag("cert", "Path to the client's TLS Certificate"). Default(""). StringVar(&kparser.certPath) app.Flag("key", "Path to the client's TLS Certificate Private Key"). Default(""). StringVar(&kparser.keyPath) app.Flag("insecure", "Controls whether a client verifies the server's certificate"+ " chain and host name"). Short('k'). BoolVar(&kparser.insecure) app.Flag("disableKeepAlives", "Disable HTTP keep-alive. For fasthttp use -H 'Connection: close'"). Short('a'). BoolVar(&kparser.disableKeepAlives) app.Flag("header", "HTTP headers to use(can be repeated)"). PlaceHolder("\"K: V\""). Short('H'). SetValue(kparser.headers) app.Flag("requests", "Number of requests"). PlaceHolder("[pos. int.]"). Short('n'). SetValue(kparser.numReqs) app.Flag("duration", "Duration of test"). PlaceHolder(defaultTestDuration.String()). Short('d'). SetValue(kparser.duration) app.Flag("rate", "Rate limit in requests per second"). PlaceHolder("[pos. int.]"). Short('r'). SetValue(kparser.rate) app.Flag("fasthttp", "Use fasthttp client"). Action(func(*kingpin.ParseContext) error { kparser.clientType = fhttp return nil }). Bool() app.Flag("http1", "Use net/http client with forced HTTP/1.x"). Action(func(*kingpin.ParseContext) error { kparser.clientType = nhttp1 return nil }). Bool() app.Flag("http2", "Use net/http client with enabled HTTP/2.0"). Action(func(*kingpin.ParseContext) error { kparser.clientType = nhttp2 return nil }). Bool() app.Flag( "print", "Specifies what to output. Comma-separated list of values"+ " 'intro' (short: 'i'), 'progress' (short: 'p'),"+ " 'result' (short: 'r'). Examples:"+ "\n\t* i,p,r (prints everything)"+ "\n\t* intro,result (intro & result)"+ "\n\t* r (result only)"+ "\n\t* result (same as above)"). PlaceHolder(""). Short('p'). SetValue(kparser.printSpec) app.Flag("no-print", "Don't output anything"). Short('q'). BoolVar(&kparser.noPrint) app.Flag("format", "Which format to use to output the result. "+ " is either a name (or its shorthand) of some format "+ "understood by bombardier or a path to the user-defined template, "+ "which uses Go's text/template syntax, prefixed with 'path:' string "+ "(without single quotes), i.e. \"path:/some/path/to/your.template\" "+ " or \"path:C:\\some\\path\\to\\your.template\" in case of Windows. "+ "Formats understood by bombardier are:"+ "\n\t* plain-text (short: pt)"+ "\n\t* json (short: j)"). PlaceHolder(""). Short('o'). StringVar(&kparser.formatSpec) app.Arg("url", "Target's URL").Required(). StringVar(&kparser.url) kparser.app = app return argsParser(kparser) } func (k *kingpinParser) parse(args []string) (config, error) { k.app.Name = args[0] _, err := k.app.Parse(args[1:]) if err != nil { return emptyConf, err } pi, pp, pr := true, true, true if k.printSpec.val != nil { pi, pp, pr, err = parsePrintSpec(*k.printSpec.val) if err != nil { return emptyConf, err } } if k.noPrint { pi, pp, pr = false, false, false } format := formatFromString(k.formatSpec) if format == nil { return emptyConf, fmt.Errorf( "unknown format or invalid format spec %q", k.formatSpec, ) } url, err := urlx.Parse(k.url) if err != nil { return emptyConf, err } return config{ numConns: k.numConns, numReqs: k.numReqs.val, duration: k.duration.val, url: url, headers: k.headers, timeout: k.timeout, method: k.method, body: k.body, bodyFilePath: k.bodyFilePath, stream: k.stream, keyPath: k.keyPath, certPath: k.certPath, printLatencies: k.latencies, insecure: k.insecure, disableKeepAlives: k.disableKeepAlives, rate: k.rate.val, clientType: k.clientType, printIntro: pi, printProgress: pp, printResult: pr, format: format, }, nil } func parsePrintSpec(spec string) (bool, bool, bool, error) { pi, pp, pr := false, false, false if spec == "" { return false, false, false, errEmptyPrintSpec } parts := strings.Split(spec, ",") partsCount := 0 for _, p := range parts { switch p { case "i", "intro": pi = true case "p", "progress": pp = true case "r", "result": pr = true default: return false, false, false, fmt.Errorf("%q is not a valid part of print spec", p) } partsCount++ } if partsCount < 1 || partsCount > 3 { return false, false, false, fmt.Errorf( "spec %q has too many parts, at most 3 are allowed", spec, ) } return pi, pp, pr, nil } ================================================ FILE: args_parser_test.go ================================================ package main import ( "fmt" "reflect" "strconv" "testing" "time" ) const ( programName = "bombardier" ) func TestInvalidArgsParsing(t *testing.T) { expectations := []struct { in []string out string }{ { []string{programName}, "required argument 'url' not provided", }, { []string{programName, "http://google.com", "http://yahoo.com"}, "unexpected http://yahoo.com", }, } for _, e := range expectations { p := newKingpinParser() if _, err := p.parse(e.in); err == nil || err.Error() != e.out { t.Error(err, e.out) } } } func TestUnspecifiedArgParsing(t *testing.T) { p := newKingpinParser() args := []string{programName, "--someunspecifiedflag"} _, err := p.parse(args) if err == nil { t.Fail() } } func TestArgsParsing(t *testing.T) { ten := uint64(10) expectations := []struct { in [][]string out config }{ { [][]string{ {programName, "localhost:8080"}, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("http://localhost:8080"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ {programName, "https://localhost"}, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://localhost"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{{programName, "https://somehost.somedomain"}}, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "-c", "10", "-n", strconv.FormatUint(defaultNumberOfReqs, decBase), "-t", "10s", "https://somehost.somedomain", }, { programName, "-c10", "-n" + strconv.FormatUint(defaultNumberOfReqs, decBase), "-t10s", "https://somehost.somedomain", }, { programName, "--connections", "10", "--requests", strconv.FormatUint(defaultNumberOfReqs, decBase), "--timeout", "10s", "https://somehost.somedomain", }, { programName, "--connections=10", "--requests=" + strconv.FormatUint(defaultNumberOfReqs, decBase), "--timeout=10s", "https://somehost.somedomain", }, }, config{ numConns: 10, timeout: 10 * time.Second, headers: new(headersList), method: "GET", numReqs: &defaultNumberOfReqs, url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--latencies", "https://somehost.somedomain", }, { programName, "-l", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), printLatencies: true, method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--insecure", "https://somehost.somedomain", }, { programName, "-k", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), insecure: true, method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--key", "testclient.key", "--cert", "testclient.cert", "https://somehost.somedomain", }, { programName, "--key=testclient.key", "--cert=testclient.cert", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", keyPath: "testclient.key", certPath: "testclient.cert", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--method", "POST", "--body", "reqbody", "https://somehost.somedomain", }, { programName, "--method=POST", "--body=reqbody", "https://somehost.somedomain", }, { programName, "-m", "POST", "-b", "reqbody", "https://somehost.somedomain", }, { programName, "-mPOST", "-breqbody", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "POST", body: "reqbody", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--header", "One: Value one", "--header", "Two: Value two", "https://somehost.somedomain", }, { programName, "-H", "One: Value one", "-H", "Two: Value two", "https://somehost.somedomain", }, { programName, "--header=One: Value one", "--header=Two: Value two", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: &headersList{ {"One", "Value one"}, {"Two", "Value two"}, }, method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--rate", "10", "https://somehost.somedomain", }, { programName, "-r", "10", "https://somehost.somedomain", }, { programName, "--rate=10", "https://somehost.somedomain", }, { programName, "-r10", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), rate: &ten, printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--fasthttp", "https://somehost.somedomain", }, { programName, "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), clientType: fhttp, printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--http1", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), clientType: nhttp1, printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--http2", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), clientType: nhttp2, printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--body-file=testbody.txt", "https://somehost.somedomain", }, { programName, "--body-file", "testbody.txt", "https://somehost.somedomain", }, { programName, "-f", "testbody.txt", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", bodyFilePath: "testbody.txt", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--stream", "https://somehost.somedomain", }, { programName, "-s", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", stream: true, url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--print=r,i,p", "https://somehost.somedomain", }, { programName, "--print", "r,i,p", "https://somehost.somedomain", }, { programName, "-p", "r,i,p", "https://somehost.somedomain", }, { programName, "--print=result,i,p", "https://somehost.somedomain", }, { programName, "--print", "r,intro,p", "https://somehost.somedomain", }, { programName, "-p", "r,i,progress", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--print=i,r", "https://somehost.somedomain", }, { programName, "--print", "i,r", "https://somehost.somedomain", }, { programName, "-p", "i,r", "https://somehost.somedomain", }, { programName, "--print=intro,r", "https://somehost.somedomain", }, { programName, "--print", "i,result", "https://somehost.somedomain", }, { programName, "-p", "intro,r", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: false, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--no-print", "https://somehost.somedomain", }, { programName, "-q", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: false, printProgress: false, printResult: false, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--format", "plain-text", "https://somehost.somedomain", }, { programName, "--format", "pt", "https://somehost.somedomain", }, { programName, "--format=plain-text", "https://somehost.somedomain", }, { programName, "--format=pt", "https://somehost.somedomain", }, { programName, "-o", "plain-text", "https://somehost.somedomain", }, { programName, "-o", "pt", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }, }, { [][]string{ { programName, "--format", "json", "https://somehost.somedomain", }, { programName, "--format", "j", "https://somehost.somedomain", }, { programName, "--format=json", "https://somehost.somedomain", }, { programName, "--format=j", "https://somehost.somedomain", }, { programName, "-o", "json", "https://somehost.somedomain", }, { programName, "-o", "j", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: knownFormat("json"), }, }, { [][]string{ { programName, "--format", "path:/path/to/tmpl.txt", "https://somehost.somedomain", }, { programName, "--format=path:/path/to/tmpl.txt", "https://somehost.somedomain", }, { programName, "-o", "path:/path/to/tmpl.txt", "https://somehost.somedomain", }, }, config{ numConns: defaultNumberOfConns, timeout: defaultTimeout, headers: new(headersList), method: "GET", url: ParseURLOrPanic("https://somehost.somedomain"), printIntro: true, printProgress: true, printResult: true, format: userDefinedTemplate("/path/to/tmpl.txt"), }, }, } for _, e := range expectations { for _, args := range e.in { p := newKingpinParser() cfg, err := p.parse(args) if err != nil { t.Error(err) continue } if !reflect.DeepEqual(cfg, e.out) { t.Logf("Expected: %#v", e.out) t.Logf("Got: %#v", cfg) t.Fail() } } } } func TestParsePrintSpec(t *testing.T) { exps := []struct { spec string results [3]bool err error }{ { "", [3]bool{}, errEmptyPrintSpec, }, { "a,b,c", [3]bool{}, fmt.Errorf("%q is not a valid part of print spec", "a"), }, { "i,p,r,i", [3]bool{}, fmt.Errorf( "spec %q has too many parts, at most 3 are allowed", "i,p,r,i", ), }, { "i", [3]bool{true, false, false}, nil, }, { "p", [3]bool{false, true, false}, nil, }, { "r", [3]bool{false, false, true}, nil, }, { "i,p,r", [3]bool{true, true, true}, nil, }, } for _, e := range exps { var ( act = [3]bool{} err error ) act[0], act[1], act[2], err = parsePrintSpec(e.spec) if !reflect.DeepEqual(err, e.err) { t.Errorf("For %q, expected err = %q, but got %q", e.spec, e.err, err, ) continue } if !reflect.DeepEqual(e.results, act) { t.Errorf("For %q, expected result = %+v, but got %+v", e.spec, e.results, act, ) } } } func TestArgsParsingWithEmptyPrintSpec(t *testing.T) { p := newKingpinParser() c, err := p.parse( []string{programName, "--print=", "somehost.somedomain"}) if err == nil { t.Fail() } if c != emptyConf { t.Fail() } } func TestArgsParsingWithInvalidPrintSpec(t *testing.T) { invalidSpecs := [][]string{ {programName, "--format", "noprefix.txt", "somehost.somedomain"}, {programName, "--format=noprefix.txt", "somehost.somedomain"}, {programName, "-o", "noprefix.txt", "somehost.somedomain"}, {programName, "--format", "unknown-format", "somehost.somedomain"}, {programName, "--format=unknown-format", "somehost.somedomain"}, {programName, "-o", "unknown-format", "somehost.somedomain"}, } p := newKingpinParser() for _, is := range invalidSpecs { c, err := p.parse(is) if err == nil || c != emptyConf { t.Errorf("invalid print spec %q parsed correctly", is) } } } func TestEmbeddedURLParsing(t *testing.T) { p := newKingpinParser() url := "http://127.0.0.1:8080/to?url=http://10.100.99.41:38667" c, err := p.parse([]string{programName, url}) if err != nil { t.Error(err) } if c.url.String() != url { t.Errorf("got %q, wanted %q", c.url, url) } } ================================================ FILE: bombardier.go ================================================ package main import ( "fmt" "io" "io/ioutil" "os" "os/signal" "strings" "sync" "sync/atomic" "text/template" "time" "github.com/codesenberg/bombardier/internal" "github.com/cheggaaa/pb" fhist "github.com/codesenberg/concurrent/float64/histogram" uhist "github.com/codesenberg/concurrent/uint64/histogram" uuid "github.com/satori/go.uuid" ) type bombardier struct { bytesRead, bytesWritten int64 // HTTP codes req1xx uint64 req2xx uint64 req3xx uint64 req4xx uint64 req5xx uint64 others uint64 conf config barrier completionBarrier ratelimiter limiter wg sync.WaitGroup timeTaken time.Duration latencies *uhist.Histogram requests *fhist.Histogram client client doneChan chan struct{} // RPS metrics rpl sync.Mutex reqs int64 start time.Time // Errors errors *errorMap // Progress bar bar *pb.ProgressBar // Output out io.Writer template *template.Template } func newBombardier(c config) (*bombardier, error) { if err := c.checkArgs(); err != nil { return nil, err } b := new(bombardier) b.conf = c b.latencies = uhist.Default() b.requests = fhist.Default() if b.conf.testType() == counted { b.bar = pb.New64(int64(*b.conf.numReqs)) b.bar.ShowSpeed = true } else if b.conf.testType() == timed { b.bar = pb.New64(b.conf.duration.Nanoseconds() / 1e9) b.bar.ShowCounters = false b.bar.ShowPercent = false } b.bar.ManualUpdate = true if b.conf.testType() == counted { b.barrier = newCountingCompletionBarrier(*b.conf.numReqs) } else { b.barrier = newTimedCompletionBarrier(*b.conf.duration) } if b.conf.rate != nil { b.ratelimiter = newBucketLimiter(*b.conf.rate) } else { b.ratelimiter = &nooplimiter{} } b.out = os.Stdout tlsConfig, err := generateTLSConfig(c) if err != nil { return nil, err } var ( pbody *string bsp bodyStreamProducer ) if c.stream { if c.bodyFilePath != "" { bsp = func() (io.ReadCloser, error) { return os.Open(c.bodyFilePath) } } else { bsp = func() (io.ReadCloser, error) { return ioutil.NopCloser( proxyReader{strings.NewReader(c.body)}, ), nil } } } else { pbody = &c.body if c.bodyFilePath != "" { var bodyBytes []byte bodyBytes, err = ioutil.ReadFile(c.bodyFilePath) if err != nil { return nil, err } sbody := string(bodyBytes) pbody = &sbody } } cc := &clientOpts{ HTTP2: false, maxConns: c.numConns, timeout: c.timeout, tlsConfig: tlsConfig, disableKeepAlives: c.disableKeepAlives, headers: c.headers, requestURL: c.url, method: c.method, body: pbody, bodProd: bsp, bytesRead: &b.bytesRead, bytesWritten: &b.bytesWritten, } b.client = makeHTTPClient(c.clientType, cc) if !b.conf.printProgress { b.bar.Output = ioutil.Discard b.bar.NotPrint = true } b.template, err = b.prepareTemplate() if err != nil { return nil, err } b.wg.Add(int(c.numConns)) b.errors = newErrorMap() b.doneChan = make(chan struct{}, 2) return b, nil } func makeHTTPClient(clientType clientTyp, cc *clientOpts) client { var cl client switch clientType { case nhttp1: cl = newHTTPClient(cc) case nhttp2: cc.HTTP2 = true cl = newHTTPClient(cc) case fhttp: fallthrough default: cl = newFastHTTPClient(cc) } return cl } func (b *bombardier) prepareTemplate() (*template.Template, error) { var ( templateBytes []byte err error ) switch f := b.conf.format.(type) { case knownFormat: templateBytes = f.template() case userDefinedTemplate: templateBytes, err = ioutil.ReadFile(string(f)) if err != nil { return nil, err } default: panic("format can't be nil at this point, this is a bug") } outputTemplate, err := template.New("output-template"). Funcs(template.FuncMap{ "WithLatencies": func() bool { return b.conf.printLatencies }, "FormatBinary": formatBinary, "FormatTimeUs": formatTimeUs, "FormatTimeUsUint64": func(us uint64) string { return formatTimeUs(float64(us)) }, "FloatsToArray": func(ps ...float64) []float64 { return ps }, "Multiply": func(num, coeff float64) float64 { return num * coeff }, "StringToBytes": func(s string) []byte { return []byte(s) }, "UUIDV1": uuid.NewV1, "UUIDV2": uuid.NewV2, "UUIDV3": uuid.NewV3, "UUIDV4": uuid.NewV4, "UUIDV5": uuid.NewV5, }).Parse(string(templateBytes)) if err != nil { return nil, err } return outputTemplate, nil } func (b *bombardier) writeStatistics( code int, usTaken uint64, ) { b.latencies.Increment(usTaken) b.rpl.Lock() b.reqs++ b.rpl.Unlock() var counter *uint64 switch code / 100 { case 1: counter = &b.req1xx case 2: counter = &b.req2xx case 3: counter = &b.req3xx case 4: counter = &b.req4xx case 5: counter = &b.req5xx default: counter = &b.others } atomic.AddUint64(counter, 1) } func (b *bombardier) performSingleRequest() { code, usTaken, err := b.client.do() if err != nil { b.errors.add(err) } b.writeStatistics(code, usTaken) } func (b *bombardier) worker() { done := b.barrier.done() for b.barrier.tryGrabWork() { if b.ratelimiter.pace(done) == brk { break } b.performSingleRequest() b.barrier.jobDone() } } func (b *bombardier) barUpdater() { done := b.barrier.done() for { select { case <-done: b.bar.Set64(b.bar.Total) b.bar.Update() b.bar.Finish() if b.conf.printProgress { fmt.Fprintln(b.out, "Done!") } b.doneChan <- struct{}{} return default: current := int64(b.barrier.completed() * float64(b.bar.Total)) b.bar.Set64(current) b.bar.Update() time.Sleep(b.bar.RefreshRate) } } } func (b *bombardier) rateMeter() { requestsInterval := 10 * time.Millisecond if b.conf.rate != nil { requestsInterval, _ = estimate(*b.conf.rate, rateLimitInterval) } requestsInterval += 10 * time.Millisecond ticker := time.NewTicker(requestsInterval) defer ticker.Stop() done := b.barrier.done() for { select { case <-ticker.C: b.recordRps() continue case <-done: b.wg.Wait() b.recordRps() b.doneChan <- struct{}{} return } } } func (b *bombardier) recordRps() { b.rpl.Lock() duration := time.Since(b.start) reqs := b.reqs b.reqs = 0 b.start = time.Now() b.rpl.Unlock() reqsf := float64(reqs) / duration.Seconds() b.requests.Increment(reqsf) } func (b *bombardier) bombard() { if b.conf.printIntro { b.printIntro() } b.bar.Start() bombardmentBegin := time.Now() b.start = time.Now() for i := uint64(0); i < b.conf.numConns; i++ { go func() { defer b.wg.Done() b.worker() }() } go b.rateMeter() go b.barUpdater() b.wg.Wait() b.timeTaken = time.Since(bombardmentBegin) <-b.doneChan <-b.doneChan } func (b *bombardier) printIntro() { if b.conf.testType() == counted { fmt.Fprintf(b.out, "Bombarding %v with %v request(s) using %v connection(s)\n", b.conf.url, *b.conf.numReqs, b.conf.numConns) } else if b.conf.testType() == timed { fmt.Fprintf(b.out, "Bombarding %v for %v using %v connection(s)\n", b.conf.url, *b.conf.duration, b.conf.numConns) } } func (b *bombardier) gatherInfo() internal.TestInfo { info := internal.TestInfo{ Spec: internal.Spec{ NumberOfConnections: b.conf.numConns, Method: b.conf.method, URL: b.conf.url, Body: b.conf.body, BodyFilePath: b.conf.bodyFilePath, CertPath: b.conf.certPath, KeyPath: b.conf.keyPath, Stream: b.conf.stream, Timeout: b.conf.timeout, ClientType: internal.ClientType(b.conf.clientType), Rate: b.conf.rate, }, Result: internal.Results{ BytesRead: b.bytesRead, BytesWritten: b.bytesWritten, TimeTaken: b.timeTaken, Req1XX: b.req1xx, Req2XX: b.req2xx, Req3XX: b.req3xx, Req4XX: b.req4xx, Req5XX: b.req5xx, Others: b.others, Latencies: b.latencies, Requests: b.requests, }, } testType := b.conf.testType() info.Spec.TestType = internal.TestType(testType) if testType == timed { info.Spec.TestDuration = *b.conf.duration } else if testType == counted { info.Spec.NumberOfRequests = *b.conf.numReqs } if b.conf.headers != nil { for _, h := range *b.conf.headers { info.Spec.Headers = append(info.Spec.Headers, internal.Header{ Key: h.key, Value: h.value, }) } } for _, ewc := range b.errors.byFrequency() { info.Result.Errors = append(info.Result.Errors, internal.ErrorWithCount{ Error: ewc.error, Count: ewc.count, }) } return info } func (b *bombardier) printStats() { info := b.gatherInfo() err := b.template.Execute(b.out, info) if err != nil { fmt.Fprintln(os.Stderr, err) } } func (b *bombardier) redirectOutputTo(out io.Writer) { b.bar.Output = out b.out = out } func (b *bombardier) disableOutput() { b.redirectOutputTo(ioutil.Discard) b.bar.NotPrint = true } func main() { cfg, err := parser.parse(os.Args) if err != nil { fmt.Println("Error parsing the arguments:", err) os.Exit(exitFailure) } bombardier, err := newBombardier(cfg) if err != nil { fmt.Println("Error initializing bombardier:", err) os.Exit(exitFailure) } c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c bombardier.barrier.cancel() }() bombardier.bombard() if bombardier.conf.printResult { bombardier.printStats() } } ================================================ FILE: bombardier_performance_test.go ================================================ package main import ( "flag" "runtime" "testing" "time" ) var ( serverPort = flag.String("port", "8080", "port to use for benchmarks") clientType = flag.String("client-type", "fasthttp", "client to use in benchmarks") ) var ( longDuration = 9001 * time.Hour highRate = uint64(1000000) ) func BenchmarkBombardierSingleReqPerf(b *testing.B) { addr := "localhost:" + *serverPort benchmarkFireRequest(config{ numConns: defaultNumberOfConns, numReqs: nil, duration: &longDuration, url: ParseURLOrPanic("http://" + addr), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", printLatencies: false, clientType: clientTypeFromString(*clientType), format: knownFormat("json"), }, b) } func BenchmarkBombardierRateLimitPerf(b *testing.B) { addr := "localhost:" + *serverPort benchmarkFireRequest(config{ numConns: defaultNumberOfConns, numReqs: nil, duration: &longDuration, url: ParseURLOrPanic("http://" + addr), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", printLatencies: false, rate: &highRate, clientType: clientTypeFromString(*clientType), format: knownFormat("json"), }, b) } func benchmarkFireRequest(c config, bm *testing.B) { b, e := newBombardier(c) if e != nil { bm.Error(e) } b.disableOutput() bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU()) bm.ResetTimer() bm.RunParallel(func(pb *testing.PB) { done := b.barrier.done() for pb.Next() { b.ratelimiter.pace(done) b.performSingleRequest() } }) } ================================================ FILE: bombardier_test.go ================================================ package main import ( "bytes" "container/ring" "crypto/tls" "crypto/x509" "errors" "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" "sync" "sync/atomic" "testing" "time" ) func TestBombardierShouldFireSpecifiedNumberOfRequests(t *testing.T) { testAllClients(t, testBombardierShouldFireSpecifiedNumberOfRequests) } func testBombardierShouldFireSpecifiedNumberOfRequests( clientType clientTyp, t *testing.T, ) { reqsReceived := uint64(0) s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { atomic.AddUint64(&reqsReceived, 1) }), ) defer s.Close() numReqs := uint64(100) noHeaders := new(headersList) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() if reqsReceived != numReqs { t.Fail() } } func TestBombardierShouldFinish(t *testing.T) { testAllClients(t, testBombardierShouldFinish) } func testBombardierShouldFinish(clientType clientTyp, t *testing.T) { reqsReceived := uint64(0) s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { atomic.AddUint64(&reqsReceived, 1) }), ) defer s.Close() noHeaders := new(headersList) desiredTestDuration := 1 * time.Second b, e := newBombardier(config{ numConns: defaultNumberOfConns, duration: &desiredTestDuration, url: ParseURLOrPanic(s.URL), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() waitCh := make(chan struct{}) go func() { b.bombard() waitCh <- struct{}{} }() select { case <-waitCh: // Do nothing here case <-time.After(desiredTestDuration + 5*time.Second): t.Fail() } if reqsReceived == 0 { t.Fail() } } func TestBombardierShouldSendHeaders(t *testing.T) { testAllClients(t, testBombardierShouldSendHeaders) } func testBombardierShouldSendHeaders(clientType clientTyp, t *testing.T) { requestHeaders := headersList([]header{ {"Header1", "Value1"}, {"Header-Two", "value-two"}, }) // It's a bit hacky, but FastHTTP can't send Host header correctly // as of now if clientType != fhttp { requestHeaders = append(requestHeaders, header{"Host", "web"}) } s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { for _, h := range requestHeaders { av := r.Header.Get(h.key) if h.key == "Host" { av = r.Host } if av != h.value { t.Logf("%q <-> %q", av, h.value) t.Fail() } } }), ) defer s.Close() numReqs := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: &requestHeaders, timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() } func TestBombardierHTTPCodeRecording(t *testing.T) { testAllClients(t, testBombardierHTTPCodeRecording) } func testBombardierHTTPCodeRecording(clientType clientTyp, t *testing.T) { cs := []int{200, 302, 404, 505, 606, 707} codes := ring.New(len(cs)) for _, v := range cs { codes.Value = v codes = codes.Next() } codes = codes.Next() var m sync.Mutex s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { m.Lock() nextCode := codes.Value.(int) codes = codes.Next() m.Unlock() if nextCode/100 == 3 { rw.Header().Set("Location", "http://localhost:666") } rw.WriteHeader(nextCode) }), ) defer s.Close() eachCodeCount := uint64(10) numReqs := uint64(len(cs)) * eachCodeCount b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() expectation := []struct { name string reqsGot uint64 expected uint64 }{ {"errored", b.others, eachCodeCount * 2}, {"2xx", b.req2xx, eachCodeCount}, {"3xx", b.req3xx, eachCodeCount}, {"4xx", b.req4xx, eachCodeCount}, {"5xx", b.req5xx, eachCodeCount}, } for _, e := range expectation { if e.reqsGot != e.expected { t.Error(e.name, e.reqsGot, e.expected) } } t.Logf("%+v", b.errors.byFrequency()) } func TestBombardierTimeoutRecoding(t *testing.T) { testAllClients(t, testBombardierTimeoutRecoding) } func testBombardierTimeoutRecoding(clientType clientTyp, t *testing.T) { shortTimeout := 10 * time.Millisecond s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { time.Sleep(shortTimeout * 10) }), ) defer s.Close() numReqs := uint64(10) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, duration: nil, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: shortTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() if b.errors.sum() != numReqs { t.Fail() } } func TestBombardierThroughputRecording(t *testing.T) { testAllClients(t, testBombardierThroughputRecording) } func testBombardierThroughputRecording(clientType clientTyp, t *testing.T) { responseSize := 1024 response := bytes.Repeat([]byte{'a'}, responseSize) s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, err := rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() numReqs := uint64(10) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() if b.bytesRead == 0 || b.bytesWritten == 0 { t.Error(b.bytesRead, b.bytesWritten) } } func TestBombardierStatsPrinting(t *testing.T) { responseSize := 1024 response := bytes.Repeat([]byte{'a'}, responseSize) s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, err := rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() numReqs := uint64(10) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", printLatencies: true, printIntro: true, printProgress: true, printResult: true, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } dummy := errors.New("dummy error") b.errors.add(dummy) out := new(bytes.Buffer) b.redirectOutputTo(out) b.bombard() b.printStats() l := out.Len() // Here we only test if anything is written if l == 0 { t.Fail() } } func TestBombardierErrorIfFailToReadClientCert(t *testing.T) { numReqs := uint64(10) _, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic("http://localhost"), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", printLatencies: true, certPath: "certPath", keyPath: "keyPath", format: knownFormat("plain-text"), }) if e == nil { t.Fail() } } func TestBombardierClientCerts(t *testing.T) { testAllClients(t, testBombardierClientCerts) } func testBombardierClientCerts(clientType clientTyp, t *testing.T) { clientCert, err := tls.LoadX509KeyPair("testclient.cert", "testclient.key") if err != nil { t.Error(err) return } clientX509Cert, err := x509.ParseCertificate(clientCert.Certificate[0]) if err != nil { t.Error(err) return } servertCert, err := tls.LoadX509KeyPair("testserver.cert", "testserver.key") if err != nil { t.Error(err) return } tlsConfig := &tls.Config{ ClientAuth: tls.RequireAnyClientCert, Certificates: []tls.Certificate{servertCert}, } server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { certs := r.TLS.PeerCertificates if numCerts := len(certs); numCerts != 1 { t.Errorf("expected 1 cert, but got %v", numCerts) rw.WriteHeader(http.StatusBadRequest) return } cert := certs[0] if !cert.Equal(clientX509Cert) { t.Error("certificates don't match") rw.WriteHeader(http.StatusBadRequest) return } rw.WriteHeader(http.StatusOK) })) server.TLS = tlsConfig server.StartTLS() singleRequest := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &singleRequest, url: ParseURLOrPanic(server.URL), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", printLatencies: true, certPath: "testclient.cert", keyPath: "testclient.key", insecure: true, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() if b.req2xx != 1 { t.Error("no 2xx responses, total =", b.reqs, ", 1xx/2xx/3xx/4xx/5xx =", b.req1xx, b.req2xx, b.req3xx, b.req4xx, b.req5xx) } server.Close() } func TestBombardierRateLimiting(t *testing.T) { testAllClients(t, testBombardierRateLimiting) } func testBombardierRateLimiting(clientType clientTyp, t *testing.T) { responseSize := 1024 response := bytes.Repeat([]byte{'a'}, responseSize) s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, err := rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() rate := uint64(5000) testDuration := 1 * time.Second b, e := newBombardier(config{ numConns: defaultNumberOfConns, duration: &testDuration, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "GET", body: "", rate: &rate, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() if float64(b.req2xx) < float64(rate)*0.75 || float64(b.req2xx) > float64(rate)*1.25 { t.Error(rate, b.req2xx) } } func testAllClients(parent *testing.T, testFun func(clientTyp, *testing.T)) { clients := []clientTyp{fhttp, nhttp1, nhttp2} for _, ct := range clients { parent.Run(ct.String(), func(t *testing.T) { testFun(ct, t) }) } } func TestBombardierSendsBody(t *testing.T) { testAllClients(t, testBombardierSendsBody) } func testBombardierSendsBody(clientType clientTyp, t *testing.T) { response := []byte("OK") requestBody := "abracadabra" s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) return } if string(body) != requestBody { t.Errorf("Expected %v, but got %v", requestBody, string(body)) } _, err = rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() one := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &one, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "POST", body: requestBody, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() } func TestBombardierSendsBodyFromFile(t *testing.T) { testAllClients(t, testBombardierSendsBodyFromFile) } func testBombardierSendsBodyFromFile(clientType clientTyp, t *testing.T) { response := []byte("OK") bodyPath := "testbody.txt" requestBody, err := ioutil.ReadFile(bodyPath) if err != nil { t.Error(err) return } s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) return } if string(body) != string(requestBody) { t.Errorf("Expected %v, but got %v", string(requestBody), string(body)) } _, err = rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() one := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &one, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "POST", bodyFilePath: bodyPath, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() } func TestBombardierFileDoesntExist(t *testing.T) { bodyPath := "/does/not/exist.forreal" _, e := newBombardier(config{ numConns: defaultNumberOfConns, url: ParseURLOrPanic("http://example.com"), headers: new(headersList), timeout: defaultTimeout, method: "POST", bodyFilePath: bodyPath, format: knownFormat("plain-text"), }) _, ok := e.(*os.PathError) if !ok { t.Errorf("Expected to get PathError, but got %v", e) } } func TestBombardierStreamsBody(t *testing.T) { testAllClients(t, testBombardierStreamsBody) } func testBombardierStreamsBody(clientType clientTyp, t *testing.T) { response := []byte("OK") requestBody := "abracadabra" s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if te := r.TransferEncoding; !reflect.DeepEqual(te, []string{"chunked"}) { t.Errorf("Expected chunked transfer encoding, but got %v", te) } body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) return } if string(body) != requestBody { t.Errorf("Expected %v, but got %v", requestBody, string(body)) } _, err = rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() one := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &one, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "POST", body: requestBody, stream: true, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() } func TestBombardierStreamsBodyFromFile(t *testing.T) { testAllClients(t, testBombardierStreamsBodyFromFile) } func testBombardierStreamsBodyFromFile(clientType clientTyp, t *testing.T) { response := []byte("OK") bodyPath := "testbody.txt" requestBody, err := ioutil.ReadFile(bodyPath) if err != nil { t.Error(err) return } s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if te := r.TransferEncoding; !reflect.DeepEqual(te, []string{"chunked"}) { t.Errorf("Expected chunked transfer encoding, but got %v", te) } body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) return } if string(body) != string(requestBody) { t.Errorf("Expected %v, but got %v", string(requestBody), string(body)) } _, err = rw.Write(response) if err != nil { t.Error(err) } }), ) defer s.Close() one := uint64(1) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &one, url: ParseURLOrPanic(s.URL), headers: new(headersList), timeout: defaultTimeout, method: "POST", bodyFilePath: bodyPath, stream: true, clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) return } b.disableOutput() b.bombard() } func TestBombardierShouldSendCustomHostHeader(t *testing.T) { testAllClients(t, testBombardierShouldSendCustomHostHeader) } func testBombardierShouldSendCustomHostHeader( clientType clientTyp, t *testing.T, ) { host := "custom-host" s := httptest.NewServer( http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if r.Host != host { t.Errorf("Host must be %q, but it's %q", host, r.Host) } }), ) defer s.Close() numReqs := uint64(100) headers := headersList([]header{ {"Host", host}, }) b, e := newBombardier(config{ numConns: defaultNumberOfConns, numReqs: &numReqs, url: ParseURLOrPanic(s.URL), headers: &headers, timeout: defaultTimeout, method: "GET", body: "", clientType: clientType, format: knownFormat("plain-text"), }) if e != nil { t.Error(e) } b.disableOutput() b.bombard() } ================================================ FILE: build.py ================================================ import argparse import os import subprocess platforms = [ ("darwin", "amd64"), ("darwin", "arm64"), ("freebsd", "386"), ("freebsd", "amd64"), ("freebsd", "arm"), ("linux", "386"), ("linux", "amd64"), ("linux", "arm"), ("linux", "arm64"), ("netbsd", "386"), ("netbsd", "amd64"), ("netbsd", "arm"), ("openbsd", "386"), ("openbsd", "amd64"), ("openbsd", "arm"), ("openbsd", "arm64"), ("windows", "386"), ("windows", "amd64"), ("windows", "arm64"), ] if __name__ == "__main__": parser = argparse.ArgumentParser(description="Auxilary build script.") parser.add_argument("-v", "--version", default="unspecified", type=str, help="string used as a version when building binaries") args = parser.parse_args() version = args.version for (build_os, build_arch) in platforms: ext = "" if build_os == "windows": ext = ".exe" build_env = os.environ.copy() build_env["GOOS"] = build_os build_env["GOARCH"] = build_arch subprocess.run(["go", "build", "-ldflags", "-s -w -X main.version=%s" % version, "-o", "bombardier-%s-%s%s" % (build_os, build_arch, ext)], env=build_env) ================================================ FILE: client_cert.go ================================================ package main import ( "crypto/tls" ) // readClientCert - helper function to read client certificate // from pem formatted certPath and keyPath files func readClientCert(certPath, keyPath string) ([]tls.Certificate, error) { // load keypair cert, err := tls.LoadX509KeyPair(certPath, keyPath) return []tls.Certificate{cert}, err } // generateTLSConfig - helper function to generate a TLS configuration based on // config func generateTLSConfig(c config) (*tls.Config, error) { var ( certs []tls.Certificate err error ) // This assumes that the caller has validated that either both or none of // the c.certPath and c.keyPath are set. if c.certPath != "" && c.keyPath != "" { certs, err = readClientCert(c.certPath, c.keyPath) if err != nil { return nil, err } } // Disable gas warning, because InsecureSkipVerify may be set to true // for the purpose of testing /* #nosec */ tlsConfig := &tls.Config{ InsecureSkipVerify: c.insecure, Certificates: certs, } return tlsConfig, nil } ================================================ FILE: client_cert_test.go ================================================ package main import ( "testing" ) func TestGenerateTLSConfig(t *testing.T) { expectations := []struct { certPath string keyPath string errIsNil bool }{ { certPath: "testclient.cert", keyPath: "testclient.key", errIsNil: true, }, { certPath: "doesnotexist.pem", keyPath: "doesnotexist.pem", errIsNil: false, }, { certPath: "", keyPath: "", errIsNil: true, }, } for _, e := range expectations { _, r := generateTLSConfig( config{ url: ParseURLOrPanic("https://doesnt.exist.com"), certPath: e.certPath, keyPath: e.keyPath, }, ) if (r == nil) != e.errIsNil { t.Error(e.certPath, e.keyPath, r) } } } ================================================ FILE: clients.go ================================================ package main import ( "crypto/tls" "io" "io/ioutil" "net/http" "net/url" "strings" "time" "github.com/valyala/fasthttp" ) type client interface { do() (code int, usTaken uint64, err error) } type bodyStreamProducer func() (io.ReadCloser, error) type clientOpts struct { HTTP2 bool maxConns uint64 timeout time.Duration tlsConfig *tls.Config disableKeepAlives bool requestURL *url.URL headers *headersList method string body *string bodProd bodyStreamProducer bytesRead, bytesWritten *int64 } type fasthttpClient struct { client *fasthttp.Client headers *fasthttp.RequestHeader uri *fasthttp.URI method string body *string bodProd bodyStreamProducer } func newFastHTTPClient(opts *clientOpts) client { c := new(fasthttpClient) uri := fasthttp.AcquireURI() if err := uri.Parse( []byte(opts.requestURL.Host), []byte(opts.requestURL.String()), ); err != nil { // opts.requestURL must always be valid panic(err) } c.uri = uri c.client = &fasthttp.Client{ MaxConnsPerHost: int(opts.maxConns), ReadTimeout: opts.timeout, WriteTimeout: opts.timeout, DisableHeaderNamesNormalizing: true, TLSConfig: opts.tlsConfig, Dial: fasthttpDialFunc( opts.bytesRead, opts.bytesWritten, opts.timeout, ), } c.headers = headersToFastHTTPHeaders(opts.headers) c.method, c.body = opts.method, opts.body c.bodProd = opts.bodProd return client(c) } func (c *fasthttpClient) do() ( code int, usTaken uint64, err error, ) { // prepare the request req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() if c.headers != nil { c.headers.CopyTo(&req.Header) } req.Header.SetMethod(c.method) req.SetURI(c.uri) req.UseHostHeader = true if c.body != nil { req.SetBodyString(*c.body) } else { bs, bserr := c.bodProd() if bserr != nil { return 0, 0, bserr } req.SetBodyStream(bs, -1) } // fire the request start := time.Now() err = c.client.Do(req, resp) if err != nil { code = -1 } else { code = resp.StatusCode() } usTaken = uint64(time.Since(start).Nanoseconds() / 1000) // release resources fasthttp.ReleaseRequest(req) fasthttp.ReleaseResponse(resp) return } type httpClient struct { client *http.Client headers http.Header url *url.URL method string body *string bodProd bodyStreamProducer } func newHTTPClient(opts *clientOpts) client { c := new(httpClient) tr := &http.Transport{ TLSClientConfig: opts.tlsConfig, MaxIdleConnsPerHost: int(opts.maxConns), DisableKeepAlives: opts.disableKeepAlives, ForceAttemptHTTP2: opts.HTTP2, DialContext: httpDialContextFunc(opts.bytesRead, opts.bytesWritten, opts.timeout), } cl := &http.Client{ Transport: tr, Timeout: opts.timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } c.client = cl c.headers = headersToHTTPHeaders(opts.headers) c.method, c.body, c.bodProd = opts.method, opts.body, opts.bodProd c.url = opts.requestURL return client(c) } func (c *httpClient) do() ( code int, usTaken uint64, err error, ) { req := &http.Request{} req.Header = c.headers req.Method = c.method req.URL = c.url if host := req.Header.Get("Host"); host != "" { req.Host = host } if c.body != nil { br := strings.NewReader(*c.body) req.ContentLength = int64(len(*c.body)) req.Body = ioutil.NopCloser(br) } else { bs, bserr := c.bodProd() if bserr != nil { return 0, 0, bserr } req.Body = bs } start := time.Now() resp, err := c.client.Do(req) if err != nil { code = -1 } else { code = resp.StatusCode _, berr := io.Copy(ioutil.Discard, resp.Body) if berr != nil { err = berr } if cerr := resp.Body.Close(); cerr != nil { err = cerr } } usTaken = uint64(time.Since(start).Nanoseconds() / 1000) return } func headersToFastHTTPHeaders(h *headersList) *fasthttp.RequestHeader { if len(*h) == 0 { return nil } res := new(fasthttp.RequestHeader) for _, header := range *h { res.Set(header.key, header.value) } return res } func headersToHTTPHeaders(h *headersList) http.Header { if len(*h) == 0 { return http.Header{} } headers := http.Header{} for _, header := range *h { headers[header.key] = []string{header.value} } return headers } ================================================ FILE: clients_test.go ================================================ package main import ( "bytes" "crypto/tls" "net/http" "net/http/httptest" "sync/atomic" "testing" "github.com/goware/urlx" ) func TestShouldReturnNilIfNoHeadersWhereSet(t *testing.T) { h := new(headersList) if headersToFastHTTPHeaders(h) != nil { t.Fail() } } func TestShouldReturnEmptyHeadersIfNoHeaadersWhereSet(t *testing.T) { h := new(headersList) if len(headersToHTTPHeaders(h)) != 0 { t.Fail() } } func TestShouldProperlyConvertToHttpHeaders(t *testing.T) { h := new(headersList) for _, hs := range []string{ "Content-Type: application/json", "Custom-Header: xxx42xxx", } { if err := h.Set(hs); err != nil { t.Error(err) } } fh := headersToFastHTTPHeaders(h) { e, a := []byte("application/json"), fh.Peek("Content-Type") if !bytes.Equal(e, a) { t.Errorf("Expected %v, but got %v", e, a) } } if e, a := []byte("xxx42xxx"), fh.Peek("Custom-Header"); !bytes.Equal(e, a) { t.Errorf("Expected %v, but got %v", e, a) } nh := headersToHTTPHeaders(h) { e, a := "application/json", nh.Get("Content-Type") if e != a { t.Errorf("Expected %v, but got %v", e, a) } } if e, a := "xxx42xxx", nh.Get("Custom-Header"); e != a { t.Errorf("Expected %v, but got %v", e, a) } } func TestHTTP2Client(t *testing.T) { responseSize := 1024 response := bytes.Repeat([]byte{'a'}, responseSize) s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !r.ProtoAtLeast(2, 0) { t.Errorf("invalid HTTP proto version: %v", r.Proto) } w.WriteHeader(http.StatusOK) _, err := w.Write(response) if err != nil { t.Error(err) } })) s.EnableHTTP2 = true s.TLS = &tls.Config{ InsecureSkipVerify: true, } s.StartTLS() defer s.Close() bytesRead, bytesWritten := int64(0), int64(0) requestURL, err := urlx.Parse(s.URL) if err != nil { t.Fatal(err) } c := newHTTPClient(&clientOpts{ HTTP2: true, headers: new(headersList), requestURL: requestURL, method: "GET", tlsConfig: &tls.Config{ InsecureSkipVerify: true, }, body: new(string), bytesRead: &bytesRead, bytesWritten: &bytesWritten, }) code, _, err := c.do() if err != nil { t.Error(err) return } if code != http.StatusOK { t.Errorf("invalid response code: %v", code) } if atomic.LoadInt64(&bytesRead) == 0 { t.Errorf("invalid response size: %v", bytesRead) } if atomic.LoadInt64(&bytesWritten) == 0 { t.Errorf("empty request of size: %v", bytesWritten) } } func TestHTTP1Clients(t *testing.T) { responseSize := 1024 response := bytes.Repeat([]byte{'a'}, responseSize) s := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor != 1 { t.Errorf("invalid HTTP proto version: %v", r.Proto) } w.WriteHeader(http.StatusOK) _, err := w.Write(response) if err != nil { t.Error(err) } }, )) defer s.Close() bytesRead, bytesWritten := int64(0), int64(0) requestURL, err := urlx.Parse(s.URL) if err != nil { t.Fatal(err) } cc := &clientOpts{ HTTP2: false, headers: new(headersList), requestURL: requestURL, method: "GET", body: new(string), bytesRead: &bytesRead, bytesWritten: &bytesWritten, } clients := []client{ newHTTPClient(cc), newFastHTTPClient(cc), } for _, c := range clients { bytesRead, bytesWritten = 0, 0 code, _, err := c.do() if err != nil { t.Error(err) return } if code != http.StatusOK { t.Errorf("invalid response code: %v", code) } if bytesRead == 0 { t.Errorf("invalid response size: %v", bytesRead) } if bytesWritten == 0 { t.Errorf("empty request of size: %v", bytesWritten) } } } ================================================ FILE: cmd/utils/simplebenchserver/doc.go ================================================ /* Simple HTTP server used for benchmarking. Following options are available: --help Show context-sensitive help (also try --help-long and --help-man). -p, --port="8080" port to use for benchmarks -s, --size=1024 size of response in bytes */ package main ================================================ FILE: cmd/utils/simplebenchserver/main.go ================================================ package main import ( "bytes" "log" "net/http" "github.com/alecthomas/kingpin" "github.com/valyala/fasthttp" ) var serverPort = kingpin.Flag("port", "port to use for benchmarks"). Default("8080"). Short('p'). String() var responseSize = kingpin.Flag("size", "size of response in bytes"). Default("1024"). Short('s'). Uint() var stdHTTP = kingpin.Flag("std-http", "use standard http library"). Default("false"). Bool() func main() { kingpin.Parse() response := bytes.Repeat([]byte("a"), int(*responseSize)) addr := "localhost:" + *serverPort log.Println("Starting HTTP server on:", addr) var lserr error if *stdHTTP { lserr = http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, werr := w.Write(response) if werr != nil { log.Println(werr) } })) } else { lserr = fasthttp.ListenAndServe(addr, func(c *fasthttp.RequestCtx) { _, werr := c.Write(response) if werr != nil { log.Println(werr) } }) } if lserr != nil { log.Println(lserr) } } ================================================ FILE: common.go ================================================ package main import ( "errors" "net/url" "sort" "time" "github.com/goware/urlx" ) const ( decBase = 10 rateLimitInterval = 10 * time.Millisecond oneSecond = 1 * time.Second exitFailure = 1 ) var ( version = "unspecified" emptyConf = config{} parser = newKingpinParser() defaultTestDuration = 10 * time.Second defaultNumberOfConns = uint64(125) defaultTimeout = 2 * time.Second httpMethods = []string{ "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", } cantHaveBody = []string{"HEAD"} errUnsupportedScheme = errors.New("unsupported scheme") errInvalidNumberOfConns = errors.New( "invalid number of connections(must be > 0)") errInvalidNumberOfRequests = errors.New( "invalid number of requests(must be > 0)") errInvalidTestDuration = errors.New( "invalid test duration(must be >= 1s)") errNegativeTimeout = errors.New( "timeout can't be negative") errBodyNotAllowed = errors.New( "HEAD requests cannot have body") errNoPathToCert = errors.New( "no Path to TLS Client Certificate") errNoPathToKey = errors.New( "no Path to TLS Client Certificate Private Key") errZeroRate = errors.New( "rate can't be less than 1") errBodyProvidedTwice = errors.New("use either --body or --body-file") errInvalidHeaderFormat = errors.New("invalid header format") errEmptyPrintSpec = errors.New( "empty print spec is not a valid print spec") ) func ParseURLOrPanic(s string) *url.URL { u, err := urlx.Parse(s) if err != nil { panic(err) } return u } func init() { sort.Strings(httpMethods) sort.Strings(cantHaveBody) } ================================================ FILE: completion_barriers.go ================================================ package main import ( "sync" "sync/atomic" "time" ) type completionBarrier interface { completed() float64 tryGrabWork() bool jobDone() done() <-chan struct{} cancel() } type countingCompletionBarrier struct { numReqs, reqsGrabbed, reqsDone uint64 doneChan chan struct{} closeOnce sync.Once } func newCountingCompletionBarrier(numReqs uint64) completionBarrier { c := new(countingCompletionBarrier) c.reqsDone, c.reqsGrabbed, c.numReqs = 0, 0, numReqs c.doneChan = make(chan struct{}) return completionBarrier(c) } func (c *countingCompletionBarrier) tryGrabWork() bool { select { case <-c.doneChan: return false default: reqsDone := atomic.AddUint64(&c.reqsGrabbed, 1) return reqsDone <= c.numReqs } } func (c *countingCompletionBarrier) jobDone() { reqsDone := atomic.AddUint64(&c.reqsDone, 1) if reqsDone == c.numReqs { c.closeOnce.Do(func() { close(c.doneChan) }) } } func (c *countingCompletionBarrier) done() <-chan struct{} { return c.doneChan } func (c *countingCompletionBarrier) cancel() { c.closeOnce.Do(func() { close(c.doneChan) }) } func (c *countingCompletionBarrier) completed() float64 { select { case <-c.doneChan: return 1.0 default: reqsDone := atomic.LoadUint64(&c.reqsDone) return float64(reqsDone) / float64(c.numReqs) } } type timedCompletionBarrier struct { doneChan chan struct{} closeOnce sync.Once start time.Time duration time.Duration } func newTimedCompletionBarrier(duration time.Duration) completionBarrier { if duration < 0 { panic("timedCompletionBarrier: negative duration") } c := new(timedCompletionBarrier) c.doneChan = make(chan struct{}) c.start = time.Now() c.duration = duration go func() { time.AfterFunc(duration, func() { c.closeOnce.Do(func() { close(c.doneChan) }) }) }() return completionBarrier(c) } func (c *timedCompletionBarrier) tryGrabWork() bool { select { case <-c.doneChan: return false default: return true } } func (c *timedCompletionBarrier) jobDone() { } func (c *timedCompletionBarrier) done() <-chan struct{} { return c.doneChan } func (c *timedCompletionBarrier) cancel() { c.closeOnce.Do(func() { close(c.doneChan) }) } func (c *timedCompletionBarrier) completed() float64 { select { case <-c.doneChan: return 1.0 default: return float64(time.Since(c.start).Nanoseconds()) / float64(c.duration.Nanoseconds()) } } ================================================ FILE: completion_barriers_test.go ================================================ package main import ( "math" "testing" "time" ) func TestCouintingCompletionBarrierWait(t *testing.T) { parties := uint64(10) b := newCountingCompletionBarrier(1000) for i := uint64(0); i < parties; i++ { go func() { for b.tryGrabWork() { b.jobDone() } }() } wc := make(chan struct{}) go func() { <-b.done() wc <- struct{}{} }() select { case <-wc: return case <-time.After(100 * time.Millisecond): t.Fail() } } func TestTimedCompletionBarrierWait(t *testing.T) { parties := uint64(10) duration := 100 * time.Millisecond timeout := duration * 2 err := 15 * time.Millisecond sleepDuration := 2 * time.Millisecond b := newTimedCompletionBarrier(duration) for i := uint64(0); i < parties; i++ { go func() { for b.tryGrabWork() { time.Sleep(sleepDuration) b.jobDone() } }() } wc := make(chan time.Duration) go func() { start := time.Now() <-b.done() wc <- time.Since(start) }() select { case actual := <-wc: if !approximatelyEqual(duration, actual, sleepDuration+err) { t.Errorf("Expected to run %v, but ran %v instead", duration, actual) } case <-time.After(timeout): t.Error("Barrier hanged") } } func TestTimeBarrierCancel(t *testing.T) { b := newTimedCompletionBarrier(9000 * time.Second) sleepTime := 100 * time.Millisecond go func() { time.Sleep(sleepTime) b.cancel() }() select { case <-b.done(): if c := b.completed(); c != 1.0 { t.Error(c) } case <-time.After(sleepTime * 2): t.Fail() } } func TestCountedBarrierCancel(t *testing.T) { parties := uint64(10) b := newCountingCompletionBarrier(math.MaxUint64) sleepTime := 100 * time.Millisecond for i := uint64(0); i < parties; i++ { go func() { for b.tryGrabWork() { b.jobDone() } }() } go func() { time.Sleep(sleepTime) b.cancel() }() select { case <-b.done(): if c := b.completed(); c != 1.0 { t.Error(c) } case <-time.After(5 * time.Second): t.Fail() } } func TestTimeBarrierPanicOnBadDuration(t *testing.T) { defer func() { r := recover() if r == nil { t.Error("shouldn't be empty") t.Fail() } }() newTimedCompletionBarrier(-1 * time.Second) t.Error("unreachable") t.Fail() } func approximatelyEqual(expected, actual, err time.Duration) bool { return expected-err < actual && actual < expected+err } ================================================ FILE: config.go ================================================ package main import ( "fmt" "net/url" "sort" "time" ) type config struct { numConns uint64 numReqs *uint64 disableKeepAlives bool duration *time.Duration url *url.URL method, certPath, keyPath string body, bodyFilePath string stream bool headers *headersList timeout time.Duration // TODO(codesenberg): printLatencies should probably be // re(named&maked) into printPercentiles or even let // users provide their own percentiles and not just // calculate for [0.5, 0.75, 0.9, 0.99] printLatencies, insecure bool rate *uint64 clientType clientTyp printIntro, printProgress, printResult bool format format } type testTyp int const ( none testTyp = iota timed counted ) type invalidHTTPMethodError struct { method string } func (i *invalidHTTPMethodError) Error() string { return fmt.Sprintf("Unknown HTTP method: %v", i.method) } func (c *config) checkArgs() error { c.checkOrSetDefaultTestType() checks := []func() error{ c.checkURL, c.checkRate, c.checkRunParameters, c.checkTimeoutDuration, c.checkHTTPParameters, c.checkCertPaths, } for _, check := range checks { if err := check(); err != nil { return err } } return nil } func (c *config) checkOrSetDefaultTestType() { if c.testType() == none { c.duration = &defaultTestDuration } } func (c *config) testType() testTyp { typ := none if c.numReqs != nil { typ = counted } else if c.duration != nil { typ = timed } return typ } func (c *config) checkURL() error { if c.url.Scheme != "http" && c.url.Scheme != "https" { return errUnsupportedScheme } return nil } func (c *config) checkRate() error { if c.rate != nil && *c.rate < 1 { return errZeroRate } return nil } func (c *config) checkRunParameters() error { if c.numConns < uint64(1) { return errInvalidNumberOfConns } if c.testType() == counted && *c.numReqs < uint64(1) { return errInvalidNumberOfRequests } if c.testType() == timed && *c.duration < time.Second { return errInvalidTestDuration } return nil } func (c *config) checkTimeoutDuration() error { if c.timeout < 0 { return errNegativeTimeout } return nil } func (c *config) checkHTTPParameters() error { if !allowedHTTPMethod(c.method) { return &invalidHTTPMethodError{method: c.method} } if !canHaveBody(c.method) && (c.body != "" || c.bodyFilePath != "") { return errBodyNotAllowed } if c.body != "" && c.bodyFilePath != "" { return errBodyProvidedTwice } return nil } func (c *config) checkCertPaths() error { if c.certPath != "" && c.keyPath == "" { return errNoPathToKey } else if c.certPath == "" && c.keyPath != "" { return errNoPathToCert } return nil } func (c *config) timeoutMillis() uint64 { return uint64(c.timeout.Nanoseconds() / 1000) } func allowedHTTPMethod(method string) bool { i := sort.SearchStrings(httpMethods, method) return i < len(httpMethods) && httpMethods[i] == method } func canHaveBody(method string) bool { i := sort.SearchStrings(cantHaveBody, method) return !(i < len(cantHaveBody) && cantHaveBody[i] == method) } type clientTyp int const ( fhttp clientTyp = iota nhttp1 nhttp2 ) func (ct clientTyp) String() string { switch ct { case fhttp: return "FastHTTP" case nhttp1: return "net/http v1.x" case nhttp2: return "net/http v2.0" } return "unknown client" } ================================================ FILE: config_test.go ================================================ package main import ( "testing" "time" ) var ( defaultNumberOfReqs = uint64(10000) ) func TestCanHaveBody(t *testing.T) { expectations := []struct { in string out bool }{ {"HEAD", false}, {"GET", true}, {"POST", true}, {"PUT", true}, {"DELETE", true}, {"OPTIONS", true}, } for _, e := range expectations { if r := canHaveBody(e.in); r != e.out { t.Error(e.in, e.out, r) } } } func TestAllowedHttpMethod(t *testing.T) { expectations := []struct { in string out bool }{ {"GET", true}, {"POST", true}, {"PUT", true}, {"DELETE", true}, {"HEAD", true}, {"OPTIONS", true}, {"TRUNCATE", false}, } for _, e := range expectations { if r := allowedHTTPMethod(e.in); r != e.out { t.Logf("Expected f(%v) = %v, but got %v", e.in, e.out, r) t.Fail() } } } func TestCheckArgs(t *testing.T) { invalidNumberOfReqs := uint64(0) smallTestDuration := 99 * time.Millisecond negativeTimeoutDuration := -1 * time.Second noHeaders := new(headersList) zeroRate := uint64(0) expectations := []struct { in config out error }{ { config{ numConns: 0, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", format: knownFormat("plain-text"), }, errInvalidNumberOfConns, }, { config{ numConns: defaultNumberOfConns, numReqs: &invalidNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", format: knownFormat("plain-text"), }, errInvalidNumberOfRequests, }, { config{ numConns: defaultNumberOfConns, numReqs: nil, duration: &smallTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", format: knownFormat("plain-text"), }, errInvalidTestDuration, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: negativeTimeoutDuration, method: "GET", body: "", format: knownFormat("plain-text"), }, errNegativeTimeout, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "HEAD", body: "BODY", format: knownFormat("plain-text"), }, errBodyNotAllowed, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "HEAD", bodyFilePath: "testbody.txt", format: knownFormat("plain-text"), }, errBodyNotAllowed, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "BODY", format: knownFormat("plain-text"), }, nil, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", bodyFilePath: "testbody.txt", format: knownFormat("plain-text"), }, nil, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", format: knownFormat("plain-text"), }, nil, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", certPath: "test_cert.pem", keyPath: "", format: knownFormat("plain-text"), }, errNoPathToKey, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", body: "", certPath: "", keyPath: "test_key.pem", format: knownFormat("plain-text"), }, errNoPathToCert, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "GET", rate: &zeroRate, format: knownFormat("plain-text"), }, errZeroRate, }, { config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: noHeaders, timeout: defaultTimeout, method: "POST", body: "abracadabra", bodyFilePath: "testbody.txt", format: knownFormat("plain-text"), }, errBodyProvidedTwice, }, } for _, e := range expectations { if r := e.in.checkArgs(); r != e.out { t.Logf("Expected (%v).checkArgs to return %v, but got %v", e.in, e.out, r) t.Fail() } if _, r := newBombardier(e.in); r != e.out { t.Logf("Expected newBombardier(%v) to return %v, but got %v", e.in, e.out, r) t.Fail() } } } func TestCheckArgsUnsupportedURLScheme(t *testing.T) { c := config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("ftp://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "GET", body: "", } if c.checkArgs() != errUnsupportedScheme { t.Fail() } } func TestCheckArgsInvalidRequestMethod(t *testing.T) { c := config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "ABRACADABRA", body: "", } e := c.checkArgs() if e == nil { t.Fail() } if _, ok := e.(*invalidHTTPMethodError); !ok { t.Fail() } } func TestCheckArgsTestType(t *testing.T) { countedConfig := config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: nil, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "GET", body: "", } timedConfig := config{ numConns: defaultNumberOfConns, numReqs: nil, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "GET", body: "", } both := config{ numConns: defaultNumberOfConns, numReqs: &defaultNumberOfReqs, duration: &defaultTestDuration, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "GET", body: "", } defaultConfig := config{ numConns: defaultNumberOfConns, numReqs: nil, duration: nil, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: defaultTimeout, method: "GET", body: "", } if err := countedConfig.checkArgs(); err != nil || countedConfig.testType() != counted { t.Fail() } if err := timedConfig.checkArgs(); err != nil || timedConfig.testType() != timed { t.Fail() } if err := both.checkArgs(); err != nil || both.testType() != counted { t.Fail() } if err := defaultConfig.checkArgs(); err != nil || defaultConfig.testType() != timed || defaultConfig.duration != &defaultTestDuration { t.Fail() } } func TestTimeoutMillis(t *testing.T) { defaultConfig := config{ numConns: defaultNumberOfConns, numReqs: nil, duration: nil, url: ParseURLOrPanic("http://localhost:8080"), headers: nil, timeout: 2 * time.Second, method: "GET", body: "", } if defaultConfig.timeoutMillis() != 2000000 { t.Fail() } } func TestInvalidHTTPMethodError(t *testing.T) { invalidMethod := "NOSUCHMETHOD" want := "Unknown HTTP method: " + invalidMethod err := &invalidHTTPMethodError{invalidMethod} if got := err.Error(); got != want { t.Error(got, want) } } func TestClientTypToStringConversion(t *testing.T) { expectations := []struct { in clientTyp out string }{ {fhttp, "FastHTTP"}, {nhttp1, "net/http v1.x"}, {nhttp2, "net/http v2.0"}, {42, "unknown client"}, } for _, exp := range expectations { act := exp.in.String() if act != exp.out { t.Errorf("Expected %v, but got %v", exp.out, act) } } } func clientTypeFromString(s string) clientTyp { switch s { case "fasthttp": return fhttp case "http1": return nhttp1 case "http2": return nhttp2 default: return fhttp } } ================================================ FILE: dialer.go ================================================ package main import ( "context" "net" "sync/atomic" "time" ) type countingConn struct { net.Conn bytesRead, bytesWritten *int64 } func (cc *countingConn) Read(b []byte) (n int, err error) { n, err = cc.Conn.Read(b) if err == nil { atomic.AddInt64(cc.bytesRead, int64(n)) } return } func (cc *countingConn) Write(b []byte) (n int, err error) { n, err = cc.Conn.Write(b) if err == nil { atomic.AddInt64(cc.bytesWritten, int64(n)) } return } var fasthttpDialFunc = func( bytesRead, bytesWritten *int64, dialTimeout time.Duration, ) func(string) (net.Conn, error) { return func(address string) (net.Conn, error) { conn, err := net.DialTimeout("tcp", address, dialTimeout) if err != nil { return nil, err } wrappedConn := &countingConn{ Conn: conn, bytesRead: bytesRead, bytesWritten: bytesWritten, } return wrappedConn, nil } } var httpDialContextFunc = func( bytesRead, bytesWritten *int64, dialTimeout time.Duration, ) func(context.Context, string, string) (net.Conn, error) { dialer := &net.Dialer{Timeout: dialTimeout} return func(ctx context.Context, network, address string) (net.Conn, error) { conn, err := dialer.DialContext(ctx, network, address) if err != nil { return nil, err } wrappedConn := &countingConn{ Conn: conn, bytesRead: bytesRead, bytesWritten: bytesWritten, } return wrappedConn, nil } } ================================================ FILE: doc.go ================================================ /* Command line utility bombardier is a fast cross-platform HTTP benchmarking tool written in Go. Installation with Go 1.17+: go install github.com/codesenberg/bombardier@latest Installation with older versions of Go: go get -u github.com/codesenberg/bombardier Usage: bombardier [] Flags: --help Show context-sensitive help (also try --help-long and --help-man). --version Show application version. -c, --connections=125 Maximum number of concurrent connections -t, --timeout=2s Socket/request timeout -l, --latencies Print latency statistics -m, --method=GET Request method -b, --body="" Request body -f, --body-file="" File to use as request body -s, --stream Specify whether to stream body using chunked transfer encoding or to serve it from memory --cert="" Path to the client's TLS Certificate --key="" Path to the client's TLS Certificate Private Key -k, --insecure Controls whether a client verifies the server's certificate chain and host name -H, --header="K: V" ... HTTP headers to use(can be repeated) -n, --requests=[pos. int.] Number of requests -d, --duration=10s Duration of test -r, --rate=[pos. int.] Rate limit in requests per second --fasthttp Use fasthttp client --http1 Use net/http client with forced HTTP/1.x --http2 Use net/http client with enabled HTTP/2.0 -p, --print= Specifies what to output. Comma-separated list of values 'intro' (short: 'i'), 'progress' (short: 'p'), 'result' (short: 'r'). Examples: * i,p,r (prints everything) * intro,result (intro & result) * r (result only) * result (same as above) -q, --no-print Don't output anything -o, --format= Which format to use to output the result. is either a name (or its shorthand) of some format understood by bombardier or a path to the user-defined template, which uses Go's text/template syntax, prefixed with 'path:' string (without single quotes), i.e. "path:/some/path/to/your.template" or "path:C:\some\path\to\your.template" in case of Windows. Formats understood by bombardier are: * plain-text (short: pt) * json (short: j) Args: Target's URL For detailed documentation on user-defined templates see documentation for package github.com/codesenberg/bombardier/template. Link (GoDoc): https://godoc.org/github.com/codesenberg/bombardier/template */ package main ================================================ FILE: docs/CONTRIBUTING.md ================================================ ### Contribution Guidelines For relevant info on how to format commit messages and check the code before submitting pull requests see [PULL_REQUEST_TEMPLATE](https://github.com/codesenberg/bombardier/blob/master/.github/PULL_REQUEST_TEMPLATE.md). ### Reporting issues Please open an issue if you would like to discuss anything that could be improved, have a suggestion or want to report a bug. In latter case refer to [ISSUE_TEMPLATE](https://github.com/codesenberg/bombardier/blob/master/.github/ISSUE_TEMPLATE.md). ================================================ FILE: error_map.go ================================================ package main import ( "sort" "strconv" "sync" "sync/atomic" ) type errorMap struct { mu sync.RWMutex m map[string]*uint64 } func newErrorMap() *errorMap { em := new(errorMap) em.m = make(map[string]*uint64) return em } func (e *errorMap) add(err error) { s := err.Error() e.mu.RLock() c, ok := e.m[s] e.mu.RUnlock() if !ok { e.mu.Lock() c, ok = e.m[s] if !ok { c = new(uint64) e.m[s] = c } e.mu.Unlock() } atomic.AddUint64(c, 1) } func (e *errorMap) get(err error) uint64 { s := err.Error() e.mu.RLock() defer e.mu.RUnlock() c := e.m[s] if c == nil { return uint64(0) } return *c } func (e *errorMap) sum() uint64 { e.mu.RLock() defer e.mu.RUnlock() sum := uint64(0) for _, v := range e.m { sum += *v } return sum } type errorWithCount struct { error string count uint64 } func (ewc *errorWithCount) String() string { return "<" + ewc.error + ":" + strconv.FormatUint(ewc.count, decBase) + ">" } type errorsByFrequency []*errorWithCount func (ebf errorsByFrequency) Len() int { return len(ebf) } func (ebf errorsByFrequency) Less(i, j int) bool { return ebf[i].count > ebf[j].count } func (ebf errorsByFrequency) Swap(i, j int) { ebf[i], ebf[j] = ebf[j], ebf[i] } func (e *errorMap) byFrequency() errorsByFrequency { e.mu.RLock() byFreq := make(errorsByFrequency, 0, len(e.m)) for err, count := range e.m { byFreq = append(byFreq, &errorWithCount{err, *count}) } e.mu.RUnlock() sort.Sort(byFreq) return byFreq } ================================================ FILE: error_map_test.go ================================================ package main import ( "errors" "reflect" "testing" ) func TestErrorMapAdd(t *testing.T) { m := newErrorMap() err := errors.New("add") m.add(err) if c := m.get(err); c != 1 { t.Error(c) } } func TestErrorMapGet(t *testing.T) { m := newErrorMap() err := errors.New("get") if c := m.get(err); c != 0 { t.Error(c) } } func TestByFrequency(t *testing.T) { m := newErrorMap() a := errors.New("A") b := errors.New("B") c := errors.New("C") m.add(a) m.add(a) m.add(b) m.add(b) m.add(b) m.add(c) e := errorsByFrequency{ {"B", 3}, {"A", 2}, {"C", 1}, } if a := m.byFrequency(); !reflect.DeepEqual(a, e) { t.Logf("Expected: %+v", e) t.Logf("Got: %+v", a) t.Fail() } } func TestErrorWithCountToStringConversion(t *testing.T) { ewc := errorWithCount{"A", 1} exp := "" if act := ewc.String(); act != exp { t.Logf("Expected: %+v", exp) t.Logf("Got: %+v", act) t.Fail() } } func BenchmarkErrorMapAdd(b *testing.B) { m := newErrorMap() err := errors.New("benchmark") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { m.add(err) } }) } func BenchmarkErrorMapGet(b *testing.B) { m := newErrorMap() err := errors.New("benchmark") m.add(err) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { m.get(err) } }) } ================================================ FILE: flags.go ================================================ package main import ( "strconv" "time" ) const ( nilStr = "nil" ) type nullableUint64 struct { val *uint64 } func (n *nullableUint64) String() string { if n.val == nil { return nilStr } return strconv.FormatUint(*n.val, 10) } func (n *nullableUint64) Set(value string) error { res, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } n.val = new(uint64) *n.val = res return nil } type nullableDuration struct { val *time.Duration } func (n *nullableDuration) String() string { if n.val == nil { return nilStr } return n.val.String() } func (n *nullableDuration) Set(value string) error { res, err := time.ParseDuration(value) if err != nil { return err } n.val = &res return nil } type nullableString struct { val *string } func (n *nullableString) String() string { if n.val == nil { return nilStr } return *n.val } func (n *nullableString) Set(value string) error { n.val = new(string) *n.val = value return nil } ================================================ FILE: flags_test.go ================================================ package main import ( "math" "math/big" "strconv" "testing" "time" ) func TestNullableUint64ConversionToString(t *testing.T) { nilint := &nullableUint64{val: nil} if s := nilint.String(); s != "nil" { t.Errorf("Expected \"nil\", but got %v", s) } v := uint64(42) nonnilint := &nullableUint64{val: &v} if s, e := nonnilint.String(), strconv.FormatUint(v, 10); s != e { t.Errorf("Expected %v, but got %v", e, s) } } func TestNullableUint64Parsing(t *testing.T) { n := &nullableUint64{} if err := n.Set("-1"); err == nil { t.Error("Should fail on negative values") } if err := n.Set(""); err == nil { t.Error("Should fail on empty string") } b := big.NewInt(0) b.SetUint64(math.MaxUint64) b.Add(b, big.NewInt(1)) if err := n.Set(b.String()); err == nil { t.Error("Should fail on large values") } max := strconv.FormatUint(math.MaxUint64, 10) if err := n.Set(max); err != nil || *n.val != uint64(18446744073709551615) { t.Error("Shouldn't fail on max value") } } func TestNullableDurationConversionToString(t *testing.T) { nildur := &nullableDuration{val: nil} if s := nildur.String(); s != "nil" { t.Errorf("Expected \"nil\", but got %v", s) } d := time.Second nonnildir := &nullableDuration{val: &d} if s := nonnildir.String(); s != "1s" { t.Errorf("Expected 1s, but got %v", s) } } func TestNullableDurationParsing(t *testing.T) { d := &nullableDuration{} if err := d.Set(""); err == nil { t.Error("Should fail on empty string") } if err := d.Set("Wubba lubba dub dub!"); err == nil { t.Error("Should fail on incorrect values") } if err := d.Set("1s"); err != nil || *d.val != time.Second { t.Error("Shouldn't fail on correct values") } } func TestNullableStringConversionToString(t *testing.T) { ns := new(nullableString) if act := ns.String(); act != nilStr { t.Error("Unset nullableString should convert to \"nil\"") } someVal := "someval" if err := ns.Set(someVal); err != nil { t.Errorf("Couldn't set nullableString to %q", someVal) } if act := ns.String(); act != someVal { t.Errorf("Expected %q, but got %q", someVal, act) } } ================================================ FILE: format.go ================================================ package main import ( "fmt" ) type units struct { scale uint64 base string units []string } var ( binaryUnits = &units{ scale: 1024, base: "", units: []string{"KB", "MB", "GB", "TB", "PB"}, } timeUnitsUs = &units{ scale: 1000, base: "us", units: []string{"ms", "s"}, } timeUnitsS = &units{ scale: 60, base: "s", units: []string{"m", "h"}, } ) func formatUnits(n float64, m *units, prec int) string { amt := n unit := m.base scale := float64(m.scale) * 0.85 for i := 0; i < len(m.units) && amt >= scale; i++ { amt /= float64(m.scale) unit = m.units[i] } return fmt.Sprintf("%.*f%s", prec, amt, unit) } func formatBinary(n float64) string { return formatUnits(n, binaryUnits, 2) } func formatTimeUs(n float64) string { units := timeUnitsUs if n >= 1000000.0 { n /= 1000000.0 units = timeUnitsS } return formatUnits(n, units, 2) } ================================================ FILE: format_test.go ================================================ package main import ( "testing" ) const ( KB = 1024 MB = KB * 1024 GB = MB * 1024 K = 1000 M = K * 1000 ) func TestShouldFormatBinary(t *testing.T) { expectations := []struct { in float64 out string }{ {10.0, "10.00"}, {10.001, "10.00"}, {1.0 * KB, "1.00KB"}, {1.2 * KB, "1.20KB"}, {1.202 * KB, "1.20KB"}, {5 * KB, "5.00KB"}, {1.0 * MB, "1.00MB"}, {1.3 * MB, "1.30MB"}, {1.302 * MB, "1.30MB"}, {6 * MB, "6.00MB"}, {1.0 * GB, "1.00GB"}, {1.4 * GB, "1.40GB"}, {1.402 * GB, "1.40GB"}, {7 * GB, "7.00GB"}, } for _, e := range expectations { actual := formatBinary(e.in) expected := e.out if expected != actual { t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual) } } } func TestShouldFormatUs(t *testing.T) { expectations := []struct { in float64 out string }{ {20, "20.00us"}, {22.222, "22.22us"}, {20 * K, "20.00ms"}, {20 * M, "20.00s"}, {60 * M, "1.00m"}, {10 * 60 * M, "10.00m"}, {90 * 60 * M, "1.50h"}, } for _, e := range expectations { actual := formatTimeUs(e.in) expected := e.out if expected != actual { t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual) } } } ================================================ FILE: go.mod ================================================ module github.com/codesenberg/bombardier go 1.22 toolchain go1.24.0 require ( github.com/alecthomas/kingpin v2.2.6+incompatible github.com/cheggaaa/pb v1.0.29 github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964 github.com/goware/urlx v0.3.2 github.com/juju/ratelimit v1.0.2 github.com/satori/go.uuid v1.2.0 github.com/valyala/fasthttp v1.59.0 ) require ( github.com/PuerkitoBio/purell v1.2.1 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964 h1:9MVnbW3h0Dl4E2oADqwyvODphl9jY1r5HMtcB8U5mGs= github.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964/go.mod h1:82C6OyVM6eVk7qpBAZXE9uszHUuXWJMHHOeY+b/CSIA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo= github.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: headers.go ================================================ package main import ( "fmt" "strings" ) type header struct { key, value string } type headersList []header func (h *headersList) String() string { return fmt.Sprint(*h) } func (h *headersList) IsCumulative() bool { return true } func (h *headersList) Set(value string) error { res := strings.SplitN(value, ":", 2) if len(res) != 2 { return errInvalidHeaderFormat } *h = append(*h, header{ res[0], strings.Trim(res[1], " "), }) return nil } ================================================ FILE: headers_test.go ================================================ package main import ( "testing" ) func TestHeadersToStringConversion(t *testing.T) { expectations := []struct { in headersList out string }{ { []header{}, "[]", }, { []header{ {"Key1", "Value1"}, {"Key2", "Value2"}}, "[{Key1 Value1} {Key2 Value2}]", }, } for _, e := range expectations { actual := e.in.String() expected := e.out if expected != actual { t.Errorf("Expected \"%v\", but got \"%v\"", expected, actual) } } } func TestShouldErrorOnInvalidFormat(t *testing.T) { h := new(headersList) if err := h.Set("Yaba daba do"); err == nil { t.Error("Should fail on strings without colon") } } func TestShouldProperlyAddValidHeaders(t *testing.T) { h := new(headersList) for _, hs := range []string{"Key1: Value1", "Key2: Value2"} { if err := h.Set(hs); err != nil { t.Error(err) } } e := []header{{"Key1", "Value1"}, {"Key2", "Value2"}} for i, v := range *h { if e[i] != v { t.Fail() } } } func TestShouldTrimHeaderValues(t *testing.T) { h := new(headersList) if err := h.Set("Key: Value "); err != nil { t.Error(err) } if (*h)[0].key != "Key" || (*h)[0].value != "Value" { t.Fail() } } ================================================ FILE: internal/test_info.go ================================================ package internal import ( "math" "net/url" "sort" "time" ) // TestInfo holds information about what specification was used // to perform the test and results of the test. type TestInfo struct { Spec Spec Result Results } // Header represents HTTP header. type Header struct { Key, Value string } // Spec contains information about test performed. type Spec struct { NumberOfConnections uint64 TestType TestType NumberOfRequests uint64 TestDuration time.Duration Method string URL *url.URL Headers []Header Body string BodyFilePath string CertPath string KeyPath string Stream bool Timeout time.Duration ClientType ClientType Rate *uint64 } // RequestURL returns URL as string. func (s Spec) RequestURL() string { return s.URL.String() } // IsTimedTest tells if the test was limited by time. func (s Spec) IsTimedTest() bool { return s.TestType == ByTime } // IsTestWithNumberOfReqs tells if the test was limited by the number // of requests. func (s Spec) IsTestWithNumberOfReqs() bool { return s.TestType == ByNumberOfReqs } // IsFastHTTP tells whether fasthttp were used as HTTP client to // perform the test. func (s Spec) IsFastHTTP() bool { return s.ClientType == FastHTTP } // IsNetHTTPV1 tells whether Go's default net/http library and // HTTP/1.x were used to perform the test. func (s Spec) IsNetHTTPV1() bool { return s.ClientType == NetHTTP1 } // IsNetHTTPV2 tells whether Go's default net/http library and // HTTP/1.x (or HTTP/2.0, if possible) were used to perform the test. func (s Spec) IsNetHTTPV2() bool { return s.ClientType == NetHTTP2 } // Results holds results of the test. type Results struct { BytesRead, BytesWritten int64 TimeTaken time.Duration Req1XX, Req2XX, Req3XX, Req4XX, Req5XX uint64 Others uint64 Errors []ErrorWithCount Latencies ReadonlyUint64Histogram Requests ReadonlyFloat64Histogram } // ReadonlyUint64Histogram is a readonly histogram with uint64 keys type ReadonlyUint64Histogram interface { Get(uint64) uint64 VisitAll(func(uint64, uint64) bool) Count() uint64 } // ReadonlyFloat64Histogram is a readonly histogram with float64 keys type ReadonlyFloat64Histogram interface { Get(float64) uint64 VisitAll(func(float64, uint64) bool) Count() uint64 } // Throughput returns total throughput (read + write) in bytes per // second func (r Results) Throughput() float64 { return float64(r.BytesRead+r.BytesWritten) / r.TimeTaken.Seconds() } // LatenciesStats contains statistical information about latencies. type LatenciesStats struct { // These are in microseconds Mean float64 Stddev float64 Max float64 // This is map[0.0 <= p <= 1.0 (percentile)]microseconds Percentiles map[float64]uint64 } // LatenciesStats performs various statistical calculations on // latencies. func (r Results) LatenciesStats(percentiles []float64) *LatenciesStats { h := r.Latencies sum := uint64(0) count := uint64(0) max := uint64(0) pairs := make([]struct{ k, v uint64 }, 0, h.Count()) // Gather all the data h.VisitAll(func(f uint64, c uint64) bool { if f > max { max = f } sum += f * c count += c pairs = append(pairs, struct{ k, v uint64 }{f, c}) return true }) if count < 1 { return nil } // Calculate percentiles sort.Slice(pairs, func(i, j int) bool { return pairs[i].k < pairs[j].k }) percentilesMap := map[float64]uint64{} for _, pc := range percentiles { if _, calculated := percentilesMap[pc]; calculated { continue } if pc < 0 || pc > 1 { // Drop percentiles outside of [0, 1] range continue } rank := uint64(pc*float64(count) + 0.5) total := uint64(0) for _, p := range pairs { total += p.v if total >= rank { percentilesMap[pc] = p.k break } } } // Calculate mean and standard deviation mean := float64(sum) / float64(count) sumOfSquares := float64(0) h.VisitAll(func(f uint64, c uint64) bool { sumOfSquares += math.Pow(float64(f)-mean, 2) return true }) stddev := 0.0 if count > 2 { stddev = math.Sqrt(sumOfSquares / float64(count)) } return &LatenciesStats{ Mean: mean, Stddev: stddev, Max: float64(max), Percentiles: percentilesMap, } } // RequestsStats contains statistical information about requests. type RequestsStats struct { // These are in requests per second. Mean float64 Stddev float64 Max float64 // This is map[0.0 <= p <= 1.0 (percentile)](req-s per second) Percentiles map[float64]float64 } // RequestsStats performs various statistical calculations on // latencies. func (r Results) RequestsStats(percentiles []float64) *RequestsStats { h := r.Requests sum := float64(0) count := uint64(0) max := float64(0) pairs := make([]struct { k float64 v uint64 }, 0, h.Count()) // Gather all the data h.VisitAll(func(f float64, c uint64) bool { if math.IsInf(f, 0) || math.IsNaN(f) { return true } if f > max { max = f } sum += f * float64(c) count += c pairs = append(pairs, struct { k float64 v uint64 }{f, c}) return true }) if count < 1 { return nil } // Calculate percentiles sort.Slice(pairs, func(i, j int) bool { return pairs[i].k < pairs[j].k }) percentilesMap := map[float64]float64{} for _, pc := range percentiles { if _, calculated := percentilesMap[pc]; calculated { continue } if pc < 0 || pc > 1 { // Drop percentiles outside of [0, 1] range continue } rank := uint64(pc*float64(count) + 0.5) total := uint64(0) for _, p := range pairs { total += p.v if total >= rank { percentilesMap[pc] = p.k break } } } // Calculate mean and standard deviation mean := sum / float64(count) sumOfSquares := float64(0) h.VisitAll(func(f float64, c uint64) bool { if math.IsInf(f, 0) || math.IsNaN(f) { return true } sumOfSquares += math.Pow(f-mean, 2) return true }) stddev := 0.0 if count > 2 { const besselCorrection = 1.0 stddev = math.Sqrt(sumOfSquares / (float64(count) - besselCorrection)) } return &RequestsStats{ Mean: mean, Stddev: stddev, Max: max, Percentiles: percentilesMap, } } // ErrorWithCount contains error description alongside with number of // times this error occurred. type ErrorWithCount struct { Error string Count uint64 } // TestType represents the type of test that were performed. type TestType int const ( _ TestType = iota // ByTime is a test limited by durations. ByTime // ByNumberOfReqs is a test limited by number of requests // performed. ByNumberOfReqs ) // ClientType is the type of HTTP client used in test type ClientType int const ( // FastHTTP is fasthttp's HTTP client FastHTTP ClientType = iota // NetHTTP1 is Go's default HTTP client with forced HTTP/1.x NetHTTP1 // NetHTTP2 is Go's default HTTP client with HTTP/2.0 permitted. NetHTTP2 ) ================================================ FILE: limiter.go ================================================ package main import ( "math" "sync" "time" "github.com/juju/ratelimit" ) type token uint64 const ( brk token = iota cont ) type limiter interface { pace(<-chan struct{}) token } type nooplimiter struct{} func (n *nooplimiter) pace(<-chan struct{}) token { return cont } type bucketlimiter struct { limiter *ratelimit.Bucket timerPool *sync.Pool } func newBucketLimiter(rate uint64) limiter { fillInterval, quantum := estimate(rate, rateLimitInterval) return &bucketlimiter{ ratelimit.NewBucketWithQuantum( fillInterval, int64(quantum), int64(quantum), ), &sync.Pool{ New: func() interface{} { return time.NewTimer(math.MaxInt64) }, }, } } func (b *bucketlimiter) pace(done <-chan struct{}) (res token) { wd := b.limiter.Take(1) if wd <= 0 { return cont } timer := b.timerPool.Get().(*time.Timer) timer.Reset(wd) select { case <-timer.C: res = cont case <-done: res = brk } b.timerPool.Put(timer) return } ================================================ FILE: limiter_barrier_test.go ================================================ package main import ( "sync" "sync/atomic" "testing" ) func TestNoopLimiterCounterBarrierCombination(t *testing.T) { expectations := []uint64{ 1, 15, 50, 100, 150, 500, 1000, 1500, 5000, } done := make(chan struct{}) for _, count := range expectations { b := newCountingCompletionBarrier(count) var lim limiter = &nooplimiter{} counter := uint64(0) numParties := 10 var wg sync.WaitGroup wg.Add(numParties) for i := 0; i < numParties; i++ { go func() { defer wg.Done() for b.tryGrabWork() { lim.pace(done) atomic.AddUint64(&counter, 1) b.jobDone() } }() } wg.Wait() if counter != count { t.Error(count, counter) } } } func TestBucketLimiterCounterBarrierCombination(t *testing.T) { expectations := []struct { count, rate uint64 }{ {10, 100}, {10, 1000}, {100, 1000}, {100, 10000}, {1000, 10000}, {1000, 100000}, } done := make(chan struct{}) var expWg sync.WaitGroup expWg.Add(len(expectations)) for i := range expectations { exp := expectations[i] go func() { defer expWg.Done() b := newCountingCompletionBarrier(exp.count) lim := newBucketLimiter(exp.rate) counter := uint64(0) numParties := 10 var wg sync.WaitGroup wg.Add(numParties) for i := 0; i < numParties; i++ { go func() { defer wg.Done() for b.tryGrabWork() { lim.pace(done) atomic.AddUint64(&counter, 1) b.jobDone() } }() } wg.Wait() if counter != exp.count { t.Error(exp.count, counter) } }() } expWg.Wait() } ================================================ FILE: limiter_test.go ================================================ package main import ( "runtime" "sync" "sync/atomic" "testing" "time" ) const maxRps = 10000000 func TestNoopLimiter(t *testing.T) { var lim limiter = &nooplimiter{} done := make(chan struct{}) counter := uint64(0) var wg sync.WaitGroup wg.Add(int(defaultNumberOfConns)) for i := uint64(0); i < defaultNumberOfConns; i++ { go func() { defer wg.Done() for { res := lim.pace(done) if res != cont { t.Error("nooplimiter should always return cont") } atomic.AddUint64(&counter, 1) select { case <-done: return default: } } }() } time.Sleep(100 * time.Millisecond) close(done) wg.Wait() if counter == 0 { t.Error("no events happened") } } func BenchmarkNoopLimiter(bm *testing.B) { var lim limiter = &nooplimiter{} done := make(chan struct{}) bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU()) bm.ResetTimer() bm.RunParallel(func(pb *testing.PB) { for pb.Next() { lim.pace(done) } }) } func TestBucketLimiterLowRates(t *testing.T) { expectations := []struct { rate uint64 duration time.Duration }{ {1, 1 * time.Second}, {10, 1 * time.Second}, {15, 1 * time.Second}, {50, 1 * time.Second}, {100, 1 * time.Second}, {150, 1 * time.Second}, {500, 1 * time.Second}, {1000, 1 * time.Second}, {1500, 1 * time.Second}, {5000, 1 * time.Second}, } for i := range expectations { exp := expectations[i] lim := newBucketLimiter(exp.rate) done := make(chan struct{}) counter := uint64(0) waitChan := make(chan struct{}) go func() { defer func() { waitChan <- struct{}{} }() for lim.pace(done) == cont { counter++ } }() time.Sleep(exp.duration) close(done) select { case <-waitChan: case <-time.After(exp.duration + 100*time.Millisecond): t.Error("failed to complete: ", exp) return } expcounter := float64(exp.rate) * exp.duration.Seconds() var ( lowerBound = 0.5 * expcounter upperBound = 1.2*expcounter + 5 ) if float64(counter) < lowerBound || float64(counter) > upperBound { t.Errorf("(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)", lowerBound, counter, upperBound) } } } func TestBucketLimiterHighRates(t *testing.T) { expectations := []struct { rate uint64 duration time.Duration }{ {100000, 100 * time.Millisecond}, {150000, 100 * time.Millisecond}, {200000, 100 * time.Millisecond}, {500000, 100 * time.Millisecond}, {1000000, 100 * time.Millisecond}, } for i := range expectations { exp := expectations[i] lim := newBucketLimiter(exp.rate) counter := uint64(0) done := make(chan struct{}) waitChan := make(chan struct{}) go func() { defer func() { waitChan <- struct{}{} }() for lim.pace(done) == cont { counter++ } }() time.Sleep(exp.duration) close(done) select { case <-waitChan: case <-time.After(exp.duration + 50*time.Millisecond): t.Error("failed to complete: ", exp) return } expcounter := float64(exp.rate) * exp.duration.Seconds() var ( lowerBound = 0.5 * expcounter upperBound = 1.2*expcounter + 5 ) if float64(counter) < lowerBound || float64(counter) > upperBound { t.Errorf("(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)", lowerBound, counter, upperBound) } } } func BenchmarkBucketLimiter(bm *testing.B) { lim := newBucketLimiter(maxRps) done := make(chan struct{}) bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU()) bm.ResetTimer() bm.RunParallel(func(pb *testing.PB) { for pb.Next() { lim.pace(done) } }) } ================================================ FILE: proxy_reader.go ================================================ package main import "io" type proxyReader struct { io.Reader } ================================================ FILE: rateestimator.go ================================================ package main import ( "math/big" "time" ) const ( panicZeroRate = "rate can't be zero" panicNegativeAdjustTo = "adjustTo can't be negative or zero" ) func estimate(rate uint64, adjustTo time.Duration) (time.Duration, uint64) { if rate == 0 { panic(panicZeroRate) } if adjustTo <= 0 { panic(panicNegativeAdjustTo) } br := new(big.Int).SetUint64(rate) bd := new(big.Int).SetInt64(oneSecond.Nanoseconds()) gcd := new(big.Int).GCD(nil, nil, br, bd).Uint64() nr, nd := rate/gcd, uint64(oneSecond.Nanoseconds())/gcd adjustInt := uint64(adjustTo.Nanoseconds()) if nd >= adjustInt { return time.Duration(nd), nr } coef := adjustInt / nd return time.Duration(coef * nd), coef * nr } ================================================ FILE: rateestimator_test.go ================================================ package main import ( "testing" "time" ) func TestRateEstimatorPanicWithZeroRate(t *testing.T) { defer func() { pv, ok := recover().(string) if !ok { t.Error("expected string value") return } if pv != panicZeroRate { t.Error(panicZeroRate, pv) } }() _, _ = estimate(0, 10*time.Second) t.Error("should fail with rate == 0") } func TestRateEstimatorPanicWithNegativeAdjustTo(t *testing.T) { defer func() { pv, ok := recover().(string) if !ok { t.Error("expected string value") return } if pv != panicNegativeAdjustTo { t.Error(panicNegativeAdjustTo, pv) } }() _, _ = estimate(10, -10*time.Second) t.Error("should fail with adjustTo <= 0") } func TestRateEstimatorAccuracy(t *testing.T) { defer func() { rv := recover() if rv != nil { t.Error(rv) } }() expectations := []struct { rate uint64 adjustTo time.Duration expectedQuantum uint64 expectedFillInterval time.Duration }{ {1, 100 * time.Millisecond, 1, 1 * time.Second}, {1, 1000 * time.Millisecond, 1, 1 * time.Second}, {1, 2000 * time.Millisecond, 2, 2 * time.Second}, {1, 3000 * time.Millisecond, 3, 3 * time.Second}, {4, 3000 * time.Millisecond, 12, 3 * time.Second}, {10000, 100 * time.Millisecond, 1000, 100 * time.Millisecond}, {100000, 100 * time.Millisecond, 10000, 100 * time.Millisecond}, {1000000, 100 * time.Millisecond, 100000, 100 * time.Millisecond}, } for _, exp := range expectations { actualFillInterval, actualQuantum := estimate(exp.rate, exp.adjustTo) if actualFillInterval != exp.expectedFillInterval || actualQuantum != exp.expectedQuantum { t.Log("Expected: ", exp.expectedQuantum, exp.expectedFillInterval) t.Log("Actual: ", actualQuantum, actualFillInterval) t.Fail() } } } ================================================ FILE: template/doc.go ================================================ /* Package template documents the way user-defined output templates are ment to be used. User-defined templates use Go's text/template package, so you might want to check its documentation first. There are a bunch of helper methods available inside a template besides those described in aforementioned documentation, namely: - URLString() Returns the URL string used for the load test. - WithLatencies() Tells whether --latencies flag were activated. - FormatBinary(numberOfBytes float64) string Converts bytes to kilo-, mega-, giga-, etc.- bytes, and appends appropriate suffix "KB", "MB", "GB", etc. - FormatTimeUs(us float64) string Converts microseconds to milliseconds, seconds, minutes or hours and appends appropriate suffix. - FormatTimeUsUint64(us uint64) string Same as above, but for uint64, since type conversions are not available in templates. - FloatsToArray(ps ...float64) []float64 Converts a bunch of floats into array, since, again, type conversions are not available in templates. - Multiply(num, coeff float64) float64 Arithmetics are not available inside of templates either. - StringToBytes(s string) []byte Convenience function to convert string to []byte. - UUIDV1() (UUID, error) Generates UUID Version 1, based on timestamp and MAC address (RFC 4122) - UUIDV2(domain byte) (UUID, error) Generates UUID Version 2, based on timestamp, MAC address and POSIX UID/GID (DCE 1.1) - UUIDV3(ns UUID, name string) UUID Generates UUID Version 3, based on MD5 hashing (RFC 4122) - UUIDV4() (UUID, error) Generates UUID Version 4, based on random numbers (RFC 4122) - UUIDV5(ns UUID, name string) UUID Generates UUID Version 5, based on SHA-1 hashing (RFC 4122) The structure that gets passed to the template is documented in the package github.com/codesenberg/bombardier/internal. The structure of interest is TestInfo. It basically consists of Spec and Result fields, the former contains various information about the test (number of connections, URL, HTTP method, headers, body, rate, etc.) performed, while the latter contains results obtained during the execution of this test (bytes read/written, time taken, RPS, etc.). Link to GoDoc for the structure used in template: https://godoc.org/github.com/codesenberg/bombardier/internal#TestInfo Examples of templates can be found in: https://github.com/codesenberg/bombardier/blob/master/templates.go */ package template ================================================ FILE: templates.go ================================================ package main import "strings" var ( templates = map[string][]byte{ "plain-text": []byte(plainTextTemplate), "json": []byte(jsonTemplate), } ) type format interface{} type knownFormat string func (kf knownFormat) template() []byte { return templates[string(kf)] } type filePath string type userDefinedTemplate filePath func formatFromString(formatSpec string) format { const prefix = "path:" if strings.HasPrefix(formatSpec, prefix) { return userDefinedTemplate(formatSpec[len(prefix):]) } switch formatSpec { case "pt", "plain-text": return knownFormat("plain-text") case "j", "json": return knownFormat("json") } // nil represents unknown format return nil } const ( plainTextTemplate = ` {{- printf "%10v %10v %10v %10v" "Statistics" "Avg" "Stdev" "Max" }} {{ with .Result.RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }} {{- printf " %-10v %10.2f %10.2f %10.2f" "Reqs/sec" .Mean .Stddev .Max -}} {{ else }} {{- print " There wasn't enough data to compute statistics for requests." }} {{ end }} {{ with .Result.LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }} {{- printf " %-10v %10v %10v %10v" "Latency" (FormatTimeUs .Mean) (FormatTimeUs .Stddev) (FormatTimeUs .Max) }} {{- if WithLatencies }} {{- "\n Latency Distribution" }} {{- range $pc, $lat := .Percentiles }} {{- printf "\n %2.0f%% %10s" (Multiply $pc 100) (FormatTimeUsUint64 $lat) -}} {{ end -}} {{ end }} {{ else }} {{- print " There wasn't enough data to compute statistics for latencies." }} {{ end -}} {{ with .Result -}} {{ " HTTP codes:" }} {{ printf " 1xx - %v, 2xx - %v, 3xx - %v, 4xx - %v, 5xx - %v" .Req1XX .Req2XX .Req3XX .Req4XX .Req5XX }} {{- printf "\n others - %v" .Others }} {{- with .Errors }} {{- "\n Errors:"}} {{- range . }} {{- printf "\n %10v - %v" .Error .Count }} {{- end -}} {{ end -}} {{ end }} {{ printf " %-10v %10v/s\n" "Throughput:" (FormatBinary .Result.Throughput)}}` jsonTemplate = `{"spec":{ {{- with .Spec -}} "numberOfConnections":{{ .NumberOfConnections }} {{- if .IsTimedTest -}} ,"testType":"timed","testDurationSeconds":{{ .TestDuration.Seconds }} {{- else -}} ,"testType":"number-of-requests","numberOfRequests":{{ .NumberOfRequests }} {{- end -}} ,"method":"{{ .Method }}","url":{{ .RequestURL | printf "%q" }} {{- with .Headers -}} ,"headers":[ {{- range $index, $header := . -}} {{- if ne $index 0 -}},{{- end -}} {"key":{{ .Key | printf "%q" }},"value":{{ .Value | printf "%q" }}} {{- end -}} ] {{- end -}} {{- if .BodyFilePath -}} ,"bodyFilePath":{{ .BodyFilePath | printf "%q" }} {{- else -}} ,"body":{{ .Body | printf "%q" }} {{- end -}} {{- if .CertPath -}} ,"certPath":{{ .CertPath | printf "%q" }} {{- end -}} {{- if .KeyPath -}} ,"keyPath":{{ .KeyPath | printf "%q" }} {{- end -}} ,"stream":{{ .Stream }},"timeoutSeconds":{{ .Timeout.Seconds }} {{- if .IsFastHTTP -}} ,"client":"fasthttp" {{- end -}} {{- if .IsNetHTTPV1 -}} ,"client":"net/http.v1" {{- end -}} {{- if .IsNetHTTPV2 -}} ,"client":"net/http.v2" {{- end -}} {{- with .Rate -}} ,"rate":{{ . }} {{- end -}} {{- end -}} }, {{- with .Result -}} "result":{"bytesRead":{{ .BytesRead -}} ,"bytesWritten":{{ .BytesWritten -}} ,"timeTakenSeconds":{{ .TimeTaken.Seconds -}} ,"req1xx":{{ .Req1XX -}} ,"req2xx":{{ .Req2XX -}} ,"req3xx":{{ .Req3XX -}} ,"req4xx":{{ .Req4XX -}} ,"req5xx":{{ .Req5XX -}} ,"others":{{ .Others -}} {{- with .Errors -}} ,"errors":[ {{- range $index, $error := . -}} {{- if ne $index 0 -}},{{- end -}} {"description":{{ .Error | printf "%q" }},"count":{{ .Count }}} {{- end -}} ] {{- end -}} {{- with .LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}} ,"latency":{"mean":{{ .Mean -}} ,"stddev":{{ .Stddev -}} ,"max":{{ .Max -}} {{- if WithLatencies -}} ,"percentiles":{ {{- range $pc, $lat := .Percentiles }} {{- if ne $pc 0.5 -}},{{- end -}} {{- printf "\"%2.0f\":%d" (Multiply $pc 100) $lat -}} {{- end -}} } {{- end -}} } {{- end -}} {{- with .RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}} ,"rps":{"mean":{{ .Mean -}} ,"stddev":{{ .Stddev -}} ,"max":{{ .Max -}} ,"percentiles":{ {{- range $pc, $rps := .Percentiles }} {{- if ne $pc 0.5 -}},{{- end -}} {{- printf "\"%2.0f\":%f" (Multiply $pc 100) $rps -}} {{- end -}} }} {{- end -}} }} {{- end -}}` ) ================================================ FILE: testbody.txt ================================================ abracadabra ================================================ FILE: testclient.cert ================================================ -----BEGIN CERTIFICATE----- MIIFITCCAwmgAwIBAgIJAMx2fpQ+fhOZMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV BAMMG0JvbWJhcmRpZXIgQ2xpZW50IFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2MDda GA8zMDE2MDUyOTA2MzYwN1owJjEkMCIGA1UEAwwbQm9tYmFyZGllciBDbGllbnQg VGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1TE9F36z fsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLszEjmL3Gtux02GaqZ8u3GdfeZ/ Am2KqFnLq/YiZwYJpwpB54PAtKQqCiIcAPCENZdPzuya4bWTP+/bLiLdDY0kJhxM rReufBL+xBOFrRhLcsW+tECGsu+d39o7JY7oWnm7IQYX7cxK1JgaFL5kmUuaoJN0 iJhB7V3JIQJMC68Yr5dzrYdzwzc1uxm43Y696HYAPkygf41ZoHo5UKWCI9V9M0iz 8oUoWrLdlXHOVaQKpPV9+aQYBiG1KNePvWZ4PCvukXv+zgLP1SvvqNTQKi2HCV37 RZQd0M0Do9aqrtlDrQLeKE39XZQMBKrype7Vr5JcnXMlaC4A8WFF/+cl11V6m4eY 8GLnoTv+l/G2Hbjwm6/oLPCvrErqd0M+6JK9jXnXHwNr05FpEVqwdYZ5K10bYBBU wY+oZM8sGrT0Hd0N0PHtcs2eZ6yYLNrAaTvZT3w/sFgEqrDKn6c5WJdKO9PKSvbb E7whD+WkZPeN2ndh+lGYAEnVzyzVgKmNPOPGFa244QEIUpeZv4d+ivPN9eOwgAVH l4Ms9+u38VjuIE5LNZCiqOlIzaMBD+dPbOpx7rtEacMs8UgyMVIGPiJcsqzw++Ji pWHOKRAi82TLLcqt30wgIjCfu49hFPbnfIMCAwEAAaNQME4wHQYDVR0OBBYEFAyp 8Do7nsAhXWAPamH+Vn8ntZ3pMB8GA1UdIwQYMBaAFAyp8Do7nsAhXWAPamH+Vn8n tZ3pMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAKZcYn3AlK77Yg9K THZym7Cmr07HA9138PnlMLlVTqJnwAU9nGZ41h/Vth1nZLy9SppefuFwXrmpI5W4 lGqY/t+saEgSzwI8cQ1F6AP0XyeQaGcqNpwHnBNF414Um42Emu+lbBJuYFV53Pgq nhxGUD7/PbUHSCkTj/LOuvVOeXKMl2muuMpk2lwJnQNAv4mB29F+6mwxntxuSk2O NFk13QXB5ii1NawT16yW/bYSMZsLQKvz/e/9B+tCjGdbQ8ga3OcEzjGgKRhh4Z/E gdgl7vtu+ZkE+LHzP9V2lUVlbSP4UhQiZJSEawItSwKlIq/hTjra1kby8UbGMtLp /vjAznZleF5nG4eQbbM3wDtnpWim0ODXH+p1JNPuK/6GzbWh4eykHQ6JElci26Hg ug+/7iGAxBcCiPPRX1vT92rnKAIicqzZ1UInlun2X01+pxFo+xGKCYDYAJwyS8Y/ Zy4eFbXJJMGznTef7fNhtBLk6jni90YWBbLgaY62T+y1gYsyfYzQE28Idc7LiKOu DNEfa4nXAB1zIg9JMKzqbDy4WN0B8tKbBBQIwtRn5DyW7YsmgsgZt6Gs+ochvZdh 5Zlr6T6nvFMunVGNUZP901Xa+HYqCTV7PcrvCDVMOfiNkmrlWf6amFTzL8kBELCC 0O6WGsL6rVCeSVy2BPSna0GzH36g -----END CERTIFICATE----- ================================================ FILE: testclient.key ================================================ -----BEGIN RSA PRIVATE KEY----- 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 -----END RSA PRIVATE KEY----- ================================================ FILE: testserver.cert ================================================ -----BEGIN CERTIFICATE----- MIIFITCCAwmgAwIBAgIJALu5MYN2H+2PMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV BAMMG0JvbWJhcmRpZXIgU2VydmVyIFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2NTRa GA8zMDE2MDUyOTA2MzY1NFowJjEkMCIGA1UEAwwbQm9tYmFyZGllciBTZXJ2ZXIg VGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1laXP9o5 eo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1LIq0ECJ3+JzGrzIkYjrj6an0c 3bV5GDW/XA6yH4iMV36ADc+D+SWixQQkkWNrIL3nTHnnxOFbml4uCCDEV5TGWv8Z GNraUPPSIZAM/OO4uKTMHKiacpnGZ8ZqUuJ0E0F3OHoLwxpJsAhiKv18iy7mHvva PBCQ/aq8LYUOLRwiL019EVx+LFuMj9Ugc9G5lYHnn0jI/AcMdWeXil01aFEFtGpG S7b4FV5d3x1C8l1PDDWQfuBzu4wrztnCUnvQyUTZAmBJFXt2V5MxO6p69qvGav// IdPTxToeaz0oil+O+7cJ4gqjUBsr8xPuGLYjytZ2KJEqTIJJogXFylpwDWIvbXBw pFhDsuE9J2oz9gPC8R54lb93cuNAeGrWZfoJ8qSzOsO4CKgasdTTrv3uFTgnovh4 qJ4SaRjOtiSmWLZhy7fSG86kB+ZuGkEOUJiFok2aNJL1UI10QuSc095JLFnqZ3LP DG3gkdwqmYMYFnSKv43Z+azO1+t5BUSNyddSb2ZEF4d5J/UzN/D5XAxgJJoyfMK7 vHfOlglnfBkSdyxw1/BbOPLBFVDWinV33sabtgEismgdDaYjActBuRdl1md4BPRJ Z714Noquv7qcOYXNuqdhLNWjWh4d0HwA0yECAwEAAaNQME4wHQYDVR0OBBYEFB0Y it+Qidk1AgL+N00nyYDWgeEFMB8GA1UdIwQYMBaAFB0Yit+Qidk1AgL+N00nyYDW geEFMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABjxo7L4N+3lsHya 14dk8ycssuHkJvE2PhbandywV0E5bfqkoVGOmSVw50BIkpQ3WMTwACJZsjeTF1OJ CtDHSx3EJ3wPOMChAA06QdWSwG6Gxi6NYLrZ+60VjX3f64zwvmasD2xHT20icqAR H/405qzv5MyELvDzD9u+agdIvf2yXGywGiT8p1yPQZX5pn/1omWZEMSNICOzhFqv U6FfYOGu/g+FJYmV5JRitMBNcr1sTJI0eHEYWW/d7yxBCtmoN4UoH1TZepijb65n CT9xl9WX4CRGiG6/T4wcYmL2Q8OwjEn/JNUCSxU9tcyJr66JIIx4BUhUJ012BPXu BbohBaNA8ygMpt1sweBteVCC5O/O1J7YKYG1o0J2Fd4DLol+bWfM8IijoT23XYF3 I5fcf/Y61iAxx9PqCe7BsRsPi7WXrPxZJlfocXIWbdwVwpQUngjQjiVgdcPH7LnB NI1E2PDcDVJDVsS7XB/zK2nyY31DlQ8QZYpOzIeHjy4UcepClzk8JqeIQEmh01S7 p7fIIt51N2s2TpvZGC/wGL/0iFn/4mwyXvH9RZn8AUGBWrdB/kY/DD+Nmz8tHtlH PpM4uDdRwh+Ks62Rw1qde5xIZ82PjLxJ+P79rQ+wYejmfenLr1PPnl1q0W/MT8gT Puxw1k5iEsR8O9CjyEYP6bhQoZfb -----END CERTIFICATE----- ================================================ FILE: testserver.key ================================================ -----BEGIN RSA PRIVATE KEY----- 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 -----END RSA PRIVATE KEY-----