loadtest 5.1.2 → 6.0.0
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/.eslintrc +1 -1
- package/README.md +80 -45
- package/bin/loadtest.js +26 -154
- package/bin/testserver.js +5 -12
- package/index.js +6 -12
- package/lib/baseClient.js +5 -22
- package/lib/config.js +9 -24
- package/lib/headers.js +3 -64
- package/lib/hrtimer.js +2 -55
- package/lib/httpClient.js +19 -64
- package/lib/latency.js +25 -147
- package/lib/loadtest.js +19 -150
- package/lib/options.js +156 -0
- package/lib/testserver.js +15 -65
- package/lib/websocket.js +8 -54
- package/package.json +8 -8
- package/sample/post-file.js +13 -0
- package/sample/request-generator.js +2 -4
- package/test/all.js +33 -0
- package/test/body-generator.js +41 -0
- package/test/headers.js +51 -0
- package/test/hrtimer.js +39 -0
- package/test/httpClient.js +22 -0
- package/{lib → test}/integration.js +13 -35
- package/test/latency.js +81 -0
- package/test/loadtest.js +87 -0
- package/{lib → test}/request-generator.js +9 -21
- package/test/testserver.js +24 -0
- package/test/websocket.js +23 -0
- package/test.js +0 -36
- /package/{test → sample}/.loadtestrc +0 -0
package/lib/loadtest.js
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import * as https from 'https'
|
|
3
|
+
import * as httpClient from './httpClient.js'
|
|
4
|
+
import * as websocket from './websocket.js'
|
|
5
|
+
import {Latency} from './latency.js'
|
|
6
|
+
import {HighResolutionTimer} from './hrtimer.js'
|
|
7
|
+
import {processOptions} from './options.js'
|
|
2
8
|
|
|
3
|
-
/**
|
|
4
|
-
* Load Test a URL, website or websocket.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// requires
|
|
10
|
-
const Log = require('log');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const https = require('https');
|
|
13
|
-
const testing = require('testing');
|
|
14
|
-
const httpClient = require('./httpClient.js');
|
|
15
|
-
const websocket = require('./websocket.js');
|
|
16
|
-
const {Latency} = require('./latency.js');
|
|
17
|
-
const {HighResolutionTimer} = require('./hrtimer.js');
|
|
18
|
-
|
|
19
|
-
// globals
|
|
20
|
-
const log = new Log('info');
|
|
21
|
-
|
|
22
|
-
// constants
|
|
23
9
|
const SHOW_INTERVAL_MS = 5000;
|
|
24
10
|
|
|
25
|
-
// init
|
|
26
11
|
http.globalAgent.maxSockets = 1000;
|
|
27
12
|
https.globalAgent.maxSockets = 1000;
|
|
28
13
|
|
|
@@ -41,42 +26,23 @@ https.globalAgent.maxSockets = 1000;
|
|
|
41
26
|
* - contentType: the MIME type to use for the body, default text/plain.
|
|
42
27
|
* - requestsPerSecond: how many requests per second to send.
|
|
43
28
|
* - agentKeepAlive: if true, then use connection keep-alive.
|
|
44
|
-
* - debug: show debug messages.
|
|
45
|
-
* - quiet: do not log any messages.
|
|
46
29
|
* - indexParam: string to replace with a unique index.
|
|
47
30
|
* - insecure: allow https using self-signed certs.
|
|
31
|
+
* - debug: show debug messages (deprecated).
|
|
32
|
+
* - quiet: do not log any messages (deprecated).
|
|
48
33
|
* An optional callback will be called if/when the test finishes.
|
|
49
34
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
options.concurrency = options.concurrency || 1;
|
|
56
|
-
if (options.requestsPerSecond) {
|
|
57
|
-
options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
|
|
58
|
-
}
|
|
59
|
-
if (options.debug) {
|
|
60
|
-
log.level = Log.DEBUG;
|
|
61
|
-
}
|
|
62
|
-
if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
|
|
63
|
-
log.error('Invalid URL %s, must be http://, https:// or ws://', options.url);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (callback && !('quiet' in options)) {
|
|
67
|
-
options.quiet = true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (options.url.startsWith('ws:')) {
|
|
71
|
-
if (options.requestsPerSecond) {
|
|
72
|
-
log.error('"requestsPerSecond" not supported for WebSockets');
|
|
35
|
+
export function loadTest(options, callback) {
|
|
36
|
+
processOptions(options, error => {
|
|
37
|
+
if (error) {
|
|
38
|
+
if (callback) return callback(error)
|
|
39
|
+
throw new error
|
|
73
40
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
41
|
+
const operation = new Operation(options, callback);
|
|
42
|
+
operation.start();
|
|
43
|
+
return operation;
|
|
44
|
+
})
|
|
45
|
+
}
|
|
80
46
|
|
|
81
47
|
/**
|
|
82
48
|
* Used to keep track of individual load test Operation runs.
|
|
@@ -122,9 +88,6 @@ class Operation {
|
|
|
122
88
|
if (this.completedRequests == this.options.maxRequests) {
|
|
123
89
|
this.stop();
|
|
124
90
|
}
|
|
125
|
-
if (this.requests > this.options.maxRequests) {
|
|
126
|
-
log.debug('Should have no more running clients');
|
|
127
|
-
}
|
|
128
91
|
}
|
|
129
92
|
if (this.running && next) {
|
|
130
93
|
next();
|
|
@@ -202,97 +165,3 @@ class Operation {
|
|
|
202
165
|
}
|
|
203
166
|
}
|
|
204
167
|
|
|
205
|
-
/**
|
|
206
|
-
* A load test with max seconds.
|
|
207
|
-
*/
|
|
208
|
-
function testMaxSeconds(callback) {
|
|
209
|
-
const options = {
|
|
210
|
-
url: 'http://localhost:7357/',
|
|
211
|
-
maxSeconds: 0.1,
|
|
212
|
-
concurrency: 1,
|
|
213
|
-
quiet: true,
|
|
214
|
-
};
|
|
215
|
-
exports.loadTest(options, callback);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* A load test with max seconds.
|
|
221
|
-
*/
|
|
222
|
-
function testWSEcho(callback) {
|
|
223
|
-
const options = {
|
|
224
|
-
url: 'ws://localhost:7357/',
|
|
225
|
-
maxSeconds: 0.1,
|
|
226
|
-
concurrency: 1,
|
|
227
|
-
quiet: true,
|
|
228
|
-
};
|
|
229
|
-
exports.loadTest(options, callback);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function testIndexParam(callback) {
|
|
233
|
-
const options = {
|
|
234
|
-
url: 'http://localhost:7357/replace',
|
|
235
|
-
concurrency:1,
|
|
236
|
-
quiet: true,
|
|
237
|
-
maxSeconds: 0.1,
|
|
238
|
-
indexParam: "replace"
|
|
239
|
-
};
|
|
240
|
-
exports.loadTest(options, callback);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function testIndexParamWithBody(callback) {
|
|
244
|
-
const options = {
|
|
245
|
-
url: 'http://localhost:7357/replace',
|
|
246
|
-
concurrency:1,
|
|
247
|
-
quiet: true,
|
|
248
|
-
maxSeconds: 0.1,
|
|
249
|
-
indexParam: "replace",
|
|
250
|
-
body: '{"id": "replace"}'
|
|
251
|
-
};
|
|
252
|
-
exports.loadTest(options, callback);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function testIndexParamWithCallback(callback) {
|
|
256
|
-
const options = {
|
|
257
|
-
url: 'http://localhost:7357/replace',
|
|
258
|
-
concurrency:1,
|
|
259
|
-
quiet: true,
|
|
260
|
-
maxSeconds: 0.1,
|
|
261
|
-
indexParam: "replace",
|
|
262
|
-
indexParamCallback: function() {
|
|
263
|
-
//https://gist.github.com/6174/6062387
|
|
264
|
-
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
exports.loadTest(options, callback);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function testIndexParamWithCallbackAndBody(callback) {
|
|
271
|
-
const options = {
|
|
272
|
-
url: 'http://localhost:7357/replace',
|
|
273
|
-
concurrency:1,
|
|
274
|
-
quiet: true,
|
|
275
|
-
maxSeconds: 0.1,
|
|
276
|
-
body: '{"id": "replace"}',
|
|
277
|
-
indexParam: "replace",
|
|
278
|
-
indexParamCallback: function() {
|
|
279
|
-
//https://gist.github.com/6174/6062387
|
|
280
|
-
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
exports.loadTest(options, callback);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Run all tests.
|
|
289
|
-
*/
|
|
290
|
-
exports.test = function(callback) {
|
|
291
|
-
testing.run([testMaxSeconds, testWSEcho, testIndexParam, testIndexParamWithBody, testIndexParamWithCallback, testIndexParamWithCallbackAndBody], callback);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// run tests if invoked directly
|
|
295
|
-
if (__filename == process.argv[1]) {
|
|
296
|
-
exports.test(testing.show);
|
|
297
|
-
}
|
|
298
|
-
|
package/lib/options.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {readFile} from 'fs/promises'
|
|
4
|
+
import * as path from 'path'
|
|
5
|
+
import * as urlLib from 'url'
|
|
6
|
+
import {addHeaders} from '../lib/headers.js'
|
|
7
|
+
import {loadConfig} from '../lib/config.js'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function processOptions(options, callback) {
|
|
11
|
+
processOptionsAsync(options).then(result => callback(null, result)).catch(error => callback(error))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function processOptionsAsync(options) {
|
|
15
|
+
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
|
|
16
|
+
if (!options.url) {
|
|
17
|
+
throw new Error('Missing URL in options')
|
|
18
|
+
}
|
|
19
|
+
options.concurrency = options.concurrency || 1;
|
|
20
|
+
if (options.requestsPerSecond) {
|
|
21
|
+
options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
|
|
22
|
+
}
|
|
23
|
+
if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
|
|
24
|
+
throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
|
|
25
|
+
}
|
|
26
|
+
if (options.url.startsWith('ws:')) {
|
|
27
|
+
if (options.requestsPerSecond) {
|
|
28
|
+
throw new Error(`"requestsPerSecond" not supported for WebSockets`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const configuration = loadConfig();
|
|
32
|
+
|
|
33
|
+
options.agentKeepAlive = options.keepalive || options.agent || configuration.agentKeepAlive;
|
|
34
|
+
options.indexParam = options.index || configuration.indexParam;
|
|
35
|
+
|
|
36
|
+
//TODO: add index Param
|
|
37
|
+
// Allow a post body string in options
|
|
38
|
+
// Ex -P '{"foo": "bar"}'
|
|
39
|
+
if (options.postBody) {
|
|
40
|
+
options.method = 'POST';
|
|
41
|
+
options.body = options.postBody;
|
|
42
|
+
}
|
|
43
|
+
if (options.postFile) {
|
|
44
|
+
options.method = 'POST';
|
|
45
|
+
options.body = await readBody(options.postFile, '-p');
|
|
46
|
+
}
|
|
47
|
+
if (options.data) {
|
|
48
|
+
options.body = JSON.parse(options.data);
|
|
49
|
+
}
|
|
50
|
+
if (options.method) {
|
|
51
|
+
const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
|
|
52
|
+
if (acceptedMethods.indexOf(options.method) === -1) {
|
|
53
|
+
options.method = 'GET';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (options.putFile) {
|
|
57
|
+
options.method = 'PUT';
|
|
58
|
+
options.body = await readBody(options.putFile, '-u');
|
|
59
|
+
}
|
|
60
|
+
if (options.patchBody) {
|
|
61
|
+
options.method = 'PATCH';
|
|
62
|
+
options.body = options.patchBody;
|
|
63
|
+
}
|
|
64
|
+
if (options.patchFile) {
|
|
65
|
+
options.method = 'PATCH';
|
|
66
|
+
options.body = await readBody(options.patchFile, '-a');
|
|
67
|
+
}
|
|
68
|
+
if (!options.method) {
|
|
69
|
+
options.method = configuration.method;
|
|
70
|
+
}
|
|
71
|
+
if (!options.body) {
|
|
72
|
+
if(configuration.body) {
|
|
73
|
+
options.body = configuration.body;
|
|
74
|
+
} else if (configuration.file) {
|
|
75
|
+
options.body = await readBody(configuration.file, 'configuration.request.file');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
|
|
79
|
+
if (!options.key) {
|
|
80
|
+
options.key = configuration.key;
|
|
81
|
+
}
|
|
82
|
+
if (options.key) {
|
|
83
|
+
options.key = await readFile(options.key)
|
|
84
|
+
}
|
|
85
|
+
if (!options.cert) {
|
|
86
|
+
options.cert = configuration.cert;
|
|
87
|
+
}
|
|
88
|
+
if (options.cert) {
|
|
89
|
+
options.cert = await readFile(options.cert);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const defaultHeaders = options.headers || !configuration.headers ? {} : configuration.headers;
|
|
93
|
+
defaultHeaders['host'] = urlLib.parse(options.url).host;
|
|
94
|
+
defaultHeaders['user-agent'] = 'loadtest/' + packageJson.version;
|
|
95
|
+
defaultHeaders['accept'] = '*/*';
|
|
96
|
+
|
|
97
|
+
if (options.headers) {
|
|
98
|
+
addHeaders(options.headers, defaultHeaders);
|
|
99
|
+
console.log('headers: %s, %j', typeof defaultHeaders, defaultHeaders);
|
|
100
|
+
}
|
|
101
|
+
options.headers = defaultHeaders;
|
|
102
|
+
|
|
103
|
+
if (!options.requestGenerator) {
|
|
104
|
+
options.requestGenerator = configuration.requestGenerator;
|
|
105
|
+
}
|
|
106
|
+
if (typeof options.requestGenerator == 'string') {
|
|
107
|
+
options.requestGenerator = await import(options.requestGenerator)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use configuration file for other values
|
|
111
|
+
if(!options.maxRequests) {
|
|
112
|
+
options.maxRequests = configuration.maxRequests;
|
|
113
|
+
}
|
|
114
|
+
if(!options.concurrency) {
|
|
115
|
+
options.concurrency = configuration.concurrency;
|
|
116
|
+
}
|
|
117
|
+
if(!options.maxSeconds) {
|
|
118
|
+
options.maxSeconds = configuration.maxSeconds;
|
|
119
|
+
}
|
|
120
|
+
if(!options.timeout && configuration.timeout) {
|
|
121
|
+
options.timeout = configuration.timeout;
|
|
122
|
+
}
|
|
123
|
+
if(!options.contentType) {
|
|
124
|
+
options.contentType = configuration.contentType;
|
|
125
|
+
}
|
|
126
|
+
if(!options.cookies) {
|
|
127
|
+
options.cookies = configuration.cookies;
|
|
128
|
+
}
|
|
129
|
+
if(!options.secureProtocol) {
|
|
130
|
+
options.secureProtocol = configuration.secureProtocol;
|
|
131
|
+
}
|
|
132
|
+
if(!options.insecure) {
|
|
133
|
+
options.insecure = configuration.insecure;
|
|
134
|
+
}
|
|
135
|
+
if(!options.recover) {
|
|
136
|
+
options.recover = configuration.recover;
|
|
137
|
+
}
|
|
138
|
+
if(!options.proxy) {
|
|
139
|
+
options.proxy = configuration.proxy;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readBody(filename, option) {
|
|
144
|
+
if (typeof filename !== 'string') {
|
|
145
|
+
throw new Error(`Invalid file to open with ${option}: ${filename}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (path.extname(filename) === '.js') {
|
|
149
|
+
return await import(new URL(filename, `file://${process.cwd()}/`))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
|
|
153
|
+
|
|
154
|
+
return ret;
|
|
155
|
+
}
|
|
156
|
+
|
package/lib/testserver.js
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import {server as WebSocketServer} from 'websocket'
|
|
3
|
+
import * as util from 'util'
|
|
4
|
+
import * as net from 'net'
|
|
5
|
+
import {Latency} from './latency.js'
|
|
2
6
|
|
|
3
|
-
/**
|
|
4
|
-
* Test server to load test.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// requires
|
|
10
|
-
const testing = require('testing');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const WebSocketServer = require('websocket').server;
|
|
13
|
-
const util = require('util');
|
|
14
|
-
const net = require('net');
|
|
15
|
-
const Log = require('log');
|
|
16
|
-
const {Latency} = require('./latency.js');
|
|
17
|
-
|
|
18
|
-
// globals
|
|
19
|
-
const log = new Log('info');
|
|
20
|
-
|
|
21
|
-
// constants
|
|
22
7
|
const PORT = 7357;
|
|
23
8
|
const LOG_HEADERS_INTERVAL_SECONDS = 1;
|
|
24
9
|
|
|
@@ -34,9 +19,6 @@ class TestServer {
|
|
|
34
19
|
this.wsServer = null;
|
|
35
20
|
this.latency = new Latency({});
|
|
36
21
|
this.debuggedTime = Date.now();
|
|
37
|
-
if (options.quiet) {
|
|
38
|
-
log.level = 'notice';
|
|
39
|
-
}
|
|
40
22
|
}
|
|
41
23
|
|
|
42
24
|
/**
|
|
@@ -63,7 +45,7 @@ class TestServer {
|
|
|
63
45
|
return this.createError('Could not start server on port ' + this.port + ': ' + error, callback);
|
|
64
46
|
});
|
|
65
47
|
this.server.listen(this.port, () => {
|
|
66
|
-
|
|
48
|
+
console.info(`Listening on http://localhost:${this.port}/`);
|
|
67
49
|
if (callback) {
|
|
68
50
|
callback();
|
|
69
51
|
}
|
|
@@ -71,18 +53,15 @@ class TestServer {
|
|
|
71
53
|
this.wsServer.on('request', request => {
|
|
72
54
|
// explicity omitting origin check here.
|
|
73
55
|
const connection = request.accept(null, request.origin);
|
|
74
|
-
log.debug(' Connection accepted.');
|
|
75
56
|
connection.on('message', message => {
|
|
76
57
|
if (message.type === 'utf8') {
|
|
77
|
-
log.debug('Received Message: ' + message.utf8Data);
|
|
78
58
|
connection.sendUTF(message.utf8Data);
|
|
79
59
|
} else if (message.type === 'binary') {
|
|
80
|
-
log.debug('Received Binary Message of ' + message.binaryData.length + ' bytes');
|
|
81
60
|
connection.sendBytes(message.binaryData);
|
|
82
61
|
}
|
|
83
62
|
});
|
|
84
63
|
connection.on('close', () => {
|
|
85
|
-
|
|
64
|
+
console.info('Peer %s disconnected', connection.remoteAddress);
|
|
86
65
|
});
|
|
87
66
|
});
|
|
88
67
|
return this.server;
|
|
@@ -93,7 +72,7 @@ class TestServer {
|
|
|
93
72
|
*/
|
|
94
73
|
createError(message, callback) {
|
|
95
74
|
if (!callback) {
|
|
96
|
-
return
|
|
75
|
+
return console.error(message);
|
|
97
76
|
}
|
|
98
77
|
callback(message);
|
|
99
78
|
}
|
|
@@ -127,7 +106,7 @@ class TestServer {
|
|
|
127
106
|
*/
|
|
128
107
|
socketListen(socket) {
|
|
129
108
|
socket.on('error', error => {
|
|
130
|
-
|
|
109
|
+
console.error('socket error: %s', error);
|
|
131
110
|
socket.end();
|
|
132
111
|
});
|
|
133
112
|
socket.on('data', data => this.readData(data));
|
|
@@ -137,16 +116,16 @@ class TestServer {
|
|
|
137
116
|
* Read some data off the socket.
|
|
138
117
|
*/
|
|
139
118
|
readData(data) {
|
|
140
|
-
|
|
119
|
+
console.info('data: %s', data);
|
|
141
120
|
}
|
|
142
121
|
|
|
143
122
|
/**
|
|
144
123
|
* Debug headers and other interesting information: POST body.
|
|
145
124
|
*/
|
|
146
125
|
debug(request) {
|
|
147
|
-
|
|
126
|
+
console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
|
|
148
127
|
if (request.body) {
|
|
149
|
-
|
|
128
|
+
console.info('Body: %s', request.body);
|
|
150
129
|
}
|
|
151
130
|
}
|
|
152
131
|
|
|
@@ -173,7 +152,7 @@ class TestServer {
|
|
|
173
152
|
}
|
|
174
153
|
const percent = parseInt(this.options.percent, 10);
|
|
175
154
|
if (!percent) {
|
|
176
|
-
|
|
155
|
+
console.error('Invalid error percent %s', this.options.percent);
|
|
177
156
|
return false;
|
|
178
157
|
}
|
|
179
158
|
return (Math.random() < percent / 100);
|
|
@@ -184,42 +163,13 @@ class TestServer {
|
|
|
184
163
|
* Start a test server. Options can contain:
|
|
185
164
|
* - port: the port to use, default 7357.
|
|
186
165
|
* - delay: wait the given milliseconds before answering.
|
|
187
|
-
* - quiet: do not log any messages.
|
|
166
|
+
* - quiet: do not log any messages (deprecated).
|
|
188
167
|
* - percent: give an error (default 500) on some % of requests.
|
|
189
168
|
* - error: set an HTTP error code, default is 500.
|
|
190
169
|
* An optional callback is called after the server has started.
|
|
191
|
-
* In this case the quiet option is enabled.
|
|
192
170
|
*/
|
|
193
|
-
|
|
194
|
-
if (callback) {
|
|
195
|
-
options.quiet = true;
|
|
196
|
-
}
|
|
171
|
+
export function startServer(options, callback) {
|
|
197
172
|
const server = new TestServer(options);
|
|
198
173
|
return server.start(callback);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
function testStartServer(callback) {
|
|
202
|
-
const options = {
|
|
203
|
-
port: 10530,
|
|
204
|
-
};
|
|
205
|
-
const server = exports.startServer(options, error => {
|
|
206
|
-
testing.check(error, 'Could not start server', callback);
|
|
207
|
-
server.close(error => {
|
|
208
|
-
testing.check(error, 'Could not stop server', callback);
|
|
209
|
-
testing.success(callback);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Run the tests.
|
|
216
|
-
*/
|
|
217
|
-
exports.test = function(callback) {
|
|
218
|
-
testing.run([testStartServer], 5000, callback);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// start server if invoked directly
|
|
222
|
-
if (__filename == process.argv[1]) {
|
|
223
|
-
exports.test(testing.show);
|
|
224
174
|
}
|
|
225
175
|
|
package/lib/websocket.js
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import websocket from 'websocket'
|
|
2
|
+
import {BaseClient} from './baseClient.js'
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Load test a websocket.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// requires
|
|
10
|
-
const WebSocketClient = require('websocket').client;
|
|
11
|
-
const testing = require('testing');
|
|
12
|
-
const Log = require('log');
|
|
13
|
-
const BaseClient = require('./baseClient.js').BaseClient;
|
|
14
|
-
|
|
15
|
-
// globals
|
|
16
|
-
const log = new Log('info');
|
|
17
4
|
let latency;
|
|
18
5
|
|
|
19
6
|
|
|
20
7
|
/**
|
|
21
8
|
* Create a client for a websocket.
|
|
22
9
|
*/
|
|
23
|
-
|
|
10
|
+
export function create(operation, params) {
|
|
24
11
|
return new WebsocketClient(operation, params);
|
|
25
|
-
}
|
|
12
|
+
}
|
|
26
13
|
|
|
27
14
|
/**
|
|
28
15
|
* A client that connects to a websocket.
|
|
@@ -40,13 +27,10 @@ class WebsocketClient extends BaseClient {
|
|
|
40
27
|
* Start the websocket client.
|
|
41
28
|
*/
|
|
42
29
|
start() {
|
|
43
|
-
this.client = new
|
|
44
|
-
this.client.on('connectFailed',
|
|
45
|
-
log.debug('WebSocket client connection error ' + error);
|
|
46
|
-
});
|
|
30
|
+
this.client = new websocket.client();
|
|
31
|
+
this.client.on('connectFailed', () => {});
|
|
47
32
|
this.client.on('connect', connection => this.connect(connection));
|
|
48
33
|
this.client.connect(this.params.url, []);
|
|
49
|
-
log.debug('WebSocket client connected to ' + this.params.url);
|
|
50
34
|
}
|
|
51
35
|
|
|
52
36
|
/**
|
|
@@ -55,7 +39,6 @@ class WebsocketClient extends BaseClient {
|
|
|
55
39
|
stop() {
|
|
56
40
|
if (this.connection) {
|
|
57
41
|
this.connection.close();
|
|
58
|
-
log.debug('WebSocket client disconnected from ' + this.params.url);
|
|
59
42
|
}
|
|
60
43
|
}
|
|
61
44
|
|
|
@@ -100,7 +83,7 @@ class WebsocketClient extends BaseClient {
|
|
|
100
83
|
ended = true;
|
|
101
84
|
|
|
102
85
|
if (message.type != 'utf8') {
|
|
103
|
-
|
|
86
|
+
console.error('Invalid message type ' + message.type);
|
|
104
87
|
return;
|
|
105
88
|
}
|
|
106
89
|
let json;
|
|
@@ -108,12 +91,10 @@ class WebsocketClient extends BaseClient {
|
|
|
108
91
|
json = JSON.parse(message.utf8Data);
|
|
109
92
|
}
|
|
110
93
|
catch(e) {
|
|
111
|
-
|
|
94
|
+
console.error('Invalid JSON: ' + message.utf8Data);
|
|
112
95
|
return;
|
|
113
96
|
}
|
|
114
97
|
|
|
115
|
-
log.debug("Received response %j", json);
|
|
116
|
-
|
|
117
98
|
// eat the client_connected message we get at the beginning
|
|
118
99
|
if ((json && json[0] && json[0][0] == 'client_connected')) {
|
|
119
100
|
ended = false;
|
|
@@ -123,7 +104,6 @@ class WebsocketClient extends BaseClient {
|
|
|
123
104
|
if (this.lastCall) {
|
|
124
105
|
const newCall = new Date().getTime();
|
|
125
106
|
latency.add(newCall - this.lastCall);
|
|
126
|
-
log.debug('latency: ' + (newCall - this.lastCall));
|
|
127
107
|
this.lastCall = null;
|
|
128
108
|
}
|
|
129
109
|
|
|
@@ -152,29 +132,3 @@ class WebsocketClient extends BaseClient {
|
|
|
152
132
|
}
|
|
153
133
|
}
|
|
154
134
|
|
|
155
|
-
|
|
156
|
-
function testWebsocketClient(callback) {
|
|
157
|
-
const options = {
|
|
158
|
-
url: 'ws://localhost:7357/',
|
|
159
|
-
maxSeconds: 0.1,
|
|
160
|
-
concurrency: 1,
|
|
161
|
-
quiet: true,
|
|
162
|
-
};
|
|
163
|
-
exports.create({}, options);
|
|
164
|
-
testing.success(callback);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Run tests, currently nothing.
|
|
169
|
-
*/
|
|
170
|
-
exports.test = function(callback) {
|
|
171
|
-
testing.run([
|
|
172
|
-
testWebsocketClient,
|
|
173
|
-
], callback);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// start tests if invoked directly
|
|
177
|
-
if (__filename == process.argv[1]) {
|
|
178
|
-
exports.test(testing.show);
|
|
179
|
-
}
|
|
180
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"type": "module",
|
|
4
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.",
|
|
5
6
|
"homepage": "https://github.com/alexfernandez/loadtest",
|
|
6
7
|
"contributors": [
|
|
@@ -17,13 +18,12 @@
|
|
|
17
18
|
"agentkeepalive": "^2.0.3",
|
|
18
19
|
"confinode": "^2.1.1",
|
|
19
20
|
"https-proxy-agent": "^2.2.1",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"websocket": "^1.0.28"
|
|
21
|
+
"stdio": "0.2.7",
|
|
22
|
+
"testing": "^3.0.3",
|
|
23
|
+
"websocket": "^1.0.34"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"eslint": "^
|
|
26
|
+
"eslint": "^8.47.0"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"testing",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"black box"
|
|
36
36
|
],
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
38
|
+
"node": ">=16"
|
|
39
39
|
},
|
|
40
40
|
"bin": {
|
|
41
41
|
"loadtest": "bin/loadtest.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"preferGlobal": true,
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "node test.js",
|
|
46
|
+
"test": "node test/all.js",
|
|
47
47
|
"posttest": "eslint ."
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample post-file content for test.
|
|
3
|
+
* Contains a single exported function that generates the body.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export default function bodyGenerator(requestId) {
|
|
7
|
+
// this object will be serialized to JSON and sent in the body of the request
|
|
8
|
+
return {
|
|
9
|
+
key: 'value',
|
|
10
|
+
requestId: requestId
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|