[
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "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.\n\n### What version of bombardier are you using?\nHash of the commit, like \n\n[00d7965d6cae34c62042abb0f6c45c45b870dcf3](https://github.com/codesenberg/bombardier/commit/00d7965d6cae34c62042abb0f6c45c45b870dcf3)\n\nin case you've built _bombardier_ yourself or version obtained by\n```\nbombardier --version\n```\nin case you are using binaries.\n\n### What operating system and processor architecture are you using (if relevant)?\nExamples are `windows/amd64`, `linux/amd64`, `darwin/amd64`, etc.\n\n### What did you do?\n\nDescribe steps that can be used to reproduce the error.\n\n### What you expected to happen?\n\n### What actually happened?\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "Before submitting a pull request be sure to check the code with [gometalinter](https://github.com/alecthomas/gometalinter).\nThis can save a considerable amount of time during code review.\n\nGenerally, try to follow this format when writing commit messages:\n```\n<name of the subsystem of bombardier if applicable or \"all\">: <short description of changes>\n\n<A more elaborate description of the changes and maybe some explanations go here.>\n\n<Fixes #<number of issue>, updates #<number of issue>, closes #<number of issue>. If applicable.>\n```\n\nExamples of such commit messages can be found in the commit log of this project or \n[in this section](https://golang.org/doc/contribute.html#commit_changes) of Go's Contribution Guidelines,\nfrom which this format was adopted.\n\nThe pull request itself can contain a short description of changes made, questions or provide some other information, etc.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\ncover.*\nbombardier\nbombardier-*"
  },
  {
    "path": ".semaphore/semaphore.yml",
    "content": "version: v1.0\nname: codesenberg/bombardier\nagent:\n  machine:\n    type: e1-standard-2\n    os_image: ubuntu2004\nblocks:\n  - name: Test\n    task:\n      prologue:\n        commands:\n          - checkout\n          - go install gotest.tools/gotestsum@latest\n      jobs:\n        - name: Test go 1.21\n          commands:\n            - sem-version go 1.21\n            - gotestsum --junitfile report.xml ./...\n        - name: Test go 1.22\n          commands:\n            - sem-version go 1.22\n            - gotestsum --junitfile report.xml ./...\n      epilogue:\n        always:\n          commands:\n            - '[[ -f report.xml ]] && test-results publish report.xml'\nafter_pipeline:\n  task:\n    jobs:\n      - name: Publish test results\n        commands:\n          - test-results gen-pipeline-report\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Максим Федосеев\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 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)\r\n![Logo](https://raw.githubusercontent.com/codesenberg/bombardier/master/img/logo.png)\r\nbombardier 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. \r\n\r\nWith `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.\r\n\r\n## Installation\r\nYou can grab binaries in the [releases](https://github.com/codesenberg/bombardier/releases) section.\r\nAlternatively, to get latest and greatest run:\r\n\r\nGo 1.18+: `go install github.com/codesenberg/bombardier@latest`\r\n\r\n## Usage\r\n```\r\nbombardier [<flags>] <url>\r\n```\r\n\r\nFor a more detailed information about flags consult [GoDoc](http://godoc.org/github.com/codesenberg/bombardier).\r\n\r\n## Known issues\r\nAFAIK, it's impossible to pass Host header correctly with `fasthttp`, you can use `net/http`(`--http1`/`--http2` flags) to workaround this issue.\r\n\r\n## Examples\r\nExample of running `bombardier` against [this server](https://godoc.org/github.com/codesenberg/bombardier/cmd/utils/simplebenchserver):\r\n```\r\n> bombardier -c 125 -n 10000000 http://localhost:8080\r\nBombarding http://localhost:8080 with 10000000 requests using 125 connections\r\n 10000000 / 10000000 [============================================] 100.00% 37s Done!\r\nStatistics        Avg      Stdev        Max\r\n  Reqs/sec    264560.00   10733.06     268434\r\n  Latency      471.00us   522.34us    51.00ms\r\n  HTTP codes:\r\n    1xx - 0, 2xx - 10000000, 3xx - 0, 4xx - 0, 5xx - 0\r\n    others - 0\r\n  Throughput:   292.92MB/s\r\n```\r\nOr, against a realworld server(with latency distribution):\r\n```\r\n> bombardier -c 200 -d 10s -l http://ya.ru\r\nBombarding http://ya.ru for 10s using 200 connections\r\n[=========================================================================] 10s Done!\r\nStatistics        Avg      Stdev        Max\r\n  Reqs/sec      6607.00     524.56       7109\r\n  Latency       29.86ms     5.36ms   305.02ms\r\n  Latency Distribution\r\n     50%    28.00ms\r\n     75%    32.00ms\r\n     90%    34.00ms\r\n     99%    48.00ms\r\n  HTTP codes:\r\n    1xx - 0, 2xx - 0, 3xx - 66561, 4xx - 0, 5xx - 0\r\n    others - 5\r\n  Errors:\r\n    dialing to the given TCP address timed out - 5\r\n  Throughput:     3.06MB/s\r\n```\r\n"
  },
  {
    "path": "args_parser.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin\"\n\t\"github.com/goware/urlx\"\n)\n\ntype argsParser interface {\n\tparse([]string) (config, error)\n}\n\ntype kingpinParser struct {\n\tapp *kingpin.Application\n\n\turl string\n\n\tnumReqs           *nullableUint64\n\tduration          *nullableDuration\n\theaders           *headersList\n\tnumConns          uint64\n\ttimeout           time.Duration\n\tlatencies         bool\n\tinsecure          bool\n\tdisableKeepAlives bool\n\tmethod            string\n\tbody              string\n\tbodyFilePath      string\n\tstream            bool\n\tcertPath          string\n\tkeyPath           string\n\trate              *nullableUint64\n\tclientType        clientTyp\n\n\tprintSpec *nullableString\n\tnoPrint   bool\n\n\tformatSpec string\n}\n\nfunc newKingpinParser() argsParser {\n\tkparser := &kingpinParser{\n\t\tnumReqs:      new(nullableUint64),\n\t\tduration:     new(nullableDuration),\n\t\theaders:      new(headersList),\n\t\tnumConns:     defaultNumberOfConns,\n\t\ttimeout:      defaultTimeout,\n\t\tlatencies:    false,\n\t\tmethod:       \"GET\",\n\t\tbody:         \"\",\n\t\tbodyFilePath: \"\",\n\t\tstream:       false,\n\t\tcertPath:     \"\",\n\t\tkeyPath:      \"\",\n\t\tinsecure:     false,\n\t\turl:          \"\",\n\t\trate:         new(nullableUint64),\n\t\tclientType:   fhttp,\n\t\tprintSpec:    new(nullableString),\n\t\tnoPrint:      false,\n\t\tformatSpec:   \"plain-text\",\n\t}\n\n\tapp := kingpin.New(\"\", \"Fast cross-platform HTTP benchmarking tool\").\n\t\tVersion(\"bombardier version \" + version + \" \" + runtime.GOOS + \"/\" +\n\t\t\truntime.GOARCH)\n\tapp.Flag(\"connections\", \"Maximum number of concurrent connections\").\n\t\tShort('c').\n\t\tPlaceHolder(strconv.FormatUint(defaultNumberOfConns, decBase)).\n\t\tUint64Var(&kparser.numConns)\n\tapp.Flag(\"timeout\", \"Socket/request timeout\").\n\t\tPlaceHolder(defaultTimeout.String()).\n\t\tShort('t').\n\t\tDurationVar(&kparser.timeout)\n\tapp.Flag(\"latencies\", \"Print latency statistics\").\n\t\tShort('l').\n\t\tBoolVar(&kparser.latencies)\n\tapp.Flag(\"method\", \"Request method\").\n\t\tPlaceHolder(\"GET\").\n\t\tShort('m').\n\t\tStringVar(&kparser.method)\n\tapp.Flag(\"body\", \"Request body\").\n\t\tDefault(\"\").\n\t\tShort('b').\n\t\tStringVar(&kparser.body)\n\tapp.Flag(\"body-file\", \"File to use as request body\").\n\t\tDefault(\"\").\n\t\tShort('f').\n\t\tStringVar(&kparser.bodyFilePath)\n\tapp.Flag(\"stream\", \"Specify whether to stream body using \"+\n\t\t\"chunked transfer encoding or to serve it from memory\").\n\t\tShort('s').\n\t\tBoolVar(&kparser.stream)\n\tapp.Flag(\"cert\", \"Path to the client's TLS Certificate\").\n\t\tDefault(\"\").\n\t\tStringVar(&kparser.certPath)\n\tapp.Flag(\"key\", \"Path to the client's TLS Certificate Private Key\").\n\t\tDefault(\"\").\n\t\tStringVar(&kparser.keyPath)\n\tapp.Flag(\"insecure\",\n\t\t\"Controls whether a client verifies the server's certificate\"+\n\t\t\t\" chain and host name\").\n\t\tShort('k').\n\t\tBoolVar(&kparser.insecure)\n\tapp.Flag(\"disableKeepAlives\",\n\t\t\"Disable HTTP keep-alive. For fasthttp use -H 'Connection: close'\").\n\t\tShort('a').\n\t\tBoolVar(&kparser.disableKeepAlives)\n\n\tapp.Flag(\"header\", \"HTTP headers to use(can be repeated)\").\n\t\tPlaceHolder(\"\\\"K: V\\\"\").\n\t\tShort('H').\n\t\tSetValue(kparser.headers)\n\tapp.Flag(\"requests\", \"Number of requests\").\n\t\tPlaceHolder(\"[pos. int.]\").\n\t\tShort('n').\n\t\tSetValue(kparser.numReqs)\n\tapp.Flag(\"duration\", \"Duration of test\").\n\t\tPlaceHolder(defaultTestDuration.String()).\n\t\tShort('d').\n\t\tSetValue(kparser.duration)\n\n\tapp.Flag(\"rate\", \"Rate limit in requests per second\").\n\t\tPlaceHolder(\"[pos. int.]\").\n\t\tShort('r').\n\t\tSetValue(kparser.rate)\n\n\tapp.Flag(\"fasthttp\", \"Use fasthttp client\").\n\t\tAction(func(*kingpin.ParseContext) error {\n\t\t\tkparser.clientType = fhttp\n\t\t\treturn nil\n\t\t}).\n\t\tBool()\n\tapp.Flag(\"http1\", \"Use net/http client with forced HTTP/1.x\").\n\t\tAction(func(*kingpin.ParseContext) error {\n\t\t\tkparser.clientType = nhttp1\n\t\t\treturn nil\n\t\t}).\n\t\tBool()\n\tapp.Flag(\"http2\", \"Use net/http client with enabled HTTP/2.0\").\n\t\tAction(func(*kingpin.ParseContext) error {\n\t\t\tkparser.clientType = nhttp2\n\t\t\treturn nil\n\t\t}).\n\t\tBool()\n\n\tapp.Flag(\n\t\t\"print\", \"Specifies what to output. Comma-separated list of values\"+\n\t\t\t\" 'intro' (short: 'i'), 'progress' (short: 'p'),\"+\n\t\t\t\" 'result' (short: 'r'). Examples:\"+\n\t\t\t\"\\n\\t* i,p,r (prints everything)\"+\n\t\t\t\"\\n\\t* intro,result (intro & result)\"+\n\t\t\t\"\\n\\t* r (result only)\"+\n\t\t\t\"\\n\\t* result (same as above)\").\n\t\tPlaceHolder(\"<spec>\").\n\t\tShort('p').\n\t\tSetValue(kparser.printSpec)\n\tapp.Flag(\"no-print\", \"Don't output anything\").\n\t\tShort('q').\n\t\tBoolVar(&kparser.noPrint)\n\n\tapp.Flag(\"format\", \"Which format to use to output the result. \"+\n\t\t\"<spec> is either a name (or its shorthand) of some format \"+\n\t\t\"understood by bombardier or a path to the user-defined template, \"+\n\t\t\"which uses Go's text/template syntax, prefixed with 'path:' string \"+\n\t\t\"(without single quotes), i.e. \\\"path:/some/path/to/your.template\\\" \"+\n\t\t\" or \\\"path:C:\\\\some\\\\path\\\\to\\\\your.template\\\" in case of Windows. \"+\n\t\t\"Formats understood by bombardier are:\"+\n\t\t\"\\n\\t* plain-text (short: pt)\"+\n\t\t\"\\n\\t* json (short: j)\").\n\t\tPlaceHolder(\"<spec>\").\n\t\tShort('o').\n\t\tStringVar(&kparser.formatSpec)\n\n\tapp.Arg(\"url\", \"Target's URL\").Required().\n\t\tStringVar(&kparser.url)\n\n\tkparser.app = app\n\treturn argsParser(kparser)\n}\n\nfunc (k *kingpinParser) parse(args []string) (config, error) {\n\tk.app.Name = args[0]\n\t_, err := k.app.Parse(args[1:])\n\tif err != nil {\n\t\treturn emptyConf, err\n\t}\n\tpi, pp, pr := true, true, true\n\tif k.printSpec.val != nil {\n\t\tpi, pp, pr, err = parsePrintSpec(*k.printSpec.val)\n\t\tif err != nil {\n\t\t\treturn emptyConf, err\n\t\t}\n\t}\n\tif k.noPrint {\n\t\tpi, pp, pr = false, false, false\n\t}\n\tformat := formatFromString(k.formatSpec)\n\tif format == nil {\n\t\treturn emptyConf, fmt.Errorf(\n\t\t\t\"unknown format or invalid format spec %q\", k.formatSpec,\n\t\t)\n\t}\n\turl, err := urlx.Parse(k.url)\n\tif err != nil {\n\t\treturn emptyConf, err\n\t}\n\treturn config{\n\t\tnumConns:          k.numConns,\n\t\tnumReqs:           k.numReqs.val,\n\t\tduration:          k.duration.val,\n\t\turl:               url,\n\t\theaders:           k.headers,\n\t\ttimeout:           k.timeout,\n\t\tmethod:            k.method,\n\t\tbody:              k.body,\n\t\tbodyFilePath:      k.bodyFilePath,\n\t\tstream:            k.stream,\n\t\tkeyPath:           k.keyPath,\n\t\tcertPath:          k.certPath,\n\t\tprintLatencies:    k.latencies,\n\t\tinsecure:          k.insecure,\n\t\tdisableKeepAlives: k.disableKeepAlives,\n\t\trate:              k.rate.val,\n\t\tclientType:        k.clientType,\n\t\tprintIntro:        pi,\n\t\tprintProgress:     pp,\n\t\tprintResult:       pr,\n\t\tformat:            format,\n\t}, nil\n}\n\nfunc parsePrintSpec(spec string) (bool, bool, bool, error) {\n\tpi, pp, pr := false, false, false\n\tif spec == \"\" {\n\t\treturn false, false, false, errEmptyPrintSpec\n\t}\n\tparts := strings.Split(spec, \",\")\n\tpartsCount := 0\n\tfor _, p := range parts {\n\t\tswitch p {\n\t\tcase \"i\", \"intro\":\n\t\t\tpi = true\n\t\tcase \"p\", \"progress\":\n\t\t\tpp = true\n\t\tcase \"r\", \"result\":\n\t\t\tpr = true\n\t\tdefault:\n\t\t\treturn false, false, false,\n\t\t\t\tfmt.Errorf(\"%q is not a valid part of print spec\", p)\n\t\t}\n\t\tpartsCount++\n\t}\n\tif partsCount < 1 || partsCount > 3 {\n\t\treturn false, false, false,\n\t\t\tfmt.Errorf(\n\t\t\t\t\"spec %q has too many parts, at most 3 are allowed\", spec,\n\t\t\t)\n\t}\n\treturn pi, pp, pr, nil\n}\n"
  },
  {
    "path": "args_parser_test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst (\n\tprogramName = \"bombardier\"\n)\n\nfunc TestInvalidArgsParsing(t *testing.T) {\n\texpectations := []struct {\n\t\tin  []string\n\t\tout string\n\t}{\n\t\t{\n\t\t\t[]string{programName},\n\t\t\t\"required argument 'url' not provided\",\n\t\t},\n\t\t{\n\t\t\t[]string{programName, \"http://google.com\", \"http://yahoo.com\"},\n\t\t\t\"unexpected http://yahoo.com\",\n\t\t},\n\t}\n\tfor _, e := range expectations {\n\t\tp := newKingpinParser()\n\t\tif _, err := p.parse(e.in); err == nil ||\n\t\t\terr.Error() != e.out {\n\t\t\tt.Error(err, e.out)\n\t\t}\n\t}\n}\n\nfunc TestUnspecifiedArgParsing(t *testing.T) {\n\tp := newKingpinParser()\n\targs := []string{programName, \"--someunspecifiedflag\"}\n\t_, err := p.parse(args)\n\tif err == nil {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestArgsParsing(t *testing.T) {\n\tten := uint64(10)\n\texpectations := []struct {\n\t\tin  [][]string\n\t\tout config\n\t}{\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{programName, \"localhost:8080\"},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{programName, \"https://localhost\"},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://localhost\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{{programName, \"https://somehost.somedomain\"}},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-c\", \"10\",\n\t\t\t\t\t\"-n\", strconv.FormatUint(defaultNumberOfReqs, decBase),\n\t\t\t\t\t\"-t\", \"10s\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-c10\",\n\t\t\t\t\t\"-n\" + strconv.FormatUint(defaultNumberOfReqs, decBase),\n\t\t\t\t\t\"-t10s\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--connections\", \"10\",\n\t\t\t\t\t\"--requests\", strconv.FormatUint(defaultNumberOfReqs, decBase),\n\t\t\t\t\t\"--timeout\", \"10s\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--connections=10\",\n\t\t\t\t\t\"--requests=\" + strconv.FormatUint(defaultNumberOfReqs, decBase),\n\t\t\t\t\t\"--timeout=10s\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      10,\n\t\t\t\ttimeout:       10 * time.Second,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\tnumReqs:       &defaultNumberOfReqs,\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--latencies\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-l\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:       defaultNumberOfConns,\n\t\t\t\ttimeout:        defaultTimeout,\n\t\t\t\theaders:        new(headersList),\n\t\t\t\tprintLatencies: true,\n\t\t\t\tmethod:         \"GET\",\n\t\t\t\turl:            ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:     true,\n\t\t\t\tprintProgress:  true,\n\t\t\t\tprintResult:    true,\n\t\t\t\tformat:         knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--insecure\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-k\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tinsecure:      true,\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--key\", \"testclient.key\",\n\t\t\t\t\t\"--cert\", \"testclient.cert\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--key=testclient.key\",\n\t\t\t\t\t\"--cert=testclient.cert\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\tkeyPath:       \"testclient.key\",\n\t\t\t\tcertPath:      \"testclient.cert\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--method\", \"POST\",\n\t\t\t\t\t\"--body\", \"reqbody\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--method=POST\",\n\t\t\t\t\t\"--body=reqbody\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-m\", \"POST\",\n\t\t\t\t\t\"-b\", \"reqbody\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-mPOST\",\n\t\t\t\t\t\"-breqbody\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"POST\",\n\t\t\t\tbody:          \"reqbody\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--header\", \"One: Value one\",\n\t\t\t\t\t\"--header\", \"Two: Value two\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-H\", \"One: Value one\",\n\t\t\t\t\t\"-H\", \"Two: Value two\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--header=One: Value one\",\n\t\t\t\t\t\"--header=Two: Value two\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\theaders: &headersList{\n\t\t\t\t\t{\"One\", \"Value one\"},\n\t\t\t\t\t{\"Two\", \"Value two\"},\n\t\t\t\t},\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--rate\", \"10\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-r\", \"10\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--rate=10\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-r10\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\trate:          &ten,\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--fasthttp\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tclientType:    fhttp,\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--http1\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tclientType:    nhttp1,\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--http2\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tclientType:    nhttp2,\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--body-file=testbody.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--body-file\", \"testbody.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-f\", \"testbody.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\tbodyFilePath:  \"testbody.txt\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--stream\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-s\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\tstream:        true,\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print=r,i,p\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print\", \"r,i,p\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-p\", \"r,i,p\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print=result,i,p\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print\", \"r,intro,p\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-p\", \"r,i,progress\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print=i,r\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print\", \"i,r\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-p\", \"i,r\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print=intro,r\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--print\", \"i,result\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-p\", \"intro,r\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: false,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--no-print\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-q\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    false,\n\t\t\t\tprintProgress: false,\n\t\t\t\tprintResult:   false,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format\", \"plain-text\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format\", \"pt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format=plain-text\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format=pt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-o\", \"plain-text\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-o\", \"pt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format\", \"json\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format\", \"j\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format=json\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format=j\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-o\", \"json\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-o\", \"j\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        knownFormat(\"json\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[][]string{\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format\", \"path:/path/to/tmpl.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"--format=path:/path/to/tmpl.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprogramName,\n\t\t\t\t\t\"-o\", \"path:/path/to/tmpl.txt\",\n\t\t\t\t\t\"https://somehost.somedomain\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig{\n\t\t\t\tnumConns:      defaultNumberOfConns,\n\t\t\t\ttimeout:       defaultTimeout,\n\t\t\t\theaders:       new(headersList),\n\t\t\t\tmethod:        \"GET\",\n\t\t\t\turl:           ParseURLOrPanic(\"https://somehost.somedomain\"),\n\t\t\t\tprintIntro:    true,\n\t\t\t\tprintProgress: true,\n\t\t\t\tprintResult:   true,\n\t\t\t\tformat:        userDefinedTemplate(\"/path/to/tmpl.txt\"),\n\t\t\t},\n\t\t},\n\t}\n\tfor _, e := range expectations {\n\t\tfor _, args := range e.in {\n\t\t\tp := newKingpinParser()\n\t\t\tcfg, err := p.parse(args)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(cfg, e.out) {\n\t\t\t\tt.Logf(\"Expected: %#v\", e.out)\n\t\t\t\tt.Logf(\"Got:      %#v\", cfg)\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestParsePrintSpec(t *testing.T) {\n\texps := []struct {\n\t\tspec    string\n\t\tresults [3]bool\n\t\terr     error\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\t[3]bool{},\n\t\t\terrEmptyPrintSpec,\n\t\t},\n\t\t{\n\t\t\t\"a,b,c\",\n\t\t\t[3]bool{},\n\t\t\tfmt.Errorf(\"%q is not a valid part of print spec\", \"a\"),\n\t\t},\n\t\t{\n\t\t\t\"i,p,r,i\",\n\t\t\t[3]bool{},\n\t\t\tfmt.Errorf(\n\t\t\t\t\"spec %q has too many parts, at most 3 are allowed\", \"i,p,r,i\",\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\t\"i\",\n\t\t\t[3]bool{true, false, false},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"p\",\n\t\t\t[3]bool{false, true, false},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"r\",\n\t\t\t[3]bool{false, false, true},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"i,p,r\",\n\t\t\t[3]bool{true, true, true},\n\t\t\tnil,\n\t\t},\n\t}\n\tfor _, e := range exps {\n\t\tvar (\n\t\t\tact = [3]bool{}\n\t\t\terr error\n\t\t)\n\t\tact[0], act[1], act[2], err = parsePrintSpec(e.spec)\n\t\tif !reflect.DeepEqual(err, e.err) {\n\t\t\tt.Errorf(\"For %q, expected err = %q, but got %q\",\n\t\t\t\te.spec, e.err, err,\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(e.results, act) {\n\t\t\tt.Errorf(\"For %q, expected result = %+v, but got %+v\",\n\t\t\t\te.spec, e.results, act,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestArgsParsingWithEmptyPrintSpec(t *testing.T) {\n\tp := newKingpinParser()\n\tc, err := p.parse(\n\t\t[]string{programName, \"--print=\", \"somehost.somedomain\"})\n\tif err == nil {\n\t\tt.Fail()\n\t}\n\tif c != emptyConf {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestArgsParsingWithInvalidPrintSpec(t *testing.T) {\n\tinvalidSpecs := [][]string{\n\t\t{programName, \"--format\", \"noprefix.txt\", \"somehost.somedomain\"},\n\t\t{programName, \"--format=noprefix.txt\", \"somehost.somedomain\"},\n\t\t{programName, \"-o\", \"noprefix.txt\", \"somehost.somedomain\"},\n\t\t{programName, \"--format\", \"unknown-format\", \"somehost.somedomain\"},\n\t\t{programName, \"--format=unknown-format\", \"somehost.somedomain\"},\n\t\t{programName, \"-o\", \"unknown-format\", \"somehost.somedomain\"},\n\t}\n\tp := newKingpinParser()\n\tfor _, is := range invalidSpecs {\n\t\tc, err := p.parse(is)\n\t\tif err == nil || c != emptyConf {\n\t\t\tt.Errorf(\"invalid print spec %q parsed correctly\", is)\n\t\t}\n\t}\n}\n\nfunc TestEmbeddedURLParsing(t *testing.T) {\n\tp := newKingpinParser()\n\turl := \"http://127.0.0.1:8080/to?url=http://10.100.99.41:38667\"\n\tc, err := p.parse([]string{programName, url})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif c.url.String() != url {\n\t\tt.Errorf(\"got %q, wanted %q\", c.url, url)\n\t}\n}\n"
  },
  {
    "path": "bombardier.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/codesenberg/bombardier/internal\"\n\n\t\"github.com/cheggaaa/pb\"\n\tfhist \"github.com/codesenberg/concurrent/float64/histogram\"\n\tuhist \"github.com/codesenberg/concurrent/uint64/histogram\"\n\tuuid \"github.com/satori/go.uuid\"\n)\n\ntype bombardier struct {\n\tbytesRead, bytesWritten int64\n\n\t// HTTP codes\n\treq1xx uint64\n\treq2xx uint64\n\treq3xx uint64\n\treq4xx uint64\n\treq5xx uint64\n\tothers uint64\n\n\tconf        config\n\tbarrier     completionBarrier\n\tratelimiter limiter\n\twg          sync.WaitGroup\n\n\ttimeTaken time.Duration\n\tlatencies *uhist.Histogram\n\trequests  *fhist.Histogram\n\n\tclient   client\n\tdoneChan chan struct{}\n\n\t// RPS metrics\n\trpl   sync.Mutex\n\treqs  int64\n\tstart time.Time\n\n\t// Errors\n\terrors *errorMap\n\n\t// Progress bar\n\tbar *pb.ProgressBar\n\n\t// Output\n\tout      io.Writer\n\ttemplate *template.Template\n}\n\nfunc newBombardier(c config) (*bombardier, error) {\n\tif err := c.checkArgs(); err != nil {\n\t\treturn nil, err\n\t}\n\tb := new(bombardier)\n\tb.conf = c\n\tb.latencies = uhist.Default()\n\tb.requests = fhist.Default()\n\n\tif b.conf.testType() == counted {\n\t\tb.bar = pb.New64(int64(*b.conf.numReqs))\n\t\tb.bar.ShowSpeed = true\n\t} else if b.conf.testType() == timed {\n\t\tb.bar = pb.New64(b.conf.duration.Nanoseconds() / 1e9)\n\t\tb.bar.ShowCounters = false\n\t\tb.bar.ShowPercent = false\n\t}\n\tb.bar.ManualUpdate = true\n\n\tif b.conf.testType() == counted {\n\t\tb.barrier = newCountingCompletionBarrier(*b.conf.numReqs)\n\t} else {\n\t\tb.barrier = newTimedCompletionBarrier(*b.conf.duration)\n\t}\n\n\tif b.conf.rate != nil {\n\t\tb.ratelimiter = newBucketLimiter(*b.conf.rate)\n\t} else {\n\t\tb.ratelimiter = &nooplimiter{}\n\t}\n\n\tb.out = os.Stdout\n\n\ttlsConfig, err := generateTLSConfig(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tpbody *string\n\t\tbsp   bodyStreamProducer\n\t)\n\tif c.stream {\n\t\tif c.bodyFilePath != \"\" {\n\t\t\tbsp = func() (io.ReadCloser, error) {\n\t\t\t\treturn os.Open(c.bodyFilePath)\n\t\t\t}\n\t\t} else {\n\t\t\tbsp = func() (io.ReadCloser, error) {\n\t\t\t\treturn ioutil.NopCloser(\n\t\t\t\t\tproxyReader{strings.NewReader(c.body)},\n\t\t\t\t), nil\n\t\t\t}\n\t\t}\n\t} else {\n\t\tpbody = &c.body\n\t\tif c.bodyFilePath != \"\" {\n\t\t\tvar bodyBytes []byte\n\t\t\tbodyBytes, err = ioutil.ReadFile(c.bodyFilePath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsbody := string(bodyBytes)\n\t\t\tpbody = &sbody\n\t\t}\n\t}\n\n\tcc := &clientOpts{\n\t\tHTTP2:             false,\n\t\tmaxConns:          c.numConns,\n\t\ttimeout:           c.timeout,\n\t\ttlsConfig:         tlsConfig,\n\t\tdisableKeepAlives: c.disableKeepAlives,\n\n\t\theaders:      c.headers,\n\t\trequestURL:   c.url,\n\t\tmethod:       c.method,\n\t\tbody:         pbody,\n\t\tbodProd:      bsp,\n\t\tbytesRead:    &b.bytesRead,\n\t\tbytesWritten: &b.bytesWritten,\n\t}\n\tb.client = makeHTTPClient(c.clientType, cc)\n\n\tif !b.conf.printProgress {\n\t\tb.bar.Output = ioutil.Discard\n\t\tb.bar.NotPrint = true\n\t}\n\n\tb.template, err = b.prepareTemplate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb.wg.Add(int(c.numConns))\n\tb.errors = newErrorMap()\n\tb.doneChan = make(chan struct{}, 2)\n\treturn b, nil\n}\n\nfunc makeHTTPClient(clientType clientTyp, cc *clientOpts) client {\n\tvar cl client\n\tswitch clientType {\n\tcase nhttp1:\n\t\tcl = newHTTPClient(cc)\n\tcase nhttp2:\n\t\tcc.HTTP2 = true\n\t\tcl = newHTTPClient(cc)\n\tcase fhttp:\n\t\tfallthrough\n\tdefault:\n\t\tcl = newFastHTTPClient(cc)\n\t}\n\treturn cl\n}\n\nfunc (b *bombardier) prepareTemplate() (*template.Template, error) {\n\tvar (\n\t\ttemplateBytes []byte\n\t\terr           error\n\t)\n\tswitch f := b.conf.format.(type) {\n\tcase knownFormat:\n\t\ttemplateBytes = f.template()\n\tcase userDefinedTemplate:\n\t\ttemplateBytes, err = ioutil.ReadFile(string(f))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\tpanic(\"format can't be nil at this point, this is a bug\")\n\t}\n\toutputTemplate, err := template.New(\"output-template\").\n\t\tFuncs(template.FuncMap{\n\t\t\t\"WithLatencies\": func() bool {\n\t\t\t\treturn b.conf.printLatencies\n\t\t\t},\n\t\t\t\"FormatBinary\": formatBinary,\n\t\t\t\"FormatTimeUs\": formatTimeUs,\n\t\t\t\"FormatTimeUsUint64\": func(us uint64) string {\n\t\t\t\treturn formatTimeUs(float64(us))\n\t\t\t},\n\t\t\t\"FloatsToArray\": func(ps ...float64) []float64 {\n\t\t\t\treturn ps\n\t\t\t},\n\t\t\t\"Multiply\": func(num, coeff float64) float64 {\n\t\t\t\treturn num * coeff\n\t\t\t},\n\t\t\t\"StringToBytes\": func(s string) []byte {\n\t\t\t\treturn []byte(s)\n\t\t\t},\n\t\t\t\"UUIDV1\": uuid.NewV1,\n\t\t\t\"UUIDV2\": uuid.NewV2,\n\t\t\t\"UUIDV3\": uuid.NewV3,\n\t\t\t\"UUIDV4\": uuid.NewV4,\n\t\t\t\"UUIDV5\": uuid.NewV5,\n\t\t}).Parse(string(templateBytes))\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn outputTemplate, nil\n}\n\nfunc (b *bombardier) writeStatistics(\n\tcode int, usTaken uint64,\n) {\n\tb.latencies.Increment(usTaken)\n\tb.rpl.Lock()\n\tb.reqs++\n\tb.rpl.Unlock()\n\tvar counter *uint64\n\tswitch code / 100 {\n\tcase 1:\n\t\tcounter = &b.req1xx\n\tcase 2:\n\t\tcounter = &b.req2xx\n\tcase 3:\n\t\tcounter = &b.req3xx\n\tcase 4:\n\t\tcounter = &b.req4xx\n\tcase 5:\n\t\tcounter = &b.req5xx\n\tdefault:\n\t\tcounter = &b.others\n\t}\n\tatomic.AddUint64(counter, 1)\n}\n\nfunc (b *bombardier) performSingleRequest() {\n\tcode, usTaken, err := b.client.do()\n\tif err != nil {\n\t\tb.errors.add(err)\n\t}\n\tb.writeStatistics(code, usTaken)\n}\n\nfunc (b *bombardier) worker() {\n\tdone := b.barrier.done()\n\tfor b.barrier.tryGrabWork() {\n\t\tif b.ratelimiter.pace(done) == brk {\n\t\t\tbreak\n\t\t}\n\t\tb.performSingleRequest()\n\t\tb.barrier.jobDone()\n\t}\n}\n\nfunc (b *bombardier) barUpdater() {\n\tdone := b.barrier.done()\n\tfor {\n\t\tselect {\n\t\tcase <-done:\n\t\t\tb.bar.Set64(b.bar.Total)\n\t\t\tb.bar.Update()\n\t\t\tb.bar.Finish()\n\t\t\tif b.conf.printProgress {\n\t\t\t\tfmt.Fprintln(b.out, \"Done!\")\n\t\t\t}\n\t\t\tb.doneChan <- struct{}{}\n\t\t\treturn\n\t\tdefault:\n\t\t\tcurrent := int64(b.barrier.completed() * float64(b.bar.Total))\n\t\t\tb.bar.Set64(current)\n\t\t\tb.bar.Update()\n\t\t\ttime.Sleep(b.bar.RefreshRate)\n\t\t}\n\t}\n}\n\nfunc (b *bombardier) rateMeter() {\n\trequestsInterval := 10 * time.Millisecond\n\tif b.conf.rate != nil {\n\t\trequestsInterval, _ = estimate(*b.conf.rate, rateLimitInterval)\n\t}\n\trequestsInterval += 10 * time.Millisecond\n\tticker := time.NewTicker(requestsInterval)\n\tdefer ticker.Stop()\n\tdone := b.barrier.done()\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tb.recordRps()\n\t\t\tcontinue\n\t\tcase <-done:\n\t\t\tb.wg.Wait()\n\t\t\tb.recordRps()\n\t\t\tb.doneChan <- struct{}{}\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (b *bombardier) recordRps() {\n\tb.rpl.Lock()\n\tduration := time.Since(b.start)\n\treqs := b.reqs\n\tb.reqs = 0\n\tb.start = time.Now()\n\tb.rpl.Unlock()\n\n\treqsf := float64(reqs) / duration.Seconds()\n\tb.requests.Increment(reqsf)\n}\n\nfunc (b *bombardier) bombard() {\n\tif b.conf.printIntro {\n\t\tb.printIntro()\n\t}\n\tb.bar.Start()\n\tbombardmentBegin := time.Now()\n\tb.start = time.Now()\n\tfor i := uint64(0); i < b.conf.numConns; i++ {\n\t\tgo func() {\n\t\t\tdefer b.wg.Done()\n\t\t\tb.worker()\n\t\t}()\n\t}\n\tgo b.rateMeter()\n\tgo b.barUpdater()\n\tb.wg.Wait()\n\tb.timeTaken = time.Since(bombardmentBegin)\n\t<-b.doneChan\n\t<-b.doneChan\n}\n\nfunc (b *bombardier) printIntro() {\n\tif b.conf.testType() == counted {\n\t\tfmt.Fprintf(b.out,\n\t\t\t\"Bombarding %v with %v request(s) using %v connection(s)\\n\",\n\t\t\tb.conf.url, *b.conf.numReqs, b.conf.numConns)\n\t} else if b.conf.testType() == timed {\n\t\tfmt.Fprintf(b.out, \"Bombarding %v for %v using %v connection(s)\\n\",\n\t\t\tb.conf.url, *b.conf.duration, b.conf.numConns)\n\t}\n}\n\nfunc (b *bombardier) gatherInfo() internal.TestInfo {\n\tinfo := internal.TestInfo{\n\t\tSpec: internal.Spec{\n\t\t\tNumberOfConnections: b.conf.numConns,\n\n\t\t\tMethod: b.conf.method,\n\t\t\tURL:    b.conf.url,\n\n\t\t\tBody:         b.conf.body,\n\t\t\tBodyFilePath: b.conf.bodyFilePath,\n\n\t\t\tCertPath: b.conf.certPath,\n\t\t\tKeyPath:  b.conf.keyPath,\n\n\t\t\tStream:     b.conf.stream,\n\t\t\tTimeout:    b.conf.timeout,\n\t\t\tClientType: internal.ClientType(b.conf.clientType),\n\n\t\t\tRate: b.conf.rate,\n\t\t},\n\t\tResult: internal.Results{\n\t\t\tBytesRead:    b.bytesRead,\n\t\t\tBytesWritten: b.bytesWritten,\n\t\t\tTimeTaken:    b.timeTaken,\n\n\t\t\tReq1XX: b.req1xx,\n\t\t\tReq2XX: b.req2xx,\n\t\t\tReq3XX: b.req3xx,\n\t\t\tReq4XX: b.req4xx,\n\t\t\tReq5XX: b.req5xx,\n\t\t\tOthers: b.others,\n\n\t\t\tLatencies: b.latencies,\n\t\t\tRequests:  b.requests,\n\t\t},\n\t}\n\n\ttestType := b.conf.testType()\n\tinfo.Spec.TestType = internal.TestType(testType)\n\tif testType == timed {\n\t\tinfo.Spec.TestDuration = *b.conf.duration\n\t} else if testType == counted {\n\t\tinfo.Spec.NumberOfRequests = *b.conf.numReqs\n\t}\n\n\tif b.conf.headers != nil {\n\t\tfor _, h := range *b.conf.headers {\n\t\t\tinfo.Spec.Headers = append(info.Spec.Headers,\n\t\t\t\tinternal.Header{\n\t\t\t\t\tKey:   h.key,\n\t\t\t\t\tValue: h.value,\n\t\t\t\t})\n\t\t}\n\t}\n\n\tfor _, ewc := range b.errors.byFrequency() {\n\t\tinfo.Result.Errors = append(info.Result.Errors,\n\t\t\tinternal.ErrorWithCount{\n\t\t\t\tError: ewc.error,\n\t\t\t\tCount: ewc.count,\n\t\t\t})\n\t}\n\n\treturn info\n}\n\nfunc (b *bombardier) printStats() {\n\tinfo := b.gatherInfo()\n\terr := b.template.Execute(b.out, info)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t}\n}\n\nfunc (b *bombardier) redirectOutputTo(out io.Writer) {\n\tb.bar.Output = out\n\tb.out = out\n}\n\nfunc (b *bombardier) disableOutput() {\n\tb.redirectOutputTo(ioutil.Discard)\n\tb.bar.NotPrint = true\n}\n\nfunc main() {\n\tcfg, err := parser.parse(os.Args)\n\tif err != nil {\n\t\tfmt.Println(\"Error parsing the arguments:\", err)\n\t\tos.Exit(exitFailure)\n\t}\n\tbombardier, err := newBombardier(cfg)\n\tif err != nil {\n\t\tfmt.Println(\"Error initializing bombardier:\", err)\n\t\tos.Exit(exitFailure)\n\t}\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt)\n\tgo func() {\n\t\t<-c\n\t\tbombardier.barrier.cancel()\n\t}()\n\tbombardier.bombard()\n\tif bombardier.conf.printResult {\n\t\tbombardier.printStats()\n\t}\n}\n"
  },
  {
    "path": "bombardier_performance_test.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tserverPort = flag.String(\"port\", \"8080\", \"port to use for benchmarks\")\n\tclientType = flag.String(\"client-type\", \"fasthttp\",\n\t\t\"client to use in benchmarks\")\n)\n\nvar (\n\tlongDuration = 9001 * time.Hour\n\thighRate     = uint64(1000000)\n)\n\nfunc BenchmarkBombardierSingleReqPerf(b *testing.B) {\n\taddr := \"localhost:\" + *serverPort\n\tbenchmarkFireRequest(config{\n\t\tnumConns:       defaultNumberOfConns,\n\t\tnumReqs:        nil,\n\t\tduration:       &longDuration,\n\t\turl:            ParseURLOrPanic(\"http://\" + addr),\n\t\theaders:        new(headersList),\n\t\ttimeout:        defaultTimeout,\n\t\tmethod:         \"GET\",\n\t\tbody:           \"\",\n\t\tprintLatencies: false,\n\t\tclientType:     clientTypeFromString(*clientType),\n\t\tformat:         knownFormat(\"json\"),\n\t}, b)\n}\n\nfunc BenchmarkBombardierRateLimitPerf(b *testing.B) {\n\taddr := \"localhost:\" + *serverPort\n\tbenchmarkFireRequest(config{\n\t\tnumConns:       defaultNumberOfConns,\n\t\tnumReqs:        nil,\n\t\tduration:       &longDuration,\n\t\turl:            ParseURLOrPanic(\"http://\" + addr),\n\t\theaders:        new(headersList),\n\t\ttimeout:        defaultTimeout,\n\t\tmethod:         \"GET\",\n\t\tbody:           \"\",\n\t\tprintLatencies: false,\n\t\trate:           &highRate,\n\t\tclientType:     clientTypeFromString(*clientType),\n\t\tformat:         knownFormat(\"json\"),\n\t}, b)\n}\n\nfunc benchmarkFireRequest(c config, bm *testing.B) {\n\tb, e := newBombardier(c)\n\tif e != nil {\n\t\tbm.Error(e)\n\t}\n\tb.disableOutput()\n\tbm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())\n\tbm.ResetTimer()\n\tbm.RunParallel(func(pb *testing.PB) {\n\t\tdone := b.barrier.done()\n\t\tfor pb.Next() {\n\t\t\tb.ratelimiter.pace(done)\n\t\t\tb.performSingleRequest()\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "bombardier_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"container/ring\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestBombardierShouldFireSpecifiedNumberOfRequests(t *testing.T) {\n\ttestAllClients(t, testBombardierShouldFireSpecifiedNumberOfRequests)\n}\n\nfunc testBombardierShouldFireSpecifiedNumberOfRequests(\n\tclientType clientTyp, t *testing.T,\n) {\n\treqsReceived := uint64(0)\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tatomic.AddUint64(&reqsReceived, 1)\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(100)\n\tnoHeaders := new(headersList)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    noHeaders,\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n\tif reqsReceived != numReqs {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestBombardierShouldFinish(t *testing.T) {\n\ttestAllClients(t, testBombardierShouldFinish)\n}\n\nfunc testBombardierShouldFinish(clientType clientTyp, t *testing.T) {\n\treqsReceived := uint64(0)\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tatomic.AddUint64(&reqsReceived, 1)\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnoHeaders := new(headersList)\n\tdesiredTestDuration := 1 * time.Second\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tduration:   &desiredTestDuration,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    noHeaders,\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\twaitCh := make(chan struct{})\n\tgo func() {\n\t\tb.bombard()\n\t\twaitCh <- struct{}{}\n\t}()\n\tselect {\n\tcase <-waitCh:\n\t// Do nothing here\n\tcase <-time.After(desiredTestDuration + 5*time.Second):\n\t\tt.Fail()\n\t}\n\tif reqsReceived == 0 {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestBombardierShouldSendHeaders(t *testing.T) {\n\ttestAllClients(t, testBombardierShouldSendHeaders)\n}\n\nfunc testBombardierShouldSendHeaders(clientType clientTyp, t *testing.T) {\n\trequestHeaders := headersList([]header{\n\t\t{\"Header1\", \"Value1\"},\n\t\t{\"Header-Two\", \"value-two\"},\n\t})\n\n\t// It's a bit hacky, but FastHTTP can't send Host header correctly\n\t// as of now\n\tif clientType != fhttp {\n\t\trequestHeaders = append(requestHeaders, header{\"Host\", \"web\"})\n\t}\n\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tfor _, h := range requestHeaders {\n\t\t\t\tav := r.Header.Get(h.key)\n\t\t\t\tif h.key == \"Host\" {\n\t\t\t\t\tav = r.Host\n\t\t\t\t}\n\t\t\t\tif av != h.value {\n\t\t\t\t\tt.Logf(\"%q <-> %q\", av, h.value)\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    &requestHeaders,\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n\nfunc TestBombardierHTTPCodeRecording(t *testing.T) {\n\ttestAllClients(t, testBombardierHTTPCodeRecording)\n}\n\nfunc testBombardierHTTPCodeRecording(clientType clientTyp, t *testing.T) {\n\tcs := []int{200, 302, 404, 505, 606, 707}\n\tcodes := ring.New(len(cs))\n\tfor _, v := range cs {\n\t\tcodes.Value = v\n\t\tcodes = codes.Next()\n\t}\n\tcodes = codes.Next()\n\tvar m sync.Mutex\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tm.Lock()\n\t\t\tnextCode := codes.Value.(int)\n\t\t\tcodes = codes.Next()\n\t\t\tm.Unlock()\n\t\t\tif nextCode/100 == 3 {\n\t\t\t\trw.Header().Set(\"Location\", \"http://localhost:666\")\n\t\t\t}\n\t\t\trw.WriteHeader(nextCode)\n\t\t}),\n\t)\n\tdefer s.Close()\n\teachCodeCount := uint64(10)\n\tnumReqs := uint64(len(cs)) * eachCodeCount\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n\texpectation := []struct {\n\t\tname     string\n\t\treqsGot  uint64\n\t\texpected uint64\n\t}{\n\t\t{\"errored\", b.others, eachCodeCount * 2},\n\t\t{\"2xx\", b.req2xx, eachCodeCount},\n\t\t{\"3xx\", b.req3xx, eachCodeCount},\n\t\t{\"4xx\", b.req4xx, eachCodeCount},\n\t\t{\"5xx\", b.req5xx, eachCodeCount},\n\t}\n\tfor _, e := range expectation {\n\t\tif e.reqsGot != e.expected {\n\t\t\tt.Error(e.name, e.reqsGot, e.expected)\n\t\t}\n\t}\n\tt.Logf(\"%+v\", b.errors.byFrequency())\n}\n\nfunc TestBombardierTimeoutRecoding(t *testing.T) {\n\ttestAllClients(t, testBombardierTimeoutRecoding)\n}\n\nfunc testBombardierTimeoutRecoding(clientType clientTyp, t *testing.T) {\n\tshortTimeout := 10 * time.Millisecond\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\ttime.Sleep(shortTimeout * 10)\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(10)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\tduration:   nil,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    shortTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n\tif b.errors.sum() != numReqs {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestBombardierThroughputRecording(t *testing.T) {\n\ttestAllClients(t, testBombardierThroughputRecording)\n}\n\nfunc testBombardierThroughputRecording(clientType clientTyp, t *testing.T) {\n\tresponseSize := 1024\n\tresponse := bytes.Repeat([]byte{'a'}, responseSize)\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\t_, err := rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(10)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n\tif b.bytesRead == 0 || b.bytesWritten == 0 {\n\t\tt.Error(b.bytesRead, b.bytesWritten)\n\t}\n}\n\nfunc TestBombardierStatsPrinting(t *testing.T) {\n\tresponseSize := 1024\n\tresponse := bytes.Repeat([]byte{'a'}, responseSize)\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\t_, err := rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(10)\n\tb, e := newBombardier(config{\n\t\tnumConns:       defaultNumberOfConns,\n\t\tnumReqs:        &numReqs,\n\t\turl:            ParseURLOrPanic(s.URL),\n\t\theaders:        new(headersList),\n\t\ttimeout:        defaultTimeout,\n\t\tmethod:         \"GET\",\n\t\tbody:           \"\",\n\t\tprintLatencies: true,\n\t\tprintIntro:     true,\n\t\tprintProgress:  true,\n\t\tprintResult:    true,\n\t\tformat:         knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tdummy := errors.New(\"dummy error\")\n\tb.errors.add(dummy)\n\n\tout := new(bytes.Buffer)\n\tb.redirectOutputTo(out)\n\tb.bombard()\n\n\tb.printStats()\n\tl := out.Len()\n\t// Here we only test if anything is written\n\tif l == 0 {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestBombardierErrorIfFailToReadClientCert(t *testing.T) {\n\tnumReqs := uint64(10)\n\t_, e := newBombardier(config{\n\t\tnumConns:       defaultNumberOfConns,\n\t\tnumReqs:        &numReqs,\n\t\turl:            ParseURLOrPanic(\"http://localhost\"),\n\t\theaders:        new(headersList),\n\t\ttimeout:        defaultTimeout,\n\t\tmethod:         \"GET\",\n\t\tbody:           \"\",\n\t\tprintLatencies: true,\n\t\tcertPath:       \"certPath\",\n\t\tkeyPath:        \"keyPath\",\n\t\tformat:         knownFormat(\"plain-text\"),\n\t})\n\tif e == nil {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestBombardierClientCerts(t *testing.T) {\n\ttestAllClients(t, testBombardierClientCerts)\n}\n\nfunc testBombardierClientCerts(clientType clientTyp, t *testing.T) {\n\tclientCert, err := tls.LoadX509KeyPair(\"testclient.cert\", \"testclient.key\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tclientX509Cert, err := x509.ParseCertificate(clientCert.Certificate[0])\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tservertCert, err := tls.LoadX509KeyPair(\"testserver.cert\", \"testserver.key\")\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tClientAuth:   tls.RequireAnyClientCert,\n\t\tCertificates: []tls.Certificate{servertCert},\n\t}\n\n\tserver := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\tcerts := r.TLS.PeerCertificates\n\t\tif numCerts := len(certs); numCerts != 1 {\n\t\t\tt.Errorf(\"expected 1 cert, but got %v\", numCerts)\n\t\t\trw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tcert := certs[0]\n\t\tif !cert.Equal(clientX509Cert) {\n\t\t\tt.Error(\"certificates don't match\")\n\t\t\trw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\trw.WriteHeader(http.StatusOK)\n\t}))\n\n\tserver.TLS = tlsConfig\n\tserver.StartTLS()\n\n\tsingleRequest := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:       defaultNumberOfConns,\n\t\tnumReqs:        &singleRequest,\n\t\turl:            ParseURLOrPanic(server.URL),\n\t\theaders:        new(headersList),\n\t\ttimeout:        defaultTimeout,\n\t\tmethod:         \"GET\",\n\t\tbody:           \"\",\n\t\tprintLatencies: true,\n\t\tcertPath:       \"testclient.cert\",\n\t\tkeyPath:        \"testclient.key\",\n\t\tinsecure:       true,\n\t\tclientType:     clientType,\n\t\tformat:         knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\n\tb.bombard()\n\tif b.req2xx != 1 {\n\t\tt.Error(\"no 2xx responses, total =\", b.reqs, \", 1xx/2xx/3xx/4xx/5xx =\", b.req1xx, b.req2xx, b.req3xx, b.req4xx, b.req5xx)\n\t}\n\n\tserver.Close()\n}\n\nfunc TestBombardierRateLimiting(t *testing.T) {\n\ttestAllClients(t, testBombardierRateLimiting)\n}\n\nfunc testBombardierRateLimiting(clientType clientTyp, t *testing.T) {\n\tresponseSize := 1024\n\tresponse := bytes.Repeat([]byte{'a'}, responseSize)\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\t_, err := rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\trate := uint64(5000)\n\ttestDuration := 1 * time.Second\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tduration:   &testDuration,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\trate:       &rate,\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\tb.bombard()\n\tif float64(b.req2xx) < float64(rate)*0.75 ||\n\t\tfloat64(b.req2xx) > float64(rate)*1.25 {\n\t\tt.Error(rate, b.req2xx)\n\t}\n}\n\nfunc testAllClients(parent *testing.T, testFun func(clientTyp, *testing.T)) {\n\tclients := []clientTyp{fhttp, nhttp1, nhttp2}\n\tfor _, ct := range clients {\n\t\tparent.Run(ct.String(), func(t *testing.T) {\n\t\t\ttestFun(ct, t)\n\t\t})\n\t}\n}\n\nfunc TestBombardierSendsBody(t *testing.T) {\n\ttestAllClients(t, testBombardierSendsBody)\n}\n\nfunc testBombardierSendsBody(clientType clientTyp, t *testing.T) {\n\tresponse := []byte(\"OK\")\n\trequestBody := \"abracadabra\"\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tbody, err := ioutil.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(body) != requestBody {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", requestBody, string(body))\n\t\t\t}\n\t\t\t_, err = rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tone := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &one,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"POST\",\n\t\tbody:       requestBody,\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n\nfunc TestBombardierSendsBodyFromFile(t *testing.T) {\n\ttestAllClients(t, testBombardierSendsBodyFromFile)\n}\n\nfunc testBombardierSendsBodyFromFile(clientType clientTyp, t *testing.T) {\n\tresponse := []byte(\"OK\")\n\tbodyPath := \"testbody.txt\"\n\trequestBody, err := ioutil.ReadFile(bodyPath)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tbody, err := ioutil.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(body) != string(requestBody) {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", string(requestBody), string(body))\n\t\t\t}\n\t\t\t_, err = rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tone := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:     defaultNumberOfConns,\n\t\tnumReqs:      &one,\n\t\turl:          ParseURLOrPanic(s.URL),\n\t\theaders:      new(headersList),\n\t\ttimeout:      defaultTimeout,\n\t\tmethod:       \"POST\",\n\t\tbodyFilePath: bodyPath,\n\t\tclientType:   clientType,\n\t\tformat:       knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n\nfunc TestBombardierFileDoesntExist(t *testing.T) {\n\tbodyPath := \"/does/not/exist.forreal\"\n\t_, e := newBombardier(config{\n\t\tnumConns:     defaultNumberOfConns,\n\t\turl:          ParseURLOrPanic(\"http://example.com\"),\n\t\theaders:      new(headersList),\n\t\ttimeout:      defaultTimeout,\n\t\tmethod:       \"POST\",\n\t\tbodyFilePath: bodyPath,\n\t\tformat:       knownFormat(\"plain-text\"),\n\t})\n\t_, ok := e.(*os.PathError)\n\tif !ok {\n\t\tt.Errorf(\"Expected to get PathError, but got %v\", e)\n\t}\n}\n\nfunc TestBombardierStreamsBody(t *testing.T) {\n\ttestAllClients(t, testBombardierStreamsBody)\n}\n\nfunc testBombardierStreamsBody(clientType clientTyp, t *testing.T) {\n\tresponse := []byte(\"OK\")\n\trequestBody := \"abracadabra\"\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tif te := r.TransferEncoding; !reflect.DeepEqual(te, []string{\"chunked\"}) {\n\t\t\t\tt.Errorf(\"Expected chunked transfer encoding, but got %v\", te)\n\t\t\t}\n\t\t\tbody, err := ioutil.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(body) != requestBody {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", requestBody, string(body))\n\t\t\t}\n\t\t\t_, err = rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tone := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &one,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    new(headersList),\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"POST\",\n\t\tbody:       requestBody,\n\t\tstream:     true,\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n\nfunc TestBombardierStreamsBodyFromFile(t *testing.T) {\n\ttestAllClients(t, testBombardierStreamsBodyFromFile)\n}\n\nfunc testBombardierStreamsBodyFromFile(clientType clientTyp, t *testing.T) {\n\tresponse := []byte(\"OK\")\n\tbodyPath := \"testbody.txt\"\n\trequestBody, err := ioutil.ReadFile(bodyPath)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tif te := r.TransferEncoding; !reflect.DeepEqual(te, []string{\"chunked\"}) {\n\t\t\t\tt.Errorf(\"Expected chunked transfer encoding, but got %v\", te)\n\t\t\t}\n\t\t\tbody, err := ioutil.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(body) != string(requestBody) {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", string(requestBody), string(body))\n\t\t\t}\n\t\t\t_, err = rw.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tone := uint64(1)\n\tb, e := newBombardier(config{\n\t\tnumConns:     defaultNumberOfConns,\n\t\tnumReqs:      &one,\n\t\turl:          ParseURLOrPanic(s.URL),\n\t\theaders:      new(headersList),\n\t\ttimeout:      defaultTimeout,\n\t\tmethod:       \"POST\",\n\t\tbodyFilePath: bodyPath,\n\t\tstream:       true,\n\t\tclientType:   clientType,\n\t\tformat:       knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t\treturn\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n\nfunc TestBombardierShouldSendCustomHostHeader(t *testing.T) {\n\ttestAllClients(t, testBombardierShouldSendCustomHostHeader)\n}\n\nfunc testBombardierShouldSendCustomHostHeader(\n\tclientType clientTyp, t *testing.T,\n) {\n\thost := \"custom-host\"\n\ts := httptest.NewServer(\n\t\thttp.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Host != host {\n\t\t\t\tt.Errorf(\"Host must be %q, but it's %q\", host, r.Host)\n\t\t\t}\n\t\t}),\n\t)\n\tdefer s.Close()\n\tnumReqs := uint64(100)\n\theaders := headersList([]header{\n\t\t{\"Host\", host},\n\t})\n\tb, e := newBombardier(config{\n\t\tnumConns:   defaultNumberOfConns,\n\t\tnumReqs:    &numReqs,\n\t\turl:        ParseURLOrPanic(s.URL),\n\t\theaders:    &headers,\n\t\ttimeout:    defaultTimeout,\n\t\tmethod:     \"GET\",\n\t\tbody:       \"\",\n\t\tclientType: clientType,\n\t\tformat:     knownFormat(\"plain-text\"),\n\t})\n\tif e != nil {\n\t\tt.Error(e)\n\t}\n\tb.disableOutput()\n\tb.bombard()\n}\n"
  },
  {
    "path": "build.py",
    "content": "import argparse\nimport os\nimport subprocess\n\nplatforms = [\n    (\"darwin\", \"amd64\"),\n    (\"darwin\", \"arm64\"),\n    (\"freebsd\", \"386\"),\n    (\"freebsd\", \"amd64\"),\n    (\"freebsd\", \"arm\"),\n    (\"linux\", \"386\"),\n    (\"linux\", \"amd64\"),\n    (\"linux\", \"arm\"),\n    (\"linux\", \"arm64\"),\n    (\"netbsd\", \"386\"),\n    (\"netbsd\", \"amd64\"),\n    (\"netbsd\", \"arm\"),\n    (\"openbsd\", \"386\"),\n    (\"openbsd\", \"amd64\"),\n    (\"openbsd\", \"arm\"),\n    (\"openbsd\", \"arm64\"),\n    (\"windows\", \"386\"),\n    (\"windows\", \"amd64\"),\n    (\"windows\", \"arm64\"),\n]\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Auxilary build script.\")\n    parser.add_argument(\"-v\", \"--version\", default=\"unspecified\",\n                        type=str, help=\"string used as a version when building binaries\")\n    args = parser.parse_args()\n    version = args.version\n    for (build_os, build_arch) in platforms:\n        ext = \"\"\n        if build_os == \"windows\":\n            ext = \".exe\"\n        build_env = os.environ.copy()\n        build_env[\"GOOS\"] = build_os\n        build_env[\"GOARCH\"] = build_arch\n        subprocess.run([\"go\", \"build\", \"-ldflags\", \"-s -w -X main.version=%s\" %\n                        version, \"-o\", \"bombardier-%s-%s%s\" % (build_os, build_arch, ext)], env=build_env)\n"
  },
  {
    "path": "client_cert.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n)\n\n// readClientCert - helper function to read client certificate\n// from pem formatted certPath and keyPath files\nfunc readClientCert(certPath, keyPath string) ([]tls.Certificate, error) {\n\t// load keypair\n\tcert, err := tls.LoadX509KeyPair(certPath, keyPath)\n\treturn []tls.Certificate{cert}, err\n}\n\n// generateTLSConfig - helper function to generate a TLS configuration based on\n// config\nfunc generateTLSConfig(c config) (*tls.Config, error) {\n\tvar (\n\t\tcerts []tls.Certificate\n\t\terr   error\n\t)\n\t// This assumes that the caller has validated that either both or none of\n\t// the c.certPath and c.keyPath are set.\n\tif c.certPath != \"\" && c.keyPath != \"\" {\n\t\tcerts, err = readClientCert(c.certPath, c.keyPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Disable gas warning, because InsecureSkipVerify may be set to true\n\t// for the purpose of testing\n\t/* #nosec */\n\ttlsConfig := &tls.Config{\n\t\tInsecureSkipVerify: c.insecure,\n\t\tCertificates:       certs,\n\t}\n\treturn tlsConfig, nil\n}\n"
  },
  {
    "path": "client_cert_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGenerateTLSConfig(t *testing.T) {\n\texpectations := []struct {\n\t\tcertPath string\n\t\tkeyPath  string\n\t\terrIsNil bool\n\t}{\n\t\t{\n\t\t\tcertPath: \"testclient.cert\",\n\t\t\tkeyPath:  \"testclient.key\",\n\t\t\terrIsNil: true,\n\t\t},\n\t\t{\n\t\t\tcertPath: \"doesnotexist.pem\",\n\t\t\tkeyPath:  \"doesnotexist.pem\",\n\t\t\terrIsNil: false,\n\t\t},\n\t\t{\n\t\t\tcertPath: \"\",\n\t\t\tkeyPath:  \"\",\n\t\t\terrIsNil: true,\n\t\t},\n\t}\n\tfor _, e := range expectations {\n\t\t_, r := generateTLSConfig(\n\t\t\tconfig{\n\t\t\t\turl:      ParseURLOrPanic(\"https://doesnt.exist.com\"),\n\t\t\t\tcertPath: e.certPath,\n\t\t\t\tkeyPath:  e.keyPath,\n\t\t\t},\n\t\t)\n\t\tif (r == nil) != e.errIsNil {\n\t\t\tt.Error(e.certPath, e.keyPath, r)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "clients.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\ntype client interface {\n\tdo() (code int, usTaken uint64, err error)\n}\n\ntype bodyStreamProducer func() (io.ReadCloser, error)\n\ntype clientOpts struct {\n\tHTTP2 bool\n\n\tmaxConns          uint64\n\ttimeout           time.Duration\n\ttlsConfig         *tls.Config\n\tdisableKeepAlives bool\n\n\trequestURL *url.URL\n\theaders    *headersList\n\tmethod     string\n\n\tbody    *string\n\tbodProd bodyStreamProducer\n\n\tbytesRead, bytesWritten *int64\n}\n\ntype fasthttpClient struct {\n\tclient *fasthttp.Client\n\n\theaders *fasthttp.RequestHeader\n\turi     *fasthttp.URI\n\tmethod  string\n\n\tbody    *string\n\tbodProd bodyStreamProducer\n}\n\nfunc newFastHTTPClient(opts *clientOpts) client {\n\tc := new(fasthttpClient)\n\turi := fasthttp.AcquireURI()\n\tif err := uri.Parse(\n\t\t[]byte(opts.requestURL.Host),\n\t\t[]byte(opts.requestURL.String()),\n\t); err != nil {\n\t\t// opts.requestURL must always be valid\n\t\tpanic(err)\n\t}\n\tc.uri = uri\n\tc.client = &fasthttp.Client{\n\t\tMaxConnsPerHost:               int(opts.maxConns),\n\t\tReadTimeout:                   opts.timeout,\n\t\tWriteTimeout:                  opts.timeout,\n\t\tDisableHeaderNamesNormalizing: true,\n\t\tTLSConfig:                     opts.tlsConfig,\n\t\tDial: fasthttpDialFunc(\n\t\t\topts.bytesRead, opts.bytesWritten,\n\t\t\topts.timeout,\n\t\t),\n\t}\n\tc.headers = headersToFastHTTPHeaders(opts.headers)\n\tc.method, c.body = opts.method, opts.body\n\tc.bodProd = opts.bodProd\n\treturn client(c)\n}\n\nfunc (c *fasthttpClient) do() (\n\tcode int, usTaken uint64, err error,\n) {\n\t// prepare the request\n\treq := fasthttp.AcquireRequest()\n\tresp := fasthttp.AcquireResponse()\n\tif c.headers != nil {\n\t\tc.headers.CopyTo(&req.Header)\n\t}\n\treq.Header.SetMethod(c.method)\n\treq.SetURI(c.uri)\n\treq.UseHostHeader = true\n\tif c.body != nil {\n\t\treq.SetBodyString(*c.body)\n\t} else {\n\t\tbs, bserr := c.bodProd()\n\t\tif bserr != nil {\n\t\t\treturn 0, 0, bserr\n\t\t}\n\t\treq.SetBodyStream(bs, -1)\n\t}\n\n\t// fire the request\n\tstart := time.Now()\n\terr = c.client.Do(req, resp)\n\tif err != nil {\n\t\tcode = -1\n\t} else {\n\t\tcode = resp.StatusCode()\n\t}\n\tusTaken = uint64(time.Since(start).Nanoseconds() / 1000)\n\n\t// release resources\n\tfasthttp.ReleaseRequest(req)\n\tfasthttp.ReleaseResponse(resp)\n\n\treturn\n}\n\ntype httpClient struct {\n\tclient *http.Client\n\n\theaders http.Header\n\turl     *url.URL\n\tmethod  string\n\n\tbody    *string\n\tbodProd bodyStreamProducer\n}\n\nfunc newHTTPClient(opts *clientOpts) client {\n\tc := new(httpClient)\n\ttr := &http.Transport{\n\t\tTLSClientConfig:     opts.tlsConfig,\n\t\tMaxIdleConnsPerHost: int(opts.maxConns),\n\t\tDisableKeepAlives:   opts.disableKeepAlives,\n\t\tForceAttemptHTTP2:   opts.HTTP2,\n\t\tDialContext:         httpDialContextFunc(opts.bytesRead, opts.bytesWritten, opts.timeout),\n\t}\n\n\tcl := &http.Client{\n\t\tTransport: tr,\n\t\tTimeout:   opts.timeout,\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n\tc.client = cl\n\n\tc.headers = headersToHTTPHeaders(opts.headers)\n\tc.method, c.body, c.bodProd = opts.method, opts.body, opts.bodProd\n\tc.url = opts.requestURL\n\n\treturn client(c)\n}\n\nfunc (c *httpClient) do() (\n\tcode int, usTaken uint64, err error,\n) {\n\treq := &http.Request{}\n\n\treq.Header = c.headers\n\treq.Method = c.method\n\treq.URL = c.url\n\n\tif host := req.Header.Get(\"Host\"); host != \"\" {\n\t\treq.Host = host\n\t}\n\n\tif c.body != nil {\n\t\tbr := strings.NewReader(*c.body)\n\t\treq.ContentLength = int64(len(*c.body))\n\t\treq.Body = ioutil.NopCloser(br)\n\t} else {\n\t\tbs, bserr := c.bodProd()\n\t\tif bserr != nil {\n\t\t\treturn 0, 0, bserr\n\t\t}\n\t\treq.Body = bs\n\t}\n\n\tstart := time.Now()\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\tcode = -1\n\t} else {\n\t\tcode = resp.StatusCode\n\n\t\t_, berr := io.Copy(ioutil.Discard, resp.Body)\n\t\tif berr != nil {\n\t\t\terr = berr\n\t\t}\n\n\t\tif cerr := resp.Body.Close(); cerr != nil {\n\t\t\terr = cerr\n\t\t}\n\t}\n\tusTaken = uint64(time.Since(start).Nanoseconds() / 1000)\n\n\treturn\n}\n\nfunc headersToFastHTTPHeaders(h *headersList) *fasthttp.RequestHeader {\n\tif len(*h) == 0 {\n\t\treturn nil\n\t}\n\tres := new(fasthttp.RequestHeader)\n\tfor _, header := range *h {\n\t\tres.Set(header.key, header.value)\n\t}\n\treturn res\n}\n\nfunc headersToHTTPHeaders(h *headersList) http.Header {\n\tif len(*h) == 0 {\n\t\treturn http.Header{}\n\t}\n\theaders := http.Header{}\n\n\tfor _, header := range *h {\n\t\theaders[header.key] = []string{header.value}\n\t}\n\treturn headers\n}\n"
  },
  {
    "path": "clients_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/goware/urlx\"\n)\n\nfunc TestShouldReturnNilIfNoHeadersWhereSet(t *testing.T) {\n\th := new(headersList)\n\tif headersToFastHTTPHeaders(h) != nil {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestShouldReturnEmptyHeadersIfNoHeaadersWhereSet(t *testing.T) {\n\th := new(headersList)\n\tif len(headersToHTTPHeaders(h)) != 0 {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestShouldProperlyConvertToHttpHeaders(t *testing.T) {\n\th := new(headersList)\n\tfor _, hs := range []string{\n\t\t\"Content-Type: application/json\", \"Custom-Header: xxx42xxx\",\n\t} {\n\t\tif err := h.Set(hs); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n\tfh := headersToFastHTTPHeaders(h)\n\t{\n\t\te, a := []byte(\"application/json\"), fh.Peek(\"Content-Type\")\n\t\tif !bytes.Equal(e, a) {\n\t\t\tt.Errorf(\"Expected %v, but got %v\", e, a)\n\t\t}\n\t}\n\tif e, a := []byte(\"xxx42xxx\"), fh.Peek(\"Custom-Header\"); !bytes.Equal(e, a) {\n\t\tt.Errorf(\"Expected %v, but got %v\", e, a)\n\t}\n\n\tnh := headersToHTTPHeaders(h)\n\t{\n\t\te, a := \"application/json\", nh.Get(\"Content-Type\")\n\t\tif e != a {\n\t\t\tt.Errorf(\"Expected %v, but got %v\", e, a)\n\t\t}\n\t}\n\tif e, a := \"xxx42xxx\", nh.Get(\"Custom-Header\"); e != a {\n\t\tt.Errorf(\"Expected %v, but got %v\", e, a)\n\t}\n}\n\nfunc TestHTTP2Client(t *testing.T) {\n\tresponseSize := 1024\n\tresponse := bytes.Repeat([]byte{'a'}, responseSize)\n\ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !r.ProtoAtLeast(2, 0) {\n\t\t\tt.Errorf(\"invalid HTTP proto version: %v\", r.Proto)\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\t_, err := w.Write(response)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}))\n\ts.EnableHTTP2 = true\n\ts.TLS = &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t}\n\ts.StartTLS()\n\tdefer s.Close()\n\n\tbytesRead, bytesWritten := int64(0), int64(0)\n\trequestURL, err := urlx.Parse(s.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tc := newHTTPClient(&clientOpts{\n\t\tHTTP2: true,\n\n\t\theaders:    new(headersList),\n\t\trequestURL: requestURL,\n\t\tmethod:     \"GET\",\n\t\ttlsConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t},\n\n\t\tbody: new(string),\n\n\t\tbytesRead:    &bytesRead,\n\t\tbytesWritten: &bytesWritten,\n\t})\n\tcode, _, err := c.do()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif code != http.StatusOK {\n\t\tt.Errorf(\"invalid response code: %v\", code)\n\t}\n\tif atomic.LoadInt64(&bytesRead) == 0 {\n\t\tt.Errorf(\"invalid response size: %v\", bytesRead)\n\t}\n\tif atomic.LoadInt64(&bytesWritten) == 0 {\n\t\tt.Errorf(\"empty request of size: %v\", bytesWritten)\n\t}\n}\n\nfunc TestHTTP1Clients(t *testing.T) {\n\tresponseSize := 1024\n\tresponse := bytes.Repeat([]byte{'a'}, responseSize)\n\ts := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.ProtoMajor != 1 {\n\t\t\t\tt.Errorf(\"invalid HTTP proto version: %v\", r.Proto)\n\t\t\t}\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_, err := w.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t},\n\t))\n\tdefer s.Close()\n\n\tbytesRead, bytesWritten := int64(0), int64(0)\n\trequestURL, err := urlx.Parse(s.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcc := &clientOpts{\n\t\tHTTP2: false,\n\n\t\theaders:    new(headersList),\n\t\trequestURL: requestURL,\n\t\tmethod:     \"GET\",\n\n\t\tbody: new(string),\n\n\t\tbytesRead:    &bytesRead,\n\t\tbytesWritten: &bytesWritten,\n\t}\n\tclients := []client{\n\t\tnewHTTPClient(cc),\n\t\tnewFastHTTPClient(cc),\n\t}\n\tfor _, c := range clients {\n\t\tbytesRead, bytesWritten = 0, 0\n\t\tcode, _, err := c.do()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif code != http.StatusOK {\n\t\t\tt.Errorf(\"invalid response code: %v\", code)\n\t\t}\n\t\tif bytesRead == 0 {\n\t\t\tt.Errorf(\"invalid response size: %v\", bytesRead)\n\t\t}\n\t\tif bytesWritten == 0 {\n\t\t\tt.Errorf(\"empty request of size: %v\", bytesWritten)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/utils/simplebenchserver/doc.go",
    "content": "/*\nSimple HTTP server used for benchmarking.\n\nFollowing options are available:\n\n\t    --help         Show context-sensitive help (also try --help-long and\n\t                   --help-man).\n\t-p, --port=\"8080\"  port to use for benchmarks\n\t-s, --size=1024    size of response in bytes\n*/\npackage main\n"
  },
  {
    "path": "cmd/utils/simplebenchserver/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/alecthomas/kingpin\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nvar serverPort = kingpin.Flag(\"port\", \"port to use for benchmarks\").\n\tDefault(\"8080\").\n\tShort('p').\n\tString()\nvar responseSize = kingpin.Flag(\"size\", \"size of response in bytes\").\n\tDefault(\"1024\").\n\tShort('s').\n\tUint()\nvar stdHTTP = kingpin.Flag(\"std-http\", \"use standard http library\").\n\tDefault(\"false\").\n\tBool()\n\nfunc main() {\n\tkingpin.Parse()\n\tresponse := bytes.Repeat([]byte(\"a\"), int(*responseSize))\n\taddr := \"localhost:\" + *serverPort\n\tlog.Println(\"Starting HTTP server on:\", addr)\n\tvar lserr error\n\tif *stdHTTP {\n\t\tlserr = http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t_, werr := w.Write(response)\n\t\t\tif werr != nil {\n\t\t\t\tlog.Println(werr)\n\t\t\t}\n\t\t}))\n\t} else {\n\t\tlserr = fasthttp.ListenAndServe(addr, func(c *fasthttp.RequestCtx) {\n\t\t\t_, werr := c.Write(response)\n\t\t\tif werr != nil {\n\t\t\t\tlog.Println(werr)\n\t\t\t}\n\t\t})\n\t}\n\tif lserr != nil {\n\t\tlog.Println(lserr)\n\t}\n}\n"
  },
  {
    "path": "common.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/goware/urlx\"\n)\n\nconst (\n\tdecBase = 10\n\n\trateLimitInterval = 10 * time.Millisecond\n\toneSecond         = 1 * time.Second\n\n\texitFailure = 1\n)\n\nvar (\n\tversion = \"unspecified\"\n\n\temptyConf = config{}\n\tparser    = newKingpinParser()\n\n\tdefaultTestDuration  = 10 * time.Second\n\tdefaultNumberOfConns = uint64(125)\n\tdefaultTimeout       = 2 * time.Second\n\n\thttpMethods = []string{\n\t\t\"GET\", \"POST\", \"PUT\", \"DELETE\", \"HEAD\", \"OPTIONS\",\n\t\t\"PATCH\",\n\t}\n\tcantHaveBody = []string{\"HEAD\"}\n\n\terrUnsupportedScheme    = errors.New(\"unsupported scheme\")\n\terrInvalidNumberOfConns = errors.New(\n\t\t\"invalid number of connections(must be > 0)\")\n\terrInvalidNumberOfRequests = errors.New(\n\t\t\"invalid number of requests(must be > 0)\")\n\terrInvalidTestDuration = errors.New(\n\t\t\"invalid test duration(must be >= 1s)\")\n\terrNegativeTimeout = errors.New(\n\t\t\"timeout can't be negative\")\n\terrBodyNotAllowed = errors.New(\n\t\t\"HEAD requests cannot have body\")\n\terrNoPathToCert = errors.New(\n\t\t\"no Path to TLS Client Certificate\")\n\terrNoPathToKey = errors.New(\n\t\t\"no Path to TLS Client Certificate Private Key\")\n\terrZeroRate = errors.New(\n\t\t\"rate can't be less than 1\")\n\terrBodyProvidedTwice = errors.New(\"use either --body or --body-file\")\n\n\terrInvalidHeaderFormat = errors.New(\"invalid header format\")\n\terrEmptyPrintSpec      = errors.New(\n\t\t\"empty print spec is not a valid print spec\")\n)\n\nfunc ParseURLOrPanic(s string) *url.URL {\n\tu, err := urlx.Parse(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n\nfunc init() {\n\tsort.Strings(httpMethods)\n\tsort.Strings(cantHaveBody)\n}\n"
  },
  {
    "path": "completion_barriers.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype completionBarrier interface {\n\tcompleted() float64\n\ttryGrabWork() bool\n\tjobDone()\n\tdone() <-chan struct{}\n\tcancel()\n}\n\ntype countingCompletionBarrier struct {\n\tnumReqs, reqsGrabbed, reqsDone uint64\n\tdoneChan                       chan struct{}\n\tcloseOnce                      sync.Once\n}\n\nfunc newCountingCompletionBarrier(numReqs uint64) completionBarrier {\n\tc := new(countingCompletionBarrier)\n\tc.reqsDone, c.reqsGrabbed, c.numReqs = 0, 0, numReqs\n\tc.doneChan = make(chan struct{})\n\treturn completionBarrier(c)\n}\n\nfunc (c *countingCompletionBarrier) tryGrabWork() bool {\n\tselect {\n\tcase <-c.doneChan:\n\t\treturn false\n\tdefault:\n\t\treqsDone := atomic.AddUint64(&c.reqsGrabbed, 1)\n\t\treturn reqsDone <= c.numReqs\n\t}\n}\n\nfunc (c *countingCompletionBarrier) jobDone() {\n\treqsDone := atomic.AddUint64(&c.reqsDone, 1)\n\tif reqsDone == c.numReqs {\n\t\tc.closeOnce.Do(func() {\n\t\t\tclose(c.doneChan)\n\t\t})\n\t}\n}\n\nfunc (c *countingCompletionBarrier) done() <-chan struct{} {\n\treturn c.doneChan\n}\n\nfunc (c *countingCompletionBarrier) cancel() {\n\tc.closeOnce.Do(func() {\n\t\tclose(c.doneChan)\n\t})\n}\n\nfunc (c *countingCompletionBarrier) completed() float64 {\n\tselect {\n\tcase <-c.doneChan:\n\t\treturn 1.0\n\tdefault:\n\t\treqsDone := atomic.LoadUint64(&c.reqsDone)\n\t\treturn float64(reqsDone) / float64(c.numReqs)\n\t}\n}\n\ntype timedCompletionBarrier struct {\n\tdoneChan  chan struct{}\n\tcloseOnce sync.Once\n\tstart     time.Time\n\tduration  time.Duration\n}\n\nfunc newTimedCompletionBarrier(duration time.Duration) completionBarrier {\n\tif duration < 0 {\n\t\tpanic(\"timedCompletionBarrier: negative duration\")\n\t}\n\tc := new(timedCompletionBarrier)\n\tc.doneChan = make(chan struct{})\n\tc.start = time.Now()\n\tc.duration = duration\n\tgo func() {\n\t\ttime.AfterFunc(duration, func() {\n\t\t\tc.closeOnce.Do(func() {\n\t\t\t\tclose(c.doneChan)\n\t\t\t})\n\t\t})\n\t}()\n\treturn completionBarrier(c)\n}\n\nfunc (c *timedCompletionBarrier) tryGrabWork() bool {\n\tselect {\n\tcase <-c.doneChan:\n\t\treturn false\n\tdefault:\n\t\treturn true\n\t}\n}\n\nfunc (c *timedCompletionBarrier) jobDone() {\n}\n\nfunc (c *timedCompletionBarrier) done() <-chan struct{} {\n\treturn c.doneChan\n}\n\nfunc (c *timedCompletionBarrier) cancel() {\n\tc.closeOnce.Do(func() {\n\t\tclose(c.doneChan)\n\t})\n}\n\nfunc (c *timedCompletionBarrier) completed() float64 {\n\tselect {\n\tcase <-c.doneChan:\n\t\treturn 1.0\n\tdefault:\n\t\treturn float64(time.Since(c.start).Nanoseconds()) /\n\t\t\tfloat64(c.duration.Nanoseconds())\n\t}\n}\n"
  },
  {
    "path": "completion_barriers_test.go",
    "content": "package main\n\nimport (\n\t\"math\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCouintingCompletionBarrierWait(t *testing.T) {\n\tparties := uint64(10)\n\tb := newCountingCompletionBarrier(1000)\n\tfor i := uint64(0); i < parties; i++ {\n\t\tgo func() {\n\t\t\tfor b.tryGrabWork() {\n\t\t\t\tb.jobDone()\n\t\t\t}\n\t\t}()\n\t}\n\twc := make(chan struct{})\n\tgo func() {\n\t\t<-b.done()\n\t\twc <- struct{}{}\n\t}()\n\tselect {\n\tcase <-wc:\n\t\treturn\n\tcase <-time.After(100 * time.Millisecond):\n\t\tt.Fail()\n\t}\n}\n\nfunc TestTimedCompletionBarrierWait(t *testing.T) {\n\tparties := uint64(10)\n\tduration := 100 * time.Millisecond\n\ttimeout := duration * 2\n\terr := 15 * time.Millisecond\n\tsleepDuration := 2 * time.Millisecond\n\tb := newTimedCompletionBarrier(duration)\n\tfor i := uint64(0); i < parties; i++ {\n\t\tgo func() {\n\t\t\tfor b.tryGrabWork() {\n\t\t\t\ttime.Sleep(sleepDuration)\n\t\t\t\tb.jobDone()\n\t\t\t}\n\t\t}()\n\t}\n\twc := make(chan time.Duration)\n\tgo func() {\n\t\tstart := time.Now()\n\t\t<-b.done()\n\t\twc <- time.Since(start)\n\t}()\n\tselect {\n\tcase actual := <-wc:\n\t\tif !approximatelyEqual(duration, actual, sleepDuration+err) {\n\t\t\tt.Errorf(\"Expected to run %v, but ran %v instead\", duration, actual)\n\t\t}\n\tcase <-time.After(timeout):\n\t\tt.Error(\"Barrier hanged\")\n\t}\n}\n\nfunc TestTimeBarrierCancel(t *testing.T) {\n\tb := newTimedCompletionBarrier(9000 * time.Second)\n\tsleepTime := 100 * time.Millisecond\n\tgo func() {\n\t\ttime.Sleep(sleepTime)\n\t\tb.cancel()\n\t}()\n\tselect {\n\tcase <-b.done():\n\t\tif c := b.completed(); c != 1.0 {\n\t\t\tt.Error(c)\n\t\t}\n\tcase <-time.After(sleepTime * 2):\n\t\tt.Fail()\n\t}\n}\n\nfunc TestCountedBarrierCancel(t *testing.T) {\n\tparties := uint64(10)\n\tb := newCountingCompletionBarrier(math.MaxUint64)\n\tsleepTime := 100 * time.Millisecond\n\tfor i := uint64(0); i < parties; i++ {\n\t\tgo func() {\n\t\t\tfor b.tryGrabWork() {\n\t\t\t\tb.jobDone()\n\t\t\t}\n\t\t}()\n\t}\n\tgo func() {\n\t\ttime.Sleep(sleepTime)\n\t\tb.cancel()\n\t}()\n\tselect {\n\tcase <-b.done():\n\t\tif c := b.completed(); c != 1.0 {\n\t\t\tt.Error(c)\n\t\t}\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fail()\n\t}\n}\n\nfunc TestTimeBarrierPanicOnBadDuration(t *testing.T) {\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil {\n\t\t\tt.Error(\"shouldn't be empty\")\n\t\t\tt.Fail()\n\t\t}\n\t}()\n\tnewTimedCompletionBarrier(-1 * time.Second)\n\tt.Error(\"unreachable\")\n\tt.Fail()\n}\n\nfunc approximatelyEqual(expected, actual, err time.Duration) bool {\n\treturn expected-err < actual && actual < expected+err\n}\n"
  },
  {
    "path": "config.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n)\n\ntype config struct {\n\tnumConns                  uint64\n\tnumReqs                   *uint64\n\tdisableKeepAlives         bool\n\tduration                  *time.Duration\n\turl                       *url.URL\n\tmethod, certPath, keyPath string\n\tbody, bodyFilePath        string\n\tstream                    bool\n\theaders                   *headersList\n\ttimeout                   time.Duration\n\t// TODO(codesenberg): printLatencies should probably be\n\t// re(named&maked) into printPercentiles or even let\n\t// users provide their own percentiles and not just\n\t// calculate for [0.5, 0.75, 0.9, 0.99]\n\tprintLatencies, insecure bool\n\trate                     *uint64\n\tclientType               clientTyp\n\n\tprintIntro, printProgress, printResult bool\n\n\tformat format\n}\n\ntype testTyp int\n\nconst (\n\tnone testTyp = iota\n\ttimed\n\tcounted\n)\n\ntype invalidHTTPMethodError struct {\n\tmethod string\n}\n\nfunc (i *invalidHTTPMethodError) Error() string {\n\treturn fmt.Sprintf(\"Unknown HTTP method: %v\", i.method)\n}\n\nfunc (c *config) checkArgs() error {\n\tc.checkOrSetDefaultTestType()\n\n\tchecks := []func() error{\n\t\tc.checkURL,\n\t\tc.checkRate,\n\t\tc.checkRunParameters,\n\t\tc.checkTimeoutDuration,\n\t\tc.checkHTTPParameters,\n\t\tc.checkCertPaths,\n\t}\n\n\tfor _, check := range checks {\n\t\tif err := check(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *config) checkOrSetDefaultTestType() {\n\tif c.testType() == none {\n\t\tc.duration = &defaultTestDuration\n\t}\n}\n\nfunc (c *config) testType() testTyp {\n\ttyp := none\n\tif c.numReqs != nil {\n\t\ttyp = counted\n\t} else if c.duration != nil {\n\t\ttyp = timed\n\t}\n\treturn typ\n}\n\nfunc (c *config) checkURL() error {\n\tif c.url.Scheme != \"http\" && c.url.Scheme != \"https\" {\n\t\treturn errUnsupportedScheme\n\t}\n\treturn nil\n}\n\nfunc (c *config) checkRate() error {\n\tif c.rate != nil && *c.rate < 1 {\n\t\treturn errZeroRate\n\t}\n\treturn nil\n}\n\nfunc (c *config) checkRunParameters() error {\n\tif c.numConns < uint64(1) {\n\t\treturn errInvalidNumberOfConns\n\t}\n\tif c.testType() == counted && *c.numReqs < uint64(1) {\n\t\treturn errInvalidNumberOfRequests\n\t}\n\tif c.testType() == timed && *c.duration < time.Second {\n\t\treturn errInvalidTestDuration\n\t}\n\treturn nil\n}\n\nfunc (c *config) checkTimeoutDuration() error {\n\tif c.timeout < 0 {\n\t\treturn errNegativeTimeout\n\t}\n\treturn nil\n}\n\nfunc (c *config) checkHTTPParameters() error {\n\tif !allowedHTTPMethod(c.method) {\n\t\treturn &invalidHTTPMethodError{method: c.method}\n\t}\n\tif !canHaveBody(c.method) && (c.body != \"\" || c.bodyFilePath != \"\") {\n\t\treturn errBodyNotAllowed\n\t}\n\tif c.body != \"\" && c.bodyFilePath != \"\" {\n\t\treturn errBodyProvidedTwice\n\t}\n\treturn nil\n}\n\nfunc (c *config) checkCertPaths() error {\n\tif c.certPath != \"\" && c.keyPath == \"\" {\n\t\treturn errNoPathToKey\n\t} else if c.certPath == \"\" && c.keyPath != \"\" {\n\t\treturn errNoPathToCert\n\t}\n\treturn nil\n}\n\nfunc (c *config) timeoutMillis() uint64 {\n\treturn uint64(c.timeout.Nanoseconds() / 1000)\n}\n\nfunc allowedHTTPMethod(method string) bool {\n\ti := sort.SearchStrings(httpMethods, method)\n\treturn i < len(httpMethods) && httpMethods[i] == method\n}\n\nfunc canHaveBody(method string) bool {\n\ti := sort.SearchStrings(cantHaveBody, method)\n\treturn !(i < len(cantHaveBody) && cantHaveBody[i] == method)\n}\n\ntype clientTyp int\n\nconst (\n\tfhttp clientTyp = iota\n\tnhttp1\n\tnhttp2\n)\n\nfunc (ct clientTyp) String() string {\n\tswitch ct {\n\tcase fhttp:\n\t\treturn \"FastHTTP\"\n\tcase nhttp1:\n\t\treturn \"net/http v1.x\"\n\tcase nhttp2:\n\t\treturn \"net/http v2.0\"\n\t}\n\treturn \"unknown client\"\n}\n"
  },
  {
    "path": "config_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tdefaultNumberOfReqs = uint64(10000)\n)\n\nfunc TestCanHaveBody(t *testing.T) {\n\texpectations := []struct {\n\t\tin  string\n\t\tout bool\n\t}{\n\t\t{\"HEAD\", false},\n\t\t{\"GET\", true},\n\t\t{\"POST\", true},\n\t\t{\"PUT\", true},\n\t\t{\"DELETE\", true},\n\t\t{\"OPTIONS\", true},\n\t}\n\tfor _, e := range expectations {\n\t\tif r := canHaveBody(e.in); r != e.out {\n\t\t\tt.Error(e.in, e.out, r)\n\t\t}\n\t}\n}\n\nfunc TestAllowedHttpMethod(t *testing.T) {\n\texpectations := []struct {\n\t\tin  string\n\t\tout bool\n\t}{\n\t\t{\"GET\", true},\n\t\t{\"POST\", true},\n\t\t{\"PUT\", true},\n\t\t{\"DELETE\", true},\n\t\t{\"HEAD\", true},\n\t\t{\"OPTIONS\", true},\n\t\t{\"TRUNCATE\", false},\n\t}\n\tfor _, e := range expectations {\n\t\tif r := allowedHTTPMethod(e.in); r != e.out {\n\t\t\tt.Logf(\"Expected f(%v) = %v, but got %v\", e.in, e.out, r)\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\nfunc TestCheckArgs(t *testing.T) {\n\tinvalidNumberOfReqs := uint64(0)\n\tsmallTestDuration := 99 * time.Millisecond\n\tnegativeTimeoutDuration := -1 * time.Second\n\tnoHeaders := new(headersList)\n\tzeroRate := uint64(0)\n\texpectations := []struct {\n\t\tin  config\n\t\tout error\n\t}{\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: 0,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrInvalidNumberOfConns,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &invalidNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrInvalidNumberOfRequests,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  nil,\n\t\t\t\tduration: &smallTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrInvalidTestDuration,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  negativeTimeoutDuration,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrNegativeTimeout,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"HEAD\",\n\t\t\t\tbody:     \"BODY\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrBodyNotAllowed,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns:     defaultNumberOfConns,\n\t\t\t\tnumReqs:      &defaultNumberOfReqs,\n\t\t\t\tduration:     &defaultTestDuration,\n\t\t\t\turl:          ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:      noHeaders,\n\t\t\t\ttimeout:      defaultTimeout,\n\t\t\t\tmethod:       \"HEAD\",\n\t\t\t\tbodyFilePath: \"testbody.txt\",\n\t\t\t\tformat:       knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrBodyNotAllowed,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"BODY\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns:     defaultNumberOfConns,\n\t\t\t\tnumReqs:      &defaultNumberOfReqs,\n\t\t\t\tduration:     &defaultTestDuration,\n\t\t\t\turl:          ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:      noHeaders,\n\t\t\t\ttimeout:      defaultTimeout,\n\t\t\t\tmethod:       \"GET\",\n\t\t\t\tbodyFilePath: \"testbody.txt\",\n\t\t\t\tformat:       knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tcertPath: \"test_cert.pem\",\n\t\t\t\tkeyPath:  \"\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrNoPathToKey,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\tbody:     \"\",\n\t\t\t\tcertPath: \"\",\n\t\t\t\tkeyPath:  \"test_key.pem\",\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrNoPathToCert,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns: defaultNumberOfConns,\n\t\t\t\tnumReqs:  &defaultNumberOfReqs,\n\t\t\t\tduration: &defaultTestDuration,\n\t\t\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:  noHeaders,\n\t\t\t\ttimeout:  defaultTimeout,\n\t\t\t\tmethod:   \"GET\",\n\t\t\t\trate:     &zeroRate,\n\t\t\t\tformat:   knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrZeroRate,\n\t\t},\n\t\t{\n\t\t\tconfig{\n\t\t\t\tnumConns:     defaultNumberOfConns,\n\t\t\t\tnumReqs:      &defaultNumberOfReqs,\n\t\t\t\tduration:     &defaultTestDuration,\n\t\t\t\turl:          ParseURLOrPanic(\"http://localhost:8080\"),\n\t\t\t\theaders:      noHeaders,\n\t\t\t\ttimeout:      defaultTimeout,\n\t\t\t\tmethod:       \"POST\",\n\t\t\t\tbody:         \"abracadabra\",\n\t\t\t\tbodyFilePath: \"testbody.txt\",\n\t\t\t\tformat:       knownFormat(\"plain-text\"),\n\t\t\t},\n\t\t\terrBodyProvidedTwice,\n\t\t},\n\t}\n\tfor _, e := range expectations {\n\t\tif r := e.in.checkArgs(); r != e.out {\n\t\t\tt.Logf(\"Expected (%v).checkArgs to return %v, but got %v\", e.in, e.out, r)\n\t\t\tt.Fail()\n\t\t}\n\t\tif _, r := newBombardier(e.in); r != e.out {\n\t\t\tt.Logf(\"Expected newBombardier(%v) to return %v, but got %v\", e.in, e.out, r)\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\nfunc TestCheckArgsUnsupportedURLScheme(t *testing.T) {\n\tc := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  &defaultNumberOfReqs,\n\t\tduration: &defaultTestDuration,\n\t\turl:      ParseURLOrPanic(\"ftp://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\tif c.checkArgs() != errUnsupportedScheme {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestCheckArgsInvalidRequestMethod(t *testing.T) {\n\tc := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  &defaultNumberOfReqs,\n\t\tduration: &defaultTestDuration,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"ABRACADABRA\",\n\t\tbody:     \"\",\n\t}\n\te := c.checkArgs()\n\tif e == nil {\n\t\tt.Fail()\n\t}\n\tif _, ok := e.(*invalidHTTPMethodError); !ok {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestCheckArgsTestType(t *testing.T) {\n\tcountedConfig := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  &defaultNumberOfReqs,\n\t\tduration: nil,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\ttimedConfig := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  nil,\n\t\tduration: &defaultTestDuration,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\tboth := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  &defaultNumberOfReqs,\n\t\tduration: &defaultTestDuration,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\tdefaultConfig := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  nil,\n\t\tduration: nil,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  defaultTimeout,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\tif err := countedConfig.checkArgs(); err != nil ||\n\t\tcountedConfig.testType() != counted {\n\t\tt.Fail()\n\t}\n\tif err := timedConfig.checkArgs(); err != nil ||\n\t\ttimedConfig.testType() != timed {\n\t\tt.Fail()\n\t}\n\tif err := both.checkArgs(); err != nil ||\n\t\tboth.testType() != counted {\n\t\tt.Fail()\n\t}\n\tif err := defaultConfig.checkArgs(); err != nil ||\n\t\tdefaultConfig.testType() != timed ||\n\t\tdefaultConfig.duration != &defaultTestDuration {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestTimeoutMillis(t *testing.T) {\n\tdefaultConfig := config{\n\t\tnumConns: defaultNumberOfConns,\n\t\tnumReqs:  nil,\n\t\tduration: nil,\n\t\turl:      ParseURLOrPanic(\"http://localhost:8080\"),\n\t\theaders:  nil,\n\t\ttimeout:  2 * time.Second,\n\t\tmethod:   \"GET\",\n\t\tbody:     \"\",\n\t}\n\tif defaultConfig.timeoutMillis() != 2000000 {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestInvalidHTTPMethodError(t *testing.T) {\n\tinvalidMethod := \"NOSUCHMETHOD\"\n\twant := \"Unknown HTTP method: \" + invalidMethod\n\terr := &invalidHTTPMethodError{invalidMethod}\n\tif got := err.Error(); got != want {\n\t\tt.Error(got, want)\n\t}\n}\n\nfunc TestClientTypToStringConversion(t *testing.T) {\n\texpectations := []struct {\n\t\tin  clientTyp\n\t\tout string\n\t}{\n\t\t{fhttp, \"FastHTTP\"},\n\t\t{nhttp1, \"net/http v1.x\"},\n\t\t{nhttp2, \"net/http v2.0\"},\n\t\t{42, \"unknown client\"},\n\t}\n\tfor _, exp := range expectations {\n\t\tact := exp.in.String()\n\t\tif act != exp.out {\n\t\t\tt.Errorf(\"Expected %v, but got %v\", exp.out, act)\n\t\t}\n\t}\n}\n\nfunc clientTypeFromString(s string) clientTyp {\n\tswitch s {\n\tcase \"fasthttp\":\n\t\treturn fhttp\n\tcase \"http1\":\n\t\treturn nhttp1\n\tcase \"http2\":\n\t\treturn nhttp2\n\tdefault:\n\t\treturn fhttp\n\t}\n}\n"
  },
  {
    "path": "dialer.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype countingConn struct {\n\tnet.Conn\n\tbytesRead, bytesWritten *int64\n}\n\nfunc (cc *countingConn) Read(b []byte) (n int, err error) {\n\tn, err = cc.Conn.Read(b)\n\n\tif err == nil {\n\t\tatomic.AddInt64(cc.bytesRead, int64(n))\n\t}\n\n\treturn\n}\n\nfunc (cc *countingConn) Write(b []byte) (n int, err error) {\n\tn, err = cc.Conn.Write(b)\n\n\tif err == nil {\n\t\tatomic.AddInt64(cc.bytesWritten, int64(n))\n\t}\n\n\treturn\n}\n\nvar fasthttpDialFunc = func(\n\tbytesRead, bytesWritten *int64,\n\tdialTimeout time.Duration,\n) func(string) (net.Conn, error) {\n\treturn func(address string) (net.Conn, error) {\n\t\tconn, err := net.DialTimeout(\"tcp\", address, dialTimeout)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\twrappedConn := &countingConn{\n\t\t\tConn:         conn,\n\t\t\tbytesRead:    bytesRead,\n\t\t\tbytesWritten: bytesWritten,\n\t\t}\n\n\t\treturn wrappedConn, nil\n\t}\n}\n\nvar httpDialContextFunc = func(\n\tbytesRead, bytesWritten *int64,\n\tdialTimeout time.Duration,\n) func(context.Context, string, string) (net.Conn, error) {\n\tdialer := &net.Dialer{Timeout: dialTimeout}\n\treturn func(ctx context.Context, network, address string) (net.Conn, error) {\n\t\tconn, err := dialer.DialContext(ctx, network, address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\twrappedConn := &countingConn{\n\t\t\tConn:         conn,\n\t\t\tbytesRead:    bytesRead,\n\t\t\tbytesWritten: bytesWritten,\n\t\t}\n\n\t\treturn wrappedConn, nil\n\t}\n}\n"
  },
  {
    "path": "doc.go",
    "content": "/*\nCommand line utility bombardier is a fast cross-platform HTTP\nbenchmarking tool written in Go.\n\nInstallation with Go 1.17+:\n\n\tgo install github.com/codesenberg/bombardier@latest\n\nInstallation with older versions of Go:\n\n\tgo get -u github.com/codesenberg/bombardier\n\nUsage:\n\n\tbombardier [<flags>] <url>\n\nFlags:\n\n\t    --help                  Show context-sensitive help (also try --help-long\n\t                            and --help-man).\n\t    --version               Show application version.\n\t-c, --connections=125       Maximum number of concurrent connections\n\t-t, --timeout=2s            Socket/request timeout\n\t-l, --latencies             Print latency statistics\n\t-m, --method=GET            Request method\n\t-b, --body=\"\"               Request body\n\t-f, --body-file=\"\"          File to use as request body\n\t-s, --stream                Specify whether to stream body using chunked\n\t                            transfer encoding or to serve it from memory\n\t    --cert=\"\"               Path to the client's TLS Certificate\n\t    --key=\"\"                Path to the client's TLS Certificate Private Key\n\t-k, --insecure              Controls whether a client verifies the server's\n\t                            certificate chain and host name\n\t-H, --header=\"K: V\" ...     HTTP headers to use(can be repeated)\n\t-n, --requests=[pos. int.]  Number of requests\n\t-d, --duration=10s          Duration of test\n\t-r, --rate=[pos. int.]      Rate limit in requests per second\n\t    --fasthttp              Use fasthttp client\n\t    --http1                 Use net/http client with forced HTTP/1.x\n\t    --http2                 Use net/http client with enabled HTTP/2.0\n\t-p, --print=<spec>          Specifies what to output. Comma-separated list of\n\t                            values 'intro' (short: 'i'), 'progress' (short:\n\t                            'p'), 'result' (short: 'r'). Examples:\n\n\t                              * i,p,r (prints everything)\n\t                              * intro,result (intro & result)\n\t                              * r (result only)\n\t                              * result (same as above)\n\t-q, --no-print              Don't output anything\n\t-o, --format=<spec>         Which format to use to output the result. <spec>\n\t                            is either a name (or its shorthand) of some format\n\t                            understood by bombardier or a path to the\n\t                            user-defined template, which uses Go's\n\t                            text/template syntax, prefixed with 'path:' string\n\t                            (without single quotes), i.e.\n\t                            \"path:/some/path/to/your.template\" or\n\t                            \"path:C:\\some\\path\\to\\your.template\" in case of\n\t                            Windows. Formats understood by bombardier are:\n\n\t                              * plain-text (short: pt)\n\t                              * json (short: j)\n\nArgs:\n\n\t<url>  Target's URL\n\nFor detailed documentation on user-defined templates see\ndocumentation for package github.com/codesenberg/bombardier/template.\nLink (GoDoc):\nhttps://godoc.org/github.com/codesenberg/bombardier/template\n*/\npackage main\n"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "content": "### Contribution Guidelines\nFor 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).\n\n### Reporting issues\nPlease open an issue if you would like to discuss anything that could be improved, have a suggestion or want to report a bug.\nIn latter case refer to [ISSUE_TEMPLATE](https://github.com/codesenberg/bombardier/blob/master/.github/ISSUE_TEMPLATE.md).\n"
  },
  {
    "path": "error_map.go",
    "content": "package main\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\ntype errorMap struct {\n\tmu sync.RWMutex\n\tm  map[string]*uint64\n}\n\nfunc newErrorMap() *errorMap {\n\tem := new(errorMap)\n\tem.m = make(map[string]*uint64)\n\treturn em\n}\n\nfunc (e *errorMap) add(err error) {\n\ts := err.Error()\n\te.mu.RLock()\n\tc, ok := e.m[s]\n\te.mu.RUnlock()\n\tif !ok {\n\t\te.mu.Lock()\n\t\tc, ok = e.m[s]\n\t\tif !ok {\n\t\t\tc = new(uint64)\n\t\t\te.m[s] = c\n\t\t}\n\t\te.mu.Unlock()\n\t}\n\tatomic.AddUint64(c, 1)\n}\n\nfunc (e *errorMap) get(err error) uint64 {\n\ts := err.Error()\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\tc := e.m[s]\n\tif c == nil {\n\t\treturn uint64(0)\n\t}\n\treturn *c\n}\n\nfunc (e *errorMap) sum() uint64 {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\tsum := uint64(0)\n\tfor _, v := range e.m {\n\t\tsum += *v\n\t}\n\treturn sum\n}\n\ntype errorWithCount struct {\n\terror string\n\tcount uint64\n}\n\nfunc (ewc *errorWithCount) String() string {\n\treturn \"<\" + ewc.error + \":\" +\n\t\tstrconv.FormatUint(ewc.count, decBase) + \">\"\n}\n\ntype errorsByFrequency []*errorWithCount\n\nfunc (ebf errorsByFrequency) Len() int {\n\treturn len(ebf)\n}\n\nfunc (ebf errorsByFrequency) Less(i, j int) bool {\n\treturn ebf[i].count > ebf[j].count\n}\n\nfunc (ebf errorsByFrequency) Swap(i, j int) {\n\tebf[i], ebf[j] = ebf[j], ebf[i]\n}\n\nfunc (e *errorMap) byFrequency() errorsByFrequency {\n\te.mu.RLock()\n\tbyFreq := make(errorsByFrequency, 0, len(e.m))\n\tfor err, count := range e.m {\n\t\tbyFreq = append(byFreq, &errorWithCount{err, *count})\n\t}\n\te.mu.RUnlock()\n\tsort.Sort(byFreq)\n\treturn byFreq\n}\n"
  },
  {
    "path": "error_map_test.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestErrorMapAdd(t *testing.T) {\n\tm := newErrorMap()\n\terr := errors.New(\"add\")\n\tm.add(err)\n\tif c := m.get(err); c != 1 {\n\t\tt.Error(c)\n\t}\n}\n\nfunc TestErrorMapGet(t *testing.T) {\n\tm := newErrorMap()\n\terr := errors.New(\"get\")\n\tif c := m.get(err); c != 0 {\n\t\tt.Error(c)\n\t}\n}\n\nfunc TestByFrequency(t *testing.T) {\n\tm := newErrorMap()\n\ta := errors.New(\"A\")\n\tb := errors.New(\"B\")\n\tc := errors.New(\"C\")\n\tm.add(a)\n\tm.add(a)\n\tm.add(b)\n\tm.add(b)\n\tm.add(b)\n\tm.add(c)\n\te := errorsByFrequency{\n\t\t{\"B\", 3},\n\t\t{\"A\", 2},\n\t\t{\"C\", 1},\n\t}\n\tif a := m.byFrequency(); !reflect.DeepEqual(a, e) {\n\t\tt.Logf(\"Expected: %+v\", e)\n\t\tt.Logf(\"Got: %+v\", a)\n\t\tt.Fail()\n\t}\n}\n\nfunc TestErrorWithCountToStringConversion(t *testing.T) {\n\tewc := errorWithCount{\"A\", 1}\n\texp := \"<A:1>\"\n\tif act := ewc.String(); act != exp {\n\t\tt.Logf(\"Expected: %+v\", exp)\n\t\tt.Logf(\"Got: %+v\", act)\n\t\tt.Fail()\n\t}\n}\n\nfunc BenchmarkErrorMapAdd(b *testing.B) {\n\tm := newErrorMap()\n\terr := errors.New(\"benchmark\")\n\tb.ResetTimer()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tm.add(err)\n\t\t}\n\t})\n}\n\nfunc BenchmarkErrorMapGet(b *testing.B) {\n\tm := newErrorMap()\n\terr := errors.New(\"benchmark\")\n\tm.add(err)\n\tb.ResetTimer()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tm.get(err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "flags.go",
    "content": "package main\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\tnilStr = \"nil\"\n)\n\ntype nullableUint64 struct {\n\tval *uint64\n}\n\nfunc (n *nullableUint64) String() string {\n\tif n.val == nil {\n\t\treturn nilStr\n\t}\n\treturn strconv.FormatUint(*n.val, 10)\n}\n\nfunc (n *nullableUint64) Set(value string) error {\n\tres, err := strconv.ParseUint(value, 10, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\tn.val = new(uint64)\n\t*n.val = res\n\treturn nil\n}\n\ntype nullableDuration struct {\n\tval *time.Duration\n}\n\nfunc (n *nullableDuration) String() string {\n\tif n.val == nil {\n\t\treturn nilStr\n\t}\n\treturn n.val.String()\n}\n\nfunc (n *nullableDuration) Set(value string) error {\n\tres, err := time.ParseDuration(value)\n\tif err != nil {\n\t\treturn err\n\t}\n\tn.val = &res\n\treturn nil\n}\n\ntype nullableString struct {\n\tval *string\n}\n\nfunc (n *nullableString) String() string {\n\tif n.val == nil {\n\t\treturn nilStr\n\t}\n\treturn *n.val\n}\n\nfunc (n *nullableString) Set(value string) error {\n\tn.val = new(string)\n\t*n.val = value\n\treturn nil\n}\n"
  },
  {
    "path": "flags_test.go",
    "content": "package main\n\nimport (\n\t\"math\"\n\t\"math/big\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestNullableUint64ConversionToString(t *testing.T) {\n\tnilint := &nullableUint64{val: nil}\n\tif s := nilint.String(); s != \"nil\" {\n\t\tt.Errorf(\"Expected \\\"nil\\\", but got %v\", s)\n\t}\n\tv := uint64(42)\n\tnonnilint := &nullableUint64{val: &v}\n\tif s, e := nonnilint.String(), strconv.FormatUint(v, 10); s != e {\n\t\tt.Errorf(\"Expected %v, but got %v\", e, s)\n\t}\n}\n\nfunc TestNullableUint64Parsing(t *testing.T) {\n\tn := &nullableUint64{}\n\tif err := n.Set(\"-1\"); err == nil {\n\t\tt.Error(\"Should fail on negative values\")\n\t}\n\tif err := n.Set(\"\"); err == nil {\n\t\tt.Error(\"Should fail on empty string\")\n\t}\n\tb := big.NewInt(0)\n\tb.SetUint64(math.MaxUint64)\n\tb.Add(b, big.NewInt(1))\n\tif err := n.Set(b.String()); err == nil {\n\t\tt.Error(\"Should fail on large values\")\n\t}\n\tmax := strconv.FormatUint(math.MaxUint64, 10)\n\tif err := n.Set(max); err != nil || *n.val != uint64(18446744073709551615) {\n\t\tt.Error(\"Shouldn't fail on max value\")\n\t}\n}\n\nfunc TestNullableDurationConversionToString(t *testing.T) {\n\tnildur := &nullableDuration{val: nil}\n\tif s := nildur.String(); s != \"nil\" {\n\t\tt.Errorf(\"Expected \\\"nil\\\", but got %v\", s)\n\t}\n\td := time.Second\n\tnonnildir := &nullableDuration{val: &d}\n\tif s := nonnildir.String(); s != \"1s\" {\n\t\tt.Errorf(\"Expected 1s, but got %v\", s)\n\t}\n}\n\nfunc TestNullableDurationParsing(t *testing.T) {\n\td := &nullableDuration{}\n\tif err := d.Set(\"\"); err == nil {\n\t\tt.Error(\"Should fail on empty string\")\n\t}\n\tif err := d.Set(\"Wubba lubba dub dub!\"); err == nil {\n\t\tt.Error(\"Should fail on incorrect values\")\n\t}\n\tif err := d.Set(\"1s\"); err != nil || *d.val != time.Second {\n\t\tt.Error(\"Shouldn't fail on correct values\")\n\t}\n}\n\nfunc TestNullableStringConversionToString(t *testing.T) {\n\tns := new(nullableString)\n\tif act := ns.String(); act != nilStr {\n\t\tt.Error(\"Unset nullableString should convert to \\\"nil\\\"\")\n\t}\n\tsomeVal := \"someval\"\n\tif err := ns.Set(someVal); err != nil {\n\t\tt.Errorf(\"Couldn't set nullableString to %q\", someVal)\n\t}\n\tif act := ns.String(); act != someVal {\n\t\tt.Errorf(\"Expected %q, but got %q\", someVal, act)\n\t}\n}\n"
  },
  {
    "path": "format.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n)\n\ntype units struct {\n\tscale uint64\n\tbase  string\n\tunits []string\n}\n\nvar (\n\tbinaryUnits = &units{\n\t\tscale: 1024,\n\t\tbase:  \"\",\n\t\tunits: []string{\"KB\", \"MB\", \"GB\", \"TB\", \"PB\"},\n\t}\n\ttimeUnitsUs = &units{\n\t\tscale: 1000,\n\t\tbase:  \"us\",\n\t\tunits: []string{\"ms\", \"s\"},\n\t}\n\ttimeUnitsS = &units{\n\t\tscale: 60,\n\t\tbase:  \"s\",\n\t\tunits: []string{\"m\", \"h\"},\n\t}\n)\n\nfunc formatUnits(n float64, m *units, prec int) string {\n\tamt := n\n\tunit := m.base\n\n\tscale := float64(m.scale) * 0.85\n\n\tfor i := 0; i < len(m.units) && amt >= scale; i++ {\n\t\tamt /= float64(m.scale)\n\t\tunit = m.units[i]\n\t}\n\treturn fmt.Sprintf(\"%.*f%s\", prec, amt, unit)\n}\n\nfunc formatBinary(n float64) string {\n\treturn formatUnits(n, binaryUnits, 2)\n}\n\nfunc formatTimeUs(n float64) string {\n\tunits := timeUnitsUs\n\tif n >= 1000000.0 {\n\t\tn /= 1000000.0\n\t\tunits = timeUnitsS\n\t}\n\treturn formatUnits(n, units, 2)\n}\n"
  },
  {
    "path": "format_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nconst (\n\tKB = 1024\n\tMB = KB * 1024\n\tGB = MB * 1024\n\n\tK = 1000\n\tM = K * 1000\n)\n\nfunc TestShouldFormatBinary(t *testing.T) {\n\texpectations := []struct {\n\t\tin  float64\n\t\tout string\n\t}{\n\t\t{10.0, \"10.00\"},\n\t\t{10.001, \"10.00\"},\n\t\t{1.0 * KB, \"1.00KB\"},\n\t\t{1.2 * KB, \"1.20KB\"},\n\t\t{1.202 * KB, \"1.20KB\"},\n\t\t{5 * KB, \"5.00KB\"},\n\t\t{1.0 * MB, \"1.00MB\"},\n\t\t{1.3 * MB, \"1.30MB\"},\n\t\t{1.302 * MB, \"1.30MB\"},\n\t\t{6 * MB, \"6.00MB\"},\n\t\t{1.0 * GB, \"1.00GB\"},\n\t\t{1.4 * GB, \"1.40GB\"},\n\t\t{1.402 * GB, \"1.40GB\"},\n\t\t{7 * GB, \"7.00GB\"},\n\t}\n\tfor _, e := range expectations {\n\t\tactual := formatBinary(e.in)\n\t\texpected := e.out\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"Expected \\\"%v\\\", but got \\\"%v\\\"\", expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestShouldFormatUs(t *testing.T) {\n\texpectations := []struct {\n\t\tin  float64\n\t\tout string\n\t}{\n\t\t{20, \"20.00us\"},\n\t\t{22.222, \"22.22us\"},\n\t\t{20 * K, \"20.00ms\"},\n\t\t{20 * M, \"20.00s\"},\n\t\t{60 * M, \"1.00m\"},\n\t\t{10 * 60 * M, \"10.00m\"},\n\t\t{90 * 60 * M, \"1.50h\"},\n\t}\n\tfor _, e := range expectations {\n\t\tactual := formatTimeUs(e.in)\n\t\texpected := e.out\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"Expected \\\"%v\\\", but got \\\"%v\\\"\", expected, actual)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/codesenberg/bombardier\n\ngo 1.22\n\ntoolchain go1.24.0\n\nrequire (\n\tgithub.com/alecthomas/kingpin v2.2.6+incompatible\n\tgithub.com/cheggaaa/pb v1.0.29\n\tgithub.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964\n\tgithub.com/goware/urlx v0.3.2\n\tgithub.com/juju/ratelimit v1.0.2\n\tgithub.com/satori/go.uuid v1.2.0\n\tgithub.com/valyala/fasthttp v1.59.0\n)\n\nrequire (\n\tgithub.com/PuerkitoBio/purell v1.2.1 // indirect\n\tgithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect\n\tgithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect\n\tgithub.com/andybalholm/brotli v1.1.1 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgolang.org/x/net v0.35.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n\tgolang.org/x/text v0.22.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=\ngithub.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=\ngithub.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=\ngithub.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=\ngithub.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=\ngithub.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=\ngithub.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=\ngithub.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964 h1:9MVnbW3h0Dl4E2oADqwyvODphl9jY1r5HMtcB8U5mGs=\ngithub.com/codesenberg/concurrent v0.0.0-20180531114123-64560cfcf964/go.mod h1:82C6OyVM6eVk7qpBAZXE9uszHUuXWJMHHOeY+b/CSIA=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo=\ngithub.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw=\ngithub.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=\ngithub.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=\ngithub.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=\ngolang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=\ngolang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "headers.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype header struct {\n\tkey, value string\n}\n\ntype headersList []header\n\nfunc (h *headersList) String() string {\n\treturn fmt.Sprint(*h)\n}\n\nfunc (h *headersList) IsCumulative() bool {\n\treturn true\n}\n\nfunc (h *headersList) Set(value string) error {\n\tres := strings.SplitN(value, \":\", 2)\n\tif len(res) != 2 {\n\t\treturn errInvalidHeaderFormat\n\t}\n\t*h = append(*h, header{\n\t\tres[0], strings.Trim(res[1], \" \"),\n\t})\n\treturn nil\n}\n"
  },
  {
    "path": "headers_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHeadersToStringConversion(t *testing.T) {\n\texpectations := []struct {\n\t\tin  headersList\n\t\tout string\n\t}{\n\t\t{\n\t\t\t[]header{},\n\t\t\t\"[]\",\n\t\t},\n\t\t{\n\t\t\t[]header{\n\t\t\t\t{\"Key1\", \"Value1\"},\n\t\t\t\t{\"Key2\", \"Value2\"}},\n\t\t\t\"[{Key1 Value1} {Key2 Value2}]\",\n\t\t},\n\t}\n\tfor _, e := range expectations {\n\t\tactual := e.in.String()\n\t\texpected := e.out\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"Expected \\\"%v\\\", but got \\\"%v\\\"\", expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestShouldErrorOnInvalidFormat(t *testing.T) {\n\th := new(headersList)\n\tif err := h.Set(\"Yaba daba do\"); err == nil {\n\t\tt.Error(\"Should fail on strings without colon\")\n\t}\n}\n\nfunc TestShouldProperlyAddValidHeaders(t *testing.T) {\n\th := new(headersList)\n\tfor _, hs := range []string{\"Key1: Value1\", \"Key2: Value2\"} {\n\t\tif err := h.Set(hs); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n\te := []header{{\"Key1\", \"Value1\"}, {\"Key2\", \"Value2\"}}\n\tfor i, v := range *h {\n\t\tif e[i] != v {\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\nfunc TestShouldTrimHeaderValues(t *testing.T) {\n\th := new(headersList)\n\tif err := h.Set(\"Key:   Value   \"); err != nil {\n\t\tt.Error(err)\n\t}\n\tif (*h)[0].key != \"Key\" || (*h)[0].value != \"Value\" {\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "internal/test_info.go",
    "content": "package internal\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"sort\"\n\t\"time\"\n)\n\n// TestInfo holds information about what specification was used\n// to perform the test and results of the test.\ntype TestInfo struct {\n\tSpec   Spec\n\tResult Results\n}\n\n// Header represents HTTP header.\ntype Header struct {\n\tKey, Value string\n}\n\n// Spec contains information about test performed.\ntype Spec struct {\n\tNumberOfConnections uint64\n\n\tTestType         TestType\n\tNumberOfRequests uint64\n\tTestDuration     time.Duration\n\n\tMethod string\n\tURL    *url.URL\n\n\tHeaders []Header\n\n\tBody         string\n\tBodyFilePath string\n\n\tCertPath string\n\tKeyPath  string\n\n\tStream     bool\n\tTimeout    time.Duration\n\tClientType ClientType\n\n\tRate *uint64\n}\n\n// RequestURL returns URL as string.\nfunc (s Spec) RequestURL() string {\n\treturn s.URL.String()\n}\n\n// IsTimedTest tells if the test was limited by time.\nfunc (s Spec) IsTimedTest() bool {\n\treturn s.TestType == ByTime\n}\n\n// IsTestWithNumberOfReqs tells if the test was limited by the number\n// of requests.\nfunc (s Spec) IsTestWithNumberOfReqs() bool {\n\treturn s.TestType == ByNumberOfReqs\n}\n\n// IsFastHTTP tells whether fasthttp were used as HTTP client to\n// perform the test.\nfunc (s Spec) IsFastHTTP() bool {\n\treturn s.ClientType == FastHTTP\n}\n\n// IsNetHTTPV1 tells whether Go's default net/http library and\n// HTTP/1.x were used to perform the test.\nfunc (s Spec) IsNetHTTPV1() bool {\n\treturn s.ClientType == NetHTTP1\n}\n\n// IsNetHTTPV2 tells whether Go's default net/http library and\n// HTTP/1.x (or HTTP/2.0, if possible) were used to perform the test.\nfunc (s Spec) IsNetHTTPV2() bool {\n\treturn s.ClientType == NetHTTP2\n}\n\n// Results holds results of the test.\ntype Results struct {\n\tBytesRead, BytesWritten int64\n\tTimeTaken               time.Duration\n\n\tReq1XX, Req2XX, Req3XX, Req4XX, Req5XX uint64\n\tOthers                                 uint64\n\n\tErrors []ErrorWithCount\n\n\tLatencies ReadonlyUint64Histogram\n\tRequests  ReadonlyFloat64Histogram\n}\n\n// ReadonlyUint64Histogram is a readonly histogram with uint64 keys\ntype ReadonlyUint64Histogram interface {\n\tGet(uint64) uint64\n\tVisitAll(func(uint64, uint64) bool)\n\tCount() uint64\n}\n\n// ReadonlyFloat64Histogram is a readonly histogram with float64 keys\ntype ReadonlyFloat64Histogram interface {\n\tGet(float64) uint64\n\tVisitAll(func(float64, uint64) bool)\n\tCount() uint64\n}\n\n// Throughput returns total throughput (read + write) in bytes per\n// second\nfunc (r Results) Throughput() float64 {\n\treturn float64(r.BytesRead+r.BytesWritten) / r.TimeTaken.Seconds()\n}\n\n// LatenciesStats contains statistical information about latencies.\ntype LatenciesStats struct {\n\t// These are in microseconds\n\tMean   float64\n\tStddev float64\n\tMax    float64\n\n\t// This is  map[0.0 <= p <= 1.0 (percentile)]microseconds\n\tPercentiles map[float64]uint64\n}\n\n// LatenciesStats performs various statistical calculations on\n// latencies.\nfunc (r Results) LatenciesStats(percentiles []float64) *LatenciesStats {\n\th := r.Latencies\n\tsum := uint64(0)\n\tcount := uint64(0)\n\tmax := uint64(0)\n\tpairs := make([]struct{ k, v uint64 }, 0, h.Count())\n\n\t// Gather all the data\n\th.VisitAll(func(f uint64, c uint64) bool {\n\t\tif f > max {\n\t\t\tmax = f\n\t\t}\n\t\tsum += f * c\n\t\tcount += c\n\t\tpairs = append(pairs, struct{ k, v uint64 }{f, c})\n\t\treturn true\n\t})\n\tif count < 1 {\n\t\treturn nil\n\t}\n\n\t// Calculate percentiles\n\tsort.Slice(pairs, func(i, j int) bool {\n\t\treturn pairs[i].k < pairs[j].k\n\t})\n\tpercentilesMap := map[float64]uint64{}\n\tfor _, pc := range percentiles {\n\t\tif _, calculated := percentilesMap[pc]; calculated {\n\t\t\tcontinue\n\t\t}\n\t\tif pc < 0 || pc > 1 {\n\t\t\t// Drop percentiles outside of [0, 1] range\n\t\t\tcontinue\n\t\t}\n\t\trank := uint64(pc*float64(count) + 0.5)\n\t\ttotal := uint64(0)\n\t\tfor _, p := range pairs {\n\t\t\ttotal += p.v\n\t\t\tif total >= rank {\n\t\t\t\tpercentilesMap[pc] = p.k\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Calculate mean and standard deviation\n\tmean := float64(sum) / float64(count)\n\tsumOfSquares := float64(0)\n\th.VisitAll(func(f uint64, c uint64) bool {\n\t\tsumOfSquares += math.Pow(float64(f)-mean, 2)\n\t\treturn true\n\t})\n\tstddev := 0.0\n\tif count > 2 {\n\t\tstddev = math.Sqrt(sumOfSquares / float64(count))\n\t}\n\treturn &LatenciesStats{\n\t\tMean:   mean,\n\t\tStddev: stddev,\n\t\tMax:    float64(max),\n\n\t\tPercentiles: percentilesMap,\n\t}\n}\n\n// RequestsStats contains statistical information about requests.\ntype RequestsStats struct {\n\t// These are in requests per second.\n\tMean   float64\n\tStddev float64\n\tMax    float64\n\n\t// This is  map[0.0 <= p <= 1.0 (percentile)](req-s per second)\n\tPercentiles map[float64]float64\n}\n\n// RequestsStats performs various statistical calculations on\n// latencies.\nfunc (r Results) RequestsStats(percentiles []float64) *RequestsStats {\n\th := r.Requests\n\tsum := float64(0)\n\tcount := uint64(0)\n\tmax := float64(0)\n\tpairs := make([]struct {\n\t\tk float64\n\t\tv uint64\n\t}, 0, h.Count())\n\n\t// Gather all the data\n\th.VisitAll(func(f float64, c uint64) bool {\n\t\tif math.IsInf(f, 0) || math.IsNaN(f) {\n\t\t\treturn true\n\t\t}\n\t\tif f > max {\n\t\t\tmax = f\n\t\t}\n\t\tsum += f * float64(c)\n\t\tcount += c\n\t\tpairs = append(pairs, struct {\n\t\t\tk float64\n\t\t\tv uint64\n\t\t}{f, c})\n\t\treturn true\n\t})\n\tif count < 1 {\n\t\treturn nil\n\t}\n\n\t// Calculate percentiles\n\tsort.Slice(pairs, func(i, j int) bool {\n\t\treturn pairs[i].k < pairs[j].k\n\t})\n\tpercentilesMap := map[float64]float64{}\n\tfor _, pc := range percentiles {\n\t\tif _, calculated := percentilesMap[pc]; calculated {\n\t\t\tcontinue\n\t\t}\n\t\tif pc < 0 || pc > 1 {\n\t\t\t// Drop percentiles outside of [0, 1] range\n\t\t\tcontinue\n\t\t}\n\t\trank := uint64(pc*float64(count) + 0.5)\n\t\ttotal := uint64(0)\n\t\tfor _, p := range pairs {\n\t\t\ttotal += p.v\n\t\t\tif total >= rank {\n\t\t\t\tpercentilesMap[pc] = p.k\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Calculate mean and standard deviation\n\tmean := sum / float64(count)\n\tsumOfSquares := float64(0)\n\th.VisitAll(func(f float64, c uint64) bool {\n\t\tif math.IsInf(f, 0) || math.IsNaN(f) {\n\t\t\treturn true\n\t\t}\n\t\tsumOfSquares += math.Pow(f-mean, 2)\n\t\treturn true\n\t})\n\tstddev := 0.0\n\tif count > 2 {\n\t\tconst besselCorrection = 1.0\n\t\tstddev = math.Sqrt(sumOfSquares / (float64(count) - besselCorrection))\n\t}\n\treturn &RequestsStats{\n\t\tMean:   mean,\n\t\tStddev: stddev,\n\t\tMax:    max,\n\n\t\tPercentiles: percentilesMap,\n\t}\n}\n\n// ErrorWithCount contains error description alongside with number of\n// times this error occurred.\ntype ErrorWithCount struct {\n\tError string\n\tCount uint64\n}\n\n// TestType represents the type of test that were performed.\ntype TestType int\n\nconst (\n\t_ TestType = iota\n\t// ByTime is a test limited by durations.\n\tByTime\n\t// ByNumberOfReqs is a test limited by number of requests\n\t// performed.\n\tByNumberOfReqs\n)\n\n// ClientType is the type of HTTP client used in test\ntype ClientType int\n\nconst (\n\t// FastHTTP is fasthttp's HTTP client\n\tFastHTTP ClientType = iota\n\t// NetHTTP1 is Go's default HTTP client with forced HTTP/1.x\n\tNetHTTP1\n\t// NetHTTP2 is Go's default HTTP client with HTTP/2.0 permitted.\n\tNetHTTP2\n)\n"
  },
  {
    "path": "limiter.go",
    "content": "package main\n\nimport (\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/juju/ratelimit\"\n)\n\ntype token uint64\n\nconst (\n\tbrk token = iota\n\tcont\n)\n\ntype limiter interface {\n\tpace(<-chan struct{}) token\n}\n\ntype nooplimiter struct{}\n\nfunc (n *nooplimiter) pace(<-chan struct{}) token {\n\treturn cont\n}\n\ntype bucketlimiter struct {\n\tlimiter   *ratelimit.Bucket\n\ttimerPool *sync.Pool\n}\n\nfunc newBucketLimiter(rate uint64) limiter {\n\tfillInterval, quantum := estimate(rate, rateLimitInterval)\n\treturn &bucketlimiter{\n\t\tratelimit.NewBucketWithQuantum(\n\t\t\tfillInterval, int64(quantum), int64(quantum),\n\t\t),\n\t\t&sync.Pool{\n\t\t\tNew: func() interface{} {\n\t\t\t\treturn time.NewTimer(math.MaxInt64)\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (b *bucketlimiter) pace(done <-chan struct{}) (res token) {\n\twd := b.limiter.Take(1)\n\tif wd <= 0 {\n\t\treturn cont\n\t}\n\n\ttimer := b.timerPool.Get().(*time.Timer)\n\ttimer.Reset(wd)\n\tselect {\n\tcase <-timer.C:\n\t\tres = cont\n\tcase <-done:\n\t\tres = brk\n\t}\n\tb.timerPool.Put(timer)\n\treturn\n}\n"
  },
  {
    "path": "limiter_barrier_test.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestNoopLimiterCounterBarrierCombination(t *testing.T) {\n\texpectations := []uint64{\n\t\t1, 15, 50, 100, 150, 500, 1000, 1500, 5000,\n\t}\n\tdone := make(chan struct{})\n\tfor _, count := range expectations {\n\t\tb := newCountingCompletionBarrier(count)\n\t\tvar lim limiter = &nooplimiter{}\n\t\tcounter := uint64(0)\n\t\tnumParties := 10\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(numParties)\n\t\tfor i := 0; i < numParties; i++ {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tfor b.tryGrabWork() {\n\t\t\t\t\tlim.pace(done)\n\t\t\t\t\tatomic.AddUint64(&counter, 1)\n\t\t\t\t\tb.jobDone()\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t\tif counter != count {\n\t\t\tt.Error(count, counter)\n\t\t}\n\t}\n}\n\nfunc TestBucketLimiterCounterBarrierCombination(t *testing.T) {\n\texpectations := []struct {\n\t\tcount, rate uint64\n\t}{\n\t\t{10, 100},\n\t\t{10, 1000},\n\t\t{100, 1000},\n\t\t{100, 10000},\n\t\t{1000, 10000},\n\t\t{1000, 100000},\n\t}\n\tdone := make(chan struct{})\n\tvar expWg sync.WaitGroup\n\texpWg.Add(len(expectations))\n\tfor i := range expectations {\n\t\texp := expectations[i]\n\t\tgo func() {\n\t\t\tdefer expWg.Done()\n\t\t\tb := newCountingCompletionBarrier(exp.count)\n\t\t\tlim := newBucketLimiter(exp.rate)\n\t\t\tcounter := uint64(0)\n\t\t\tnumParties := 10\n\t\t\tvar wg sync.WaitGroup\n\t\t\twg.Add(numParties)\n\t\t\tfor i := 0; i < numParties; i++ {\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tfor b.tryGrabWork() {\n\t\t\t\t\t\tlim.pace(done)\n\t\t\t\t\t\tatomic.AddUint64(&counter, 1)\n\t\t\t\t\t\tb.jobDone()\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\twg.Wait()\n\t\t\tif counter != exp.count {\n\t\t\t\tt.Error(exp.count, counter)\n\t\t\t}\n\t\t}()\n\t}\n\texpWg.Wait()\n}\n"
  },
  {
    "path": "limiter_test.go",
    "content": "package main\n\nimport (\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst maxRps = 10000000\n\nfunc TestNoopLimiter(t *testing.T) {\n\tvar lim limiter = &nooplimiter{}\n\tdone := make(chan struct{})\n\tcounter := uint64(0)\n\tvar wg sync.WaitGroup\n\twg.Add(int(defaultNumberOfConns))\n\tfor i := uint64(0); i < defaultNumberOfConns; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor {\n\t\t\t\tres := lim.pace(done)\n\t\t\t\tif res != cont {\n\t\t\t\t\tt.Error(\"nooplimiter should always return cont\")\n\t\t\t\t}\n\t\t\t\tatomic.AddUint64(&counter, 1)\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\ttime.Sleep(100 * time.Millisecond)\n\tclose(done)\n\twg.Wait()\n\tif counter == 0 {\n\t\tt.Error(\"no events happened\")\n\t}\n}\n\nfunc BenchmarkNoopLimiter(bm *testing.B) {\n\tvar lim limiter = &nooplimiter{}\n\tdone := make(chan struct{})\n\tbm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())\n\tbm.ResetTimer()\n\tbm.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tlim.pace(done)\n\t\t}\n\t})\n}\n\nfunc TestBucketLimiterLowRates(t *testing.T) {\n\texpectations := []struct {\n\t\trate     uint64\n\t\tduration time.Duration\n\t}{\n\t\t{1, 1 * time.Second},\n\t\t{10, 1 * time.Second},\n\t\t{15, 1 * time.Second},\n\t\t{50, 1 * time.Second},\n\t\t{100, 1 * time.Second},\n\t\t{150, 1 * time.Second},\n\t\t{500, 1 * time.Second},\n\t\t{1000, 1 * time.Second},\n\t\t{1500, 1 * time.Second},\n\t\t{5000, 1 * time.Second},\n\t}\n\tfor i := range expectations {\n\t\texp := expectations[i]\n\t\tlim := newBucketLimiter(exp.rate)\n\t\tdone := make(chan struct{})\n\t\tcounter := uint64(0)\n\t\twaitChan := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\twaitChan <- struct{}{}\n\t\t\t}()\n\t\t\tfor lim.pace(done) == cont {\n\t\t\t\tcounter++\n\t\t\t}\n\t\t}()\n\t\ttime.Sleep(exp.duration)\n\t\tclose(done)\n\t\tselect {\n\t\tcase <-waitChan:\n\t\tcase <-time.After(exp.duration + 100*time.Millisecond):\n\t\t\tt.Error(\"failed to complete: \", exp)\n\t\t\treturn\n\t\t}\n\t\texpcounter := float64(exp.rate) * exp.duration.Seconds()\n\t\tvar (\n\t\t\tlowerBound = 0.5 * expcounter\n\t\t\tupperBound = 1.2*expcounter + 5\n\t\t)\n\t\tif float64(counter) < lowerBound ||\n\t\t\tfloat64(counter) > upperBound {\n\t\t\tt.Errorf(\"(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)\", lowerBound, counter, upperBound)\n\t\t}\n\t}\n}\n\nfunc TestBucketLimiterHighRates(t *testing.T) {\n\texpectations := []struct {\n\t\trate     uint64\n\t\tduration time.Duration\n\t}{\n\t\t{100000, 100 * time.Millisecond},\n\t\t{150000, 100 * time.Millisecond},\n\t\t{200000, 100 * time.Millisecond},\n\t\t{500000, 100 * time.Millisecond},\n\t\t{1000000, 100 * time.Millisecond},\n\t}\n\tfor i := range expectations {\n\t\texp := expectations[i]\n\t\tlim := newBucketLimiter(exp.rate)\n\t\tcounter := uint64(0)\n\t\tdone := make(chan struct{})\n\t\twaitChan := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\twaitChan <- struct{}{}\n\t\t\t}()\n\t\t\tfor lim.pace(done) == cont {\n\t\t\t\tcounter++\n\t\t\t}\n\t\t}()\n\t\ttime.Sleep(exp.duration)\n\t\tclose(done)\n\t\tselect {\n\t\tcase <-waitChan:\n\t\tcase <-time.After(exp.duration + 50*time.Millisecond):\n\t\t\tt.Error(\"failed to complete: \", exp)\n\t\t\treturn\n\t\t}\n\t\texpcounter := float64(exp.rate) * exp.duration.Seconds()\n\t\tvar (\n\t\t\tlowerBound = 0.5 * expcounter\n\t\t\tupperBound = 1.2*expcounter + 5\n\t\t)\n\t\tif float64(counter) < lowerBound ||\n\t\t\tfloat64(counter) > upperBound {\n\t\t\tt.Errorf(\"(lower bound, actual, upper bound): (%11.2f, %11d, %11.2f)\", lowerBound, counter, upperBound)\n\t\t}\n\t}\n}\n\nfunc BenchmarkBucketLimiter(bm *testing.B) {\n\tlim := newBucketLimiter(maxRps)\n\tdone := make(chan struct{})\n\tbm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())\n\tbm.ResetTimer()\n\tbm.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tlim.pace(done)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "proxy_reader.go",
    "content": "package main\n\nimport \"io\"\n\ntype proxyReader struct {\n\tio.Reader\n}\n"
  },
  {
    "path": "rateestimator.go",
    "content": "package main\n\nimport (\n\t\"math/big\"\n\t\"time\"\n)\n\nconst (\n\tpanicZeroRate         = \"rate can't be zero\"\n\tpanicNegativeAdjustTo = \"adjustTo can't be negative or zero\"\n)\n\nfunc estimate(rate uint64, adjustTo time.Duration) (time.Duration, uint64) {\n\tif rate == 0 {\n\t\tpanic(panicZeroRate)\n\t}\n\tif adjustTo <= 0 {\n\t\tpanic(panicNegativeAdjustTo)\n\t}\n\tbr := new(big.Int).SetUint64(rate)\n\tbd := new(big.Int).SetInt64(oneSecond.Nanoseconds())\n\tgcd := new(big.Int).GCD(nil, nil, br, bd).Uint64()\n\tnr, nd := rate/gcd, uint64(oneSecond.Nanoseconds())/gcd\n\tadjustInt := uint64(adjustTo.Nanoseconds())\n\tif nd >= adjustInt {\n\t\treturn time.Duration(nd), nr\n\t}\n\tcoef := adjustInt / nd\n\treturn time.Duration(coef * nd), coef * nr\n}\n"
  },
  {
    "path": "rateestimator_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRateEstimatorPanicWithZeroRate(t *testing.T) {\n\tdefer func() {\n\t\tpv, ok := recover().(string)\n\t\tif !ok {\n\t\t\tt.Error(\"expected string value\")\n\t\t\treturn\n\t\t}\n\t\tif pv != panicZeroRate {\n\t\t\tt.Error(panicZeroRate, pv)\n\t\t}\n\t}()\n\t_, _ = estimate(0, 10*time.Second)\n\tt.Error(\"should fail with rate == 0\")\n}\n\nfunc TestRateEstimatorPanicWithNegativeAdjustTo(t *testing.T) {\n\tdefer func() {\n\t\tpv, ok := recover().(string)\n\t\tif !ok {\n\t\t\tt.Error(\"expected string value\")\n\t\t\treturn\n\t\t}\n\t\tif pv != panicNegativeAdjustTo {\n\t\t\tt.Error(panicNegativeAdjustTo, pv)\n\t\t}\n\t}()\n\t_, _ = estimate(10, -10*time.Second)\n\tt.Error(\"should fail with adjustTo <= 0\")\n}\n\nfunc TestRateEstimatorAccuracy(t *testing.T) {\n\tdefer func() {\n\t\trv := recover()\n\t\tif rv != nil {\n\t\t\tt.Error(rv)\n\t\t}\n\t}()\n\texpectations := []struct {\n\t\trate                 uint64\n\t\tadjustTo             time.Duration\n\t\texpectedQuantum      uint64\n\t\texpectedFillInterval time.Duration\n\t}{\n\t\t{1, 100 * time.Millisecond, 1, 1 * time.Second},\n\t\t{1, 1000 * time.Millisecond, 1, 1 * time.Second},\n\t\t{1, 2000 * time.Millisecond, 2, 2 * time.Second},\n\t\t{1, 3000 * time.Millisecond, 3, 3 * time.Second},\n\t\t{4, 3000 * time.Millisecond, 12, 3 * time.Second},\n\t\t{10000, 100 * time.Millisecond, 1000, 100 * time.Millisecond},\n\t\t{100000, 100 * time.Millisecond, 10000, 100 * time.Millisecond},\n\t\t{1000000, 100 * time.Millisecond, 100000, 100 * time.Millisecond},\n\t}\n\tfor _, exp := range expectations {\n\t\tactualFillInterval, actualQuantum := estimate(exp.rate, exp.adjustTo)\n\t\tif actualFillInterval != exp.expectedFillInterval ||\n\t\t\tactualQuantum != exp.expectedQuantum {\n\t\t\tt.Log(\"Expected: \", exp.expectedQuantum, exp.expectedFillInterval)\n\t\t\tt.Log(\"Actual: \", actualQuantum, actualFillInterval)\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "template/doc.go",
    "content": "/*\nPackage template documents the way user-defined output templates are\nment to be used.\n\nUser-defined templates use Go's text/template package, so you might\nwant to check its documentation first.\nThere are a bunch of helper methods available inside a template\nbesides those described in aforementioned documentation, namely:\n  - URLString()\n    Returns the URL string used for the load test.\n  - WithLatencies()\n    Tells whether --latencies flag were activated.\n  - FormatBinary(numberOfBytes float64) string\n    Converts bytes to kilo-, mega-, giga-, etc.- bytes, and\n    appends appropriate suffix \"KB\", \"MB\", \"GB\", etc.\n  - FormatTimeUs(us float64) string\n    Converts microseconds to milliseconds, seconds, minutes or\n    hours and appends appropriate suffix.\n  - FormatTimeUsUint64(us uint64) string\n    Same as above, but for uint64, since type conversions are\n    not available in templates.\n  - FloatsToArray(ps ...float64) []float64\n    Converts a bunch of floats into array, since, again,\n    type conversions are not available in templates.\n  - Multiply(num, coeff float64) float64\n    Arithmetics are not available inside of templates either.\n  - StringToBytes(s string) []byte\n    Convenience function to convert string to []byte.\n  - UUIDV1() (UUID, error)\n    Generates UUID Version 1, based on timestamp and\n    MAC address (RFC 4122)\n  - UUIDV2(domain byte) (UUID, error)\n    Generates UUID Version 2, based on timestamp, MAC address\n    and POSIX UID/GID (DCE 1.1)\n  - UUIDV3(ns UUID, name string) UUID\n    Generates UUID Version 3, based on MD5 hashing (RFC 4122)\n  - UUIDV4() (UUID, error)\n    Generates UUID Version 4, based on random numbers (RFC 4122)\n  - UUIDV5(ns UUID, name string) UUID\n    Generates UUID Version 5, based on SHA-1 hashing (RFC 4122)\n\nThe structure that gets passed to the template is documented in\nthe package github.com/codesenberg/bombardier/internal. The structure\nof interest is TestInfo. It basically consists of Spec and Result\nfields, the former contains various information about the test\n(number of connections, URL, HTTP method, headers, body, rate, etc.)\nperformed, while the latter contains results obtained during the\nexecution of this test (bytes read/written, time taken, RPS, etc.).\n\nLink to GoDoc for the structure used in template:\nhttps://godoc.org/github.com/codesenberg/bombardier/internal#TestInfo\n\nExamples of templates can be found in:\nhttps://github.com/codesenberg/bombardier/blob/master/templates.go\n*/\npackage template\n"
  },
  {
    "path": "templates.go",
    "content": "package main\n\nimport \"strings\"\n\nvar (\n\ttemplates = map[string][]byte{\n\t\t\"plain-text\": []byte(plainTextTemplate),\n\t\t\"json\":       []byte(jsonTemplate),\n\t}\n)\n\ntype format interface{}\ntype knownFormat string\n\nfunc (kf knownFormat) template() []byte {\n\treturn templates[string(kf)]\n}\n\ntype filePath string\ntype userDefinedTemplate filePath\n\nfunc formatFromString(formatSpec string) format {\n\tconst prefix = \"path:\"\n\tif strings.HasPrefix(formatSpec, prefix) {\n\t\treturn userDefinedTemplate(formatSpec[len(prefix):])\n\t}\n\tswitch formatSpec {\n\tcase \"pt\", \"plain-text\":\n\t\treturn knownFormat(\"plain-text\")\n\tcase \"j\", \"json\":\n\t\treturn knownFormat(\"json\")\n\t}\n\t// nil represents unknown format\n\treturn nil\n}\n\nconst (\n\tplainTextTemplate = `\n{{- printf \"%10v %10v %10v %10v\" \"Statistics\" \"Avg\" \"Stdev\" \"Max\" }}\n{{ with .Result.RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }}\n\t{{- printf \"  %-10v %10.2f %10.2f %10.2f\" \"Reqs/sec\" .Mean .Stddev .Max -}}\n{{ else }}\n\t{{- print \"  There wasn't enough data to compute statistics for requests.\" }}\n{{ end }}\n{{ with .Result.LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) }}\n\t{{- printf \"  %-10v %10v %10v %10v\" \"Latency\" (FormatTimeUs .Mean) (FormatTimeUs .Stddev) (FormatTimeUs .Max) }}\n\t{{- if WithLatencies }}\n  \t\t{{- \"\\n  Latency Distribution\" }}\n\t\t{{- range $pc, $lat := .Percentiles }}\n\t\t\t{{- printf \"\\n     %2.0f%% %10s\" (Multiply $pc 100) (FormatTimeUsUint64 $lat) -}}\n\t\t{{ end -}}\n\t{{ end }}\n{{ else }}\n\t{{- print \"  There wasn't enough data to compute statistics for latencies.\" }}\n{{ end -}}\n{{ with .Result -}}\n{{ \"  HTTP codes:\" }}\n{{ printf \"    1xx - %v, 2xx - %v, 3xx - %v, 4xx - %v, 5xx - %v\" .Req1XX .Req2XX .Req3XX .Req4XX .Req5XX }}\n\t{{- printf \"\\n    others - %v\" .Others }}\n\t{{- with .Errors }}\n\t\t{{- \"\\n  Errors:\"}}\n\t\t{{- range . }}\n\t\t\t{{- printf \"\\n    %10v - %v\" .Error .Count }}\n\t\t{{- end -}}\n\t{{ end -}}\n{{ end }}\n{{ printf \"  %-10v %10v/s\\n\" \"Throughput:\" (FormatBinary .Result.Throughput)}}`\n\tjsonTemplate = `{\"spec\":{\n{{- with .Spec -}}\n\"numberOfConnections\":{{ .NumberOfConnections }}\n\n{{- if .IsTimedTest -}}\n,\"testType\":\"timed\",\"testDurationSeconds\":{{ .TestDuration.Seconds }}\n{{- else -}}\n,\"testType\":\"number-of-requests\",\"numberOfRequests\":{{ .NumberOfRequests }}\n{{- end -}}\n\n,\"method\":\"{{ .Method }}\",\"url\":{{ .RequestURL | printf \"%q\" }}\n\n{{- with .Headers -}}\n,\"headers\":[\n{{- range $index, $header :=  . -}}\n{{- if ne $index 0 -}},{{- end -}}\n{\"key\":{{ .Key | printf \"%q\" }},\"value\":{{ .Value | printf \"%q\" }}}\n{{- end -}}\n]\n{{- end -}}\n\n{{- if .BodyFilePath -}}\n,\"bodyFilePath\":{{ .BodyFilePath | printf \"%q\" }}\n{{- else -}}\n,\"body\":{{ .Body | printf \"%q\" }}\n{{- end -}}\n\n{{- if .CertPath -}}\n,\"certPath\":{{ .CertPath | printf \"%q\" }}\n{{- end -}}\n{{- if .KeyPath -}}\n,\"keyPath\":{{ .KeyPath | printf \"%q\" }}\n{{- end -}}\n\n,\"stream\":{{ .Stream }},\"timeoutSeconds\":{{ .Timeout.Seconds }}\n\n{{- if .IsFastHTTP -}}\n,\"client\":\"fasthttp\"\n{{- end -}}\n{{- if .IsNetHTTPV1 -}}\n,\"client\":\"net/http.v1\"\n{{- end -}}\n{{- if .IsNetHTTPV2 -}}\n,\"client\":\"net/http.v2\"\n{{- end -}}\n\n{{- with .Rate -}}\n,\"rate\":{{ . }}\n{{- end -}}\n{{- end -}}\n},\n\n{{- with .Result -}}\n\"result\":{\"bytesRead\":{{ .BytesRead -}}\n,\"bytesWritten\":{{ .BytesWritten -}}\n,\"timeTakenSeconds\":{{ .TimeTaken.Seconds -}}\n\n,\"req1xx\":{{ .Req1XX -}}\n,\"req2xx\":{{ .Req2XX -}}\n,\"req3xx\":{{ .Req3XX -}}\n,\"req4xx\":{{ .Req4XX -}}\n,\"req5xx\":{{ .Req5XX -}}\n,\"others\":{{ .Others -}}\n\n{{- with .Errors -}}\n,\"errors\":[\n{{- range $index, $error :=  . -}}\n{{- if ne $index 0 -}},{{- end -}}\n{\"description\":{{ .Error | printf \"%q\" }},\"count\":{{ .Count }}}\n{{- end -}}\n]\n{{- end -}}\n\n{{- with .LatenciesStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}}\n,\"latency\":{\"mean\":{{ .Mean -}}\n,\"stddev\":{{ .Stddev -}}\n,\"max\":{{ .Max -}}\n\n{{- if WithLatencies -}}\n,\"percentiles\":{\n{{- range $pc, $lat := .Percentiles }}\n{{- if ne $pc 0.5 -}},{{- end -}}\n{{- printf \"\\\"%2.0f\\\":%d\" (Multiply $pc 100) $lat -}}\n{{- end -}}\n}\n{{- end -}}\n\n}\n{{- end -}}\n\n{{- with .RequestsStats (FloatsToArray 0.5 0.75 0.9 0.95 0.99) -}}\n,\"rps\":{\"mean\":{{ .Mean -}}\n,\"stddev\":{{ .Stddev -}}\n,\"max\":{{ .Max -}}\n,\"percentiles\":{\n{{- range $pc, $rps := .Percentiles }}\n{{- if ne $pc 0.5 -}},{{- end -}}\n{{- printf \"\\\"%2.0f\\\":%f\" (Multiply $pc 100) $rps -}}\n{{- end -}}\n}}\n{{- end -}}\n}}\n{{- end -}}`\n)\n"
  },
  {
    "path": "testbody.txt",
    "content": "abracadabra"
  },
  {
    "path": "testclient.cert",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIJAMx2fpQ+fhOZMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV\nBAMMG0JvbWJhcmRpZXIgQ2xpZW50IFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2MDda\nGA8zMDE2MDUyOTA2MzYwN1owJjEkMCIGA1UEAwwbQm9tYmFyZGllciBDbGllbnQg\nVGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1TE9F36z\nfsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLszEjmL3Gtux02GaqZ8u3GdfeZ/\nAm2KqFnLq/YiZwYJpwpB54PAtKQqCiIcAPCENZdPzuya4bWTP+/bLiLdDY0kJhxM\nrReufBL+xBOFrRhLcsW+tECGsu+d39o7JY7oWnm7IQYX7cxK1JgaFL5kmUuaoJN0\niJhB7V3JIQJMC68Yr5dzrYdzwzc1uxm43Y696HYAPkygf41ZoHo5UKWCI9V9M0iz\n8oUoWrLdlXHOVaQKpPV9+aQYBiG1KNePvWZ4PCvukXv+zgLP1SvvqNTQKi2HCV37\nRZQd0M0Do9aqrtlDrQLeKE39XZQMBKrype7Vr5JcnXMlaC4A8WFF/+cl11V6m4eY\n8GLnoTv+l/G2Hbjwm6/oLPCvrErqd0M+6JK9jXnXHwNr05FpEVqwdYZ5K10bYBBU\nwY+oZM8sGrT0Hd0N0PHtcs2eZ6yYLNrAaTvZT3w/sFgEqrDKn6c5WJdKO9PKSvbb\nE7whD+WkZPeN2ndh+lGYAEnVzyzVgKmNPOPGFa244QEIUpeZv4d+ivPN9eOwgAVH\nl4Ms9+u38VjuIE5LNZCiqOlIzaMBD+dPbOpx7rtEacMs8UgyMVIGPiJcsqzw++Ji\npWHOKRAi82TLLcqt30wgIjCfu49hFPbnfIMCAwEAAaNQME4wHQYDVR0OBBYEFAyp\n8Do7nsAhXWAPamH+Vn8ntZ3pMB8GA1UdIwQYMBaAFAyp8Do7nsAhXWAPamH+Vn8n\ntZ3pMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAKZcYn3AlK77Yg9K\nTHZym7Cmr07HA9138PnlMLlVTqJnwAU9nGZ41h/Vth1nZLy9SppefuFwXrmpI5W4\nlGqY/t+saEgSzwI8cQ1F6AP0XyeQaGcqNpwHnBNF414Um42Emu+lbBJuYFV53Pgq\nnhxGUD7/PbUHSCkTj/LOuvVOeXKMl2muuMpk2lwJnQNAv4mB29F+6mwxntxuSk2O\nNFk13QXB5ii1NawT16yW/bYSMZsLQKvz/e/9B+tCjGdbQ8ga3OcEzjGgKRhh4Z/E\ngdgl7vtu+ZkE+LHzP9V2lUVlbSP4UhQiZJSEawItSwKlIq/hTjra1kby8UbGMtLp\n/vjAznZleF5nG4eQbbM3wDtnpWim0ODXH+p1JNPuK/6GzbWh4eykHQ6JElci26Hg\nug+/7iGAxBcCiPPRX1vT92rnKAIicqzZ1UInlun2X01+pxFo+xGKCYDYAJwyS8Y/\nZy4eFbXJJMGznTef7fNhtBLk6jni90YWBbLgaY62T+y1gYsyfYzQE28Idc7LiKOu\nDNEfa4nXAB1zIg9JMKzqbDy4WN0B8tKbBBQIwtRn5DyW7YsmgsgZt6Gs+ochvZdh\n5Zlr6T6nvFMunVGNUZP901Xa+HYqCTV7PcrvCDVMOfiNkmrlWf6amFTzL8kBELCC\n0O6WGsL6rVCeSVy2BPSna0GzH36g\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "testclient.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA1TE9F36zfsSmXbxkfNonYggVA7skrb+10iuLyey5snkOHLsz\nEjmL3Gtux02GaqZ8u3GdfeZ/Am2KqFnLq/YiZwYJpwpB54PAtKQqCiIcAPCENZdP\nzuya4bWTP+/bLiLdDY0kJhxMrReufBL+xBOFrRhLcsW+tECGsu+d39o7JY7oWnm7\nIQYX7cxK1JgaFL5kmUuaoJN0iJhB7V3JIQJMC68Yr5dzrYdzwzc1uxm43Y696HYA\nPkygf41ZoHo5UKWCI9V9M0iz8oUoWrLdlXHOVaQKpPV9+aQYBiG1KNePvWZ4PCvu\nkXv+zgLP1SvvqNTQKi2HCV37RZQd0M0Do9aqrtlDrQLeKE39XZQMBKrype7Vr5Jc\nnXMlaC4A8WFF/+cl11V6m4eY8GLnoTv+l/G2Hbjwm6/oLPCvrErqd0M+6JK9jXnX\nHwNr05FpEVqwdYZ5K10bYBBUwY+oZM8sGrT0Hd0N0PHtcs2eZ6yYLNrAaTvZT3w/\nsFgEqrDKn6c5WJdKO9PKSvbbE7whD+WkZPeN2ndh+lGYAEnVzyzVgKmNPOPGFa24\n4QEIUpeZv4d+ivPN9eOwgAVHl4Ms9+u38VjuIE5LNZCiqOlIzaMBD+dPbOpx7rtE\nacMs8UgyMVIGPiJcsqzw++JipWHOKRAi82TLLcqt30wgIjCfu49hFPbnfIMCAwEA\nAQKCAgEAm+JU+Uj7lkXUH9YQ4/nfsh6WvxOnziPPns2YeR1O6uD5IKkAvuK1EYa8\niZ52GqWBrs10iwpu9CeEq3R9KE/g99PCWxF0/wOndG5VDvPB5i33ffgVswfud/t8\nn9OSQDndyHrbY8JtjmMygiahgl2D8P1CrblJqCNGWrA6j+PSO7Qy0XURDySVeptW\nW/yblW9hv3U4qxEmtHogOp/I4Qn88M4nDr1/J/NTAfrsntJACkDFO6SMqQD+mkWQ\ns3arUfyzG+COm2EdsscKqsb+nreIV7aK0fNvGYqSxmj/Pc3gnGzAnb7Bwj8YISqN\nLSHjK1/wleaURpUhlc6nvnUppDLiuYC8yB3Xk3XqgEIw96Bn7DLWdXZdCHnm02C1\nWiV6SDI6TDY0oNw6qhUZ/Aq/hc4gN+MX/g1TJUXfk2R060Ul4ZY4Ywl89K45cUoo\nv7uZM0hkZEukTv1LjlbkMHHMyAoHIVBgjGeIUGUOc3/2zsvRHM5MBhuZQRuTxiGg\nvcR9/qdX6I0nZlUpjwLJduWn/rF8v2/lFABB36n9frK5wm6HeFp1hEitTljaeRMA\ns/RgM9XiHdZpzJT1YK6J6dXzJuUYCiSAeKps4n7xT4F66FTQuXMLqD2eLkiZIsEb\nnx6m0muEav+fUg8a/xi4WP/2eMNg7Ayq/PYtY76wIoxsowA7AQECggEBAPT09yFV\nwrxV2K3bFgC2+0sMcwoR984j8X82aipMHPKxXPiNE0budIP2OWhMOoIelrR3Pjdm\nxizU0bGI3lo5po9t22MhoQIrEREpv5rrUR+sg4DFUPUATRWlVGdr/EdsElQKTKKi\nZpCqrBAMd8D6Vp3EH+LGpy1i37MoY8T+ljZ9wMStsKgN0/k6vHzwDVyUwadvrzNO\nv3HuEP+XRRxC/fhuxlti2/AS4Tm9IJIwUzNaVBpio8mLiDWdwHlGZXzxP6vlE3ef\nxzHJ71D89qFlxr4EDjiKzYxiiZbGqowv2NDS+Z87rYD5mFCaVUtzrj9PJHLNTh11\nP56QizutmAEvazkCggEBAN7NriGkK1+Tkf6hc5KPBgqZaUDSDAHr8DBpSMJy7sTq\nyOmd1L+09rNqAXy0wV6isU9v3wYMB38xIoxYMkkW1HQr6tWpcyzbC9HomBKMZGoX\njNmj9MWKlaDN8cXfxx7l7CAf/1oJs7MAFMVu/oKnK35+uZoo9mvo0G6udc7vRHGa\nOLPoySq7RuAWT7z9ULd1+Ny+Y4kvWsO/hdjVU4a9r9KQf/eNXt5PWmMb69v7hNGD\n2j2CqxdlnRYdUW6y3d8919lNneTXZBbzovw6aK58l124sYjuMyDIoof5ErouWDps\nSM1gE+xo48M1cSuZrv+RJu5G6mmtDe1/aXFzeEaBGZsCggEBAKB6R9kf7TcjapPj\nnxOSzSjKnCcxxE3ZgGIeDQlu2dwpVEZFbiafG9hEHDH3FrGeRo8uO6ViAFzohAQy\nLbGgaT039G2KX4gjHMhIuI1OstP0WiannjUUIGwY5yXmOd20sIE8Sh6WFGmcVqMg\n9+eGWe57yYPxLx7t0q31vP8W5uQGGJ8BR2WhwYha8ZdMUQShNAl0gqwzX/rMw3ge\n6xjrzqTONcczCfHK/KCuBcOgQzG2cLjkfHcSoYa2tZz+AIkNJ/B+X/WTyJUWvWEq\niI0ON1jPIV3rmWPqPkd4Gc1Dn2CXhw/Jsg539lB/+3c17ybsu202kYF9CdPg0Eal\noJrOLQkCggEADS1U8yBmgEyWAd1CnJRg4xeXpgHGPAbcOcDAUN/DR1orb8Wp43ys\naogGdn2qQhKVMgGHyy/C8b7SMEK3FqOHBSfjx6cx7KE33b5H4DD1b2DdL7IGs/gy\nSURk3DMT77vhbzT1QTn5qsiCcfrSip+gbubHy1pI2LD4QtOGnCqCfcWFPP6zhxd0\nZaRsKt1AfNk5UrTf5ikq0RDutZhITFvDnkx1hQqTZcqDqgDoviXuAQYvThwASm30\nEG7DdiyV+rIJpgx1Hieu/7yBEzHRJyCvQxe9SD/uPi4fjrMobGJ5TVtCIwNfqke5\n0L3EZ7O7KdpH1yfSjVVy0W0Lq24M2v6fqQKCAQA25Xsu2HVCcE3KsEuywGJOs3hA\nkXPPJu7vyLRPDPlbDhVMtKSUOAGEHyh8h6o65VZF4322gZ++AoV+EFWlGfQYVYGu\n+/uBeTf6y4IuCPvCtsELiERtMAMbrhwSDB83/xIMcKkvs7X7DQ8GRe6n3mDZa6cg\nEjZhQRnxnKDR7AO8pM19GuMVPDeMVNdgfUJTSDO7nifuiPEO9rtAwuH/y+RNeLIJ\n9a/1zcHXU1uCJPxITlN3ckhWIVEw7ycQ0xXULt4UfcfPHNtfMR0ccUYlP6zI1eBc\nCS5K58CWPWjBSiS+SFUIIx6qPtjBDuYBcqnrWqekd7m4yYKOJIaUbXhwm4IN\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "testserver.cert",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIJALu5MYN2H+2PMA0GCSqGSIb3DQEBCwUAMCYxJDAiBgNV\nBAMMG0JvbWJhcmRpZXIgU2VydmVyIFRlc3QgQ2VydDAgFw0xNzAxMjYwNjM2NTRa\nGA8zMDE2MDUyOTA2MzY1NFowJjEkMCIGA1UEAwwbQm9tYmFyZGllciBTZXJ2ZXIg\nVGVzdCBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1laXP9o5\neo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1LIq0ECJ3+JzGrzIkYjrj6an0c\n3bV5GDW/XA6yH4iMV36ADc+D+SWixQQkkWNrIL3nTHnnxOFbml4uCCDEV5TGWv8Z\nGNraUPPSIZAM/OO4uKTMHKiacpnGZ8ZqUuJ0E0F3OHoLwxpJsAhiKv18iy7mHvva\nPBCQ/aq8LYUOLRwiL019EVx+LFuMj9Ugc9G5lYHnn0jI/AcMdWeXil01aFEFtGpG\nS7b4FV5d3x1C8l1PDDWQfuBzu4wrztnCUnvQyUTZAmBJFXt2V5MxO6p69qvGav//\nIdPTxToeaz0oil+O+7cJ4gqjUBsr8xPuGLYjytZ2KJEqTIJJogXFylpwDWIvbXBw\npFhDsuE9J2oz9gPC8R54lb93cuNAeGrWZfoJ8qSzOsO4CKgasdTTrv3uFTgnovh4\nqJ4SaRjOtiSmWLZhy7fSG86kB+ZuGkEOUJiFok2aNJL1UI10QuSc095JLFnqZ3LP\nDG3gkdwqmYMYFnSKv43Z+azO1+t5BUSNyddSb2ZEF4d5J/UzN/D5XAxgJJoyfMK7\nvHfOlglnfBkSdyxw1/BbOPLBFVDWinV33sabtgEismgdDaYjActBuRdl1md4BPRJ\nZ714Noquv7qcOYXNuqdhLNWjWh4d0HwA0yECAwEAAaNQME4wHQYDVR0OBBYEFB0Y\nit+Qidk1AgL+N00nyYDWgeEFMB8GA1UdIwQYMBaAFB0Yit+Qidk1AgL+N00nyYDW\ngeEFMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABjxo7L4N+3lsHya\n14dk8ycssuHkJvE2PhbandywV0E5bfqkoVGOmSVw50BIkpQ3WMTwACJZsjeTF1OJ\nCtDHSx3EJ3wPOMChAA06QdWSwG6Gxi6NYLrZ+60VjX3f64zwvmasD2xHT20icqAR\nH/405qzv5MyELvDzD9u+agdIvf2yXGywGiT8p1yPQZX5pn/1omWZEMSNICOzhFqv\nU6FfYOGu/g+FJYmV5JRitMBNcr1sTJI0eHEYWW/d7yxBCtmoN4UoH1TZepijb65n\nCT9xl9WX4CRGiG6/T4wcYmL2Q8OwjEn/JNUCSxU9tcyJr66JIIx4BUhUJ012BPXu\nBbohBaNA8ygMpt1sweBteVCC5O/O1J7YKYG1o0J2Fd4DLol+bWfM8IijoT23XYF3\nI5fcf/Y61iAxx9PqCe7BsRsPi7WXrPxZJlfocXIWbdwVwpQUngjQjiVgdcPH7LnB\nNI1E2PDcDVJDVsS7XB/zK2nyY31DlQ8QZYpOzIeHjy4UcepClzk8JqeIQEmh01S7\np7fIIt51N2s2TpvZGC/wGL/0iFn/4mwyXvH9RZn8AUGBWrdB/kY/DD+Nmz8tHtlH\nPpM4uDdRwh+Ks62Rw1qde5xIZ82PjLxJ+P79rQ+wYejmfenLr1PPnl1q0W/MT8gT\nPuxw1k5iEsR8O9CjyEYP6bhQoZfb\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "testserver.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA1laXP9o5eo9YFOdqK/Il1f4AePr7UZ/mq5nbBkA3Pt/5uW1L\nIq0ECJ3+JzGrzIkYjrj6an0c3bV5GDW/XA6yH4iMV36ADc+D+SWixQQkkWNrIL3n\nTHnnxOFbml4uCCDEV5TGWv8ZGNraUPPSIZAM/OO4uKTMHKiacpnGZ8ZqUuJ0E0F3\nOHoLwxpJsAhiKv18iy7mHvvaPBCQ/aq8LYUOLRwiL019EVx+LFuMj9Ugc9G5lYHn\nn0jI/AcMdWeXil01aFEFtGpGS7b4FV5d3x1C8l1PDDWQfuBzu4wrztnCUnvQyUTZ\nAmBJFXt2V5MxO6p69qvGav//IdPTxToeaz0oil+O+7cJ4gqjUBsr8xPuGLYjytZ2\nKJEqTIJJogXFylpwDWIvbXBwpFhDsuE9J2oz9gPC8R54lb93cuNAeGrWZfoJ8qSz\nOsO4CKgasdTTrv3uFTgnovh4qJ4SaRjOtiSmWLZhy7fSG86kB+ZuGkEOUJiFok2a\nNJL1UI10QuSc095JLFnqZ3LPDG3gkdwqmYMYFnSKv43Z+azO1+t5BUSNyddSb2ZE\nF4d5J/UzN/D5XAxgJJoyfMK7vHfOlglnfBkSdyxw1/BbOPLBFVDWinV33sabtgEi\nsmgdDaYjActBuRdl1md4BPRJZ714Noquv7qcOYXNuqdhLNWjWh4d0HwA0yECAwEA\nAQKCAgEAmRXPgTODyh2Hc6a1Fh4lF+oKvF3GEk56miWRYa2Lx8SAwAdnmqSoNN9j\nHutDIRrqB0Xm1Rf2/gMXMktxGXcFkbAdTIB1RWfpgpF25/BFjfHMGd6IzP5koyGy\nI1cQ2Y1Nrp/77BI3AqGNPDRo6L/SBu0+ieJqRi3F4gQiyQvV9Mz4yqf/Vr8Ul4y3\nBJt4Qew6f85HXenTvQK4C/Vd4cUekul9IPvfT/8XvubERhaazx4Dxty5afK6WgdO\nxqvueEyKUK9Nu8YL3xgXqGt18F0d66zpQHchdP0qq9E5mMu/FtqIDLi3phLPICDG\nLVZb25mvqW6WkOW2e5qnrj4Ma9uKj7q8Pj3Xh69asZaHx71K2Stc7cgEFJxjm0jm\nmDZd1huCVwokpWr1hVj353kS/VK85qfN0YoBp1Lz+mi7vqQ+I+o6+IXQblwI0hAA\nzYJ91ixGM4Krgc0Jj0g+pIY9AYbESxyvT/wnc2Ub1Fi4e7qvmeoWCSa/gUo6fcro\nVlWIcVFj1itNVhvFm29v8aypbpWJepMHPPNAm97bhvjzIiXQMh8ET7cdmcLiKUK7\nn1GVEld8UlNThmQ54/d6vSK7+aRtqq/ImVyGZdl7SW8RvVnI/AUQYQXirNdft7Lb\nG6HGbLsfk4f/xznKNJt1vGgYpg/8YB1D2allhVClvtA+XbjBYWECggEBAPrOLcsE\ny7IbsppDdzNNXSMcgay+24ZD9BEEwd6wAKV4uAc5Hq3JUaene/K8g87g+Vrkx7SS\nmDfKUOvepYwflVBwM40Mo8V/zif0riqhYbrGPFgEw6cfiGvW34WUb9KOmrP96hEl\npEQEDetkjjeaiRTWTeTU/Hy6CLXz+4aEfCXib+vKuat8c2E3AXokaTGHsl6BKuEM\nGlYod4tc62bcObId8BcwjqS+hsjAD6tWYkeeF6L4iq7rZ1XmHFUD281ZSyW2+BJL\nieSM84ZSg3hXZyTEUPvHKn4QQmNzrkm06VBmvHWDU7avFqfrvOLOcDqPBzpnxAUA\n15oKlujae4PVSh8CggEBANrHDkWB4oxSDmFPk27EkzMs83eWJ4sjpT+ZUC1wo4v3\nicp4Cc62oow4+tpoA8novx3tSwRfnfKig9w4svlVa2piHv6//NN6LM0jckx1dZdb\nTVeAFgGchpzTpCZ+F+vw4Qzgrw2r/Pa+ShyR0FyyRwu2wAQXNl74815b01iAWa3f\nUoIkvxlLu6Fy0pmeFQWyFI/WIIpXCyNrDxnrCWjcF28+et7+yngUvQ+4lRn9nF/T\noSjl4uV6RNlI7Q1KI0pqHWIFYuFYFRavgAXIshaZrPpfskE8RrX1b1IofLHi1q6P\nA1O55Gxd/WWl5YTu+lpd4HeRPb+QbL0AgRujmZ98ur8CggEAe9Ts2z5k7G2sg2oo\nIpZiFAHxLL+XV/WZPgXhSvgPeaPfCQH02c16mZKiKjlVwwFlXLF0wP1YVsN3rN3j\nUwoNCQg9C7lf6xWtTiELFVVVEYjrJnJDv/JbwxL2jde6VnW+gHwv44N4VXTDAqRF\na8LLSBR/pSpb96FKx7vNRp+HRJVGuV8AyWDK/wbPneT4Y1IiiXKxHyiAoGWekJqy\nR7kYa49IicqZw1Gm7tuVYP1nzQCLnxWkM7Va8hiJiJg9IGikJ9ztIutVDBlj68A1\n1WciMA8WBRpTKqcQgFYPiajfQalYB5Vt8dcFEqfcPQe8dc1EvluZdvbxfMcZt6KY\nNYFL9QKCAQEAyl7C9sy0kPP+VUlUqWuwdfAorf/5SB2K6A+bOM0um3Q4w07SU6Jh\nLbAvawQ4LPbcgoRTlhIUerKVoonYFAdNuzRUU3WoGr6y3nbhbZRhV8ae/kd/E7KE\nWmDzQJ/25MsGgfD8PHtRHbTbvR2sTXKjgVRkvePy6VsDU89A6mafjdQ78CKpmm6R\ne0BJSswNyhz2JC8AHrdxmCuZ5nGhXJvqGX8EDW5GP1l/oSEu2sHbelC6jKhJf9fg\nA9YPYPGpP1Z1I4yz8JqXt0pT9AW3pmw0s8z9iJaHGh2UAb1tyuZ3izTC8RnND+jJ\nUtNoQdUFQ73+uttg8OhZjWMACl8E5aBs5QKCAQEAjBAMw41WxOPbF3S9zLOU2mqq\nT+sUKhhv36Ri9nKOtyfbOWuMgtYZHDijAkkP5yXDda31kgPkx3qJH4K6F6l+1PUF\nw+rbvUck15Vfed0BrzUxXL9m4JAq59TnAWajmoI/5eaeyY6M8Vfz4XQwYNNm353h\nzTBbxxO9AKz9I8/mRnctGo9UZNhhBwh30t8oQl2hWLqC1CuY/1R71tkxy6Qz1Tg+\nwcnDU8IbyWtuLKBikBOLzTb+EvSMfEZyS9bLUhUZvWznvwJgaLCON+k5gQSjwE98\nnvR8VhuFDyq09ChAmFCAOA3plCOHCa/yYEX1BYx0tbP/yOYSePC6sKF1UzLBrw==\n-----END RSA PRIVATE KEY-----\n"
  }
]