Skip to content

Commit 7e05cf4

Browse files
authored
Merge pull request #101 from aykhans/feat/add-duration
✨ Add duration
2 parents a170588 + 934cd0a commit 7e05cf4

File tree

10 files changed

+239
-77
lines changed

10 files changed

+239
-77
lines changed

README.md

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
## Table of Contents
77

88
- [Installation](#installation)
9-
- [Using Docker (Recommended)](#using-docker-recommended)
10-
- [Using Pre-built Binaries](#using-pre-built-binaries)
11-
- [Building from Source](#building-from-source)
9+
- [Using Docker (Recommended)](#using-docker-recommended)
10+
- [Using Pre-built Binaries](#using-pre-built-binaries)
11+
- [Building from Source](#building-from-source)
1212
- [Usage](#usage)
13-
- [1. CLI Usage](#1-cli-usage)
14-
- [2. Config File Usage](#2-config-file-usage)
15-
- [2.1 JSON Example](#21-json-example)
16-
- [2.2 YAML/YML Example](#22-yamlyml-example)
17-
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
13+
- [1. CLI Usage](#1-cli-usage)
14+
- [2. Config File Usage](#2-config-file-usage)
15+
- [2.1 JSON Example](#21-json-example)
16+
- [2.2 YAML/YML Example](#22-yamlyml-example)
17+
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
1818
- [Config Parameters Reference](#config-parameters-reference)
1919

2020
## Installation
@@ -46,6 +46,7 @@ Download the latest binaries from the [releases](https://github.com/aykhans/dodo
4646
### Building from Source
4747

4848
To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
49+
4950
```sh
5051
go install -ldflags "-s -w" github.com/aykhans/dodo@latest
5152
```
@@ -56,21 +57,21 @@ Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination o
5657

5758
### 1. CLI Usage
5859

59-
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds:
60+
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 2 seconds, within a maximum duration of 1 minute:
6061

6162
```sh
62-
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s
63+
dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
6364
```
6465

6566
With Docker:
6667

6768
```sh
68-
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s
69+
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
6970
```
7071

7172
### 2. Config File Usage
7273

73-
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds:
74+
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 800 milliseconds, within a maximum duration of 250 seconds:
7475

7576
#### 2.1 JSON Example
7677

@@ -82,6 +83,7 @@ Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) a
8283
"timeout": "800ms",
8384
"dodos": 10,
8485
"requests": 1000,
86+
"duration": "250s",
8587

8688
"params": [
8789
// A random value will be selected from the list for first "key1" param on each request
@@ -159,6 +161,7 @@ yes: false
159161
timeout: "800ms"
160162
dodos: 10
161163
requests: 1000
164+
duration: "250s"
162165

163166
params:
164167
# A random value will be selected from the list for first "key1" param on each request
@@ -230,30 +233,31 @@ docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
230233
CLI arguments override config file values:
231234

232235
```sh
233-
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -t 5s
236+
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
234237
```
235238

236239
With Docker:
237240

238241
```sh
239-
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -t 5s
242+
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
240243
```
241244

242245
## Config Parameters Reference
243246

244247
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
245248

246-
| Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default |
247-
| --------------- | ---------------- | ------------ | -------------- | ------------------------------ | --------------------------------------------------------------- | ------- |
248-
| Config file | - | -config-file | -f | String | Path to local config file or http(s) URL of the config file | - |
249-
| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
250-
| URL | url | -url | -u | String | URL to send the request to | - |
251-
| Method | method | -method | -m | String | HTTP method | GET |
252-
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | 1000 |
253-
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
254-
| Timeout | timeout | -timeout | -t | Duration | Timeout for canceling each request | 10s |
255-
| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
256-
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
257-
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
258-
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - |
259-
| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - |
249+
| Parameter | config file | CLI Flag | CLI Short Flag | Type | Description | Default |
250+
| --------------- | ----------- | ------------ | -------------- | ------------------------------ | ----------------------------------------------------------- | ------- |
251+
| Config file | - | -config-file | -f | String | Path to local config file or http(s) URL of the config file | - |
252+
| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
253+
| URL | url | -url | -u | String | URL to send the request to | - |
254+
| Method | method | -method | -m | String | HTTP method | GET |
255+
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
256+
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | - |
257+
| Duration | duration | -duration | -o | Time | Maximum duration for the test | - |
258+
| Timeout | timeout | -timeout | -t | Time | Timeout for canceling each request | 10s |
259+
| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
260+
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
261+
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
262+
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - |
263+
| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - |

config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"timeout": "5s",
66
"dodos": 8,
77
"requests": 1000,
8+
"duration": "10s",
89

910
"params": [
1011
{ "key1": ["value1", "value2", "value3", "value4"] },

config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ yes: false
44
timeout: "5s"
55
dodos: 8
66
requests: 1000
7+
duration: "10s"
78

89
params:
910
- key1: ["value1", "value2", "value3", "value4"]

config/cli.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ const cliUsageText = `Usage:
1616
1717
Examples:
1818
19-
Simple usage only with URL:
20-
dodo -u https://example.com
19+
Simple usage:
20+
dodo -u https://example.com -o 1m
2121
2222
Usage with config file:
2323
dodo -f /path/to/config/file/config.json
2424
2525
Usage with all flags:
2626
dodo -f /path/to/config/file/config.json \
2727
-u https://example.com -m POST \
28-
-d 10 -r 1000 -t 3s \
28+
-d 10 -r 1000 -o 3m -t 3s \
2929
-b "body1" -body "body2" \
3030
-H "header1:value1" -header "header2:value2" \
3131
-p "param1=value1" -param "param2=value2" \
@@ -39,8 +39,9 @@ Flags:
3939
-y, -yes bool Answer yes to all questions (default %v)
4040
-f, -config-file string Path to the local config file or http(s) URL of the config file
4141
-d, -dodos uint Number of dodos(threads) (default %d)
42-
-r, -requests uint Number of total requests (default %d)
43-
-t, -timeout Duration Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
42+
-r, -requests uint Number of total requests
43+
-o, -duration Time Maximum duration for the test (e.g. 30s, 1m, 5h)
44+
-t, -timeout Time Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
4445
-u, -url string URL for stress testing
4546
-m, -method string HTTP Method for the request (default %s)
4647
-b, -body [string] Body for the request (e.g. "body text")
@@ -55,7 +56,6 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
5556
cliUsageText+"\n",
5657
DefaultYes,
5758
DefaultDodosCount,
58-
DefaultRequestCount,
5959
DefaultTimeout,
6060
DefaultMethod,
6161
)
@@ -70,6 +70,7 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
7070
dodosCount = uint(0)
7171
requestCount = uint(0)
7272
timeout time.Duration
73+
duration time.Duration
7374
)
7475

7576
{
@@ -94,6 +95,9 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
9495
flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
9596
flag.UintVar(&requestCount, "r", 0, "Number of total requests")
9697

98+
flag.DurationVar(&duration, "duration", 0, "Maximum duration of the test")
99+
flag.DurationVar(&duration, "o", 0, "Maximum duration of the test")
100+
97101
flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
98102
flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
99103

@@ -139,6 +143,8 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
139143
config.DodosCount = utils.ToPtr(dodosCount)
140144
case "requests", "r":
141145
config.RequestCount = utils.ToPtr(requestCount)
146+
case "duration", "o":
147+
config.Duration = &types.Duration{Duration: duration}
142148
case "timeout", "t":
143149
config.Timeout = &types.Timeout{Duration: timeout}
144150
case "yes", "y":

config/config.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import (
1515
)
1616

1717
const (
18-
VERSION string = "0.6.1"
18+
VERSION string = "0.6.2"
1919
DefaultUserAgent string = "Dodo/" + VERSION
2020
DefaultMethod string = "GET"
2121
DefaultTimeout time.Duration = time.Second * 10
2222
DefaultDodosCount uint = 1
23-
DefaultRequestCount uint = 1
23+
DefaultRequestCount uint = 0
24+
DefaultDuration time.Duration = 0
2425
DefaultYes bool = false
2526
)
2627

@@ -32,6 +33,7 @@ type RequestConfig struct {
3233
Timeout time.Duration
3334
DodosCount uint
3435
RequestCount uint
36+
Duration time.Duration
3537
Yes bool
3638
Params types.Params
3739
Headers types.Headers
@@ -47,6 +49,7 @@ func NewRequestConfig(conf *Config) *RequestConfig {
4749
Timeout: conf.Timeout.Duration,
4850
DodosCount: *conf.DodosCount,
4951
RequestCount: *conf.RequestCount,
52+
Duration: conf.Duration.Duration,
5053
Yes: *conf.Yes,
5154
Params: conf.Params,
5255
Headers: conf.Headers,
@@ -57,6 +60,9 @@ func NewRequestConfig(conf *Config) *RequestConfig {
5760
}
5861

5962
func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
63+
if rc.RequestCount == 0 {
64+
return rc.DodosCount
65+
}
6066
return min(rc.DodosCount, rc.RequestCount)
6167
}
6268

@@ -95,7 +101,17 @@ func (rc *RequestConfig) Print() {
95101
t.AppendSeparator()
96102
t.AppendRow(table.Row{"Dodos", rc.DodosCount})
97103
t.AppendSeparator()
98-
t.AppendRow(table.Row{"Requests", rc.RequestCount})
104+
if rc.RequestCount > 0 {
105+
t.AppendRow(table.Row{"Requests", rc.RequestCount})
106+
} else {
107+
t.AppendRow(table.Row{"Requests"})
108+
}
109+
t.AppendSeparator()
110+
if rc.Duration > 0 {
111+
t.AppendRow(table.Row{"Duration", rc.Duration})
112+
} else {
113+
t.AppendRow(table.Row{"Duration"})
114+
}
99115
t.AppendSeparator()
100116
t.AppendRow(table.Row{"Params", rc.Params.String()})
101117
t.AppendSeparator()
@@ -116,6 +132,7 @@ type Config struct {
116132
Timeout *types.Timeout `json:"timeout" yaml:"timeout"`
117133
DodosCount *uint `json:"dodos" yaml:"dodos"`
118134
RequestCount *uint `json:"requests" yaml:"requests"`
135+
Duration *types.Duration `json:"duration" yaml:"duration"`
119136
Yes *bool `json:"yes" yaml:"yes"`
120137
Params types.Params `json:"params" yaml:"params"`
121138
Headers types.Headers `json:"headers" yaml:"headers"`
@@ -162,8 +179,8 @@ func (c *Config) Validate() []error {
162179
if utils.IsNilOrZero(c.DodosCount) {
163180
errs = append(errs, errors.New("dodos count must be greater than 0"))
164181
}
165-
if utils.IsNilOrZero(c.RequestCount) {
166-
errs = append(errs, errors.New("request count must be greater than 0"))
182+
if utils.IsNilOrZero(c.Duration) && utils.IsNilOrZero(c.RequestCount) {
183+
errs = append(errs, errors.New("you should provide at least one of duration or request count"))
167184
}
168185

169186
for i, proxy := range c.Proxies {
@@ -197,6 +214,9 @@ func (config *Config) MergeConfig(newConfig *Config) {
197214
if newConfig.RequestCount != nil {
198215
config.RequestCount = newConfig.RequestCount
199216
}
217+
if newConfig.Duration != nil {
218+
config.Duration = newConfig.Duration
219+
}
200220
if newConfig.Yes != nil {
201221
config.Yes = newConfig.Yes
202222
}
@@ -230,6 +250,9 @@ func (config *Config) SetDefaults() {
230250
if config.RequestCount == nil {
231251
config.RequestCount = utils.ToPtr(DefaultRequestCount)
232252
}
253+
if config.Duration == nil {
254+
config.Duration = &types.Duration{Duration: DefaultDuration}
255+
}
233256
if config.Yes == nil {
234257
config.Yes = utils.ToPtr(DefaultYes)
235258
}

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/signal"
99
"syscall"
10+
"time"
1011

1112
"github.com/aykhans/dodo/config"
1213
"github.com/aykhans/dodo/requests"
@@ -49,6 +50,10 @@ func main() {
4950
ctx, cancel := context.WithCancel(context.Background())
5051
go listenForTermination(func() { cancel() })
5152

53+
if requestConf.Duration > 0 {
54+
time.AfterFunc(requestConf.Duration, func() { cancel() })
55+
}
56+
5257
responses, err := requests.Run(ctx, requestConf)
5358
if err != nil {
5459
if err == types.ErrInterrupt {

requests/helper.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
func streamProgress(
1818
ctx context.Context,
1919
wg *sync.WaitGroup,
20-
total int64,
20+
total uint,
2121
message string,
2222
increase <-chan int64,
2323
) {
@@ -27,21 +27,26 @@ func streamProgress(
2727
pw.SetStyle(progress.StyleBlocks)
2828
pw.SetTrackerLength(40)
2929
pw.SetUpdateFrequency(time.Millisecond * 250)
30+
if total == 0 {
31+
pw.Style().Visibility.Percentage = false
32+
}
3033
go pw.Render()
3134
dodosTracker := progress.Tracker{
3235
Message: message,
33-
Total: total,
36+
Total: int64(total),
3437
}
3538
pw.AppendTracker(&dodosTracker)
39+
3640
for {
3741
select {
3842
case <-ctx.Done():
39-
if ctx.Err() != context.Canceled {
43+
if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
44+
dodosTracker.MarkAsDone()
45+
} else {
4046
dodosTracker.MarkAsErrored()
4147
}
48+
time.Sleep(time.Millisecond * 300)
4249
fmt.Printf("\r")
43-
time.Sleep(time.Millisecond * 500)
44-
pw.Stop()
4550
return
4651

4752
case value := <-increase:

0 commit comments

Comments
 (0)