@depup/artillery 2.0.30-depup.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/README.md +63 -0
- package/bin/run +29 -0
- package/bin/run.cmd +3 -0
- package/changes.json +138 -0
- package/console-reporter.js +1 -0
- package/lib/artillery-global.js +33 -0
- package/lib/cli/banner.js +8 -0
- package/lib/cli/common-flags.js +80 -0
- package/lib/cli/hooks/version.js +20 -0
- package/lib/cmds/dino.js +109 -0
- package/lib/cmds/quick.js +122 -0
- package/lib/cmds/report.js +34 -0
- package/lib/cmds/run-aci.js +91 -0
- package/lib/cmds/run-fargate.js +192 -0
- package/lib/cmds/run-lambda.js +96 -0
- package/lib/cmds/run.js +671 -0
- package/lib/console-capture.js +92 -0
- package/lib/console-reporter.js +438 -0
- package/lib/create-bom/built-in-plugins.js +12 -0
- package/lib/create-bom/create-bom.js +301 -0
- package/lib/dispatcher.js +9 -0
- package/lib/dist.js +222 -0
- package/lib/index.js +5 -0
- package/lib/launch-platform.js +439 -0
- package/lib/load-plugins.js +113 -0
- package/lib/platform/aws/aws-cloudwatch.js +106 -0
- package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
- package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
- package/lib/platform/aws/aws-get-account-id.js +26 -0
- package/lib/platform/aws/aws-get-bucket-region.js +18 -0
- package/lib/platform/aws/aws-get-credentials.js +28 -0
- package/lib/platform/aws/aws-get-default-region.js +26 -0
- package/lib/platform/aws/aws-whoami.js +15 -0
- package/lib/platform/aws/constants.js +7 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
- package/lib/platform/aws-ecs/ecs.js +247 -0
- package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
- package/lib/platform/aws-ecs/legacy/bom.js +528 -0
- package/lib/platform/aws-ecs/legacy/constants.js +27 -0
- package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
- package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
- package/lib/platform/aws-ecs/legacy/errors.js +34 -0
- package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
- package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
- package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
- package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
- package/lib/platform/aws-ecs/legacy/tags.js +22 -0
- package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
- package/lib/platform/aws-ecs/legacy/time.js +67 -0
- package/lib/platform/aws-ecs/legacy/util.js +97 -0
- package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
- package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
- package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
- package/lib/platform/aws-lambda/dependencies.js +130 -0
- package/lib/platform/aws-lambda/index.js +734 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
- package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
- package/lib/platform/aws-lambda/prices.js +29 -0
- package/lib/platform/az/aci.js +694 -0
- package/lib/platform/az/aqs-queue-consumer.js +88 -0
- package/lib/platform/az/regions.js +52 -0
- package/lib/platform/cloud/api.js +72 -0
- package/lib/platform/cloud/cloud.js +448 -0
- package/lib/platform/cloud/http-client.js +19 -0
- package/lib/platform/local/artillery-worker-local.js +154 -0
- package/lib/platform/local/index.js +174 -0
- package/lib/platform/local/worker.js +261 -0
- package/lib/platform/worker-states.js +13 -0
- package/lib/queue-consumer/index.js +56 -0
- package/lib/stash.js +41 -0
- package/lib/telemetry.js +78 -0
- package/lib/util/await-on-ee.js +24 -0
- package/lib/util/generate-id.js +9 -0
- package/lib/util/parse-tag-string.js +21 -0
- package/lib/util/prepare-test-execution-plan.js +216 -0
- package/lib/util/sleep.js +7 -0
- package/lib/util/validate-script.js +132 -0
- package/lib/util.js +294 -0
- package/lib/utils-config.js +31 -0
- package/package.json +323 -0
- package/types.d.ts +317 -0
- package/util.js +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright (c) Artillery Software Inc.
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
//
|
|
4
|
+
// Non-evaluation use of Artillery on Azure requires a commercial license
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
const EventEmitter = require('eventemitter3');
|
|
8
|
+
|
|
9
|
+
const { QueueClient } = require('@azure/storage-queue');
|
|
10
|
+
const { DefaultAzureCredential } = require('@azure/identity');
|
|
11
|
+
|
|
12
|
+
const debug = require('debug')('platform:azure-aci');
|
|
13
|
+
|
|
14
|
+
class AzureQueueConsumer extends EventEmitter {
|
|
15
|
+
constructor(
|
|
16
|
+
opts = { poolSize: 30 },
|
|
17
|
+
{
|
|
18
|
+
queueUrl,
|
|
19
|
+
pollIntervalMsec = 5000,
|
|
20
|
+
visibilityTimeout = 60,
|
|
21
|
+
batchSize = 32,
|
|
22
|
+
handleMessage
|
|
23
|
+
}
|
|
24
|
+
) {
|
|
25
|
+
super();
|
|
26
|
+
this.queueUrl = queueUrl;
|
|
27
|
+
this.batchSize = batchSize;
|
|
28
|
+
this.visibilityTimeout = visibilityTimeout;
|
|
29
|
+
this.handleMessage = handleMessage;
|
|
30
|
+
this.pollIntervalMsec = pollIntervalMsec;
|
|
31
|
+
|
|
32
|
+
this.poolSize = opts.poolSize;
|
|
33
|
+
|
|
34
|
+
this.consumers = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start() {
|
|
38
|
+
const credential = new DefaultAzureCredential();
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
41
|
+
debug('Creating consumer in pool', i);
|
|
42
|
+
const queueClient = new QueueClient(this.queueUrl, credential);
|
|
43
|
+
const pollInterval = setInterval(async () => {
|
|
44
|
+
const messages = await queueClient.receiveMessages({
|
|
45
|
+
numberOfMessages: this.batchSize,
|
|
46
|
+
visibilityTimeout: this.visibilityTimeout
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// TODO: Handle errors - no auth, no queue, network etc
|
|
50
|
+
|
|
51
|
+
for (const messageItem of messages.receivedMessageItems) {
|
|
52
|
+
const message = {
|
|
53
|
+
Body: messageItem.messageText
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let processed = false;
|
|
57
|
+
try {
|
|
58
|
+
await this.handleMessage(message);
|
|
59
|
+
processed = true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.log(err);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (processed) {
|
|
65
|
+
try {
|
|
66
|
+
await queueClient.deleteMessage(
|
|
67
|
+
messageItem.messageId,
|
|
68
|
+
messageItem.popReceipt
|
|
69
|
+
);
|
|
70
|
+
} catch (_err) {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, this.pollIntervalMsec);
|
|
74
|
+
|
|
75
|
+
this.consumers.push(pollInterval);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stop() {
|
|
80
|
+
for (const interval of this.consumers) {
|
|
81
|
+
clearInterval(interval);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO: events: error, empty
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { QueueConsumer: AzureQueueConsumer };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const regionNames = [
|
|
2
|
+
'australiacentral',
|
|
3
|
+
'australiacentral2',
|
|
4
|
+
'australiaeast',
|
|
5
|
+
'australiasoutheast',
|
|
6
|
+
'brazilsouth',
|
|
7
|
+
'canadacentral',
|
|
8
|
+
'canadaeast',
|
|
9
|
+
'centralindia',
|
|
10
|
+
'centralus',
|
|
11
|
+
'eastasia',
|
|
12
|
+
'eastus',
|
|
13
|
+
'eastus2',
|
|
14
|
+
'francecentral',
|
|
15
|
+
'francesouth',
|
|
16
|
+
'germanynorth',
|
|
17
|
+
'germanywestcentral',
|
|
18
|
+
'israelcentral',
|
|
19
|
+
'italynorth',
|
|
20
|
+
'japaneast',
|
|
21
|
+
'japanwest',
|
|
22
|
+
'jioindiawest',
|
|
23
|
+
'koreacentral',
|
|
24
|
+
'koreasouth',
|
|
25
|
+
'mexicocentral',
|
|
26
|
+
'northcentralus',
|
|
27
|
+
'northeurope',
|
|
28
|
+
'norwayeast',
|
|
29
|
+
'norwaywest',
|
|
30
|
+
'polandcentral',
|
|
31
|
+
'qatarcentral',
|
|
32
|
+
'southafricanorth',
|
|
33
|
+
'southafricawest',
|
|
34
|
+
'southcentralus',
|
|
35
|
+
'southeastasia',
|
|
36
|
+
'southindia',
|
|
37
|
+
'spaincentral',
|
|
38
|
+
'swedencentral',
|
|
39
|
+
'switzerlandnorth',
|
|
40
|
+
'switzerlandwest',
|
|
41
|
+
'uaecentral',
|
|
42
|
+
'uaenorth',
|
|
43
|
+
'uksouth',
|
|
44
|
+
'ukwest',
|
|
45
|
+
'westcentralus',
|
|
46
|
+
'westeurope',
|
|
47
|
+
'westindia',
|
|
48
|
+
'westus',
|
|
49
|
+
'westus2'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
module.exports = { regionNames };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { cloudHttpClient: request } = require('./http-client');
|
|
2
|
+
|
|
3
|
+
class Client {
|
|
4
|
+
constructor({ apiKey, baseUrl }) {
|
|
5
|
+
this.apiKey = apiKey || process.env.ARTILLERY_CLOUD_API_KEY;
|
|
6
|
+
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
const err = new Error();
|
|
9
|
+
err.name = 'CloudAPIKeyMissing';
|
|
10
|
+
throw err;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.baseUrl =
|
|
14
|
+
baseUrl ||
|
|
15
|
+
process.env.ARTILLERY_CLOUD_ENDPOINT ||
|
|
16
|
+
'https://app.artillery.io';
|
|
17
|
+
|
|
18
|
+
this.whoamiEndpoint = `${this.baseUrl}/api/user/whoami`;
|
|
19
|
+
this.stashDetailsEndpoint = `${this.baseUrl}/api/stash`;
|
|
20
|
+
|
|
21
|
+
this.defaultHeaders = {
|
|
22
|
+
'x-auth-token': this.apiKey
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async whoami() {
|
|
27
|
+
const res = await request.get(this.whoamiEndpoint, {
|
|
28
|
+
headers: this.defaultHeaders
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const body = JSON.parse(res.body);
|
|
32
|
+
this.orgId = body.activeOrg;
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getStashDetails({ orgId }) {
|
|
37
|
+
const currentOrgId = orgId || this.orgId;
|
|
38
|
+
|
|
39
|
+
const res = await request.get(
|
|
40
|
+
`${this.baseUrl}/api/org/${currentOrgId}/stash`,
|
|
41
|
+
{
|
|
42
|
+
headers: this.defaultHeaders
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (res.statusCode === 200) {
|
|
47
|
+
let body = {};
|
|
48
|
+
try {
|
|
49
|
+
body = JSON.parse(res.body);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(err);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (body.url && body.token) {
|
|
56
|
+
return { url: body.url, token: body.token };
|
|
57
|
+
} else {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createClient(opts) {
|
|
67
|
+
return new Client(opts);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
createClient
|
|
72
|
+
};
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const debug = require('debug')('cloud');
|
|
8
|
+
const { cloudHttpClient: request } = require('./http-client');
|
|
9
|
+
const awaitOnEE = require('../../util/await-on-ee');
|
|
10
|
+
const sleep = require('../../util/sleep');
|
|
11
|
+
const util = require('node:util');
|
|
12
|
+
const chokidar = require('chokidar');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { isCI, name: ciName, GITHUB_ACTIONS } = require('ci-info');
|
|
16
|
+
|
|
17
|
+
class ArtilleryCloudPlugin {
|
|
18
|
+
constructor(_script, _events, { flags }) {
|
|
19
|
+
this.enabled = false;
|
|
20
|
+
|
|
21
|
+
const isInteractiveUse = typeof flags.record !== 'undefined';
|
|
22
|
+
const enabledInCloudWorker =
|
|
23
|
+
typeof process.env.WORKER_ID !== 'undefined' &&
|
|
24
|
+
typeof process.env.ARTILLERY_CLOUD_API_KEY !== 'undefined';
|
|
25
|
+
|
|
26
|
+
if (!isInteractiveUse && !enabledInCloudWorker) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.enabled = true;
|
|
31
|
+
|
|
32
|
+
this.apiKey = flags.key || process.env.ARTILLERY_CLOUD_API_KEY;
|
|
33
|
+
|
|
34
|
+
this.baseUrl =
|
|
35
|
+
process.env.ARTILLERY_CLOUD_ENDPOINT || 'https://app.artillery.io';
|
|
36
|
+
this.eventsEndpoint = `${this.baseUrl}/api/events`;
|
|
37
|
+
this.whoamiEndpoint = `${this.baseUrl}/api/user/whoami`;
|
|
38
|
+
this.getAssetUploadUrls = `${this.baseUrl}/api/asset-upload-urls`;
|
|
39
|
+
this.pingEndpoint = `${this.baseUrl}/api/ping`;
|
|
40
|
+
|
|
41
|
+
this.defaultHeaders = {
|
|
42
|
+
'x-auth-token': this.apiKey
|
|
43
|
+
};
|
|
44
|
+
this.unprocessedLogsCounter = 0;
|
|
45
|
+
this.cancellationRequestedBy = '';
|
|
46
|
+
|
|
47
|
+
let testEndInfo = {};
|
|
48
|
+
|
|
49
|
+
// This value is available in cloud workers only. With interactive use, it'll be set
|
|
50
|
+
// in the test:init event handler.
|
|
51
|
+
this.testRunId = process.env.ARTILLERY_TEST_RUN_ID;
|
|
52
|
+
|
|
53
|
+
if (isInteractiveUse) {
|
|
54
|
+
global.artillery.globalEvents.on('test:init', async (testInfo) => {
|
|
55
|
+
debug('test:init', testInfo);
|
|
56
|
+
|
|
57
|
+
this.testRunId = testInfo.testRunId;
|
|
58
|
+
|
|
59
|
+
const testRunUrl = `${this.baseUrl}/${this.orgId}/load-tests/${global.artillery.testRunId}`;
|
|
60
|
+
testEndInfo.testRunUrl = testRunUrl;
|
|
61
|
+
|
|
62
|
+
this.getLoadTestEndpoint = `${this.baseUrl}/api/load-tests/${this.testRunId}/status`;
|
|
63
|
+
|
|
64
|
+
let ciURL = null;
|
|
65
|
+
if (isCI && GITHUB_ACTIONS) {
|
|
66
|
+
const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } =
|
|
67
|
+
process.env;
|
|
68
|
+
if (GITHUB_SERVER_URL && GITHUB_REPOSITORY && GITHUB_RUN_ID) {
|
|
69
|
+
ciURL = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const metadata = Object.assign({}, testInfo.metadata, {
|
|
74
|
+
isCI,
|
|
75
|
+
ciName,
|
|
76
|
+
ciURL
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await this._event('testrun:init', {
|
|
80
|
+
metadata: metadata
|
|
81
|
+
});
|
|
82
|
+
this.setGetLoadTestInterval = this.setGetStatusInterval();
|
|
83
|
+
|
|
84
|
+
if (typeof testInfo.flags.note !== 'undefined') {
|
|
85
|
+
await this._event('testrun:addnote', { text: testInfo.flags.note });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.uploading = 0;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
global.artillery.globalEvents.on('phaseStarted', async (phase) => {
|
|
92
|
+
await this._event('testrun:event', {
|
|
93
|
+
eventName: 'phaseStarted',
|
|
94
|
+
eventAttributes: phase
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
global.artillery.globalEvents.on('phaseCompleted', async (phase) => {
|
|
99
|
+
await this._event('testrun:event', {
|
|
100
|
+
eventName: 'phaseCompleted',
|
|
101
|
+
eventAttributes: phase
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
global.artillery.globalEvents.on('stats', async (report) => {
|
|
106
|
+
debug('stats', new Date());
|
|
107
|
+
const ts = Number(report.period);
|
|
108
|
+
await this._event('testrun:metrics', { report, ts });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
global.artillery.globalEvents.on('done', async (report) => {
|
|
112
|
+
debug('done');
|
|
113
|
+
debug(
|
|
114
|
+
'testrun:aggregatereport: payload size:',
|
|
115
|
+
JSON.stringify(report).length
|
|
116
|
+
);
|
|
117
|
+
await this._event('testrun:aggregatereport', { aggregate: report });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
global.artillery.globalEvents.on('checks', async (checks) => {
|
|
121
|
+
debug('checks');
|
|
122
|
+
await this._event('testrun:checks', { checks });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
global.artillery.globalEvents.on('logLines', async (lines, ts) => {
|
|
126
|
+
debug('logLines event', ts);
|
|
127
|
+
this.unprocessedLogsCounter += 1;
|
|
128
|
+
|
|
129
|
+
let text = '';
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
JSON.stringify(lines);
|
|
133
|
+
} catch (stringifyErr) {
|
|
134
|
+
console.log('Could not serialize console log');
|
|
135
|
+
console.log(stringifyErr);
|
|
136
|
+
}
|
|
137
|
+
for (const args of lines) {
|
|
138
|
+
text += util.format(...Object.keys(args).map((k) => args[k])) + '\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this._event('testrun:textlog', { lines: text, ts });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
debug(err);
|
|
145
|
+
} finally {
|
|
146
|
+
this.unprocessedLogsCounter -= 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
debug('last 100 characters:');
|
|
150
|
+
debug(text.slice(text.length - 100, text.length));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
global.artillery.globalEvents.on('metadata', async (metadata) => {
|
|
154
|
+
await this._event('testrun:addmetadata', {
|
|
155
|
+
metadata
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
} // isInteractiveUse
|
|
159
|
+
|
|
160
|
+
global.artillery.ext({
|
|
161
|
+
ext: 'beforeExit',
|
|
162
|
+
method: async ({ testInfo, report }) => {
|
|
163
|
+
debug('beforeExit');
|
|
164
|
+
testEndInfo = {
|
|
165
|
+
...testEndInfo,
|
|
166
|
+
...testInfo,
|
|
167
|
+
report
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Send test end events just before the CLI shuts down. This ensures that all console
|
|
173
|
+
// output has been captured and sent to the dashboard.
|
|
174
|
+
global.artillery.ext({
|
|
175
|
+
ext: 'onShutdown',
|
|
176
|
+
method: async (opts) => {
|
|
177
|
+
if (!this.enabled || this.off) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isInteractiveUse) {
|
|
182
|
+
clearInterval(this.setGetLoadTestInterval);
|
|
183
|
+
|
|
184
|
+
// Wait for the last logLines events to be processed, as they can sometimes finish processing after shutdown has finished
|
|
185
|
+
await awaitOnEE(
|
|
186
|
+
global.artillery.globalEvents,
|
|
187
|
+
'logLines',
|
|
188
|
+
200,
|
|
189
|
+
1 * 1000 //wait at most 1 second for a final log lines event emitter to be fired
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await this.waitOnUnprocessedLogs(5 * 60 * 1000); //just waiting for ee is not enough, as the api call takes time
|
|
194
|
+
|
|
195
|
+
if (isInteractiveUse) {
|
|
196
|
+
await this._event('testrun:end', {
|
|
197
|
+
ts: testEndInfo.endTime,
|
|
198
|
+
exitCode: global.artillery.suggestedExitCode || opts.exitCode,
|
|
199
|
+
isEarlyStop: !!opts.earlyStop,
|
|
200
|
+
report: testEndInfo.report
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log('\n');
|
|
204
|
+
if (this.cancellationRequestedBy) {
|
|
205
|
+
console.log(`Test run stopped by ${this.cancellationRequestedBy}.`);
|
|
206
|
+
}
|
|
207
|
+
console.log(`Run URL: ${testEndInfo.testRunUrl}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async init() {
|
|
214
|
+
if (!this.apiKey) {
|
|
215
|
+
const err = new Error();
|
|
216
|
+
err.name = 'CloudAPIKeyMissing';
|
|
217
|
+
this.off = true;
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let res;
|
|
222
|
+
let body;
|
|
223
|
+
try {
|
|
224
|
+
res = await request.get(this.whoamiEndpoint, {
|
|
225
|
+
headers: this.defaultHeaders,
|
|
226
|
+
retry: { limit: 0 }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
body = JSON.parse(res.body);
|
|
230
|
+
debug(res.body);
|
|
231
|
+
this.orgId = body.activeOrg;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
this.off = true;
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (res.statusCode === 401) {
|
|
238
|
+
const err = new Error();
|
|
239
|
+
err.name = 'APIKeyUnauthorized';
|
|
240
|
+
this.off = true;
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let postSucceeded = false;
|
|
245
|
+
try {
|
|
246
|
+
res = await request.post(this.pingEndpoint, {
|
|
247
|
+
headers: this.defaultHeaders
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (res.statusCode === 200) {
|
|
251
|
+
postSucceeded = true;
|
|
252
|
+
}
|
|
253
|
+
} catch (_err) {
|
|
254
|
+
this.off = true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!postSucceeded) {
|
|
258
|
+
const err = new Error();
|
|
259
|
+
err.name = 'PingFailed';
|
|
260
|
+
this.off = true;
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log('Artillery Cloud reporting is configured for this test run');
|
|
265
|
+
console.log(
|
|
266
|
+
`Run URL: ${this.baseUrl}/${this.orgId}/load-tests/${global.artillery.testRunId}`
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
this.user = {
|
|
270
|
+
id: body.id,
|
|
271
|
+
email: body.email
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const outputDir =
|
|
275
|
+
process.env.PLAYWRIGHT_TRACING_OUTPUT_DIR ||
|
|
276
|
+
`/tmp/${global.artillery.testRunId}/`;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
280
|
+
} catch (_err) {}
|
|
281
|
+
|
|
282
|
+
const watcher = chokidar.watch(outputDir, {
|
|
283
|
+
ignored: /(^|[/\\])\../, // ignore dotfiles
|
|
284
|
+
persistent: true,
|
|
285
|
+
ignorePermissionErrors: true,
|
|
286
|
+
ignoreInitial: true,
|
|
287
|
+
awaitWriteFinish: {
|
|
288
|
+
stabilityThreshold: 2000,
|
|
289
|
+
pollInterval: 500
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
watcher.on('add', (fp) => {
|
|
294
|
+
if (path.basename(fp).startsWith('trace-') && fp.endsWith('.zip')) {
|
|
295
|
+
this.uploading++;
|
|
296
|
+
this._uploadAsset(fp);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async _uploadAsset(localFilename) {
|
|
302
|
+
const payload = {
|
|
303
|
+
testRunId: this.testRunId,
|
|
304
|
+
filenames: [path.basename(localFilename)]
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
debug(payload);
|
|
308
|
+
|
|
309
|
+
let url;
|
|
310
|
+
try {
|
|
311
|
+
// TODO: This could get rejected if a limit is exceeded so need to handle that case
|
|
312
|
+
const res = await request.post(this.getAssetUploadUrls, {
|
|
313
|
+
headers: this.defaultHeaders,
|
|
314
|
+
json: payload
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const body = JSON.parse(res.body);
|
|
318
|
+
debug(body);
|
|
319
|
+
|
|
320
|
+
url = body.urls[path.basename(localFilename)];
|
|
321
|
+
} catch (err) {
|
|
322
|
+
debug(err);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!url) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fileStream = fs.createReadStream(localFilename);
|
|
330
|
+
try {
|
|
331
|
+
const _response = await request.put(url, {
|
|
332
|
+
body: fileStream
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error('Failed to upload Playwright trace recording:', error);
|
|
336
|
+
console.log(error.code, error.name, error.message, error.stack);
|
|
337
|
+
} finally {
|
|
338
|
+
this.uploading--;
|
|
339
|
+
artillery.globalEvents.emit('counter', 'browser.traces.uploaded', 1);
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(localFilename);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
debug(err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async waitOnUnprocessedLogs(maxWaitTime) {
|
|
349
|
+
let waitedTime = 0;
|
|
350
|
+
while (
|
|
351
|
+
(this.unprocessedLogsCounter > 0 || this.uploading > 0) &&
|
|
352
|
+
waitedTime < maxWaitTime
|
|
353
|
+
) {
|
|
354
|
+
debug('waiting on unprocessed logs');
|
|
355
|
+
await sleep(500);
|
|
356
|
+
waitedTime += 500;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setGetStatusInterval() {
|
|
363
|
+
const interval = setInterval(async () => {
|
|
364
|
+
if (this.cancellationRequestedBy) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const res = await this._getLoadTestStatus();
|
|
368
|
+
|
|
369
|
+
if (!res) {
|
|
370
|
+
debug('No response from Artillery Cloud get status');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (res.status !== 'CANCELLATION_REQUESTED') {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log(
|
|
379
|
+
`WARNING: Artillery Cloud user ${res.cancelledBy} requested to stop the test. Stopping test run - this may take a few seconds.`
|
|
380
|
+
);
|
|
381
|
+
this.cancellationRequestedBy = res.cancelledBy;
|
|
382
|
+
global.artillery.suggestedExitCode = 8;
|
|
383
|
+
await global.artillery.shutdown({ earlyStop: true });
|
|
384
|
+
}, 5000);
|
|
385
|
+
|
|
386
|
+
return interval;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async _getLoadTestStatus() {
|
|
390
|
+
debug('☁️', 'Getting load test status');
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const res = await request.get(this.getLoadTestEndpoint, {
|
|
394
|
+
headers: this.defaultHeaders
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return JSON.parse(res.body);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
debug(error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async _event(eventName, eventPayload) {
|
|
404
|
+
debug('☁️', eventName, eventPayload);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const res = await request.post(this.eventsEndpoint, {
|
|
408
|
+
headers: this.defaultHeaders,
|
|
409
|
+
json: {
|
|
410
|
+
eventType: eventName,
|
|
411
|
+
eventData: Object.assign({}, eventPayload, {
|
|
412
|
+
testRunId: this.testRunId
|
|
413
|
+
})
|
|
414
|
+
},
|
|
415
|
+
retry: { limit: 2 }
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (res.statusCode !== 200) {
|
|
419
|
+
if (res.statusCode === 401) {
|
|
420
|
+
console.log(
|
|
421
|
+
'Error: API key is invalid. Could not send test data to Artillery Cloud.'
|
|
422
|
+
);
|
|
423
|
+
} else {
|
|
424
|
+
console.log('Error: error sending test data to Artillery Cloud');
|
|
425
|
+
console.log('Test report may be incomplete');
|
|
426
|
+
}
|
|
427
|
+
let body;
|
|
428
|
+
try {
|
|
429
|
+
body = JSON.parse(res.body);
|
|
430
|
+
} catch (_err) {}
|
|
431
|
+
|
|
432
|
+
if (body?.requestId) {
|
|
433
|
+
console.log('Request ID:', body.requestId);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
debug('☁️', eventName, 'sent');
|
|
437
|
+
} catch (err) {
|
|
438
|
+
debug(err);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
cleanup(done) {
|
|
443
|
+
debug('cleaning up');
|
|
444
|
+
done(null);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports.Plugin = ArtilleryCloudPlugin;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
const got = require('got');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 20 * 10000;
|
|
8
|
+
const DEFAULT_RETRY_LIMIT = 3;
|
|
9
|
+
|
|
10
|
+
const cloudHttpClient = got.extend({
|
|
11
|
+
timeout: { response: DEFAULT_TIMEOUT_MS },
|
|
12
|
+
retry: {
|
|
13
|
+
limit: DEFAULT_RETRY_LIMIT,
|
|
14
|
+
methods: ['GET', 'POST', 'PUT']
|
|
15
|
+
},
|
|
16
|
+
throwHttpErrors: false
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
module.exports = { cloudHttpClient };
|