@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/dist/esm/runtime.js
CHANGED
|
@@ -69,6 +69,7 @@ export const CrossPlatformTrackingConfiguration = {
|
|
|
69
69
|
return this.forDuration(configuration, durationSeconds, cancellationSignal);
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
|
+
const TRAFFIC_MIX_FEATURE = "workload.traffic_mix";
|
|
72
73
|
export class LoadStrikePluginDataTable {
|
|
73
74
|
/**
|
|
74
75
|
* Exposes the public constructor operation.
|
|
@@ -1603,6 +1604,112 @@ export class LoadStrikeContext {
|
|
|
1603
1604
|
return this.assignState(this.values, scenarios, this.runArgs);
|
|
1604
1605
|
}
|
|
1605
1606
|
}
|
|
1607
|
+
const ACCESSIBILITY_TESTING_FEATURE = "testing.accessibility";
|
|
1608
|
+
const BROWSER_WEB_VITALS_FEATURE = "testing.browser_web_vitals";
|
|
1609
|
+
export class LoadStrikeAccessibility {
|
|
1610
|
+
static createScenario(name, options, check) {
|
|
1611
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
1612
|
+
throw new Error("Scenario name must be provided.");
|
|
1613
|
+
}
|
|
1614
|
+
validateAbsoluteUrl(options?.url, "Accessibility check URL");
|
|
1615
|
+
if (typeof check !== "function") {
|
|
1616
|
+
throw new TypeError("Accessibility check callback must be provided.");
|
|
1617
|
+
}
|
|
1618
|
+
return LoadStrikeScenario
|
|
1619
|
+
.create(name, async (scenarioContext) => {
|
|
1620
|
+
const result = await check({ options, scenarioContext });
|
|
1621
|
+
const failure = evaluateAccessibilityResult(options, result);
|
|
1622
|
+
return failure
|
|
1623
|
+
? LoadStrikeResponse.fail("accessibility", failure)
|
|
1624
|
+
: LoadStrikeResponse.ok("200", 0, `Accessibility check passed with ${(result.violations ?? []).length} violation(s).`);
|
|
1625
|
+
})
|
|
1626
|
+
.withoutWarmUp()
|
|
1627
|
+
.__loadStrikeWithInternalLicenseFeatures(ACCESSIBILITY_TESTING_FEATURE);
|
|
1628
|
+
}
|
|
1629
|
+
static CreateScenario(name, options, check) {
|
|
1630
|
+
return LoadStrikeAccessibility.createScenario(name, options, check);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
export class LoadStrikeBrowserWebVitals {
|
|
1634
|
+
static createScenario(name, options, measure) {
|
|
1635
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
1636
|
+
throw new Error("Scenario name must be provided.");
|
|
1637
|
+
}
|
|
1638
|
+
validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
|
|
1639
|
+
if (typeof measure !== "function") {
|
|
1640
|
+
throw new TypeError("Browser Web Vitals measurement callback must be provided.");
|
|
1641
|
+
}
|
|
1642
|
+
return LoadStrikeScenario
|
|
1643
|
+
.create(name, async (scenarioContext) => {
|
|
1644
|
+
const result = await measure({ options, scenarioContext });
|
|
1645
|
+
const failure = evaluateWebVitalsResult(options, result);
|
|
1646
|
+
return failure
|
|
1647
|
+
? LoadStrikeResponse.fail("web_vitals", failure)
|
|
1648
|
+
: LoadStrikeResponse.ok("200", 0, "Browser Web Vitals check passed.");
|
|
1649
|
+
})
|
|
1650
|
+
.withoutWarmUp()
|
|
1651
|
+
.__loadStrikeWithInternalLicenseFeatures(BROWSER_WEB_VITALS_FEATURE);
|
|
1652
|
+
}
|
|
1653
|
+
static CreateScenario(name, options, measure) {
|
|
1654
|
+
return LoadStrikeBrowserWebVitals.createScenario(name, options, measure);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function validateAbsoluteUrl(value, label) {
|
|
1658
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1659
|
+
throw new Error(`${label} must be provided.`);
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const parsed = new URL(value);
|
|
1663
|
+
if (!parsed.protocol || !parsed.host) {
|
|
1664
|
+
throw new Error("invalid");
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
throw new Error(`${label} must be an absolute URL.`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
function evaluateAccessibilityResult(options, result) {
|
|
1672
|
+
if (!result) {
|
|
1673
|
+
return "Accessibility check did not return a result.";
|
|
1674
|
+
}
|
|
1675
|
+
const violations = Array.isArray(result.violations) ? result.violations : [];
|
|
1676
|
+
const maxViolations = Math.max(0, Number(options.maxViolations ?? 0));
|
|
1677
|
+
const critical = countAccessibilityImpact(violations, "critical");
|
|
1678
|
+
const serious = countAccessibilityImpact(violations, "serious");
|
|
1679
|
+
const maxCritical = Math.max(0, Number(options.maxCriticalViolations ?? 0));
|
|
1680
|
+
const maxSerious = Math.max(0, Number(options.maxSeriousViolations ?? 0));
|
|
1681
|
+
if (violations.length > maxViolations) {
|
|
1682
|
+
return `Accessibility violations ${violations.length} exceeded limit ${maxViolations}.`;
|
|
1683
|
+
}
|
|
1684
|
+
if (critical > maxCritical) {
|
|
1685
|
+
return `Critical accessibility violations ${critical} exceeded limit ${maxCritical}.`;
|
|
1686
|
+
}
|
|
1687
|
+
if (serious > maxSerious) {
|
|
1688
|
+
return `Serious accessibility violations ${serious} exceeded limit ${maxSerious}.`;
|
|
1689
|
+
}
|
|
1690
|
+
return "";
|
|
1691
|
+
}
|
|
1692
|
+
function countAccessibilityImpact(violations, impact) {
|
|
1693
|
+
return violations.filter((violation) => String(violation.impact ?? "").toLowerCase() === impact).length;
|
|
1694
|
+
}
|
|
1695
|
+
function evaluateWebVitalsResult(options, result) {
|
|
1696
|
+
if (!result) {
|
|
1697
|
+
return "Browser Web Vitals check did not return a result.";
|
|
1698
|
+
}
|
|
1699
|
+
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"]);
|
|
1700
|
+
}
|
|
1701
|
+
function firstWebVitalViolation(...values) {
|
|
1702
|
+
for (const [name, actualRaw, limitRaw, unit] of values) {
|
|
1703
|
+
const actual = Number(actualRaw);
|
|
1704
|
+
const limit = Number(limitRaw);
|
|
1705
|
+
if (!Number.isFinite(actual) || !Number.isFinite(limit) || actual <= limit) {
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const suffix = unit ? ` ${unit}` : "";
|
|
1709
|
+
return `${name} ${actual}${suffix} exceeded limit ${limit}${suffix}.`;
|
|
1710
|
+
}
|
|
1711
|
+
return "";
|
|
1712
|
+
}
|
|
1606
1713
|
export class LoadStrikeScenario {
|
|
1607
1714
|
constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, internalLicenseFeatures = []) {
|
|
1608
1715
|
this.name = name;
|
|
@@ -1948,6 +2055,103 @@ export class LoadStrikeScenario {
|
|
|
1948
2055
|
return this.withWeight(weight);
|
|
1949
2056
|
}
|
|
1950
2057
|
}
|
|
2058
|
+
export class LoadStrikeScenarioShare {
|
|
2059
|
+
constructor(scenario, weight) {
|
|
2060
|
+
this.scenario = scenario;
|
|
2061
|
+
this.weight = weight;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Creates a weighted scenario lane for a traffic mix.
|
|
2065
|
+
* Use this when a total workload should be distributed across several scenarios.
|
|
2066
|
+
*/
|
|
2067
|
+
static create(scenario, weight) {
|
|
2068
|
+
if (!(scenario instanceof LoadStrikeScenario)) {
|
|
2069
|
+
throw new TypeError("Scenario must be provided.");
|
|
2070
|
+
}
|
|
2071
|
+
const normalizedWeight = Math.trunc(weight);
|
|
2072
|
+
if (!Number.isFinite(weight) || normalizedWeight <= 0) {
|
|
2073
|
+
throw new RangeError("Scenario share weight should be greater than zero.");
|
|
2074
|
+
}
|
|
2075
|
+
return new LoadStrikeScenarioShare(scenario, normalizedWeight);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Creates a weighted scenario lane for a traffic mix.
|
|
2079
|
+
* Use this when a total workload should be distributed across several scenarios.
|
|
2080
|
+
*/
|
|
2081
|
+
static Create(scenario, weight) {
|
|
2082
|
+
return LoadStrikeScenarioShare.create(scenario, weight);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
export class LoadStrikeTrafficMix {
|
|
2086
|
+
constructor(name, totalLoad = [], scenarioMix = []) {
|
|
2087
|
+
this.name = name;
|
|
2088
|
+
this.totalLoad = totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
|
|
2089
|
+
this.scenarioMix = [...scenarioMix];
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Creates a traffic mix definition.
|
|
2093
|
+
* Use this when one total load profile should be split across multiple scenario lanes.
|
|
2094
|
+
*/
|
|
2095
|
+
static create(name) {
|
|
2096
|
+
return new LoadStrikeTrafficMix(requireNonEmpty(name, "Traffic mix name must be provided."));
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Creates a traffic mix definition.
|
|
2100
|
+
* Use this when one total load profile should be split across multiple scenario lanes.
|
|
2101
|
+
*/
|
|
2102
|
+
static Create(name) {
|
|
2103
|
+
return LoadStrikeTrafficMix.create(name);
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Sets the total workload shape for the mix.
|
|
2107
|
+
* Use this when the same high-level load profile should be distributed by scenario weights.
|
|
2108
|
+
*/
|
|
2109
|
+
withTotalLoad(...totalLoad) {
|
|
2110
|
+
if (!totalLoad.length) {
|
|
2111
|
+
throw new Error("At least one total load simulation should be provided.");
|
|
2112
|
+
}
|
|
2113
|
+
return new LoadStrikeTrafficMix(this.name, totalLoad, this.scenarioMix);
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Sets the total workload shape for the mix.
|
|
2117
|
+
* Use this when the same high-level load profile should be distributed by scenario weights.
|
|
2118
|
+
*/
|
|
2119
|
+
WithTotalLoad(...totalLoad) {
|
|
2120
|
+
return this.withTotalLoad(...totalLoad);
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Sets the weighted scenario lanes for the mix.
|
|
2124
|
+
* Use this when each operation should receive a fixed proportion of the total workload.
|
|
2125
|
+
*/
|
|
2126
|
+
withScenarioMix(...scenarioMix) {
|
|
2127
|
+
if (!scenarioMix.length) {
|
|
2128
|
+
throw new Error("At least one scenario share should be provided.");
|
|
2129
|
+
}
|
|
2130
|
+
if (scenarioMix.some((share) => !(share instanceof LoadStrikeScenarioShare))) {
|
|
2131
|
+
throw new TypeError("Scenario share collection cannot contain null values.");
|
|
2132
|
+
}
|
|
2133
|
+
return new LoadStrikeTrafficMix(this.name, this.totalLoad, scenarioMix);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Sets the weighted scenario lanes for the mix.
|
|
2137
|
+
* Use this when each operation should receive a fixed proportion of the total workload.
|
|
2138
|
+
*/
|
|
2139
|
+
WithScenarioMix(...scenarioMix) {
|
|
2140
|
+
return this.withScenarioMix(...scenarioMix);
|
|
2141
|
+
}
|
|
2142
|
+
expandScenarios() {
|
|
2143
|
+
return expandTrafficMixScenarios(this);
|
|
2144
|
+
}
|
|
2145
|
+
ExpandScenarios() {
|
|
2146
|
+
return this.expandScenarios();
|
|
2147
|
+
}
|
|
2148
|
+
__loadStrikeTotalLoad() {
|
|
2149
|
+
return this.totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
|
|
2150
|
+
}
|
|
2151
|
+
__loadStrikeScenarioMix() {
|
|
2152
|
+
return [...this.scenarioMix];
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1951
2155
|
export class LoadStrikeRunner {
|
|
1952
2156
|
constructor(scenarios, options, contextConfigurators = []) {
|
|
1953
2157
|
this.scenarios = scenarios;
|
|
@@ -1983,6 +2187,20 @@ export class LoadStrikeRunner {
|
|
|
1983
2187
|
static RegisterScenarios(...scenarios) {
|
|
1984
2188
|
return LoadStrikeRunner.registerScenarios(...scenarios);
|
|
1985
2189
|
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Registers a traffic mix on a fresh runnable context.
|
|
2192
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2193
|
+
*/
|
|
2194
|
+
static registerTrafficMix(trafficMix) {
|
|
2195
|
+
return LoadStrikeRunner.registerScenarios(...expandTrafficMixScenarios(trafficMix));
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Registers a traffic mix on a fresh runnable context.
|
|
2199
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2200
|
+
*/
|
|
2201
|
+
static RegisterTrafficMix(trafficMix) {
|
|
2202
|
+
return LoadStrikeRunner.registerTrafficMix(trafficMix);
|
|
2203
|
+
}
|
|
1986
2204
|
/**
|
|
1987
2205
|
* Toggles realtime console metric output.
|
|
1988
2206
|
* Use this when a local run or CI log should stream live throughput and latency updates.
|
|
@@ -2237,6 +2455,21 @@ export class LoadStrikeRunner {
|
|
|
2237
2455
|
AddScenarios(...scenarios) {
|
|
2238
2456
|
return this.addScenarios(...scenarios);
|
|
2239
2457
|
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Adds a traffic mix to the current runner.
|
|
2460
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2461
|
+
*/
|
|
2462
|
+
addTrafficMix(trafficMix) {
|
|
2463
|
+
this.scenarios = [...this.scenarios, ...expandTrafficMixScenarios(trafficMix)];
|
|
2464
|
+
return this;
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Adds a traffic mix to the current runner.
|
|
2468
|
+
* Use this when one total load profile should be split across weighted scenario lanes.
|
|
2469
|
+
*/
|
|
2470
|
+
AddTrafficMix(trafficMix) {
|
|
2471
|
+
return this.addTrafficMix(trafficMix);
|
|
2472
|
+
}
|
|
2240
2473
|
/**
|
|
2241
2474
|
* Applies a grouped configuration change to the current builder or context.
|
|
2242
2475
|
* Use this when several related settings should be supplied in one step.
|
|
@@ -4388,6 +4621,111 @@ function attachLoadSimulationProjection(simulation) {
|
|
|
4388
4621
|
});
|
|
4389
4622
|
return simulation;
|
|
4390
4623
|
}
|
|
4624
|
+
function expandTrafficMixScenarios(trafficMix) {
|
|
4625
|
+
if (!(trafficMix instanceof LoadStrikeTrafficMix)) {
|
|
4626
|
+
throw new TypeError("Traffic mix must be provided.");
|
|
4627
|
+
}
|
|
4628
|
+
const totalLoad = trafficMix.__loadStrikeTotalLoad();
|
|
4629
|
+
if (!totalLoad.length) {
|
|
4630
|
+
throw new Error("Traffic mix total load must be configured before registration.");
|
|
4631
|
+
}
|
|
4632
|
+
const scenarioMix = trafficMix.__loadStrikeScenarioMix();
|
|
4633
|
+
if (!scenarioMix.length) {
|
|
4634
|
+
throw new Error("Traffic mix scenario shares must be configured before registration.");
|
|
4635
|
+
}
|
|
4636
|
+
const weights = scenarioMix.map((share) => share.weight);
|
|
4637
|
+
return scenarioMix.map((share, index) => {
|
|
4638
|
+
const splitSimulations = totalLoad
|
|
4639
|
+
.map((simulation) => splitTrafficSimulation(simulation, weights, index))
|
|
4640
|
+
.filter((simulation) => simulation != null);
|
|
4641
|
+
const scenario = !splitSimulations.length
|
|
4642
|
+
? share.scenario.withLoadSimulations(LoadStrikeSimulation.pause(0))
|
|
4643
|
+
: share.scenario.withLoadSimulations(...splitSimulations);
|
|
4644
|
+
return scenario.__loadStrikeWithInternalLicenseFeatures(TRAFFIC_MIX_FEATURE);
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
function splitTrafficSimulation(simulation, weights, index) {
|
|
4648
|
+
const kind = String(simulation.Kind ?? "");
|
|
4649
|
+
const duringSeconds = readFiniteSimulationNumber(simulation, "DuringSeconds");
|
|
4650
|
+
const intervalSeconds = readFiniteSimulationNumber(simulation, "IntervalSeconds");
|
|
4651
|
+
if (kind === "Inject") {
|
|
4652
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4653
|
+
return rate > 0 ? LoadStrikeSimulation.inject(rate, intervalSeconds, duringSeconds) : null;
|
|
4654
|
+
}
|
|
4655
|
+
if (kind === "RampingInject") {
|
|
4656
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4657
|
+
return rate > 0 ? LoadStrikeSimulation.rampingInject(rate, intervalSeconds, duringSeconds) : null;
|
|
4658
|
+
}
|
|
4659
|
+
if (kind === "InjectRandom") {
|
|
4660
|
+
const minRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MinRate"), weights)[index];
|
|
4661
|
+
const maxRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MaxRate"), weights)[index];
|
|
4662
|
+
if (maxRate <= 0) {
|
|
4663
|
+
return null;
|
|
4664
|
+
}
|
|
4665
|
+
return LoadStrikeSimulation.injectRandom(Math.min(minRate, maxRate), maxRate, intervalSeconds, duringSeconds);
|
|
4666
|
+
}
|
|
4667
|
+
if (kind === "IterationsForInject") {
|
|
4668
|
+
const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
|
|
4669
|
+
const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
|
|
4670
|
+
return rate > 0 && iterations > 0
|
|
4671
|
+
? LoadStrikeSimulation.iterationsForInject(rate, intervalSeconds, iterations)
|
|
4672
|
+
: null;
|
|
4673
|
+
}
|
|
4674
|
+
if (kind === "IterationsForConstant") {
|
|
4675
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4676
|
+
const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
|
|
4677
|
+
return copies > 0 && iterations > 0
|
|
4678
|
+
? LoadStrikeSimulation.iterationsForConstant(copies, iterations)
|
|
4679
|
+
: null;
|
|
4680
|
+
}
|
|
4681
|
+
if (kind === "KeepConstant") {
|
|
4682
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4683
|
+
return copies > 0 ? LoadStrikeSimulation.keepConstant(copies, duringSeconds) : null;
|
|
4684
|
+
}
|
|
4685
|
+
if (kind === "RampingConstant") {
|
|
4686
|
+
const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
|
|
4687
|
+
return copies > 0 ? LoadStrikeSimulation.rampingConstant(copies, duringSeconds) : null;
|
|
4688
|
+
}
|
|
4689
|
+
if (kind === "Pause") {
|
|
4690
|
+
return LoadStrikeSimulation.pause(duringSeconds);
|
|
4691
|
+
}
|
|
4692
|
+
return attachLoadSimulationProjection({ ...simulation });
|
|
4693
|
+
}
|
|
4694
|
+
function splitTrafficValue(totalValue, weights) {
|
|
4695
|
+
const normalizedTotal = Math.trunc(Number.isFinite(totalValue) ? totalValue : 0);
|
|
4696
|
+
if (normalizedTotal <= 0) {
|
|
4697
|
+
return weights.map(() => 0);
|
|
4698
|
+
}
|
|
4699
|
+
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
|
|
4700
|
+
if (totalWeight <= 0) {
|
|
4701
|
+
throw new Error("Traffic mix scenario weights must add up to more than zero.");
|
|
4702
|
+
}
|
|
4703
|
+
const exactShares = weights.map((weight, index) => {
|
|
4704
|
+
const exact = (normalizedTotal * weight) / totalWeight;
|
|
4705
|
+
return {
|
|
4706
|
+
index,
|
|
4707
|
+
value: Math.floor(exact),
|
|
4708
|
+
remainder: exact - Math.floor(exact)
|
|
4709
|
+
};
|
|
4710
|
+
});
|
|
4711
|
+
const split = exactShares.map((share) => share.value);
|
|
4712
|
+
let remaining = normalizedTotal - split.reduce((sum, value) => sum + value, 0);
|
|
4713
|
+
for (const share of [...exactShares].sort((left, right) => {
|
|
4714
|
+
const remainderDelta = right.remainder - left.remainder;
|
|
4715
|
+
return remainderDelta !== 0 ? remainderDelta : left.index - right.index;
|
|
4716
|
+
})) {
|
|
4717
|
+
if (remaining <= 0) {
|
|
4718
|
+
break;
|
|
4719
|
+
}
|
|
4720
|
+
split[share.index] += 1;
|
|
4721
|
+
remaining -= 1;
|
|
4722
|
+
}
|
|
4723
|
+
return split;
|
|
4724
|
+
}
|
|
4725
|
+
function readFiniteSimulationNumber(simulation, key) {
|
|
4726
|
+
const value = Number(simulation[key]);
|
|
4727
|
+
return Number.isFinite(value) ? value : 0;
|
|
4728
|
+
}
|
|
4391
4729
|
function attachScenarioStatsAliases(scenario) {
|
|
4392
4730
|
const normalized = normalizeScenarioStatsValue(scenario);
|
|
4393
4731
|
normalized.ok = attachMeasurementStatsAliases(normalized.ok);
|
|
@@ -8346,6 +8684,7 @@ export const __loadstrikeTestExports = {
|
|
|
8346
8684
|
detailedToNodeStats,
|
|
8347
8685
|
evaluateThresholdsForScenarios,
|
|
8348
8686
|
executeTrackedScenarioInvocation,
|
|
8687
|
+
expandTrafficMixScenarios,
|
|
8349
8688
|
extractContextOverridesFromArgs,
|
|
8350
8689
|
extractContextOverridesFromConfig,
|
|
8351
8690
|
findCaseInsensitiveKey,
|