loadtest 6.2.0 → 6.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/httpClient.js +1 -4
- package/lib/options.js +104 -81
- package/lib/testserver.js +18 -7
- package/package.json +1 -1
package/lib/httpClient.js
CHANGED
|
@@ -37,10 +37,7 @@ class HttpClient {
|
|
|
37
37
|
*/
|
|
38
38
|
init() {
|
|
39
39
|
this.options = urlLib.parse(this.params.url);
|
|
40
|
-
this.options.headers = {}
|
|
41
|
-
if (this.params.headers) {
|
|
42
|
-
this.options.headers = this.params.headers;
|
|
43
|
-
}
|
|
40
|
+
this.options.headers = this.params.headers || {}
|
|
44
41
|
if (this.params.cert && this.params.key) {
|
|
45
42
|
this.options.cert = this.params.cert;
|
|
46
43
|
this.options.key = this.params.key;
|
package/lib/options.js
CHANGED
|
@@ -8,97 +8,120 @@ const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post',
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
export async function processOptions(options) {
|
|
11
|
-
const processed =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
const processed = new Options(options)
|
|
12
|
+
await processed.process()
|
|
13
|
+
return processed
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Options {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.url = this.getUrl(options)
|
|
19
|
+
const configuration = loadConfig();
|
|
20
|
+
this.concurrency = options.concurrency || configuration.concurrency || 1
|
|
21
|
+
const rps = options.rps ? parseFloat(options.rps) : null
|
|
22
|
+
this.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
|
|
23
|
+
this.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
|
|
24
|
+
this.indexParam = options.index || options.indexParam || configuration.indexParam;
|
|
25
|
+
this.method = options.method || configuration.method || 'GET'
|
|
26
|
+
this.readBodyAndMethod(options, configuration)
|
|
27
|
+
this.key = null
|
|
28
|
+
this.keyFile = options.key || configuration.key
|
|
29
|
+
this.cert = null
|
|
30
|
+
this.certFile = options.cert || configuration.cert
|
|
31
|
+
this.headers = configuration.headers || {}
|
|
32
|
+
this.headers['host'] = urlLib.parse(options.url).host;
|
|
33
|
+
this.headers['accept'] = '*/*';
|
|
34
|
+
if (options.headers) {
|
|
35
|
+
addHeaders(options.headers, this.headers);
|
|
22
36
|
}
|
|
37
|
+
this.requestGenerator = options.requestGenerator || configuration.requestGenerator
|
|
38
|
+
this.maxRequests = options.maxRequests || configuration.maxRequests
|
|
39
|
+
this.maxSeconds = options.maxSeconds || configuration.maxSeconds
|
|
40
|
+
this.cookies = options.cookies || configuration.cookies
|
|
41
|
+
this.contentType = options.contentType || configuration.contentType
|
|
42
|
+
this.timeout = options.timeout || configuration.timeout
|
|
43
|
+
this.secureProtocol = options.secureProtocol || configuration.secureProtocol
|
|
44
|
+
this.insecure = options.insecure || configuration.insecure
|
|
45
|
+
this.recover = options.recover || configuration.recover
|
|
46
|
+
this.proxy = options.proxy || configuration.proxy
|
|
47
|
+
this.quiet = options.quiet || configuration.quiet
|
|
23
48
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
processed.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
|
|
29
|
-
processed.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
|
|
30
|
-
processed.indexParam = options.index || options.indexParam || configuration.indexParam;
|
|
31
|
-
processed.method = options.method || configuration.method || 'GET'
|
|
32
|
-
// Allow a post body string in options
|
|
33
|
-
// Ex -P '{"foo": "bar"}'
|
|
34
|
-
if (options.postBody) {
|
|
35
|
-
processed.method = 'POST';
|
|
36
|
-
processed.body = options.postBody;
|
|
37
|
-
}
|
|
38
|
-
if (options.postFile) {
|
|
39
|
-
processed.method = 'POST';
|
|
40
|
-
processed.body = await readBody(options.postFile, '-p');
|
|
41
|
-
}
|
|
42
|
-
if (options.data) {
|
|
43
|
-
processed.body = options.data
|
|
44
|
-
}
|
|
45
|
-
if (options.putFile) {
|
|
46
|
-
processed.method = 'PUT';
|
|
47
|
-
processed.body = await readBody(options.putFile, '-u');
|
|
48
|
-
}
|
|
49
|
-
if (options.patchBody) {
|
|
50
|
-
processed.method = 'PATCH';
|
|
51
|
-
processed.body = options.patchBody;
|
|
52
|
-
}
|
|
53
|
-
if (options.patchFile) {
|
|
54
|
-
processed.method = 'PATCH';
|
|
55
|
-
processed.body = await readBody(options.patchFile, '-a');
|
|
56
|
-
}
|
|
57
|
-
// sanity check
|
|
58
|
-
if (acceptedMethods.indexOf(processed.method) === -1) {
|
|
59
|
-
throw new Error(`Invalid method ${processed.method}`)
|
|
60
|
-
}
|
|
61
|
-
if (!options.body) {
|
|
62
|
-
if(configuration.body) {
|
|
63
|
-
processed.body = configuration.body;
|
|
64
|
-
} else if (configuration.file) {
|
|
65
|
-
processed.body = await readBody(configuration.file, 'configuration.request.file');
|
|
49
|
+
|
|
50
|
+
getUrl(options) {
|
|
51
|
+
if (!options.url) {
|
|
52
|
+
throw new Error('Missing URL in options')
|
|
66
53
|
}
|
|
54
|
+
if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
|
|
55
|
+
throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
|
|
56
|
+
}
|
|
57
|
+
if (options.url.startsWith('ws:')) {
|
|
58
|
+
if (options.requestsPerSecond) {
|
|
59
|
+
throw new Error(`"requestsPerSecond" not supported for WebSockets`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return options.url
|
|
67
63
|
}
|
|
68
|
-
if (options.key || configuration.key) {
|
|
69
|
-
processed.key = await readFile(options.key || configuration.key)
|
|
70
|
-
}
|
|
71
|
-
if (options.cert || configuration.cert) {
|
|
72
|
-
processed.cert = await readFile(options.cert || configuration.cert);
|
|
73
|
-
}
|
|
74
|
-
processed.headers = configuration.headers || {}
|
|
75
|
-
processed.headers['host'] = urlLib.parse(options.url).host;
|
|
76
|
-
processed.headers['user-agent'] = 'loadtest/' + packageJson.version;
|
|
77
|
-
processed.headers['accept'] = '*/*';
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
readBodyAndMethod(options, configuration) {
|
|
66
|
+
if (options.data) {
|
|
67
|
+
this.body = options.data
|
|
68
|
+
}
|
|
69
|
+
// Allow a post body string in options
|
|
70
|
+
// Ex -P '{"foo": "bar"}'
|
|
71
|
+
if (options.postBody) {
|
|
72
|
+
this.method = 'POST';
|
|
73
|
+
this.body = options.postBody
|
|
74
|
+
}
|
|
75
|
+
if (options.postFile) {
|
|
76
|
+
this.method = 'POST';
|
|
77
|
+
this.bodyFile = options.postFile
|
|
78
|
+
}
|
|
79
|
+
if (options.putFile) {
|
|
80
|
+
this.method = 'PUT';
|
|
81
|
+
this.bodyFile = options.putFile
|
|
82
|
+
}
|
|
83
|
+
if (options.patchBody) {
|
|
84
|
+
this.method = 'PATCH';
|
|
85
|
+
this.body = options.patchBody
|
|
86
|
+
}
|
|
87
|
+
if (options.patchFile) {
|
|
88
|
+
this.method = 'PATCH';
|
|
89
|
+
this.bodyFile = options.patchFile
|
|
90
|
+
}
|
|
91
|
+
// sanity check
|
|
92
|
+
if (acceptedMethods.indexOf(this.method) === -1) {
|
|
93
|
+
throw new Error(`Invalid method ${this.method}`)
|
|
94
|
+
}
|
|
95
|
+
if (!options.body) {
|
|
96
|
+
if (configuration.body) {
|
|
97
|
+
this.body = configuration.body;
|
|
98
|
+
} else if (configuration.file) {
|
|
99
|
+
this.bodyFile = configuration.file
|
|
100
|
+
}
|
|
101
|
+
}
|
|
81
102
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
|
|
104
|
+
async process() {
|
|
105
|
+
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
|
|
106
|
+
this.headers['user-agent'] = 'loadtest/' + packageJson.version;
|
|
107
|
+
if (this.keyFile) {
|
|
108
|
+
this.key = await readFile(this.keyFile)
|
|
109
|
+
}
|
|
110
|
+
if (this.certFile) {
|
|
111
|
+
this.cert = await readFile(this.certFile);
|
|
112
|
+
}
|
|
113
|
+
if (typeof this.requestGenerator == 'string') {
|
|
114
|
+
this.requestGenerator = await import(this.requestGenerator)
|
|
115
|
+
}
|
|
116
|
+
if (this.bodyFile) {
|
|
117
|
+
this.body = await readBody(this.bodyFile);
|
|
118
|
+
}
|
|
85
119
|
}
|
|
86
|
-
processed.maxRequests = options.maxRequests || configuration.maxRequests
|
|
87
|
-
processed.maxSeconds = options.maxSeconds || configuration.maxSeconds
|
|
88
|
-
processed.cookies = options.cookies || configuration.cookies
|
|
89
|
-
processed.contentType = options.contentType || configuration.contentType
|
|
90
|
-
processed.timeout = options.timeout || configuration.timeout
|
|
91
|
-
processed.secureProtocol = options.secureProtocol || configuration.secureProtocol
|
|
92
|
-
processed.insecure = options.insecure || configuration.insecure
|
|
93
|
-
processed.recover = options.recover || configuration.recover
|
|
94
|
-
processed.proxy = options.proxy || configuration.proxy
|
|
95
|
-
processed.quiet = options.quiet || configuration.quiet
|
|
96
|
-
return processed
|
|
97
120
|
}
|
|
98
121
|
|
|
99
|
-
async function readBody(filename
|
|
122
|
+
async function readBody(filename) {
|
|
100
123
|
if (typeof filename !== 'string') {
|
|
101
|
-
throw new Error(`Invalid file to open
|
|
124
|
+
throw new Error(`Invalid file to open for body: ${filename}`);
|
|
102
125
|
}
|
|
103
126
|
if (path.extname(filename) === '.js') {
|
|
104
127
|
return await import(new URL(filename, `file://${process.cwd()}/`))
|
package/lib/testserver.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as net from 'net'
|
|
|
5
5
|
import {Latency} from './latency.js'
|
|
6
6
|
|
|
7
7
|
const PORT = 7357;
|
|
8
|
-
const
|
|
8
|
+
const LOG_HEADERS_INTERVAL_MS = 5000;
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,6 +18,7 @@ class TestServer {
|
|
|
18
18
|
this.server = null;
|
|
19
19
|
this.wsServer = null;
|
|
20
20
|
this.latency = new Latency({});
|
|
21
|
+
this.totalRequests = 0
|
|
21
22
|
this.debuggedTime = Date.now();
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -82,10 +83,10 @@ class TestServer {
|
|
|
82
83
|
request.body += data.toString();
|
|
83
84
|
});
|
|
84
85
|
request.on('end', () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
86
|
+
this.totalRequests += 1
|
|
87
|
+
const elapsedMs = Date.now() - this.debuggedTime
|
|
88
|
+
if (elapsedMs > LOG_HEADERS_INTERVAL_MS) {
|
|
89
|
+
this.debug(request, elapsedMs);
|
|
89
90
|
}
|
|
90
91
|
if (!this.options.delay) {
|
|
91
92
|
return this.end(response, id);
|
|
@@ -118,10 +119,20 @@ class TestServer {
|
|
|
118
119
|
* Debug headers and other interesting information: POST body.
|
|
119
120
|
*/
|
|
120
121
|
debug(request) {
|
|
121
|
-
if (
|
|
122
|
+
if (this.options.quiet) return
|
|
123
|
+
const headers = util.inspect(request.headers)
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
const elapsedMs = now - this.debuggedTime
|
|
126
|
+
const rps = (this.totalRequests / elapsedMs) * 1000
|
|
127
|
+
if (rps > 1) {
|
|
128
|
+
console.info(`Requests per second: ${rps.toFixed(0)}`)
|
|
129
|
+
}
|
|
130
|
+
console.info(`Headers for ${request.method} to ${request.url}: ${headers}`)
|
|
122
131
|
if (request.body) {
|
|
123
|
-
|
|
132
|
+
console.info(`Body: ${request.body}`);
|
|
124
133
|
}
|
|
134
|
+
this.debuggedTime = now;
|
|
135
|
+
this.totalRequests = 0
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run load tests for your web application. Mostly ab-compatible interface, with an option to force requests per second. Includes an API for automated load testing.",
|
|
6
6
|
"homepage": "https://github.com/alexfernandez/loadtest",
|