@loadmill/executer 0.1.34 → 0.1.38
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/dist/errors.d.ts +5 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction-combiner.d.ts +4 -2
- package/dist/extraction-combiner.js +96 -46
- package/dist/extraction-combiner.js.map +1 -1
- package/dist/mill-version.js +1 -1
- package/dist/post-script/ast-walker/index.js.map +1 -1
- package/dist/post-script/virtual-machine/vm2-virtual-machine.js +1 -1
- package/dist/post-script/virtual-machine/vm2-virtual-machine.js.map +1 -1
- package/dist/sampler.js +4 -3
- package/dist/sampler.js.map +1 -1
- package/dist/sequence.js +301 -134
- package/dist/sequence.js.map +1 -1
- package/dist/test-run-event-emitter.d.ts +11 -0
- package/dist/test-run-event-emitter.js +36 -0
- package/dist/test-run-event-emitter.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +29 -0
- package/dist/utils.js.map +1 -0
- package/dist/ws.d.ts +66 -0
- package/dist/ws.js +435 -0
- package/dist/ws.js.map +1 -0
- package/package.json +6 -6
- package/src/errors.ts +10 -0
- package/src/extraction-combiner.ts +15 -4
- package/src/mill-version.ts +1 -1
- package/src/post-script/ast-walker/index.ts +1 -1
- package/src/post-script/virtual-machine/vm2-virtual-machine.ts +1 -1
- package/src/sampler.ts +4 -3
- package/src/sequence.ts +141 -58
- package/src/single-runner.ts +3 -3
- package/src/test-run-event-emitter.ts +24 -0
- package/src/utils.ts +8 -0
- package/src/ws.ts +233 -0
- package/test/post-script-executor.spec.ts +24 -24
package/src/sequence.ts
CHANGED
|
@@ -10,7 +10,6 @@ import clamp from 'lodash/clamp';
|
|
|
10
10
|
import find from 'lodash/find';
|
|
11
11
|
import map from 'lodash/map';
|
|
12
12
|
import filter from 'lodash/filter';
|
|
13
|
-
import forEach from 'lodash/forEach';
|
|
14
13
|
|
|
15
14
|
import { ResolvedRequest } from './request-sequence-result';
|
|
16
15
|
import { Asserter } from './asserter';
|
|
@@ -52,8 +51,6 @@ const {
|
|
|
52
51
|
DEFAULT_REQUEST_DELAY,
|
|
53
52
|
} = confDefaults;
|
|
54
53
|
const {
|
|
55
|
-
MAX_API_REQUEST_BODY_LENGTH,
|
|
56
|
-
MAX_LOAD_REQUEST_BODY_LENGTH,
|
|
57
54
|
MAX_RESPONSE_BYTES,
|
|
58
55
|
MAX_RESPONSE_COLLECT,
|
|
59
56
|
MIN_REQUEST_DELAY,
|
|
@@ -71,9 +68,16 @@ import {
|
|
|
71
68
|
ALLOWED_RESPONSE_STATUSES,
|
|
72
69
|
LoopConf,
|
|
73
70
|
DEFAULT_REQUEST_TIMEOUT,
|
|
74
|
-
CachePenetrationModes
|
|
71
|
+
CachePenetrationModes,
|
|
72
|
+
RequestPostData
|
|
75
73
|
} from '@loadmill/core/dist/request';
|
|
76
74
|
|
|
75
|
+
import { testRunEventsEmitter } from './test-run-event-emitter';
|
|
76
|
+
import { WSRequest, WSRequestArguments, WSSequenceHandler } from './ws';
|
|
77
|
+
import { RequestFailuresError } from './errors';
|
|
78
|
+
import { getMaxRequestBodySize } from './utils';
|
|
79
|
+
import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
|
|
80
|
+
|
|
77
81
|
export const reqIdParamName = 'loadmill-request-id';
|
|
78
82
|
|
|
79
83
|
declare const Promise: typeof BluebirdPromise; // This is a hack for the loadmill-runner. I hate TypeScript.
|
|
@@ -102,12 +106,12 @@ export const sequence = {
|
|
|
102
106
|
httpAgent,
|
|
103
107
|
requests: LoadmillRequest[],
|
|
104
108
|
parameters: Parameters[],
|
|
105
|
-
domainsWhiteList: string[]
|
|
109
|
+
domainsWhiteList: string[],
|
|
106
110
|
) {
|
|
107
111
|
const sequenceExecutor = new SequenceExecutor(
|
|
108
112
|
httpAgent,
|
|
109
113
|
parameters,
|
|
110
|
-
domainsWhiteList
|
|
114
|
+
domainsWhiteList,
|
|
111
115
|
);
|
|
112
116
|
return sequenceExecutor.startAndPass(requests);
|
|
113
117
|
},
|
|
@@ -115,6 +119,7 @@ export const sequence = {
|
|
|
115
119
|
|
|
116
120
|
class SequenceExecutor {
|
|
117
121
|
parameters: SequenceExecutorParameters;
|
|
122
|
+
postScriptRunner: PostScriptRunner;
|
|
118
123
|
|
|
119
124
|
avgResTime = 0;
|
|
120
125
|
successfulHits = 0;
|
|
@@ -124,6 +129,7 @@ class SequenceExecutor {
|
|
|
124
129
|
resolvedRequests: ResolvedRequest[] = [];
|
|
125
130
|
keepaliveHTTPSAgent;
|
|
126
131
|
keepaliveHTTPAgent;
|
|
132
|
+
wsHandler: WSSequenceHandler;
|
|
127
133
|
|
|
128
134
|
constructor(
|
|
129
135
|
private httpAgent,
|
|
@@ -139,6 +145,8 @@ class SequenceExecutor {
|
|
|
139
145
|
timeout: SOCKET_TIMEOUT,
|
|
140
146
|
freeSocketTimeout: SOCKET_TIMEOUT / 2
|
|
141
147
|
});
|
|
148
|
+
this.postScriptRunner = new PostScriptRunner();
|
|
149
|
+
this.wsHandler = new WSSequenceHandler();
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
startAndPass(requests: LoadmillRequest[]) {
|
|
@@ -209,9 +217,11 @@ class SequenceExecutor {
|
|
|
209
217
|
log.info('Unexpected error info:', unexpectedError);
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
this.wsHandler.closeAllConnections();
|
|
212
221
|
throw error;
|
|
213
222
|
}
|
|
214
223
|
}
|
|
224
|
+
this.wsHandler.closeAllConnections();
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
shouldStop(request: LoadmillRequest) {
|
|
@@ -245,7 +255,11 @@ class SequenceExecutor {
|
|
|
245
255
|
reqIndex: number,
|
|
246
256
|
requests: LoadmillRequest[]
|
|
247
257
|
) {
|
|
248
|
-
const { skipBefore } = request;
|
|
258
|
+
const { skipBefore, disabled } = request;
|
|
259
|
+
|
|
260
|
+
if (disabled) {
|
|
261
|
+
return reqIndex + 1;
|
|
262
|
+
}
|
|
249
263
|
|
|
250
264
|
if (skipBefore) {
|
|
251
265
|
const { goTo, negate, condition } = skipBefore;
|
|
@@ -301,23 +315,27 @@ class SequenceExecutor {
|
|
|
301
315
|
let failedAssertionsHistogram, resTime;
|
|
302
316
|
let loopIteration = 0;
|
|
303
317
|
const maxIterations = getLoopIterations(requestConf.loop);
|
|
318
|
+
this.wsHandler.clearMessages();
|
|
304
319
|
|
|
305
320
|
while (loopIteration < maxIterations) {
|
|
306
321
|
loopIteration++;
|
|
307
322
|
const request = this.prepareRequest(requestConf, index);
|
|
308
323
|
|
|
309
324
|
const res = await this.sendRequest(request, index);
|
|
325
|
+
|
|
310
326
|
// Setting now to avoid possible user overwrite:
|
|
311
327
|
resTime = Number(this.parameters.__responseTime);
|
|
312
328
|
|
|
313
|
-
const { timeout } = requestConf;
|
|
329
|
+
const { timeout = DEFAULT_REQUEST_TIMEOUT } = requestConf;
|
|
314
330
|
if (resTime > timeout) {
|
|
315
331
|
throw new RequestFailuresError(
|
|
316
332
|
`Response received after timeout of ${timeout}ms exceeded`
|
|
317
333
|
);
|
|
318
334
|
}
|
|
319
335
|
|
|
320
|
-
|
|
336
|
+
res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
|
|
337
|
+
|
|
338
|
+
failedAssertionsHistogram = await this.processSuccessfulResponse(
|
|
321
339
|
index,
|
|
322
340
|
requestConf,
|
|
323
341
|
res
|
|
@@ -493,8 +511,18 @@ class SequenceExecutor {
|
|
|
493
511
|
}
|
|
494
512
|
|
|
495
513
|
prepareRequest(requestConf: LoadmillRequest, reqIndex: number) {
|
|
514
|
+
if (this.isWSRequest(requestConf) && !envUtils.isBrowser()) {
|
|
515
|
+
return this.prepareWSRequest(requestConf, reqIndex);
|
|
516
|
+
}
|
|
517
|
+
return this.prepareHttpRequest(requestConf, reqIndex);
|
|
518
|
+
}
|
|
519
|
+
private isWSRequest({ url }: LoadmillRequest) {
|
|
520
|
+
const resolvedUrl = this.resolve(url, (e) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
|
|
521
|
+
return resolvedUrl.startsWith('ws://') || resolvedUrl.startsWith('wss://');
|
|
522
|
+
}
|
|
523
|
+
private prepareHttpRequest(requestConf: LoadmillRequest, reqIndex: number) {
|
|
496
524
|
const urlObj = new URI(
|
|
497
|
-
resolveUrl(requestConf.url, this.parameters, (err) =>
|
|
525
|
+
resolveUrl(requestConf.url, this.parameters, (err: Error) =>
|
|
498
526
|
setParameterErrorHistogram(err, 'Failed to compute URL - ')
|
|
499
527
|
)
|
|
500
528
|
);
|
|
@@ -512,7 +540,7 @@ class SequenceExecutor {
|
|
|
512
540
|
|
|
513
541
|
const url = urlObj.toString();
|
|
514
542
|
this.resolvedRequests[reqIndex].url = url;
|
|
515
|
-
|
|
543
|
+
|
|
516
544
|
this.validateDomain(uriUtils.getDomain(url));
|
|
517
545
|
|
|
518
546
|
const method = requestConf.method.toLowerCase();
|
|
@@ -629,7 +657,7 @@ class SequenceExecutor {
|
|
|
629
657
|
// This makes superagent populate res.text regardless of the response's content type:
|
|
630
658
|
request.buffer();
|
|
631
659
|
|
|
632
|
-
// Otherwise we expose superagent and its version -
|
|
660
|
+
// Otherwise we expose superagent and its version -
|
|
633
661
|
// better have something more general (in case the user didnt set one):
|
|
634
662
|
const uaHeader = request.get('User-Agent');
|
|
635
663
|
if (!uaHeader || uaHeader.startsWith('node-superagent')) {
|
|
@@ -641,11 +669,71 @@ class SequenceExecutor {
|
|
|
641
669
|
request.header
|
|
642
670
|
);
|
|
643
671
|
|
|
644
|
-
request.expectedStatus = requestConf.expectedStatus;
|
|
672
|
+
request.expectedStatus = requestConf.expectedStatus || ALLOWED_RESPONSE_STATUSES.SUCCESS;
|
|
645
673
|
|
|
646
674
|
return request;
|
|
647
675
|
}
|
|
648
676
|
|
|
677
|
+
prepareWSRequest(requestConf: LoadmillRequest, reqIndex: number) {
|
|
678
|
+
const wsRequestArgs = this.resolveAndSetWSReqData(requestConf, reqIndex);
|
|
679
|
+
|
|
680
|
+
return new WSRequest(
|
|
681
|
+
wsRequestArgs,
|
|
682
|
+
this.wsHandler,
|
|
683
|
+
(e: Error) => this.setSingleFailure(reqIndex, 'Websocket error ' + e.message),
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
resolveAndSetWSReqData = (
|
|
688
|
+
{ url, headers, postData, timeout = DEFAULT_REQUEST_TIMEOUT, expectedStatus = 'SUCCESS' }: LoadmillRequest,
|
|
689
|
+
reqIndex: number
|
|
690
|
+
): WSRequestArguments => {
|
|
691
|
+
const preparedUrl = this.prepareWsUrl(url, reqIndex);
|
|
692
|
+
const preparedHeaders = this.prepareWsHeaders(headers, reqIndex);
|
|
693
|
+
const preparedMessage = this.prepareWsMessage(postData, reqIndex);
|
|
694
|
+
return {
|
|
695
|
+
expectedStatus,
|
|
696
|
+
headers: preparedHeaders,
|
|
697
|
+
message: preparedMessage,
|
|
698
|
+
timeout,
|
|
699
|
+
url: preparedUrl,
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
prepareWsUrl = (url: string, reqIndex: number) => {
|
|
704
|
+
const resolvedUrl = this.resolve(url, (e: Error) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
|
|
705
|
+
this.resolvedRequests[reqIndex].url = resolvedUrl;
|
|
706
|
+
return resolvedUrl;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
prepareWsHeaders = (headers: LoadmillHeaders[] | undefined, reqIndex: number) => {
|
|
710
|
+
const resolvedHeadersObj: LoadmillHeaders = {};
|
|
711
|
+
const resolvedHeaders: LoadmillHeaders[] = [];
|
|
712
|
+
if (headers && !isEmpty(headers)) {
|
|
713
|
+
resolvedHeaders.push(...this.resolveHeaders(headers));
|
|
714
|
+
resolvedHeaders.forEach(({ name, value }) => resolvedHeadersObj[name] = value);
|
|
715
|
+
}
|
|
716
|
+
this.resolvedRequests[reqIndex].headers = resolvedHeaders;
|
|
717
|
+
return resolvedHeadersObj;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
prepareWsMessage = (postData: RequestPostData | undefined, reqIndex: number) => {
|
|
721
|
+
let resolvedMessage: string = '';
|
|
722
|
+
if (postData) {
|
|
723
|
+
resolvedMessage = this.resolve(postData.text, (err: Error) =>
|
|
724
|
+
setParameterErrorHistogram(err, 'Failed to compute Websocket message - ')
|
|
725
|
+
);
|
|
726
|
+
if (resolvedMessage && resolvedMessage.length > getMaxRequestBodySize()) {
|
|
727
|
+
throw new RequestFailuresError('Websocket message size is too large');
|
|
728
|
+
}
|
|
729
|
+
this.resolvedRequests[reqIndex].postData = {
|
|
730
|
+
mimeType: postData.mimeType,
|
|
731
|
+
text: resolvedMessage,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return resolvedMessage;
|
|
735
|
+
};
|
|
736
|
+
|
|
649
737
|
checkProgressEvent = (
|
|
650
738
|
requestIndex: number,
|
|
651
739
|
request,
|
|
@@ -660,7 +748,7 @@ class SequenceExecutor {
|
|
|
660
748
|
};
|
|
661
749
|
|
|
662
750
|
private validateDomain(domain: string) {
|
|
663
|
-
if (isEmpty(domain)){
|
|
751
|
+
if (isEmpty(domain)) {
|
|
664
752
|
const message = 'HTTP request domain name is empty';
|
|
665
753
|
throw new RequestFailuresError(message, { [message]: 1 });
|
|
666
754
|
}
|
|
@@ -707,9 +795,9 @@ class SequenceExecutor {
|
|
|
707
795
|
)
|
|
708
796
|
);
|
|
709
797
|
|
|
710
|
-
processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
|
|
798
|
+
async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
|
|
711
799
|
// modifies parameters:
|
|
712
|
-
this.handleExtractions(requestConf, res);
|
|
800
|
+
await this.handleExtractions(requestConf, res);
|
|
713
801
|
|
|
714
802
|
if (!envUtils.isBrowser()) {
|
|
715
803
|
this.handlePostScript(requestConf, res);
|
|
@@ -737,33 +825,33 @@ class SequenceExecutor {
|
|
|
737
825
|
);
|
|
738
826
|
}
|
|
739
827
|
|
|
740
|
-
handleExtractions(requestConf: LoadmillRequest, res) {
|
|
741
|
-
requestConf.extract
|
|
742
|
-
this.extractInScope(res, extractions)
|
|
743
|
-
|
|
828
|
+
async handleExtractions(requestConf: LoadmillRequest, res) {
|
|
829
|
+
for (const extractions of (requestConf.extract || [])) {
|
|
830
|
+
await this.extractInScope(res, extractions);
|
|
831
|
+
}
|
|
744
832
|
}
|
|
745
833
|
|
|
746
|
-
extractInScope(res, extractions: Extractions) {
|
|
834
|
+
async extractInScope(res, extractions: Extractions) {
|
|
747
835
|
const contextParameters = Object.assign({}, this.parameters);
|
|
748
|
-
const extractionCombiner = new ExtractionCombiner(contextParameters, res);
|
|
836
|
+
const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
|
|
749
837
|
|
|
750
|
-
|
|
751
|
-
this.extract(name, extraction, extractionCombiner)
|
|
752
|
-
|
|
838
|
+
for (const [name, extraction] of Object.entries(extractions)) {
|
|
839
|
+
await this.extract(name, extraction, extractionCombiner);
|
|
840
|
+
}
|
|
753
841
|
}
|
|
754
842
|
|
|
755
|
-
extract(
|
|
843
|
+
async extract(
|
|
756
844
|
parameterName: string,
|
|
757
845
|
extraction: Extraction,
|
|
758
846
|
extractionCombiner: ExtractionCombiner
|
|
759
847
|
) {
|
|
760
848
|
log.trace('Parameter extraction start: ', { parameterName, extraction });
|
|
761
849
|
|
|
762
|
-
const combinedExtractor = extractionCombiner.combine(extraction);
|
|
850
|
+
const combinedExtractor = await extractionCombiner.combine(extraction);
|
|
763
851
|
let result;
|
|
764
852
|
|
|
765
853
|
try {
|
|
766
|
-
result = combinedExtractor();
|
|
854
|
+
result = await combinedExtractor();
|
|
767
855
|
} catch (error) {
|
|
768
856
|
const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
|
|
769
857
|
log.debug(genericMessage, error);
|
|
@@ -797,27 +885,34 @@ class SequenceExecutor {
|
|
|
797
885
|
}
|
|
798
886
|
|
|
799
887
|
handlePostScript({ postScript }: LoadmillRequest, { text = '' }) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
let $ = {};
|
|
803
|
-
try {
|
|
804
|
-
$ = JSON.parse(text);
|
|
805
|
-
} catch (e) {
|
|
806
|
-
log.debug('res text (body) cannot be JSON parsed', e.name, e.message);
|
|
807
|
-
}
|
|
888
|
+
if (postScript) {
|
|
889
|
+
testRunEventsEmitter.postScript.started();
|
|
808
890
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
Object.assign(this.parameters,
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
));
|
|
891
|
+
try {
|
|
892
|
+
const result = this.runPostScript(postScript, text);
|
|
893
|
+
Object.assign(this.parameters, result);
|
|
894
|
+
} catch (e) {
|
|
895
|
+
const message = ['Post Script', e.name + ':', e.message].join(' ');
|
|
896
|
+
throw new RequestFailuresError(message);
|
|
816
897
|
}
|
|
898
|
+
|
|
899
|
+
testRunEventsEmitter.postScript.finished();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private runPostScript(postScript: string, text: string) {
|
|
904
|
+
const staticContext = this.getStaticContext(text);
|
|
905
|
+
return this.postScriptRunner.run({ staticContext }, this.parameters, postScript);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private getStaticContext(text: string) {
|
|
909
|
+
let $ = {};
|
|
910
|
+
try {
|
|
911
|
+
$ = JSON.parse(text);
|
|
817
912
|
} catch (e) {
|
|
818
|
-
|
|
819
|
-
throw new RequestFailuresError(message);
|
|
913
|
+
log.debug('res text (body) cannot be JSON parsed', e.name, e.message);
|
|
820
914
|
}
|
|
915
|
+
return { $, __: parameterFunctionOperations };
|
|
821
916
|
}
|
|
822
917
|
|
|
823
918
|
handleAssertions(requestConf: LoadmillRequest) {
|
|
@@ -878,9 +973,6 @@ function isSimpleRequest(headers) {
|
|
|
878
973
|
);
|
|
879
974
|
}
|
|
880
975
|
|
|
881
|
-
const getMaxRequestBodySize = () =>
|
|
882
|
-
envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
|
|
883
|
-
|
|
884
976
|
const getLoopIterations = (LoopConf?: LoopConf) => {
|
|
885
977
|
const declared = (LoopConf && LoopConf.iterations) || 1;
|
|
886
978
|
return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
|
|
@@ -898,7 +990,7 @@ const extendResponseHeaders = (headers, redirectHeaders) => {
|
|
|
898
990
|
|
|
899
991
|
const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
|
|
900
992
|
if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
|
|
901
|
-
return 200 <= status && status < 400;
|
|
993
|
+
return (200 <= status && status < 400) || status === 101;
|
|
902
994
|
}
|
|
903
995
|
else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
|
|
904
996
|
log.debug('user asked to fail this request', url);
|
|
@@ -934,12 +1026,3 @@ const setTCPReuse = (request, agent, sslAgent) => {
|
|
|
934
1026
|
};
|
|
935
1027
|
|
|
936
1028
|
};
|
|
937
|
-
|
|
938
|
-
class RequestFailuresError extends Error {
|
|
939
|
-
constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
|
|
940
|
-
super(message);
|
|
941
|
-
|
|
942
|
-
// Workaround suggested in: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
943
|
-
Object.setPrototypeOf(this, RequestFailuresError.prototype);
|
|
944
|
-
}
|
|
945
|
-
}
|
package/src/single-runner.ts
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
} from './request-sequence-result';
|
|
12
12
|
|
|
13
13
|
export async function runSingleIterationAndMergeResolved(
|
|
14
|
-
conf: ExecutableConf
|
|
14
|
+
conf: ExecutableConf,
|
|
15
15
|
): Promise<ExtendedSequenceResult> {
|
|
16
16
|
const result = await runSingleIteration(conf);
|
|
17
17
|
return extendSequenceResult(result, conf.requests);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function runSingleIteration(
|
|
21
|
-
conf: ExecutableConf
|
|
21
|
+
conf: ExecutableConf,
|
|
22
22
|
): Promise<RequestSequenceResult> {
|
|
23
23
|
let httpAgent;
|
|
24
24
|
if (conf.useCookies && !envUtils.isBrowser()) {
|
|
@@ -35,7 +35,7 @@ export async function runSingleIteration(
|
|
|
35
35
|
httpAgent,
|
|
36
36
|
extendRequests(conf),
|
|
37
37
|
conf.parameters,
|
|
38
|
-
conf.domainsWhiteList
|
|
38
|
+
conf.domainsWhiteList,
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import log from '@loadmill/universal/dist/log';
|
|
2
|
+
|
|
3
|
+
class TestRunEventsEmitter {
|
|
4
|
+
postScript: EventsEmitter;
|
|
5
|
+
constructor() {
|
|
6
|
+
const warningMsg = ' event emitter callback not assigned';
|
|
7
|
+
this.postScript = {
|
|
8
|
+
started: () => log.warn('postScript' + warningMsg),
|
|
9
|
+
finished: () => log.warn('postScript' + warningMsg),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
setPostScriptHandlers = (socket: SocketIOClient.Socket, token: string) => {
|
|
14
|
+
this.postScript.started = () => socket.emit(`postscript-started:${token}`);
|
|
15
|
+
this.postScript.finished = () => socket.emit(`postscript-finished:${token}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const testRunEventsEmitter = new TestRunEventsEmitter();
|
|
20
|
+
export { testRunEventsEmitter };
|
|
21
|
+
|
|
22
|
+
type EventsEmitter = {
|
|
23
|
+
[funcName: string]: () => void | SocketIOClient.Socket;
|
|
24
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_API_REQUEST_BODY_LENGTH,
|
|
3
|
+
MAX_LOAD_REQUEST_BODY_LENGTH
|
|
4
|
+
} from '@loadmill/core/dist/conf/extrema';
|
|
5
|
+
import * as envUtils from '@loadmill/universal/dist/env-utils';
|
|
6
|
+
|
|
7
|
+
export const getMaxRequestBodySize = () =>
|
|
8
|
+
envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import WebSocket from 'ws'; // todo should be picked with other option (socketio or non in browser)
|
|
2
|
+
import { delay } from '@loadmill/universal/dist/promise-utils';
|
|
3
|
+
import log from '@loadmill/universal/dist/log';
|
|
4
|
+
import { HttpResponseStatus, LoadmillHeaders } from '@loadmill/core/dist/request';
|
|
5
|
+
import { RequestFailuresError } from './errors';
|
|
6
|
+
|
|
7
|
+
enum WSState {
|
|
8
|
+
CONNECTING,
|
|
9
|
+
OPEN,
|
|
10
|
+
CLOSING,
|
|
11
|
+
CLOSED,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SWITCHING_PROTOCOLS = 'Switching Protocols';
|
|
15
|
+
const WS_CONNECTION_TIMEOUT_MS = 10000;
|
|
16
|
+
|
|
17
|
+
export class WSRequest {
|
|
18
|
+
private hasErrorOccured?: boolean; // need this otherwise we have race condition between verifyConnectedAndOpen and onError
|
|
19
|
+
private ws: WebSocket;
|
|
20
|
+
public url: string; // need this for isExpectedStatus function
|
|
21
|
+
public expectedStatus: HttpResponseStatus; // need this for isExpectedStatus function
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly wsRequestArgs: WSRequestArguments,
|
|
25
|
+
private readonly wsHandler: WSSequenceHandler,
|
|
26
|
+
private readonly onError: (e: Error) => void,
|
|
27
|
+
) {
|
|
28
|
+
this.url = wsRequestArgs.url;
|
|
29
|
+
this.expectedStatus = wsRequestArgs.expectedStatus;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This function is executed when we are ready to send the ws request
|
|
34
|
+
* @param cb This callback is being executed after we successfully connected to ws and sent a ws message if there was any
|
|
35
|
+
*/
|
|
36
|
+
async ok(cb: (response: WSResponse) => boolean) {
|
|
37
|
+
const response = {
|
|
38
|
+
status: 101,
|
|
39
|
+
res: {
|
|
40
|
+
statusMessage: SWITCHING_PROTOCOLS,
|
|
41
|
+
},
|
|
42
|
+
header: {},
|
|
43
|
+
req: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
|
|
47
|
+
|
|
48
|
+
log.debug(`Connection state ${getConnectionState(existingWS)}`);
|
|
49
|
+
if (!existingWS || (existingWS && existingWS.readyState !== WSState.OPEN)) {
|
|
50
|
+
await this.addConnection(response);
|
|
51
|
+
} else {
|
|
52
|
+
this.ws = existingWS;
|
|
53
|
+
log.debug('Reusing existing ws connection', this.ws.url);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
this.sendMessage();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
log.error('Failed to send a ws message', e);
|
|
60
|
+
}
|
|
61
|
+
cb(response);
|
|
62
|
+
return { };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async addConnection(response: WSResponse) {
|
|
66
|
+
this.ws = new WebSocket(this.wsRequestArgs.url, undefined, { headers: this.wsRequestArgs.headers });
|
|
67
|
+
this.addEventListeners(response);
|
|
68
|
+
!this.hasErrorOccured && await this.verifyOpen();
|
|
69
|
+
this.wsHandler.storeConnection(this.wsRequestArgs.url, this.ws);
|
|
70
|
+
// todo add some debug details for the user to know we created a new connection in this request
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async addEventListeners(response: WSResponse) {
|
|
74
|
+
this.addOnErrorListener();
|
|
75
|
+
this.addOnUpgradeListener(response);
|
|
76
|
+
this.addOnOpenListener();
|
|
77
|
+
this.addOnMessageListener();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private addOnErrorListener() {
|
|
81
|
+
this.ws.on('error', (e) => {
|
|
82
|
+
try {
|
|
83
|
+
log.debug('inside on error', e);
|
|
84
|
+
this.onError(e);
|
|
85
|
+
this.hasErrorOccured = true;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
log.warn('Failed to handle a ws error', e);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
log.debug('closing ws');
|
|
91
|
+
this.ws.close();
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log.warn('Failed to close a ws connection', e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private addOnUpgradeListener(response: WSResponse) {
|
|
99
|
+
this.ws.on('upgrade', (event) => {
|
|
100
|
+
try {
|
|
101
|
+
const { statusCode, statusMessage, headers } = event;
|
|
102
|
+
log.debug('inside upgrade event', { statusCode, statusMessage, headers });
|
|
103
|
+
response.status = statusCode || 101;
|
|
104
|
+
response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
|
|
105
|
+
response.header = headers as LoadmillHeaders;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
log.debug('upgrade event err', e);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private addOnOpenListener() {
|
|
113
|
+
this.ws.on('open', () => {
|
|
114
|
+
log.debug('inside on open, currently doing nothing here.');
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private addOnMessageListener() {
|
|
119
|
+
this.ws.on('message', (message: string) => {
|
|
120
|
+
try {
|
|
121
|
+
log.debug('got incoming message', message);
|
|
122
|
+
this.wsHandler.addMessage(message);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
log.debug('error getting message', e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async verifyOpen() {
|
|
130
|
+
const readyState = await this.waitForConnectedState(this.wsRequestArgs.timeout);
|
|
131
|
+
log.debug(`Connection state ${WSState[readyState]}`);
|
|
132
|
+
if (readyState !== WebSocket.OPEN && !this.hasErrorOccured) {
|
|
133
|
+
log.error('Could not connect to WebSocket address', { connectionState: WSState[readyState], url: this.ws.url });
|
|
134
|
+
throw new RequestFailuresError('Could not connect to WebSocket address');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Waiting for WS connection to go out of CONNECTING state
|
|
140
|
+
* @param socket The ws connection object
|
|
141
|
+
* @param timeout The timeout in milliseconds for the waiting procedure
|
|
142
|
+
* @returns WSState
|
|
143
|
+
*/
|
|
144
|
+
waitForConnectedState = async (timeout: number = WS_CONNECTION_TIMEOUT_MS): Promise<WSState> => {
|
|
145
|
+
const WS_CONNECTION_INTERVAL_MS = 100;
|
|
146
|
+
if (this.ws.readyState !== WebSocket.CONNECTING) {
|
|
147
|
+
return this.ws.readyState;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (this.ws.readyState === WebSocket.CONNECTING && i < maxIterations) {
|
|
153
|
+
await delay(WS_CONNECTION_INTERVAL_MS);
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
return this.ws.readyState;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
private sendMessage() {
|
|
161
|
+
log.debug('about to send message', { readyState: WSState[this.ws.readyState] } );
|
|
162
|
+
if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
|
|
163
|
+
log.debug('sending message', this.wsRequestArgs.message);
|
|
164
|
+
this.ws.send(this.wsRequestArgs.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// need this because the SequenceExecutor expectes a request obj with on func prop
|
|
169
|
+
on(_eventName: string, _cb: Function) {
|
|
170
|
+
//do nothing
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class WSSequenceHandler {
|
|
175
|
+
private _connections: { [url: string]: WebSocket };
|
|
176
|
+
messages: string[];
|
|
177
|
+
constructor() {
|
|
178
|
+
this._connections = {};
|
|
179
|
+
this.clearMessages();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getConnection(url: string): WebSocket | undefined {
|
|
183
|
+
return this._connections[url];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
storeConnection(url: string, wsConnection: WebSocket) {
|
|
187
|
+
this._connections[url] = wsConnection;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async closeAllConnections() {
|
|
191
|
+
try {
|
|
192
|
+
log.debug('Closing all ws connections');
|
|
193
|
+
for (const connection of Object.values(this._connections)) {
|
|
194
|
+
connection.close();
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
log.warn('Failed to close all connections', e, { connections: this._connections });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getMessages() {
|
|
202
|
+
return this.messages;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
addMessage(message: string) {
|
|
206
|
+
this.messages.push(message);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
clearMessages() {
|
|
210
|
+
this.messages = [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getConnectionState(existingWS?: WebSocket) {
|
|
215
|
+
return existingWS ? WSState[existingWS.readyState] : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type WSRequestArguments = {
|
|
219
|
+
expectedStatus: HttpResponseStatus;
|
|
220
|
+
headers: LoadmillHeaders
|
|
221
|
+
message: string;
|
|
222
|
+
timeout: number;
|
|
223
|
+
url: string;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export type WSResponse = {
|
|
227
|
+
status: number;
|
|
228
|
+
res: {
|
|
229
|
+
statusMessage: string;
|
|
230
|
+
};
|
|
231
|
+
header: LoadmillHeaders;
|
|
232
|
+
req: {};
|
|
233
|
+
}
|