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 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
- const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
13
- if (!options.url) {
14
- throw new Error('Missing URL in options')
15
- }
16
- if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
17
- throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
18
- }
19
- if (options.url.startsWith('ws:')) {
20
- if (options.requestsPerSecond) {
21
- throw new Error(`"requestsPerSecond" not supported for WebSockets`);
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
- processed.url = options.url
25
- const configuration = loadConfig();
26
- processed.concurrency = options.concurrency || configuration.concurrency || 1
27
- const rps = options.rps ? parseFloat(options.rps) : null
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
- if (options.headers) {
80
- addHeaders(options.headers, processed.headers);
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
- processed.requestGenerator = options.requestGenerator || configuration.requestGenerator
83
- if (typeof processed.requestGenerator == 'string') {
84
- processed.requestGenerator = await import(processed.requestGenerator)
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, option) {
122
+ async function readBody(filename) {
100
123
  if (typeof filename !== 'string') {
101
- throw new Error(`Invalid file to open with ${option}: ${filename}`);
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 LOG_HEADERS_INTERVAL_SECONDS = 1;
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
- const now = Date.now();
86
- if (now - this.debuggedTime > LOG_HEADERS_INTERVAL_SECONDS * 1000) {
87
- this.debug(request);
88
- this.debuggedTime = now;
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 (!this.options.quiet) console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
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
- if (!this.options.quiet) console.info('Body: %s', request.body);
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.0",
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",