Skip to content

Commit d29d7b2

Browse files
anthony-murphy-agentanthony-murphyclaude
committed
feat(local-server-stress-tests): add support for dynamic generator weights
Enable generator weights to be functions that receive state and return a weight, instead of only supporting static numeric weights. This allows weights to vary based on the current test state. Co-Authored-By: anthony-murphy <[email protected]> Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6aaac7e commit d29d7b2

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

packages/test/local-server-stress-tests/src/baseModel.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import * as path from "node:path";
77

88
import {
99
type AsyncGenerator,
10-
type AsyncWeights,
1110
type BaseOperation,
1211
combineReducersAsync,
13-
createWeightedAsyncGenerator,
1412
done,
1513
isOperationType,
1614
type MinimizationTransform,
@@ -28,6 +26,10 @@ import {
2826
type OrderSequentially,
2927
} from "./ddsOperations";
3028
import { _dirname } from "./dirname.cjs";
29+
import {
30+
createWeightedAsyncGeneratorWithDynamicWeights,
31+
type DynamicAsyncWeights,
32+
} from "./dynamicWeightGenerator.js";
3133
import type { LocalServerStressState } from "./localServerStressHarness";
3234
import type { StressDataObjectOperations } from "./stressDataObject.js";
3335

@@ -76,9 +78,9 @@ export const reducer = combineReducersAsync<StressOperations, LocalServerStressS
7678
});
7779

7880
export function makeGenerator<T extends BaseOperation>(
79-
additional: AsyncWeights<T, LocalServerStressState> = [],
81+
additional: DynamicAsyncWeights<T, LocalServerStressState> = [],
8082
): AsyncGenerator<StressOperations | T, LocalServerStressState> {
81-
const asyncGenerator = createWeightedAsyncGenerator<
83+
const asyncGenerator = createWeightedAsyncGeneratorWithDynamicWeights<
8284
StressOperations | T,
8385
LocalServerStressState
8486
>([
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import type {
7+
AcceptanceCondition,
8+
AsyncGenerator,
9+
BaseFuzzTestState,
10+
} from "@fluid-private/stochastic-test-utils";
11+
12+
/**
13+
* A function that computes a weight dynamically based on the current state.
14+
*/
15+
export type WeightFunction<TState> = (state: TState) => number;
16+
17+
/**
18+
* A weight can be either a static number or a dynamic function that computes the weight based on state.
19+
*/
20+
export type DynamicWeight<TState> = number | WeightFunction<TState>;
21+
22+
/**
23+
* Array of weighted generators to select from, supporting dynamic weights.
24+
*/
25+
export type DynamicAsyncWeights<TOp, TState> = [
26+
TOp | AsyncGenerator<TOp, TState>,
27+
DynamicWeight<TState>,
28+
AcceptanceCondition<TState>?,
29+
][];
30+
31+
/**
32+
* Evaluates a weight, which can be either a static number or a function that computes the weight.
33+
*/
34+
function evaluateWeight<TState>(weight: DynamicWeight<TState>, state: TState): number {
35+
return typeof weight === "function" ? weight(state) : weight;
36+
}
37+
38+
/**
39+
* Creates a weighted async generator that supports dynamic weights.
40+
*
41+
* Unlike `createWeightedAsyncGenerator` from stochastic-test-utils which only accepts
42+
* static numeric weights, this function allows weights to be functions that are
43+
* evaluated at runtime with the current state.
44+
*
45+
* @param weights - Array of [generator, weight, acceptanceCondition?] tuples where
46+
* weight can be a number or a function (state) =\> number
47+
*/
48+
export function createWeightedAsyncGeneratorWithDynamicWeights<
49+
T,
50+
TState extends BaseFuzzTestState,
51+
>(weights: DynamicAsyncWeights<T, TState>): AsyncGenerator<T, TState> {
52+
return async (state) => {
53+
// Evaluate weights dynamically and compute cumulative sums
54+
const cumulativeSums: [
55+
T | AsyncGenerator<T, TState>,
56+
number,
57+
AcceptanceCondition<TState>?,
58+
][] = [];
59+
let totalWeight = 0;
60+
for (const [generator, weight, acceptCondition] of weights) {
61+
const evaluatedWeight = evaluateWeight(weight, state);
62+
const cumulativeWeight = totalWeight + evaluatedWeight;
63+
if (evaluatedWeight > 0) {
64+
cumulativeSums.push([generator, cumulativeWeight, acceptCondition]);
65+
}
66+
totalWeight = cumulativeWeight;
67+
}
68+
69+
if (totalWeight === 0) {
70+
throw new Error(
71+
"createWeightedAsyncGeneratorWithDynamicWeights must have some positive weight",
72+
);
73+
}
74+
75+
const { random } = state;
76+
const sample = (): number => {
77+
const weightSelected = random.real(0, totalWeight);
78+
79+
let opIndex = 0;
80+
while (cumulativeSums[opIndex][1] < weightSelected) {
81+
opIndex++;
82+
}
83+
84+
return opIndex;
85+
};
86+
87+
let index;
88+
let shouldAccept: AcceptanceCondition<TState> | undefined;
89+
do {
90+
index = sample();
91+
shouldAccept = cumulativeSums[index][2];
92+
} while (!(shouldAccept?.(state) ?? true));
93+
94+
const [tOrGenerator] = cumulativeSums[index];
95+
return typeof tOrGenerator === "function"
96+
? (tOrGenerator as AsyncGenerator<T, TState>)(state)
97+
: (tOrGenerator as unknown as T);
98+
};
99+
}

0 commit comments

Comments
 (0)