Skip to content

Commit 6e26f55

Browse files
mattBorosMongoDB Bot
authored andcommitted
SERVER-102989 Refactor PBT runner to use a workload model (#34208)
GitOrigin-RevId: 9256f7f
1 parent 5fb0bad commit 6e26f55

File tree

12 files changed

+171
-70
lines changed

12 files changed

+171
-70
lines changed

jstests/aggregation/sources/agg_stages_basic_behavior_pbt.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
limitArb,
3030
sortArb
3131
} from "jstests/libs/property_test_helpers/models/query_models.js";
32+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
3233
import {testProperty} from "jstests/libs/property_test_helpers/property_testing_utils.js";
3334
import {isSlowBuild} from "jstests/libs/query/aggregation_pipeline_utils.js";
3435
import {fc} from "jstests/third_party/fast_check/fc-3.1.0.js";
@@ -216,15 +217,18 @@ for (const {stageArb, checkResultsFn, failMsg} of testCases) {
216217
});
217218

218219
// Run the property with a regular collection.
219-
testProperty(propFn,
220-
{experimentColl},
221-
{collModel: getCollectionModel(), aggModel},
222-
{numRuns, numQueriesPerRun: 20});
220+
testProperty(
221+
propFn,
222+
{experimentColl},
223+
makeWorkloadModel({collModel: getCollectionModel(), aggModel, numQueriesPerRun: 20}),
224+
numRuns);
223225

224226
// TODO SERVER-101271 re-enable timeseries PBT testing.
225227
// Run the property with a TS collection.
226228
// testProperty(propFn,
227229
// {experimentColl},
228-
// {collModel: getCollectionModel({isTS: true}), aggModel},
229-
// {numRuns, numQueriesPerRun: 20});
230+
// makeWorkloadModel(
231+
// {collModel: getCollectionModel({isTS: true}), aggModel, numQueriesPerRun:
232+
// 20}),
233+
// numRuns);
230234
}

jstests/core/query/index_correctness_pbt.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import {createCorrectnessProperty} from "jstests/libs/property_test_helpers/common_properties.js";
2020
import {getCollectionModel} from "jstests/libs/property_test_helpers/models/collection_models.js";
2121
import {getAggPipelineModel} from "jstests/libs/property_test_helpers/models/query_models.js";
22+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
2223
import {testProperty} from "jstests/libs/property_test_helpers/property_testing_utils.js";
2324
import {isSlowBuild} from "jstests/libs/query/aggregation_pipeline_utils.js";
2425

@@ -37,8 +38,8 @@ const aggModel = getAggPipelineModel();
3738
// Test with a regular collection.
3839
testProperty(correctnessProperty,
3940
{controlColl, experimentColl},
40-
{collModel: getCollectionModel(), aggModel},
41-
{numRuns, numQueriesPerRun});
41+
makeWorkloadModel({collModel: getCollectionModel(), aggModel, numQueriesPerRun}),
42+
numRuns);
4243

4344
// TODO SERVER-101271 re-enable PBT testing for time-series
4445
// // Test with a TS collection.
@@ -51,7 +52,9 @@ testProperty(correctnessProperty,
5152
// }
5253
// return true;
5354
// });
54-
// testProperty(correctnessProperty,
55-
// {controlColl, experimentColl},
56-
// {collModel: getCollectionModel({isTS: true}), aggModel: tsAggModel},
57-
// {numRuns, numQueriesPerRun});
55+
// testProperty(
56+
// correctnessProperty,
57+
// {controlColl, experimentColl},
58+
// makeWorkloadModel(
59+
// {collModel: getCollectionModel({isTS: true}), aggModel: tsAggModel, numQueriesPerRun}),
60+
// numRuns);

jstests/core/query/plan_cache/cache_correctness_pbt.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from "jstests/libs/property_test_helpers/common_properties.js";
2323
import {getCollectionModel} from "jstests/libs/property_test_helpers/models/collection_models.js";
2424
import {getAggPipelineModel} from "jstests/libs/property_test_helpers/models/query_models.js";
25+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
2526
import {testProperty} from "jstests/libs/property_test_helpers/property_testing_utils.js";
2627
import {isSlowBuild} from "jstests/libs/query/aggregation_pipeline_utils.js";
2728

@@ -41,8 +42,8 @@ const aggModel = getAggPipelineModel();
4142
// Test with a regular collection.
4243
testProperty(correctnessProperty,
4344
{controlColl, experimentColl},
44-
{collModel: getCollectionModel(), aggModel},
45-
{numRuns, numQueriesPerRun});
45+
makeWorkloadModel({collModel: getCollectionModel(), aggModel, numQueriesPerRun}),
46+
numRuns);
4647

4748
// TODO SERVER-101271 re-enable PBT testing for time-series
4849
// // Test with a TS collection.
@@ -55,7 +56,9 @@ testProperty(correctnessProperty,
5556
// }
5657
// return true;
5758
// });
58-
// testProperty(correctnessProperty,
59-
// {controlColl, experimentColl},
60-
// {collModel: getCollectionModel({isTS: true}), aggModel: tsAggModel},
61-
// {numRuns, numQueriesPerRun});
59+
// testProperty(
60+
// correctnessProperty,
61+
// {controlColl, experimentColl},
62+
// makeWorkloadModel(
63+
// {collModel: getCollectionModel({isTS: true}), aggModel: tsAggModel, numQueriesPerRun}),
64+
// numRuns);

jstests/core/query/plan_cache/cache_usage_pbt.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
import {getCollectionModel} from "jstests/libs/property_test_helpers/models/collection_models.js";
1818
import {getAggPipelineModel} from "jstests/libs/property_test_helpers/models/query_models.js";
19+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
1920
import {
2021
getPlanCache,
2122
testProperty
@@ -83,11 +84,13 @@ function repeatQueriesUseCache(getQuery, testHelpers) {
8384

8485
const aggModel = getAggPipelineModel();
8586

86-
testProperty(repeatQueriesUseCache,
87-
{experimentColl},
88-
{collModel: getCollectionModel({isTS: false}), aggModel},
89-
{numRuns, numQueriesPerRun});
90-
testProperty(repeatQueriesUseCache,
91-
{experimentColl},
92-
{collModel: getCollectionModel({isTS: true}), aggModel},
93-
{numRuns, numQueriesPerRun});
87+
testProperty(
88+
repeatQueriesUseCache,
89+
{experimentColl},
90+
makeWorkloadModel({collModel: getCollectionModel({isTS: false}), aggModel, numQueriesPerRun}),
91+
numRuns);
92+
testProperty(
93+
repeatQueriesUseCache,
94+
{experimentColl},
95+
makeWorkloadModel({collModel: getCollectionModel({isTS: true}), aggModel, numQueriesPerRun}),
96+
numRuns);

jstests/core/query/plan_cache/queries_create_one_cache_entry_pbt.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import {getCollectionModel} from "jstests/libs/property_test_helpers/models/collection_models.js";
1717
import {getAggPipelineModel} from "jstests/libs/property_test_helpers/models/query_models.js";
18+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
1819
import {
1920
getPlanCache,
2021
testProperty
@@ -60,11 +61,13 @@ function identicalQueryCreatesAtMostOneCacheEntry(getQuery, testHelpers) {
6061

6162
const aggModel = getAggPipelineModel({allowOrs: false});
6263

63-
testProperty(identicalQueryCreatesAtMostOneCacheEntry,
64-
{experimentColl},
65-
{collModel: getCollectionModel({isTS: false}), aggModel},
66-
{numRuns, numQueriesPerRun});
67-
testProperty(identicalQueryCreatesAtMostOneCacheEntry,
68-
{experimentColl},
69-
{collModel: getCollectionModel({isTS: true}), aggModel},
70-
{numRuns, numQueriesPerRun});
64+
testProperty(
65+
identicalQueryCreatesAtMostOneCacheEntry,
66+
{experimentColl},
67+
makeWorkloadModel({collModel: getCollectionModel({isTS: false}), aggModel, numQueriesPerRun}),
68+
numRuns);
69+
testProperty(
70+
identicalQueryCreatesAtMostOneCacheEntry,
71+
{experimentColl},
72+
makeWorkloadModel({collModel: getCollectionModel({isTS: true}), aggModel, numQueriesPerRun}),
73+
numRuns);

jstests/core/query/run_all_plans_pbt.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import {getDifferentlyShapedQueries} from "jstests/libs/property_test_helpers/common_properties.js";
2929
import {getCollectionModel} from "jstests/libs/property_test_helpers/models/collection_models.js";
3030
import {getAggPipelineModel} from "jstests/libs/property_test_helpers/models/query_models.js";
31+
import {makeWorkloadModel} from "jstests/libs/property_test_helpers/models/workload_models.js";
3132
import {
3233
runDeoptimized,
3334
testProperty
@@ -95,8 +96,8 @@ const aggModel = getAggPipelineModel();
9596
// Test with a regular collection.
9697
testProperty(hintedQueryHasSameResultsAsControlCollScan,
9798
{controlColl, experimentColl},
98-
{collModel: getCollectionModel(), aggModel},
99-
{numRuns, numQueriesPerRun});
99+
makeWorkloadModel({collModel: getCollectionModel(), aggModel, numQueriesPerRun}),
100+
numRuns);
100101

101102
// TODO SERVER-101271 re-enable PBT testing for time-series
102103
// // Test with a TS collection.
@@ -112,6 +113,10 @@ testProperty(hintedQueryHasSameResultsAsControlCollScan,
112113
// });
113114
// testProperty(hintedQueryHasSameResultsAsControlCollScan,
114115
// {controlColl, experimentColl},
115-
// {collModel: getCollectionModel({isTS: true}), aggModel: tsAggModel},
116-
// {numRuns, numQueriesPerRun});
116+
// makeWorkloadModel({
117+
// collModel: getCollectionModel({isTS: true}),
118+
// aggModel: tsAggModel,
119+
// numQueriesPerRun
120+
// }),
121+
// numRuns);
117122
// }

jstests/libs/property_test_helpers/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,40 @@ There are inconsistencies in our query language that are accepted behavior, but
4646

4747
Floating point values are another area the PBTs avoid. Results can differ depending on the order of floating point operations. These differences can propogate. For this reason the only number values allowed are integers.
4848

49-
#### Schema
49+
## Modeling Workloads
50+
51+
A workload consists of a collection model and an aggregation model, in the following format:
52+
53+
```
54+
{
55+
collSpec: {
56+
isTS: true/false to indicate if the collection should be time-series
57+
docs: a list of documents
58+
indexes: a list of indexes
59+
},
60+
queries: a list of aggregation pipelines
61+
}
62+
```
63+
64+
Using one workload model instead of separate (and independent) collection models and agg models allows them to be interrelated.
65+
For example, if we want to model a PBT to test partial indexes where every query should satisfy the partial index filter, we can write:
66+
67+
```
68+
fc.record({
69+
partialFilter: partialFilterPredicateModel,
70+
docs: docsModel,
71+
indexes: indexesModel,
72+
aggs: aggsModel
73+
}).map(({partialFilter, docs, indexes, aggs}) => {
74+
// Append {partialFilterExpression: partialFilter} to all index options
75+
// Prefix every query with {$match: partialFilter}
76+
// Return our workload object.
77+
});
78+
```
79+
80+
and this is a valid workload model. If the collection and aggregation models are passed separately, they would be independent an unable to coordinate with shared arbitraries (like `partialFilter`).
81+
82+
### Schema
5083

5184
The Core PBT schema is:
5285

jstests/libs/property_test_helpers/models/collection_models.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ import {
1313
import {fc} from "jstests/third_party/fast_check/fc-3.1.0.js";
1414

1515
// Maximum number of documents that our collection model can generate.
16-
const kMaxNumQueries = 250;
16+
const kMaxNumDocs = 250;
1717

1818
// An array of [0...249] to label our documents with.
1919
const docIds = [];
20-
for (let i = 0; i < kMaxNumQueries; i++) {
20+
for (let i = 0; i < kMaxNumDocs; i++) {
2121
docIds.push(i);
2222
}
23-
const uniqueIdsArb =
24-
fc.shuffledSubarray(docIds, {minLength: kMaxNumQueries, maxLength: kMaxNumQueries});
23+
const uniqueIdsArb = fc.shuffledSubarray(docIds, {minLength: kMaxNumDocs, maxLength: kMaxNumDocs});
2524

2625
function getDocsModel(isTS) {
2726
const docModel = isTS ? timeseriesDocModel : defaultDocModel;
@@ -31,7 +30,7 @@ function getDocsModel(isTS) {
3130
// failure, fast-check will still minimize down to 1 document if possible.
3231
// These docs are 'unlabeled' because we have not assigned them unique _ids yet.
3332
const unlabeledDocsModel =
34-
fc.array(docModel, {minLength: 1, maxLength: kMaxNumQueries, size: '+2'});
33+
fc.array(docModel, {minLength: 1, maxLength: kMaxNumDocs, size: '+2'});
3534
// Now label the docs with unique _ids.
3635
return fc.record({unlabeledDocs: unlabeledDocsModel, _ids: uniqueIdsArb})
3736
.map(({unlabeledDocs, _ids}) => {

jstests/libs/property_test_helpers/models/query_models.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export function getSingleFieldProjectArb(isInclusion, {simpleFieldsOnly = false}
2828
return {$project: {_id: includeIdVal, [field]: includeFieldVal}};
2929
});
3030
}
31-
const projectArb = fc.oneof(getSingleFieldProjectArb(true /*isInclusion*/),
32-
getSingleFieldProjectArb(false /*isInclusion*/));
31+
const projectArb = oneof(getSingleFieldProjectArb(true /*isInclusion*/),
32+
getSingleFieldProjectArb(false /*isInclusion*/));
3333

3434
// Project from one field to another. {$project {a: '$b'}}
3535
const computedProjectArb = fc.tuple(fieldArb, dollarFieldArb).map(function([destField, srcField]) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Fast-check models for workloads. A workload is a collection model and an aggregation model.
3+
* See property_test_helpers/README.md for more detail on the design.
4+
*/
5+
import {fc} from "jstests/third_party/fast_check/fc-3.1.0.js";
6+
7+
function typeCheckSingleAggModel(aggregation) {
8+
// Should be a list of objects.
9+
assert(Array.isArray(aggregation), 'Each aggregation pipeline should be an array.');
10+
for (const aggStage of aggregation) {
11+
assert.eq(typeof aggStage, 'object', 'Each aggregation stage should be an object.');
12+
}
13+
}
14+
15+
// Sample once from the aggsModel to do some type checking. This can prevent accidentally passing
16+
// models to the wrong parameters.
17+
function typeCheckManyAggsModel(aggsModel) {
18+
const aggregations = fc.sample(aggsModel, {numRuns: 1})[0];
19+
// Should be a list of aggregation pipelines.
20+
assert(Array.isArray(aggregations), 'aggsModel should generate an array');
21+
assert.gt(aggregations.length, 0, 'aggsModel should generate a non-empty array');
22+
aggregations.forEach(agg => typeCheckSingleAggModel(agg));
23+
}
24+
25+
/*
26+
* Creates a workload model from the given collection model and aggregation model.
27+
* Can be passed:
28+
* - `aggsModel` which generates multiple aggregation pipelines at a time or
29+
* - `aggModel` and `numQueriesPerRun` which will be used to create an `aggsModel`
30+
*/
31+
export function makeWorkloadModel({collModel, aggModel, aggsModel, numQueriesPerRun} = {}) {
32+
assert(!aggsModel || !aggModel, 'Cannot specify both `aggsModel` and `aggModel`');
33+
assert(
34+
!aggsModel || !numQueriesPerRun,
35+
'Cannot specify `aggsModel` and `numQueriesPerRun`, since `numQueriesPerRun` is only used when provided `aggModel`.');
36+
if (aggModel) {
37+
aggsModel = fc.array(aggModel, {minLength: numQueriesPerRun, maxLength: numQueriesPerRun});
38+
}
39+
typeCheckManyAggsModel(aggsModel);
40+
return fc.record({collSpec: collModel, queries: aggsModel});
41+
}

jstests/libs/property_test_helpers/property_testing_utils.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,21 @@ const okIndexCreationErrorCodes = [
7777
* TODO SERVER-98132 redesign getQuery to be more opaque about how many query shapes and constants
7878
* there are.
7979
*/
80-
function runProperty(propertyFn, namespaces, collectionSpec, queries) {
80+
function runProperty(propertyFn, namespaces, workload) {
81+
const {collSpec, queries} = workload;
8182
const {controlColl, experimentColl} = namespaces;
8283

8384
// Setup the control/experiment collections, define the helper functions, then run the property.
8485
if (controlColl) {
8586
assert(controlColl.drop());
8687
createColl(controlColl);
87-
assert.commandWorked(controlColl.insert(collectionSpec.docs));
88+
assert.commandWorked(controlColl.insert(collSpec.docs));
8889
}
8990

9091
assert(experimentColl.drop());
91-
createColl(experimentColl, collectionSpec.isTS);
92-
assert.commandWorked(experimentColl.insert(collectionSpec.docs));
93-
collectionSpec.indexes.forEach((indexSpec, num) => {
92+
createColl(experimentColl, collSpec.isTS);
93+
assert.commandWorked(experimentColl.insert(collSpec.docs));
94+
collSpec.indexes.forEach((indexSpec, num) => {
9495
const name = "index_" + num;
9596
assert.commandWorkedOrFailedWithCode(
9697
experimentColl.createIndex(indexSpec.def, Object.extend(indexSpec.options, {name})),
@@ -123,9 +124,9 @@ function reporter(propertyFn, namespaces) {
123124
// about the property failure.
124125
jsTestLog('Failed property: ' + propertyFn.name);
125126
jsTestLog(runDetails);
126-
const {collSpec, queries} = runDetails.counterexample[0];
127-
jsTestLog({collSpec, queries});
128-
jsTestLog(runProperty(propertyFn, namespaces, collSpec, queries));
127+
const workload = runDetails.counterexample[0];
128+
jsTestLog(workload);
129+
jsTestLog(runProperty(propertyFn, namespaces, workload));
129130
assert(false);
130131
}
131132
};
@@ -137,8 +138,12 @@ function reporter(propertyFn, namespaces) {
137138
* failure, `runProperty` is called again in the reporter, and prints out more details about the
138139
* failed property.
139140
*/
140-
export function testProperty(
141-
propertyFn, namespaces, {collModel, aggModel}, {numRuns, numQueriesPerRun}) {
141+
export function testProperty(propertyFn, namespaces, workloadModel, numRuns) {
142+
assert.eq(typeof propertyFn, 'function');
143+
assert(Object.keys(namespaces)
144+
.every(collName => collName === 'controlColl' || collName === 'experimentColl'));
145+
assert.eq(typeof numRuns, 'number');
146+
142147
const seed = 4;
143148
jsTestLog('Running property `' + propertyFn.name + '` from test file `' + jsTestName() +
144149
'`, seed = ' + seed);
@@ -150,21 +155,17 @@ export function testProperty(
150155
// True PBT failures (uncaught) are still readable and have stack traces.
151156
TestData.traceExceptions = false;
152157

153-
const nPipelinesModel =
154-
fc.array(aggModel, {minLength: numQueriesPerRun, maxLength: numQueriesPerRun});
155-
const scenarioArb = fc.record({collSpec: collModel, queries: nPipelinesModel});
156-
157158
let alwaysPassed = true;
158-
fc.assert(fc.property(scenarioArb, ({collSpec, queries}) => {
159+
fc.assert(fc.property(workloadModel, workload => {
159160
// Only return if the property passed or not. On failure,
160161
// `runProperty` is called again and more details are exposed.
161-
const result = runProperty(propertyFn, namespaces, collSpec, queries);
162+
const result = runProperty(propertyFn, namespaces, workload);
162163
// If it failed for the first time, print that out so we have the first failure available
163164
// in case shrinking fails.
164165
if (!result.passed && alwaysPassed) {
165166
jsTestLog('The property ' + propertyFn.name + ' from ' + jsTestName() + ' failed');
166167
jsTestLog('Initial inputs **before minimization**');
167-
jsTestLog({collSpec, queries});
168+
jsTestLog(workload);
168169
jsTestLog('Initial failure details **before minimization**');
169170
jsTestLog(result);
170171
alwaysPassed = false;

0 commit comments

Comments
 (0)