loadtest 5.2.0 → 6.1.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 +79 -59
- package/bin/loadtest.js +28 -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 +16 -64
- package/lib/latency.js +18 -175
- package/lib/loadtest.js +48 -165
- package/lib/options.js +152 -0
- package/lib/show.js +47 -0
- package/lib/testserver.js +33 -78
- package/lib/websocket.js +8 -54
- package/package.json +8 -8
- package/sample/post-file.js +13 -0
- package/sample/request-generator.js +3 -5
- package/sample/request-generator.ts +2 -2
- package/test/all.js +33 -0
- package/test/body-generator.js +42 -0
- package/test/headers.js +51 -0
- package/test/hrtimer.js +39 -0
- package/test/httpClient.js +22 -0
- package/{lib → test}/integration.js +45 -39
- package/test/latency.js +81 -0
- package/test/loadtest.js +96 -0
- package/{lib → test}/request-generator.js +10 -21
- package/test/testserver.js +25 -0
- package/test/websocket.js +23 -0
- package/test.js +0 -36
- /package/{test → sample}/.loadtestrc +0 -0
package/lib/options.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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 async function processOptions(options) {
|
|
11
|
+
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url)))
|
|
12
|
+
if (!options.url) {
|
|
13
|
+
throw new Error('Missing URL in options')
|
|
14
|
+
}
|
|
15
|
+
options.concurrency = options.concurrency || 1;
|
|
16
|
+
if (options.requestsPerSecond) {
|
|
17
|
+
options.requestsPerSecond = options.requestsPerSecond / options.concurrency;
|
|
18
|
+
}
|
|
19
|
+
if (!options.url.startsWith('http://') && !options.url.startsWith('https://') && !options.url.startsWith('ws://')) {
|
|
20
|
+
throw new Error(`Invalid URL ${options.url}, must be http://, https:// or ws://'`)
|
|
21
|
+
}
|
|
22
|
+
if (options.url.startsWith('ws:')) {
|
|
23
|
+
if (options.requestsPerSecond) {
|
|
24
|
+
throw new Error(`"requestsPerSecond" not supported for WebSockets`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const configuration = loadConfig();
|
|
28
|
+
|
|
29
|
+
options.agentKeepAlive = options.keepalive || options.agent || configuration.agentKeepAlive;
|
|
30
|
+
options.indexParam = options.index || configuration.indexParam;
|
|
31
|
+
|
|
32
|
+
//TODO: add index Param
|
|
33
|
+
// Allow a post body string in options
|
|
34
|
+
// Ex -P '{"foo": "bar"}'
|
|
35
|
+
if (options.postBody) {
|
|
36
|
+
options.method = 'POST';
|
|
37
|
+
options.body = options.postBody;
|
|
38
|
+
}
|
|
39
|
+
if (options.postFile) {
|
|
40
|
+
options.method = 'POST';
|
|
41
|
+
options.body = await readBody(options.postFile, '-p');
|
|
42
|
+
}
|
|
43
|
+
if (options.data) {
|
|
44
|
+
options.body = options.data
|
|
45
|
+
}
|
|
46
|
+
if (options.method) {
|
|
47
|
+
const acceptedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'];
|
|
48
|
+
if (acceptedMethods.indexOf(options.method) === -1) {
|
|
49
|
+
options.method = 'GET';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (options.putFile) {
|
|
53
|
+
options.method = 'PUT';
|
|
54
|
+
options.body = await readBody(options.putFile, '-u');
|
|
55
|
+
}
|
|
56
|
+
if (options.patchBody) {
|
|
57
|
+
options.method = 'PATCH';
|
|
58
|
+
options.body = options.patchBody;
|
|
59
|
+
}
|
|
60
|
+
if (options.patchFile) {
|
|
61
|
+
options.method = 'PATCH';
|
|
62
|
+
options.body = await readBody(options.patchFile, '-a');
|
|
63
|
+
}
|
|
64
|
+
if (!options.method) {
|
|
65
|
+
options.method = configuration.method;
|
|
66
|
+
}
|
|
67
|
+
if (!options.body) {
|
|
68
|
+
if(configuration.body) {
|
|
69
|
+
options.body = configuration.body;
|
|
70
|
+
} else if (configuration.file) {
|
|
71
|
+
options.body = await readBody(configuration.file, 'configuration.request.file');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
options.requestsPerSecond = options.rps ? parseFloat(options.rps) : configuration.requestsPerSecond;
|
|
75
|
+
if (!options.key) {
|
|
76
|
+
options.key = configuration.key;
|
|
77
|
+
}
|
|
78
|
+
if (options.key) {
|
|
79
|
+
options.key = await readFile(options.key)
|
|
80
|
+
}
|
|
81
|
+
if (!options.cert) {
|
|
82
|
+
options.cert = configuration.cert;
|
|
83
|
+
}
|
|
84
|
+
if (options.cert) {
|
|
85
|
+
options.cert = await readFile(options.cert);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const defaultHeaders = options.headers || !configuration.headers ? {} : configuration.headers;
|
|
89
|
+
defaultHeaders['host'] = urlLib.parse(options.url).host;
|
|
90
|
+
defaultHeaders['user-agent'] = 'loadtest/' + packageJson.version;
|
|
91
|
+
defaultHeaders['accept'] = '*/*';
|
|
92
|
+
|
|
93
|
+
if (options.headers) {
|
|
94
|
+
addHeaders(options.headers, defaultHeaders);
|
|
95
|
+
console.log('headers: %s, %j', typeof defaultHeaders, defaultHeaders);
|
|
96
|
+
}
|
|
97
|
+
options.headers = defaultHeaders;
|
|
98
|
+
|
|
99
|
+
if (!options.requestGenerator) {
|
|
100
|
+
options.requestGenerator = configuration.requestGenerator;
|
|
101
|
+
}
|
|
102
|
+
if (typeof options.requestGenerator == 'string') {
|
|
103
|
+
options.requestGenerator = await import(options.requestGenerator)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Use configuration file for other values
|
|
107
|
+
if(!options.maxRequests) {
|
|
108
|
+
options.maxRequests = configuration.maxRequests;
|
|
109
|
+
}
|
|
110
|
+
if(!options.concurrency) {
|
|
111
|
+
options.concurrency = configuration.concurrency;
|
|
112
|
+
}
|
|
113
|
+
if(!options.maxSeconds) {
|
|
114
|
+
options.maxSeconds = configuration.maxSeconds;
|
|
115
|
+
}
|
|
116
|
+
if(!options.timeout && configuration.timeout) {
|
|
117
|
+
options.timeout = configuration.timeout;
|
|
118
|
+
}
|
|
119
|
+
if(!options.contentType) {
|
|
120
|
+
options.contentType = configuration.contentType;
|
|
121
|
+
}
|
|
122
|
+
if(!options.cookies) {
|
|
123
|
+
options.cookies = configuration.cookies;
|
|
124
|
+
}
|
|
125
|
+
if(!options.secureProtocol) {
|
|
126
|
+
options.secureProtocol = configuration.secureProtocol;
|
|
127
|
+
}
|
|
128
|
+
if(!options.insecure) {
|
|
129
|
+
options.insecure = configuration.insecure;
|
|
130
|
+
}
|
|
131
|
+
if(!options.recover) {
|
|
132
|
+
options.recover = configuration.recover;
|
|
133
|
+
}
|
|
134
|
+
if(!options.proxy) {
|
|
135
|
+
options.proxy = configuration.proxy;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readBody(filename, option) {
|
|
140
|
+
if (typeof filename !== 'string') {
|
|
141
|
+
throw new Error(`Invalid file to open with ${option}: ${filename}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (path.extname(filename) === '.js') {
|
|
145
|
+
return await import(new URL(filename, `file://${process.cwd()}/`))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ret = await readFile(filename, {encoding: 'utf8'}).replace("\n", "");
|
|
149
|
+
|
|
150
|
+
return ret;
|
|
151
|
+
}
|
|
152
|
+
|
package/lib/show.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Show result of a load test.
|
|
5
|
+
*/
|
|
6
|
+
export function showResult(options, result) {
|
|
7
|
+
console.info('');
|
|
8
|
+
console.info('Target URL: %s', options.url);
|
|
9
|
+
if (options.maxRequests) {
|
|
10
|
+
console.info('Max requests: %s', options.maxRequests);
|
|
11
|
+
} else if (options.maxSeconds) {
|
|
12
|
+
console.info('Max time (s): %s', options.maxSeconds);
|
|
13
|
+
}
|
|
14
|
+
console.info('Concurrency level: %s', options.concurrency);
|
|
15
|
+
let agent = 'none';
|
|
16
|
+
if (options.agentKeepAlive) {
|
|
17
|
+
agent = 'keepalive';
|
|
18
|
+
}
|
|
19
|
+
console.info('Agent: %s', agent);
|
|
20
|
+
if (options.requestsPerSecond) {
|
|
21
|
+
console.info('Requests per second: %s', options.requestsPerSecond * options.concurrency);
|
|
22
|
+
}
|
|
23
|
+
console.info('');
|
|
24
|
+
console.info('Completed requests: %s', result.totalRequests);
|
|
25
|
+
console.info('Total errors: %s', result.totalErrors);
|
|
26
|
+
console.info('Total time: %s s', result.totalTimeSeconds);
|
|
27
|
+
console.info('Requests per second: %s', result.rps);
|
|
28
|
+
console.info('Mean latency: %s ms', result.meanLatencyMs);
|
|
29
|
+
console.info('');
|
|
30
|
+
console.info('Percentage of the requests served within a certain time');
|
|
31
|
+
|
|
32
|
+
Object.keys(result.percentiles).forEach(percentile => {
|
|
33
|
+
console.info(' %s% %s ms', percentile, result.percentiles[percentile]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.info(' 100% %s ms (longest request)', result.maxLatencyMs);
|
|
37
|
+
if (result.totalErrors) {
|
|
38
|
+
console.info('');
|
|
39
|
+
Object.keys(result.errorCodes).forEach(errorCode => {
|
|
40
|
+
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
41
|
+
console.info(' %s%s: %s errors', padding, errorCode, result.errorCodes[errorCode]);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
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,14 +19,11 @@ 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
|
/**
|
|
43
25
|
* Start the server.
|
|
44
|
-
*
|
|
26
|
+
* The callback parameter will be called after the server has started.
|
|
45
27
|
*/
|
|
46
28
|
start(callback) {
|
|
47
29
|
if (this.options.socket) {
|
|
@@ -63,38 +45,30 @@ 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
|
-
|
|
67
|
-
|
|
68
|
-
callback();
|
|
69
|
-
}
|
|
48
|
+
if (!this.options.quiet) console.info(`Listening on http://localhost:${this.port}/`)
|
|
49
|
+
callback(null, this.server)
|
|
70
50
|
});
|
|
71
51
|
this.wsServer.on('request', request => {
|
|
72
52
|
// explicity omitting origin check here.
|
|
73
53
|
const connection = request.accept(null, request.origin);
|
|
74
|
-
log.debug(' Connection accepted.');
|
|
75
54
|
connection.on('message', message => {
|
|
76
55
|
if (message.type === 'utf8') {
|
|
77
|
-
log.debug('Received Message: ' + message.utf8Data);
|
|
78
56
|
connection.sendUTF(message.utf8Data);
|
|
79
57
|
} else if (message.type === 'binary') {
|
|
80
|
-
log.debug('Received Binary Message of ' + message.binaryData.length + ' bytes');
|
|
81
58
|
connection.sendBytes(message.binaryData);
|
|
82
59
|
}
|
|
83
60
|
});
|
|
84
61
|
connection.on('close', () => {
|
|
85
|
-
|
|
62
|
+
if (!this.options.quiet) console.info('Peer %s disconnected', connection.remoteAddress);
|
|
86
63
|
});
|
|
87
64
|
});
|
|
88
|
-
return this.server
|
|
65
|
+
return this.server
|
|
89
66
|
}
|
|
90
67
|
|
|
91
68
|
/**
|
|
92
69
|
* Log an error, or send to the callback if present.
|
|
93
70
|
*/
|
|
94
71
|
createError(message, callback) {
|
|
95
|
-
if (!callback) {
|
|
96
|
-
return log.error(message);
|
|
97
|
-
}
|
|
98
72
|
callback(message);
|
|
99
73
|
}
|
|
100
74
|
|
|
@@ -127,7 +101,7 @@ class TestServer {
|
|
|
127
101
|
*/
|
|
128
102
|
socketListen(socket) {
|
|
129
103
|
socket.on('error', error => {
|
|
130
|
-
|
|
104
|
+
if (!this.options.quiet) console.error('socket error: %s', error);
|
|
131
105
|
socket.end();
|
|
132
106
|
});
|
|
133
107
|
socket.on('data', data => this.readData(data));
|
|
@@ -137,16 +111,16 @@ class TestServer {
|
|
|
137
111
|
* Read some data off the socket.
|
|
138
112
|
*/
|
|
139
113
|
readData(data) {
|
|
140
|
-
|
|
114
|
+
if (!this.options.quiet) console.info('data: %s', data);
|
|
141
115
|
}
|
|
142
116
|
|
|
143
117
|
/**
|
|
144
118
|
* Debug headers and other interesting information: POST body.
|
|
145
119
|
*/
|
|
146
120
|
debug(request) {
|
|
147
|
-
|
|
121
|
+
if (!this.options.quiet) console.info('Headers for %s to %s: %s', request.method, request.url, util.inspect(request.headers));
|
|
148
122
|
if (request.body) {
|
|
149
|
-
|
|
123
|
+
if (!this.options.quiet) console.info('Body: %s', request.body);
|
|
150
124
|
}
|
|
151
125
|
}
|
|
152
126
|
|
|
@@ -173,7 +147,7 @@ class TestServer {
|
|
|
173
147
|
}
|
|
174
148
|
const percent = parseInt(this.options.percent, 10);
|
|
175
149
|
if (!percent) {
|
|
176
|
-
|
|
150
|
+
if (!this.options.quiet) console.error('Invalid error percent %s', this.options.percent);
|
|
177
151
|
return false;
|
|
178
152
|
}
|
|
179
153
|
return (Math.random() < percent / 100);
|
|
@@ -181,45 +155,26 @@ class TestServer {
|
|
|
181
155
|
}
|
|
182
156
|
|
|
183
157
|
/**
|
|
184
|
-
* Start a test server.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
158
|
+
* Start a test server. Parameters:
|
|
159
|
+
* - `options`, can contain:
|
|
160
|
+
* - port: the port to use, default 7357.
|
|
161
|
+
* - delay: wait the given milliseconds before answering.
|
|
162
|
+
* - quiet: do not log any messages.
|
|
163
|
+
* - percent: give an error (default 500) on some % of requests.
|
|
164
|
+
* - error: set an HTTP error code, default is 500.
|
|
165
|
+
* - `callback`: optional callback, called after the server has started.
|
|
166
|
+
* If not present will return a promise.
|
|
192
167
|
*/
|
|
193
|
-
|
|
168
|
+
export function startServer(options, callback) {
|
|
169
|
+
const server = new TestServer(options);
|
|
194
170
|
if (callback) {
|
|
195
|
-
|
|
171
|
+
return server.start(callback)
|
|
196
172
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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);
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
server.start((error, result) => {
|
|
175
|
+
if (error) return reject(error)
|
|
176
|
+
return resolve(result)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
224
179
|
}
|
|
225
180
|
|
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.1.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
|
+
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Sample request generator usage.
|
|
5
3
|
* Contributed by jjohnsonvng:
|
|
6
4
|
* https://github.com/alexfernandez/loadtest/issues/86#issuecomment-211579639
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
import {loadTest} from '../index.js'
|
|
10
8
|
|
|
11
9
|
const options = {
|
|
12
10
|
url: 'http://yourHost',
|
|
@@ -27,11 +25,11 @@ const options = {
|
|
|
27
25
|
}
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
loadTest(options, (error, result) => {
|
|
31
29
|
if (error) {
|
|
32
30
|
return console.error('Got an error: %s', error);
|
|
33
31
|
}
|
|
34
|
-
console.log(
|
|
32
|
+
console.log(result);
|
|
35
33
|
console.log('Tests run successfully');
|
|
36
34
|
});
|
|
37
35
|
|
|
@@ -25,9 +25,9 @@ const options: loadtest.LoadTestOptions = {
|
|
|
25
25
|
},
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
loadtest.loadTest(options, (error,
|
|
28
|
+
loadtest.loadTest(options, (error, result) => {
|
|
29
29
|
if (error) {
|
|
30
30
|
return console.error(`Got an error: ${error}`)
|
|
31
31
|
}
|
|
32
|
-
console.log("Tests run successfully", {
|
|
32
|
+
console.log("Tests run successfully", {result})
|
|
33
33
|
})
|
package/test/all.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run package tests.
|
|
3
|
+
* (C) 2013 Alex Fernández.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import testing from 'testing'
|
|
7
|
+
import {test as testHrtimer} from './hrtimer.js'
|
|
8
|
+
import {test as testHeaders} from './headers.js'
|
|
9
|
+
import {test as testLatency} from './latency.js'
|
|
10
|
+
import {test as testHttpClient} from './httpClient.js'
|
|
11
|
+
import {test as testServer} from './testserver.js'
|
|
12
|
+
import {test as testRequestGenerator} from './request-generator.js'
|
|
13
|
+
import {test as testBodyGenerator} from './body-generator.js'
|
|
14
|
+
import {test as testLoadtest} from './loadtest.js'
|
|
15
|
+
import {test as testWebsocket} from './websocket.js'
|
|
16
|
+
import {test as integrationTest} from './integration.js'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run all module tests.
|
|
21
|
+
*/
|
|
22
|
+
function test() {
|
|
23
|
+
const tests = [
|
|
24
|
+
testHrtimer, testHeaders, testLatency, testHttpClient,
|
|
25
|
+
testServer, integrationTest, testLoadtest, testWebsocket,
|
|
26
|
+
testRequestGenerator, testBodyGenerator,
|
|
27
|
+
];
|
|
28
|
+
testing.run(tests, 4200);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test()
|
|
32
|
+
|
|
33
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import testing from 'testing'
|
|
2
|
+
import {loadTest} from '../lib/loadtest.js'
|
|
3
|
+
import {startServer} from '../lib/testserver.js'
|
|
4
|
+
|
|
5
|
+
const PORT = 10453;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function testBodyGenerator(callback) {
|
|
9
|
+
const server = startServer({port: PORT, quiet: true}, error => {
|
|
10
|
+
if (error) {
|
|
11
|
+
return callback('Could not start test server');
|
|
12
|
+
}
|
|
13
|
+
const options = {
|
|
14
|
+
url: 'http://localhost:' + PORT,
|
|
15
|
+
requestsPerSecond: 100,
|
|
16
|
+
maxRequests: 100,
|
|
17
|
+
concurrency: 10,
|
|
18
|
+
postFile: 'sample/post-file.js',
|
|
19
|
+
quiet: true,
|
|
20
|
+
}
|
|
21
|
+
loadTest(options, (error, result) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
console.error(error)
|
|
24
|
+
return callback(`Could not run load test with postFile: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
server.close(error => {
|
|
27
|
+
if (error) {
|
|
28
|
+
return callback('Could not close test server');
|
|
29
|
+
}
|
|
30
|
+
return callback(null, 'bodyGenerator succeeded: ' + JSON.stringify(result));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run all tests.
|
|
38
|
+
*/
|
|
39
|
+
export function test(callback) {
|
|
40
|
+
testing.run([testBodyGenerator], 4000, callback);
|
|
41
|
+
}
|
|
42
|
+
|