@loadstrike/loadstrike-sdk 1.0.26701 → 1.0.27101
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 +7 -0
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/local.js +62 -10
- package/dist/cjs/runtime.js +344 -1
- package/dist/cjs/transports.js +346 -12
- package/dist/esm/index.js +2 -2
- package/dist/esm/local.js +62 -10
- package/dist/esm/runtime.js +339 -0
- package/dist/esm/transports.js +345 -11
- package/dist/types/contracts.d.ts +18 -0
- package/dist/types/index.d.ts +5 -5
- package/dist/types/local.d.ts +1 -2
- package/dist/types/runtime.d.ts +130 -0
- package/dist/types/transports.d.ts +80 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ LoadStrike is a developer-first load testing SDK for Node.js applications, servi
|
|
|
8
8
|
- Generate safe starter scenarios from captured HAR, OpenTelemetry trace JSON, browser recordings, or message pairs with Trace-to-test Autopilot.
|
|
9
9
|
- Exercise HTTP and event-driven workflows across multiple steps.
|
|
10
10
|
- Apply load simulations, thresholds, and custom metrics to real transactions.
|
|
11
|
+
- Split a single load profile across weighted scenario mixes.
|
|
11
12
|
- Generate local reports and, on Business and Enterprise, publish observability data to supported sinks.
|
|
12
13
|
|
|
13
14
|
Built-in transport coverage includes HTTP, Kafka, RabbitMQ, NATS, Redis Streams, Azure Event Hubs, Push Diffusion, and delegate-based custom streams. Local report output supports HTML, Markdown, TXT, and CSV, and Business and Enterprise can publish to InfluxDB, TimescaleDB, Grafana Loki, Datadog, Splunk HEC, and OpenTelemetry Collector.
|
|
@@ -53,6 +54,12 @@ const result = await LoadStrikeRunner
|
|
|
53
54
|
|
|
54
55
|
`run()` returns the detailed run result, including generated report files, scenario statistics, metrics, and sink status.
|
|
55
56
|
|
|
57
|
+
## Traffic Mixes
|
|
58
|
+
|
|
59
|
+
Use `LoadStrikeTrafficMix` on Business and Enterprise plans when one total load profile should be distributed across multiple scenario lanes. For example, a 1000 requests-per-second profile with scenario weights of 60, 30, and 10 sends roughly 600 requests per second to the first scenario, 300 to the second, and 100 to the third.
|
|
60
|
+
|
|
61
|
+
Each lane is still a normal scenario with its own named steps, thresholds, reports, and portal results. Register the mix with `LoadStrikeRunner.registerTrafficMix(...)` or add it to a runner with `.addTrafficMix(...)`.
|
|
62
|
+
|
|
56
63
|
## Trace-To-Test Autopilot
|
|
57
64
|
|
|
58
65
|
Use `await LoadStrikeAutopilot.generate(...)` to infer a starter plan from a captured artifact. Set `Options.RunnerKey` so generation can validate the Trace-To-Test Autopilot entitlement. Check `result.Readiness` and `result.ReadinessFailures` first; call `result.buildScenario()` only when it is `LoadStrikeAutopilotReadiness.Ready`, then execute the scenario through the normal runner with a valid `RunnerKey`.
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.TimescaleDbReportingSinkOptions = exports.TimescaleDbReportingSink = exports.SplunkReportingSinkOptions = exports.SplunkReportingSink = exports.PortalReportingSink = exports.OtelCollectorReportingSinkOptions = exports.OtelCollectorReportingSink = exports.InfluxDbReportingSinkOptions = void 0;
|
|
3
|
+
exports.HttpOAuth2ClientCredentialsOptions = exports.WebSocketEndpointDefinition = exports.GrpcEndpointDefinition = exports.PushDiffusionEndpointDefinition = exports.DelegateStreamEndpointDefinition = exports.SqsEndpointDefinition = exports.AzureEventHubsEndpointDefinition = exports.RedisStreamsEndpointDefinition = exports.NatsEndpointDefinition = exports.RabbitMqEndpointDefinition = exports.KafkaSaslOptions = exports.KafkaEndpointDefinition = exports.HttpEndpointDefinition = exports.TrafficEndpointDefinition = exports.LOADSTRIKE_TRACE_ID_TRACKING_FIELD = exports.LOADSTRIKE_TRACE_ID_HEADER = exports.TrackingFieldSelector = exports.TrackingPayloadBuilder = exports.RedisCorrelationStore = exports.RedisCorrelationStoreOptions = exports.InMemoryCorrelationStore = exports.CrossPlatformTrackingRuntime = exports.CorrelationStoreConfiguration = exports.LoadStrikeThreshold = exports.LoadStrikeStep = exports.LoadStrikeGauge = exports.LoadStrikeCounter = exports.LoadStrikeMetric = exports.CrossPlatformTrackingConfiguration = exports.LoadStrikeTrafficMix = exports.LoadStrikeSimulation = exports.LoadStrikeScenarioShare = exports.LoadStrikeScenario = exports.LoadStrikeRunner = exports.LoadStrikeOperationType = exports.LoadStrikeScenarioOperation = exports.LoadStrikeLogLevel = exports.LoadStrikeResponse = exports.LoadStrikeReportFormat = exports.LoadStrikeNodeType = exports.LoadStrikePluginDataTable = exports.LoadStrikePluginData = exports.LoadStrikeBrowserWebVitals = exports.LoadStrikeAccessibility = exports.LoadStrikeContext = exports.ScenarioTrackingExtensions = exports.CrossPlatformScenarioConfigurator = exports.LoadStrikeAutopilotReadiness = exports.LoadStrikeAutopilotResult = exports.LoadStrikeAutopilot = void 0;
|
|
4
|
+
exports.TimescaleDbReportingSinkOptions = exports.TimescaleDbReportingSink = exports.SplunkReportingSinkOptions = exports.SplunkReportingSink = exports.PortalReportingSink = exports.OtelCollectorReportingSinkOptions = exports.OtelCollectorReportingSink = exports.InfluxDbReportingSinkOptions = exports.InfluxDbReportingSink = exports.GrafanaLokiReportingSinkOptions = exports.GrafanaLokiReportingSink = exports.DatadogReportingSinkOptions = exports.DatadogReportingSink = exports.HttpAuthOptions = void 0;
|
|
5
5
|
var autopilot_js_1 = require("./autopilot.js");
|
|
6
6
|
Object.defineProperty(exports, "LoadStrikeAutopilot", { enumerable: true, get: function () { return autopilot_js_1.LoadStrikeAutopilot; } });
|
|
7
7
|
Object.defineProperty(exports, "LoadStrikeAutopilotResult", { enumerable: true, get: function () { return autopilot_js_1.LoadStrikeAutopilotResult; } });
|
|
@@ -11,6 +11,8 @@ var runtime_js_1 = require("./runtime.js");
|
|
|
11
11
|
Object.defineProperty(exports, "CrossPlatformScenarioConfigurator", { enumerable: true, get: function () { return runtime_js_1.CrossPlatformScenarioConfigurator; } });
|
|
12
12
|
Object.defineProperty(exports, "ScenarioTrackingExtensions", { enumerable: true, get: function () { return runtime_js_1.ScenarioTrackingExtensions; } });
|
|
13
13
|
Object.defineProperty(exports, "LoadStrikeContext", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeContext; } });
|
|
14
|
+
Object.defineProperty(exports, "LoadStrikeAccessibility", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeAccessibility; } });
|
|
15
|
+
Object.defineProperty(exports, "LoadStrikeBrowserWebVitals", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeBrowserWebVitals; } });
|
|
14
16
|
Object.defineProperty(exports, "LoadStrikePluginData", { enumerable: true, get: function () { return runtime_js_1.LoadStrikePluginData; } });
|
|
15
17
|
Object.defineProperty(exports, "LoadStrikePluginDataTable", { enumerable: true, get: function () { return runtime_js_1.LoadStrikePluginDataTable; } });
|
|
16
18
|
Object.defineProperty(exports, "LoadStrikeNodeType", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeNodeType; } });
|
|
@@ -21,7 +23,9 @@ Object.defineProperty(exports, "LoadStrikeScenarioOperation", { enumerable: true
|
|
|
21
23
|
Object.defineProperty(exports, "LoadStrikeOperationType", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeOperationType; } });
|
|
22
24
|
Object.defineProperty(exports, "LoadStrikeRunner", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeRunner; } });
|
|
23
25
|
Object.defineProperty(exports, "LoadStrikeScenario", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeScenario; } });
|
|
26
|
+
Object.defineProperty(exports, "LoadStrikeScenarioShare", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeScenarioShare; } });
|
|
24
27
|
Object.defineProperty(exports, "LoadStrikeSimulation", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeSimulation; } });
|
|
28
|
+
Object.defineProperty(exports, "LoadStrikeTrafficMix", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeTrafficMix; } });
|
|
25
29
|
Object.defineProperty(exports, "CrossPlatformTrackingConfiguration", { enumerable: true, get: function () { return runtime_js_1.CrossPlatformTrackingConfiguration; } });
|
|
26
30
|
Object.defineProperty(exports, "LoadStrikeMetric", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeMetric; } });
|
|
27
31
|
Object.defineProperty(exports, "LoadStrikeCounter", { enumerable: true, get: function () { return runtime_js_1.LoadStrikeCounter; } });
|
|
@@ -50,6 +54,8 @@ Object.defineProperty(exports, "AzureEventHubsEndpointDefinition", { enumerable:
|
|
|
50
54
|
Object.defineProperty(exports, "SqsEndpointDefinition", { enumerable: true, get: function () { return transports_js_1.SqsEndpointDefinition; } });
|
|
51
55
|
Object.defineProperty(exports, "DelegateStreamEndpointDefinition", { enumerable: true, get: function () { return transports_js_1.DelegateStreamEndpointDefinition; } });
|
|
52
56
|
Object.defineProperty(exports, "PushDiffusionEndpointDefinition", { enumerable: true, get: function () { return transports_js_1.PushDiffusionEndpointDefinition; } });
|
|
57
|
+
Object.defineProperty(exports, "GrpcEndpointDefinition", { enumerable: true, get: function () { return transports_js_1.GrpcEndpointDefinition; } });
|
|
58
|
+
Object.defineProperty(exports, "WebSocketEndpointDefinition", { enumerable: true, get: function () { return transports_js_1.WebSocketEndpointDefinition; } });
|
|
53
59
|
Object.defineProperty(exports, "HttpOAuth2ClientCredentialsOptions", { enumerable: true, get: function () { return transports_js_1.HttpOAuth2ClientCredentialsOptions; } });
|
|
54
60
|
Object.defineProperty(exports, "HttpAuthOptions", { enumerable: true, get: function () { return transports_js_1.HttpAuthOptions; } });
|
|
55
61
|
var sinks_js_1 = require("./sinks.js");
|
package/dist/cjs/local.js
CHANGED
|
@@ -32,9 +32,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
36
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
37
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
38
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
39
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
40
|
+
};
|
|
41
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
42
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
43
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
44
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
45
|
+
};
|
|
35
46
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
47
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
48
|
};
|
|
49
|
+
var _LoadStrikeLocalClient_licensingApiBaseUrl, _LoadStrikeLocalClient_signingKeyCache;
|
|
38
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
51
|
exports.__loadstrikeTestExports = exports.LoadStrikeLocalClient = void 0;
|
|
40
52
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -67,7 +79,9 @@ const TRACKING_FEATURE_BY_KIND = {
|
|
|
67
79
|
delegatestream: "endpoint.delegate_stream",
|
|
68
80
|
nats: "endpoint.nats",
|
|
69
81
|
redisstreams: "endpoint.redis_streams",
|
|
70
|
-
sqs: "endpoint.sqs"
|
|
82
|
+
sqs: "endpoint.sqs",
|
|
83
|
+
grpc: "endpoint.grpc",
|
|
84
|
+
websocket: "endpoint.websocket"
|
|
71
85
|
};
|
|
72
86
|
const CI_ENVIRONMENT_VARIABLES = [
|
|
73
87
|
"GITHUB_ACTIONS",
|
|
@@ -92,13 +106,14 @@ class LoadStrikeLocalClient {
|
|
|
92
106
|
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
93
107
|
*/
|
|
94
108
|
constructor(options = {}) {
|
|
95
|
-
this
|
|
109
|
+
_LoadStrikeLocalClient_licensingApiBaseUrl.set(this, void 0);
|
|
110
|
+
_LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
|
|
96
111
|
assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
|
|
97
|
-
this
|
|
112
|
+
__classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
|
|
98
113
|
this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
|
|
99
114
|
}
|
|
100
115
|
portalReportingIngestUrl() {
|
|
101
|
-
return `${this.
|
|
116
|
+
return `${__classPrivateFieldGet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, "f").replace(/\/+$/, "")}/api/v1/reporting/ingest`;
|
|
102
117
|
}
|
|
103
118
|
async run(request) {
|
|
104
119
|
const sanitized = sanitizeRequest(request);
|
|
@@ -367,8 +382,8 @@ class LoadStrikeLocalClient {
|
|
|
367
382
|
const nowMs = Date.now();
|
|
368
383
|
const normalizedKeyId = stringOrDefault(keyId, "default").trim() || "default";
|
|
369
384
|
const normalizedAlgorithm = stringOrDefault(algorithm, "RS256").toUpperCase();
|
|
370
|
-
const cacheKey = `${this.
|
|
371
|
-
const cached = this.
|
|
385
|
+
const cacheKey = `${__classPrivateFieldGet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, "f").toLowerCase()}|${normalizedAlgorithm}:${normalizedKeyId}`;
|
|
386
|
+
const cached = __classPrivateFieldGet(this, _LoadStrikeLocalClient_signingKeyCache, "f").get(cacheKey);
|
|
372
387
|
if (cached && cached.expiresAtUtc > nowMs) {
|
|
373
388
|
return cached;
|
|
374
389
|
}
|
|
@@ -396,7 +411,7 @@ class LoadStrikeLocalClient {
|
|
|
396
411
|
publicKeyPem,
|
|
397
412
|
expiresAtUtc: nowMs + (15 * 60 * 1000)
|
|
398
413
|
};
|
|
399
|
-
this.
|
|
414
|
+
__classPrivateFieldGet(this, _LoadStrikeLocalClient_signingKeyCache, "f").set(cacheKey, keyRecord);
|
|
400
415
|
return keyRecord;
|
|
401
416
|
}
|
|
402
417
|
finally {
|
|
@@ -448,7 +463,7 @@ class LoadStrikeLocalClient {
|
|
|
448
463
|
}
|
|
449
464
|
}
|
|
450
465
|
async postLicensingRequest(path, payload, signal) {
|
|
451
|
-
const response = await fetch(`${this.
|
|
466
|
+
const response = await fetch(`${__classPrivateFieldGet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, "f").replace(/\/+$/, "")}${path}`, {
|
|
452
467
|
method: "POST",
|
|
453
468
|
headers: {
|
|
454
469
|
"Content-Type": "application/json",
|
|
@@ -467,7 +482,7 @@ class LoadStrikeLocalClient {
|
|
|
467
482
|
return { response, json };
|
|
468
483
|
}
|
|
469
484
|
async getLicensingRequest(path, signal) {
|
|
470
|
-
const response = await fetch(`${this.
|
|
485
|
+
const response = await fetch(`${__classPrivateFieldGet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, "f").replace(/\/+$/, "")}${path}`, {
|
|
471
486
|
method: "GET",
|
|
472
487
|
headers: {
|
|
473
488
|
Accept: "application/json"
|
|
@@ -485,6 +500,7 @@ class LoadStrikeLocalClient {
|
|
|
485
500
|
}
|
|
486
501
|
}
|
|
487
502
|
exports.LoadStrikeLocalClient = LoadStrikeLocalClient;
|
|
503
|
+
_LoadStrikeLocalClient_licensingApiBaseUrl = new WeakMap(), _LoadStrikeLocalClient_signingKeyCache = new WeakMap();
|
|
488
504
|
function assertNoDisableLicenseEnforcementOption(value, source) {
|
|
489
505
|
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
490
506
|
return;
|
|
@@ -514,7 +530,43 @@ function resolveLicensingApiBaseUrl() {
|
|
|
514
530
|
return normalizeLicensingApiBaseUrl(developmentLicensingApiBaseUrlOverride);
|
|
515
531
|
}
|
|
516
532
|
function setDevelopmentLicensingApiBaseUrlOverride(value) {
|
|
517
|
-
|
|
533
|
+
if (value != null && value.trim()) {
|
|
534
|
+
assertInternalTestHarnessLicensingOverride();
|
|
535
|
+
const normalized = normalizeLicensingApiBaseUrl(value);
|
|
536
|
+
if (!isLoopbackHttpBaseUrl(normalized)) {
|
|
537
|
+
throw new TypeError("Internal development licensing API overrides must use a loopback http URL.");
|
|
538
|
+
}
|
|
539
|
+
developmentLicensingApiBaseUrlOverride = normalized;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (value != null) {
|
|
543
|
+
assertInternalTestHarnessLicensingOverride();
|
|
544
|
+
}
|
|
545
|
+
developmentLicensingApiBaseUrlOverride = undefined;
|
|
546
|
+
}
|
|
547
|
+
function assertInternalTestHarnessLicensingOverride() {
|
|
548
|
+
if (isInternalTestHarnessCall()) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
throw new TypeError("Internal development licensing API override is available only to LoadStrike SDK tests.");
|
|
552
|
+
}
|
|
553
|
+
function isInternalTestHarnessCall() {
|
|
554
|
+
const stack = String(new Error().stack ?? "").replace(/\\/g, "/").toLowerCase();
|
|
555
|
+
return stack.includes("/sdk/ts/tests/");
|
|
556
|
+
}
|
|
557
|
+
function isLoopbackHttpBaseUrl(value) {
|
|
558
|
+
let parsed;
|
|
559
|
+
try {
|
|
560
|
+
parsed = new URL(value);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
if (parsed.protocol !== "http:") {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
const host = parsed.hostname.trim().toLowerCase();
|
|
569
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.endsWith(".localhost");
|
|
518
570
|
}
|
|
519
571
|
function normalizeTimeoutMs(value) {
|
|
520
572
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
package/dist/cjs/runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.__loadstrikeTestExports = exports.LoadStrikeRunner = exports.LoadStrikeScenario = exports.LoadStrikeContext = exports.ScenarioTrackingExtensions = exports.CrossPlatformScenarioConfigurator = exports.LoadStrikeThreshold = exports.LoadStrikeMetric = exports.LoadStrikeGauge = exports.LoadStrikeCounter = exports.LoadStrikeSimulation = exports.LoadStrikeStep = exports.LoadStrikeResponse = exports.LoadStrikePluginData = exports.LoadStrikePluginDataTable = exports.CrossPlatformTrackingConfiguration = exports.LoadStrikeOperationType = exports.LoadStrikeScenarioOperation = exports.LoadStrikeLogLevel = exports.LoadStrikeReportFormat = exports.LoadStrikeNodeType = void 0;
|
|
6
|
+
exports.__loadstrikeTestExports = exports.LoadStrikeRunner = exports.LoadStrikeTrafficMix = exports.LoadStrikeScenarioShare = exports.LoadStrikeScenario = exports.LoadStrikeBrowserWebVitals = exports.LoadStrikeAccessibility = exports.LoadStrikeContext = exports.ScenarioTrackingExtensions = exports.CrossPlatformScenarioConfigurator = exports.LoadStrikeThreshold = exports.LoadStrikeMetric = exports.LoadStrikeGauge = exports.LoadStrikeCounter = exports.LoadStrikeSimulation = exports.LoadStrikeStep = exports.LoadStrikeResponse = exports.LoadStrikePluginData = exports.LoadStrikePluginDataTable = exports.CrossPlatformTrackingConfiguration = exports.LoadStrikeOperationType = exports.LoadStrikeScenarioOperation = exports.LoadStrikeLogLevel = exports.LoadStrikeReportFormat = exports.LoadStrikeNodeType = void 0;
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const node_crypto_1 = require("node:crypto");
|
|
9
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -75,6 +75,7 @@ exports.CrossPlatformTrackingConfiguration = {
|
|
|
75
75
|
return this.forDuration(configuration, durationSeconds, cancellationSignal);
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
|
+
const TRAFFIC_MIX_FEATURE = "workload.traffic_mix";
|
|
78
79
|
class LoadStrikePluginDataTable {
|
|
79
80
|
/**
|
|
80
81
|
* Exposes the public constructor operation.
|
|
@@ -1621,6 +1622,114 @@ class LoadStrikeContext {
|
|
|
1621
1622
|
}
|
|
1622
1623
|
}
|
|
1623
1624
|
exports.LoadStrikeContext = LoadStrikeContext;
|
|
1625
|
+
const ACCESSIBILITY_TESTING_FEATURE = "testing.accessibility";
|
|
1626
|
+
const BROWSER_WEB_VITALS_FEATURE = "testing.browser_web_vitals";
|
|
1627
|
+
class LoadStrikeAccessibility {
|
|
1628
|
+
static createScenario(name, options, check) {
|
|
1629
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
1630
|
+
throw new Error("Scenario name must be provided.");
|
|
1631
|
+
}
|
|
1632
|
+
validateAbsoluteUrl(options?.url, "Accessibility check URL");
|
|
1633
|
+
if (typeof check !== "function") {
|
|
1634
|
+
throw new TypeError("Accessibility check callback must be provided.");
|
|
1635
|
+
}
|
|
1636
|
+
return LoadStrikeScenario
|
|
1637
|
+
.create(name, async (scenarioContext) => {
|
|
1638
|
+
const result = await check({ options, scenarioContext });
|
|
1639
|
+
const failure = evaluateAccessibilityResult(options, result);
|
|
1640
|
+
return failure
|
|
1641
|
+
? LoadStrikeResponse.fail("accessibility", failure)
|
|
1642
|
+
: LoadStrikeResponse.ok("200", 0, `Accessibility check passed with ${(result.violations ?? []).length} violation(s).`);
|
|
1643
|
+
})
|
|
1644
|
+
.withoutWarmUp()
|
|
1645
|
+
.__loadStrikeWithInternalLicenseFeatures(ACCESSIBILITY_TESTING_FEATURE);
|
|
1646
|
+
}
|
|
1647
|
+
static CreateScenario(name, options, check) {
|
|
1648
|
+
return LoadStrikeAccessibility.createScenario(name, options, check);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
exports.LoadStrikeAccessibility = LoadStrikeAccessibility;
|
|
1652
|
+
class LoadStrikeBrowserWebVitals {
|
|
1653
|
+
static createScenario(name, options, measure) {
|
|
1654
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
1655
|
+
throw new Error("Scenario name must be provided.");
|
|
1656
|
+
}
|
|
1657
|
+
validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
|
|
1658
|
+
if (typeof measure !== "function") {
|
|
1659
|
+
throw new TypeError("Browser Web Vitals measurement callback must be provided.");
|
|
1660
|
+
}
|
|
1661
|
+
return LoadStrikeScenario
|
|
1662
|
+
.create(name, async (scenarioContext) => {
|
|
1663
|
+
const result = await measure({ options, scenarioContext });
|
|
1664
|
+
const failure = evaluateWebVitalsResult(options, result);
|
|
1665
|
+
return failure
|
|
1666
|
+
? LoadStrikeResponse.fail("web_vitals", failure)
|
|
1667
|
+
: LoadStrikeResponse.ok("200", 0, "Browser Web Vitals check passed.");
|
|
1668
|
+
})
|
|
1669
|
+
.withoutWarmUp()
|
|
1670
|
+
.__loadStrikeWithInternalLicenseFeatures(BROWSER_WEB_VITALS_FEATURE);
|
|
1671
|
+
}
|
|
1672
|
+
static CreateScenario(name, options, measure) {
|
|
1673
|
+
return LoadStrikeBrowserWebVitals.createScenario(name, options, measure);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
exports.LoadStrikeBrowserWebVitals = LoadStrikeBrowserWebVitals;
|
|
1677
|
+
function validateAbsoluteUrl(value, label) {
|
|
1678
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1679
|
+
throw new Error(`${label} must be provided.`);
|
|
1680
|
+
}
|
|
1681
|
+
try {
|
|
1682
|
+
const parsed = new URL(value);
|
|
1683
|
+
if (!parsed.protocol || !parsed.host) {
|
|
1684
|
+
throw new Error("invalid");
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
catch {
|
|
1688
|
+
throw new Error(`${label} must be an absolute URL.`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
function evaluateAccessibilityResult(options, result) {
|
|
1692
|
+
if (!result) {
|
|
1693
|
+
return "Accessibility check did not return a result.";
|
|
1694
|
+
}
|
|
1695
|
+
const violations = Array.isArray(result.violations) ? result.violations : [];
|
|
1696
|
+
const maxViolations = Math.max(0, Number(options.maxViolations ?? 0));
|
|
1697
|
+
const critical = countAccessibilityImpact(violations, "critical");
|
|
1698
|
+
const serious = countAccessibilityImpact(violations, "serious");
|
|
1699
|
+
const maxCritical = Math.max(0, Number(options.maxCriticalViolations ?? 0));
|
|
1700
|
+
const maxSerious = Math.max(0, Number(options.maxSeriousViolations ?? 0));
|
|
1701
|
+
if (violations.length > maxViolations) {
|
|
1702
|
+
return `Accessibility violations ${violations.length} exceeded limit ${maxViolations}.`;
|
|
1703
|
+
}
|
|
1704
|
+
if (critical > maxCritical) {
|
|
1705
|
+
return `Critical accessibility violations ${critical} exceeded limit ${maxCritical}.`;
|
|
1706
|
+
}
|
|
1707
|
+
if (serious > maxSerious) {
|
|
1708
|
+
return `Serious accessibility violations ${serious} exceeded limit ${maxSerious}.`;
|
|
1709
|
+
}
|
|
1710
|
+
return "";
|
|
1711
|
+
}
|
|
1712
|
+
function countAccessibilityImpact(violations, impact) {
|
|
1713
|
+
return violations.filter((violation) => String(violation.impact ?? "").toLowerCase() === impact).length;
|
|
1714
|
+
}
|
|
1715
|
+
function evaluateWebVitalsResult(options, result) {
|
|
1716
|
+
if (!result) {
|
|
1717
|
+
return "Browser Web Vitals check did not return a result.";
|
|
1718
|
+
}
|
|
1719
|
+
return firstWebVitalViolation(["LCP", result.largestContentfulPaintMs, options.maxLargestContentfulPaintMs, "ms"], ["INP", result.interactionToNextPaintMs, options.maxInteractionToNextPaintMs, "ms"], ["CLS", result.cumulativeLayoutShift, options.maxCumulativeLayoutShift, ""], ["FCP", result.firstContentfulPaintMs, options.maxFirstContentfulPaintMs, "ms"], ["TTFB", result.timeToFirstByteMs, options.maxTimeToFirstByteMs, "ms"]);
|
|
1720
|
+
}
|
|
1721
|
+
function firstWebVitalViolation(...values) {
|
|
1722
|
+
for (const [name, actualRaw, limitRaw, unit] of values) {
|
|
1723
|
+
const actual = Number(actualRaw);
|
|
1724
|
+
const limit = Number(limitRaw);
|
|
1725
|
+
if (!Number.isFinite(actual) || !Number.isFinite(limit) || actual <= limit) {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const suffix = unit ? ` ${unit}` : "";
|
|
1729
|
+
return `${name} ${actual}${suffix} exceeded limit ${limit}${suffix}.`;
|
|
1730
|
+
}
|
|
1731
|
+
return "";
|
|
1732
|
+
}
|
|
1624
1733
|
class LoadStrikeScenario {
|
|
1625
1734
|
constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, internalLicenseFeatures = []) {
|
|
1626
1735
|
this.name = name;
|
|
@@ -1967,6 +2076,105 @@ class LoadStrikeScenario {
|
|
|
1967
2076
|
}
|
|
1968
2077
|
}
|
|
1969
2078
|
exports.LoadStrikeScenario = LoadStrikeScenario;
|
|
2079
|
+
class LoadStrikeScenarioShare {
|
|
2080
|
+
constructor(scenario, weight) {
|
|
2081
|
+
this.scenario = scenario;
|
|
2082
|
+
this.weight = weight;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Creates a weighted scenario lane for a traffic mix.
|
|
2086
|
+
* Use this when a total workload should be distributed across several scenarios.
|
|
2087
|
+
*/
|
|
2088
|
+
static create(scenario, weight) {
|
|
2089
|
+
if (!(scenario instanceof LoadStrikeScenario)) {
|
|
2090
|
+
throw new TypeError("Scenario must be provided.");
|
|
2091
|
+
}
|
|
2092
|
+
const normalizedWeight = Math.trunc(weight);
|
|
2093
|
+
if (!Number.isFinite(weight) || normalizedWeight <= 0) {
|
|
2094
|
+
throw new RangeError("Scenario share weight should be greater than zero.");
|
|
2095
|
+
}
|
|
2096
|
+
return new LoadStrikeScenarioShare(scenario, normalizedWeight);
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Creates a weighted scenario lane for a traffic mix.
|
|
2100
|
+
* Use this when a total workload should be distributed across several scenarios.
|
|
2101
|
+
*/
|
|
2102
|
+
static Create(scenario, weight) {
|
|
2103
|
+
return LoadStrikeScenarioShare.create(scenario, weight);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
exports.LoadStrikeScenarioShare = LoadStrikeScenarioShare;
|
|
2107
|
+
class LoadStrikeTrafficMix {
|
|
2108
|
+
constructor(name, totalLoad = [], scenarioMix = []) {
|
|
2109
|
+
this.name = name;
|
|
2110
|
+
this.totalLoad = totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
|
|
2111
|
+
this.scenarioMix = [...scenarioMix];
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Creates a traffic mix definition.
|
|
2115
|
+
* Use this when one total load profile should be split across multiple scenario lanes.
|
|
2116
|
+
*/
|
|
2117
|
+
static create(name) {
|
|
2118
|
+
return new LoadStrikeTrafficMix(requireNonEmpty(name, "Traffic mix name must be provided."));
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Creates a traffic mix definition.
|
|
2122
|
+
* Use this when one total load profile should be split across multiple scenario lanes.
|
|
2123
|
+
*/
|
|
2124
|
+
static Create(name) {
|
|
2125
|
+
return LoadStrikeTrafficMix.create(name);
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Sets the total workload shape for the mix.
|
|
2129
|
+
* Use this when the same high-level load profile should be distributed by scenario weights.
|
|
2130
|
+
*/
|
|
2131
|
+
withTotalLoad(...totalLoad) {
|
|
2132
|
+
if (!totalLoad.length) {
|
|
2133
|
+
throw new Error("At least one total load simulation should be provided.");
|
|
2134
|
+
}
|
|
2135
|
+
return new LoadStrikeTrafficMix(this.name, totalLoad, this.scenarioMix);
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Sets the total workload shape for the mix.
|
|
2139
|
+
* Use this when the same high-level load profile should be distributed by scenario weights.
|
|
2140
|
+
*/
|
|
2141
|
+
WithTotalLoad(...totalLoad) {
|
|
2142
|
+
return this.withTotalLoad(...totalLoad);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Sets the weighted scenario lanes for the mix.
|
|
2146
|
+
* Use this when each operation should receive a fixed proportion of the total workload.
|
|
2147
|
+
*/
|
|
2148
|
+
withScenarioMix(...scenarioMix) {
|
|
2149
|
+
if (!scenarioMix.length) {
|
|
2150
|
+
throw new Error("At least one scenario share should be provided.");
|
|
2151
|
+
}
|
|
2152
|
+
if (scenarioMix.some((share) => !(share instanceof LoadStrikeScenarioShare))) {
|
|
2153
|
+
throw new TypeError("Scenario share collection cannot contain null values.");
|
|
2154
|
+
}
|
|
2155
|
+
return new LoadStrikeTrafficMix(this.name, this.totalLoad, scenarioMix);
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Sets the weighted scenario lanes for the mix.
|
|
2159
|
+
* Use this when each operation should receive a fixed proportion of the total workload.
|
|
2160
|
+
*/
|
|
2161
|
+
WithScenarioMix(...scenarioMix) {
|
|
2162
|
+
return this.withScenarioMix(...scenarioMix);
|
|
2163
|
+
}
|
|
2164
|
+
expandScenarios() {
|
|
2165
|
+
return expandTrafficMixScenarios(this);
|
|
2166
|
+
}
|
|
2167
|
+
ExpandScenarios() {
|
|
2168
|
+
return this.expandScenarios();
|
|
2169
|
+
}
|
|
2170
|
+
__loadStrikeTotalLoad() {
|
|
2171
|
+
return this.totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
|
|
2172
|
+
}
|
|
2173
|
+
__loadStrikeScenarioMix() {
|
|
2174
|
+
return [...this.scenarioMix];
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
exports.LoadStrikeTrafficMix = LoadStrikeTrafficMix;
|
|
1970
2178
|
class LoadStrikeRunner {
|
|
1971
2179
|
constructor(scenarios, options, contextConfigurators = []) {
|
|
1972
2180
|
this.scenarios = scenarios;
|
|
@@ -2002,6 +2210,20 @@ class LoadStrikeRunner {
|
|
|
2002
2210
|
static RegisterScenarios(...scenarios) {
|
|
2003
2211
|
return LoadStrikeRunner.registerScenarios(...scenarios);
|
|
2004
2212
|
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Registers a traffic mix on a fresh runnable context.
|
|
2215
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2216
|
+
*/
|
|
2217
|
+
static registerTrafficMix(trafficMix) {
|
|
2218
|
+
return LoadStrikeRunner.registerScenarios(...expandTrafficMixScenarios(trafficMix));
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Registers a traffic mix on a fresh runnable context.
|
|
2222
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2223
|
+
*/
|
|
2224
|
+
static RegisterTrafficMix(trafficMix) {
|
|
2225
|
+
return LoadStrikeRunner.registerTrafficMix(trafficMix);
|
|
2226
|
+
}
|
|
2005
2227
|
/**
|
|
2006
2228
|
* Toggles realtime console metric output.
|
|
2007
2229
|
* Use this when a local run or CI log should stream live throughput and latency updates.
|
|
@@ -2256,6 +2478,21 @@ class LoadStrikeRunner {
|
|
|
2256
2478
|
AddScenarios(...scenarios) {
|
|
2257
2479
|
return this.addScenarios(...scenarios);
|
|
2258
2480
|
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Adds a traffic mix to the current runner.
|
|
2483
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2484
|
+
*/
|
|
2485
|
+
addTrafficMix(trafficMix) {
|
|
2486
|
+
this.scenarios = [...this.scenarios, ...expandTrafficMixScenarios(trafficMix)];
|
|
2487
|
+
return this;
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Adds a traffic mix to the current runner.
|
|
2491
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2492
|
+
*/
|
|
2493
|
+
AddTrafficMix(trafficMix) {
|
|
2494
|
+
return this.addTrafficMix(trafficMix);
|
|
2495
|
+
}
|
|
2259
2496
|
/**
|
|
2260
2497
|
* Applies a grouped configuration change to the current builder or context.
|
|
2261
2498
|
* Use this when several related settings should be supplied in one step.
|
|
@@ -4408,6 +4645,111 @@ function attachLoadSimulationProjection(simulation) {
|
|
|
4408
4645
|
});
|
|
4409
4646
|
return simulation;
|
|
4410
4647
|
}
|
|
4648
|
+
function expandTrafficMixScenarios(trafficMix) {
|
|
4649
|
+
if (!(trafficMix instanceof LoadStrikeTrafficMix)) {
|
|
4650
|
+
throw new TypeError("Traffic mix must be provided.");
|
|
4651
|
+
}
|
|
4652
|
+
const totalLoad = trafficMix.__loadStrikeTotalLoad();
|
|
4653
|
+
if (!totalLoad.length) {
|
|
4654
|
+
throw new Error("Traffic mix total load must be configured before registration.");
|
|
4655
|
+
}
|
|
4656
|
+
const scenarioMix = trafficMix.__loadStrikeScenarioMix();
|
|
4657
|
+
if (!scenarioMix.length) {
|
|
4658
|
+
throw new Error("Traffic mix scenario shares must be configured before registration.");
|
|
4659
|
+
}
|
|
4660
|
+
const weights = scenarioMix.map((share) => share.weight);
|
|
4661
|
+
return scenarioMix.map((share, index) => {
|
|
4662
|
+
const splitSimulations = totalLoad
|
|
4663
|
+
.map((simulation) => splitTrafficSimulation(simulation, weights, index))
|
|
4664
|
+
.filter((simulation) => simulation != null);
|
|
4665
|
+
const scenario = !splitSimulations.length
|
|
4666
|
+
? share.scenario.withLoadSimulations(LoadStrikeSimulation.pause(0))
|
|
4667
|
+
: share.scenario.withLoadSimulations(...splitSimulations);
|
|
4668
|
+
return scenario.__loadStrikeWithInternalLicenseFeatures(TRAFFIC_MIX_FEATURE);
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
function splitTrafficSimulation(simulation, weights, index) {
|
|
4672
|
+
const kind = String(simulation.Kind ?? "");
|
|
4673
|
+
const duringSeconds = readFiniteSimulationNumber(simulation, "DuringSeconds");
|
|
4674
|
+
const intervalSeconds = readFiniteSimulationNumber(simulation, "IntervalSeconds");
|
|
4675
|
+
if (kind === "Inject") {
|
|
4676
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4677
|
+
return rate > 0 ? LoadStrikeSimulation.inject(rate, intervalSeconds, duringSeconds) : null;
|
|
4678
|
+
}
|
|
4679
|
+
if (kind === "RampingInject") {
|
|
4680
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4681
|
+
return rate > 0 ? LoadStrikeSimulation.rampingInject(rate, intervalSeconds, duringSeconds) : null;
|
|
4682
|
+
}
|
|
4683
|
+
if (kind === "InjectRandom") {
|
|
4684
|
+
const minRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MinRate"), weights)[index];
|
|
4685
|
+
const maxRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MaxRate"), weights)[index];
|
|
4686
|
+
if (maxRate <= 0) {
|
|
4687
|
+
return null;
|
|
4688
|
+
}
|
|
4689
|
+
return LoadStrikeSimulation.injectRandom(Math.min(minRate, maxRate), maxRate, intervalSeconds, duringSeconds);
|
|
4690
|
+
}
|
|
4691
|
+
if (kind === "IterationsForInject") {
|
|
4692
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4693
|
+
const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
|
|
4694
|
+
return rate > 0 && iterations > 0
|
|
4695
|
+
? LoadStrikeSimulation.iterationsForInject(rate, intervalSeconds, iterations)
|
|
4696
|
+
: null;
|
|
4697
|
+
}
|
|
4698
|
+
if (kind === "IterationsForConstant") {
|
|
4699
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4700
|
+
const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
|
|
4701
|
+
return copies > 0 && iterations > 0
|
|
4702
|
+
? LoadStrikeSimulation.iterationsForConstant(copies, iterations)
|
|
4703
|
+
: null;
|
|
4704
|
+
}
|
|
4705
|
+
if (kind === "KeepConstant") {
|
|
4706
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4707
|
+
return copies > 0 ? LoadStrikeSimulation.keepConstant(copies, duringSeconds) : null;
|
|
4708
|
+
}
|
|
4709
|
+
if (kind === "RampingConstant") {
|
|
4710
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4711
|
+
return copies > 0 ? LoadStrikeSimulation.rampingConstant(copies, duringSeconds) : null;
|
|
4712
|
+
}
|
|
4713
|
+
if (kind === "Pause") {
|
|
4714
|
+
return LoadStrikeSimulation.pause(duringSeconds);
|
|
4715
|
+
}
|
|
4716
|
+
return attachLoadSimulationProjection({ ...simulation });
|
|
4717
|
+
}
|
|
4718
|
+
function splitTrafficValue(totalValue, weights) {
|
|
4719
|
+
const normalizedTotal = Math.trunc(Number.isFinite(totalValue) ? totalValue : 0);
|
|
4720
|
+
if (normalizedTotal <= 0) {
|
|
4721
|
+
return weights.map(() => 0);
|
|
4722
|
+
}
|
|
4723
|
+
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
|
|
4724
|
+
if (totalWeight <= 0) {
|
|
4725
|
+
throw new Error("Traffic mix scenario weights must add up to more than zero.");
|
|
4726
|
+
}
|
|
4727
|
+
const exactShares = weights.map((weight, index) => {
|
|
4728
|
+
const exact = (normalizedTotal * weight) / totalWeight;
|
|
4729
|
+
return {
|
|
4730
|
+
index,
|
|
4731
|
+
value: Math.floor(exact),
|
|
4732
|
+
remainder: exact - Math.floor(exact)
|
|
4733
|
+
};
|
|
4734
|
+
});
|
|
4735
|
+
const split = exactShares.map((share) => share.value);
|
|
4736
|
+
let remaining = normalizedTotal - split.reduce((sum, value) => sum + value, 0);
|
|
4737
|
+
for (const share of [...exactShares].sort((left, right) => {
|
|
4738
|
+
const remainderDelta = right.remainder - left.remainder;
|
|
4739
|
+
return remainderDelta !== 0 ? remainderDelta : left.index - right.index;
|
|
4740
|
+
})) {
|
|
4741
|
+
if (remaining <= 0) {
|
|
4742
|
+
break;
|
|
4743
|
+
}
|
|
4744
|
+
split[share.index] += 1;
|
|
4745
|
+
remaining -= 1;
|
|
4746
|
+
}
|
|
4747
|
+
return split;
|
|
4748
|
+
}
|
|
4749
|
+
function readFiniteSimulationNumber(simulation, key) {
|
|
4750
|
+
const value = Number(simulation[key]);
|
|
4751
|
+
return Number.isFinite(value) ? value : 0;
|
|
4752
|
+
}
|
|
4411
4753
|
function attachScenarioStatsAliases(scenario) {
|
|
4412
4754
|
const normalized = normalizeScenarioStatsValue(scenario);
|
|
4413
4755
|
normalized.ok = attachMeasurementStatsAliases(normalized.ok);
|
|
@@ -8366,6 +8708,7 @@ exports.__loadstrikeTestExports = {
|
|
|
8366
8708
|
detailedToNodeStats,
|
|
8367
8709
|
evaluateThresholdsForScenarios,
|
|
8368
8710
|
executeTrackedScenarioInvocation,
|
|
8711
|
+
expandTrafficMixScenarios,
|
|
8369
8712
|
extractContextOverridesFromArgs,
|
|
8370
8713
|
extractContextOverridesFromConfig,
|
|
8371
8714
|
findCaseInsensitiveKey,
|