SERVER-92467 Add golden tests and utilities for classic & sharded suite (#31391)

GitOrigin-RevId: bbb688b725b4d937d7075ab2f5aa6f03f2343f52
This commit is contained in:
James H 2025-04-07 15:47:49 +01:00 committed by MongoDB Bot
parent 713ba3dbdc
commit 46f88aadc8
15 changed files with 270 additions and 22 deletions

View File

@ -10,6 +10,9 @@
# Hopefully we will use prettier for more file types in the future # Hopefully we will use prettier for more file types in the future
!*.md !*.md
# Ignore all golden test output files
jstests/*golden*/expected_output/*
# Ignore all template files # Ignore all template files
# When we eventually enable prettier on javascript these files are invalid and should be ignored # When we eventually enable prettier on javascript these files are invalid and should be ignored
**/*.tpl.* **/*.tpl.*

View File

@ -16,10 +16,11 @@ executor:
crashOnInvalidBSONError: "" crashOnInvalidBSONError: ""
objcheck: "" objcheck: ""
eval: | eval: |
// Keep in sync with query_golden_cqf.yml. // Keep in sync with query_golden_*.yml.
await import("jstests/libs/override_methods/detect_spawning_own_mongod.js"); await import("jstests/libs/override_methods/detect_spawning_own_mongod.js");
await import("jstests/libs/override_methods/golden_overrides.js"); await import("jstests/libs/override_methods/golden_overrides.js");
_openGoldenData(jsTestName(), {relativePath: "jstests/query_golden/expected_output"}); import {beginGoldenTest} from "jstests/libs/begin_golden_test.js";
beginGoldenTest("jstests/query_golden/expected_output");
hooks: hooks:
- class: ValidateCollections - class: ValidateCollections
shell_options: shell_options:

View File

@ -0,0 +1,26 @@
test_kind: js_test
selector:
roots:
- jstests/query_golden_sharding/**/*.js
executor:
archive:
tests:
- jstests/sharding/*reshard*.js
config:
# Based on sharding.yml
shell_options:
crashOnInvalidBSONError: ""
objcheck: ""
global_vars:
TestData:
setParameters:
defaultConfigCommandTimeoutMS: 90000
setParametersMongos:
defaultConfigCommandTimeoutMS: 90000
nodb: ""
eval: |
// Keep in sync with query_golden_*.yml.
import {beginGoldenTest} from "jstests/libs/begin_golden_test.js";
await import("jstests/libs/override_methods/sharded_golden_overrides.js");
beginGoldenTest("jstests/query_golden_sharding/expected_output", ".md");

View File

@ -944,6 +944,15 @@ tasks:
vars: vars:
suite: query_golden_classic suite: query_golden_classic
- <<: *task_template
name: query_golden_sharding
tags: ["assigned_to_jira_team_server_query_optimization", "default"]
commands:
- func: "do setup"
- func: "run tests"
vars:
suite: query_golden_sharding
################################################ ################################################
# Query Integration tasks # # Query Integration tasks #
################################################ ################################################

View File

@ -294,6 +294,7 @@ buildvariants:
- name: .multi_shard - name: .multi_shard
- name: .query_fuzzer - name: .query_fuzzer
- name: query_golden_classic - name: query_golden_classic
- name: query_golden_sharding
- name: .random_multiversion_ds - name: .random_multiversion_ds
- name: .read_only - name: .read_only
- name: .read_write_concern !.large - name: .read_write_concern !.large

View File

@ -803,6 +803,7 @@ buildvariants:
- name: .multi_shard - name: .multi_shard
- name: .query_fuzzer - name: .query_fuzzer
- name: query_golden_classic - name: query_golden_classic
- name: query_golden_sharding
- name: .random_multiversion_ds - name: .random_multiversion_ds
- name: .read_only - name: .read_only
- name: .read_write_concern !.large - name: .read_write_concern !.large

View File

@ -112,6 +112,7 @@ buildvariants:
- name: jsCore_txns_large_txns_format - name: jsCore_txns_large_txns_format
- name: json_schema - name: json_schema
- name: query_golden_classic - name: query_golden_classic
- name: query_golden_sharding
- name: libunwind_tests - name: libunwind_tests
- name: .multi_shard - name: .multi_shard
- name: multi_stmt_txn_jscore_passthrough_with_migration_gen - name: multi_stmt_txn_jscore_passthrough_with_migration_gen

View File

@ -250,7 +250,7 @@ export function flattenPlan(plan) {
]; ];
// Expand this array if you find new fields which are inconsistent across different test runs. // Expand this array if you find new fields which are inconsistent across different test runs.
const ignoreFields = ["isCached", "indexVersion", "planNodeId"]; const ignoreFields = ["isCached", "indexVersion", "filter", "planNodeId"];
// Iterates over the plan while ignoring the `ignoreFields`, to create flattened stages whenever // Iterates over the plan while ignoring the `ignoreFields`, to create flattened stages whenever
// `childFields` are encountered. // `childFields` are encountered.
@ -279,6 +279,115 @@ export function flattenPlan(plan) {
return results; return results;
} }
/**
* Returns an object containing the winning plan and an array of rejected plans for the given
* queryPlanner. Each of those plans is returned in its flattened form.
*/
export function formatQueryPlanner(queryPlanner) {
return {
winningPlan: flattenPlan(getWinningPlan(queryPlanner)),
rejectedPlans: queryPlanner.rejectedPlans.map(flattenPlan),
};
}
/**
* Formats the given pipeline, which must be an array of stage objects. Returns an array of
* formatted stages. It excludes fields which might differ in the explain across multiple executions
* of the same query.
*/
export function formatPipeline(pipeline) {
const results = [];
// Pipeline must be an array of objects
if (!pipeline || !Array.isArray(pipeline) || !pipeline.every(isPlainObject)) {
return results;
}
// Expand this array if you find new fields which are inconsistent across different test runs.
const ignoreFields = ["lsid"];
for (const stage of pipeline) {
const keys = Object.keys(stage).filter(key => key.startsWith("$"));
if (keys.length !== 1) {
throw Error("This is not a stage: " + tojson(stage));
}
const stageName = keys[0];
if (stageName == "$cursor") {
const queryPlanner = stage[stageName].queryPlanner;
results.push({[stageName]: formatQueryPlanner(queryPlanner)});
} else {
const stageCopy = {...stage[stageName]};
ignoreFields.forEach(field => delete stageCopy[field]);
// Don't keep any fields that are on the same level as the stage name
results.push({[stageName]: stageCopy});
}
}
return results;
}
/**
* Helper function to only add `field` to `dest` if it is present in `src`. A lambda can be passed
* to transform the field value when it is added to `dest`.
*/
function addIfPresent(field, src, dest, lambda = i => i) {
if (src && dest && field in src) {
dest[field] = lambda(src[field]);
}
}
/**
* If queryPlanner contains an array of shards, this returns both the merger part and shards
* part. Both are flattened.
*/
function invertShards(queryPlanner) {
const winningPlan = queryPlanner.winningPlan;
const shards = winningPlan.shards;
if (!Array.isArray(shards)) {
throw Error("Expected shards field to be array, got: " + tojson(shards));
}
const topStage = {...winningPlan};
delete topStage.shards;
const res = {mergerPart: flattenPlan(topStage), shardsPart: {}};
shards.forEach(shard => res.shardsPart[shard.shardName] = formatQueryPlanner(shard));
return res;
}
/**
* Returns a formatted version of the explain, excluding fields which might differ in the explain
* across multiple executions of the same query (e.g. caching information or UUIDs).
*/
export function formatExplainRoot(explain) {
let res = {};
if (!isPlainObject(explain)) {
return res;
}
addIfPresent("mergeType", explain, res);
if ("splitPipeline" in explain) {
addIfPresent("mergerPart", explain.splitPipeline, res, formatPipeline);
addIfPresent("shardsPart", explain.splitPipeline, res, formatPipeline);
}
if ("shards" in explain) {
for (const [shardName, shardExplain] of Object.entries(explain["shards"])) {
res[shardName] = formatPipeline(shardExplain.stages);
}
} else if ("queryPlanner" in explain && "shards" in explain.queryPlanner.winningPlan) {
res = {...res, ...invertShards(explain.queryPlanner)};
} else if ("queryPlanner" in explain) {
res = {...res, ...formatQueryPlanner(explain.queryPlanner)};
} else if ("stages" in explain) {
res.stages = formatPipeline(explain.stages);
}
return res;
}
/** /**
* Given the root stage of explain's JSON representation of a query plan ('root'), returns all * Given the root stage of explain's JSON representation of a query plan ('root'), returns all
* subdocuments whose stage is 'stage'. Returns an empty array if the plan does not have the * subdocuments whose stage is 'stage'. Returns an empty array if the plan does not have the

View File

@ -0,0 +1,31 @@
import {checkSbeStatus} from "jstests/libs/sbe_util.js";
// Run any set-up necessary for a golden jstest. This function should be called from the suite
// definition, so that individual tests don't need to remember to call it.
//
// In case the test name ends in "_md", the golden data will be outputted to a markdown file.
// However, if an explicit fileExtension is specified, it will always be used instead.
export function beginGoldenTest(relativePathToExpectedOutput, fileExtension = "") {
// Skip checking SBE status if there is no `db` object when nodb:"" is used.
if (typeof db !== 'undefined') {
let sbeStatus = checkSbeStatus(db);
if (fileExists(relativePathToExpectedOutput + "/" + sbeStatus + "/" + jsTestName())) {
relativePathToExpectedOutput += "/" + sbeStatus;
}
}
let outputName = jsTestName();
const testNameParts = jsTestName().split("_");
// If the test name ends in "_md" and no explicit file extension is specified, then remove the
// "_md" part and use it as the file extension.
// TODO SERVER-92693: Use only the file extension.
if (testNameParts.length > 0 && testNameParts[testNameParts.length - 1] === "md" &&
fileExtension === "") {
fileExtension = ".md";
outputName = testNameParts.slice(0, -1).join("_");
}
_openGoldenData(outputName + fileExtension, {relativePath: relativePathToExpectedOutput});
}

View File

@ -1,17 +1,25 @@
export function tojsonOnelineSortKeys(x) { export function tojsonOnelineSortKeys(x) {
let indent = " "; return tojson(x, " " /*indent*/, true /*nolint*/, undefined /*depth*/, true /*sortKeys*/);
let nolint = true;
let depth = undefined;
let sortKeys = true;
return tojson(x, indent, nolint, depth, sortKeys);
} }
// Takes an array of documents. export function tojsonMultiLineSortKeys(x) {
// - Discards the field ordering, by recursively sorting the fields of each object. return tojson(
// - Discards the result-set ordering by sorting the array of normalized documents. x, undefined /*indent*/, false /*nolint*/, undefined /*depth*/, true /*sortKeys*/);
}
// Takes an array of documents ('result').
// If `shouldSort` is true:
// - Discards the field ordering, by recursively sorting the fields of each object.
// - Discards the result-set ordering by sorting the array of normalized documents.
// Returns a string. // Returns a string.
export function normalize(result) { export function normalizeArray(result, shouldSort = true) {
return result.map(d => tojsonOnelineSortKeys(d)).sort().join('\n') + '\n'; if (!Array.isArray(result)) {
throw Error("The result is not an array: " + tojson(result));
}
const normalizedResults = shouldSort ? result.map(d => tojsonOnelineSortKeys(d)).sort()
: result.map(d => tojsononeline(d));
return normalizedResults.join('\n') + '\n';
} }
// Takes an array or cursor, and prints a normalized version of it. // Takes an array or cursor, and prints a normalized version of it.
@ -31,13 +39,5 @@ export function show(cursorOrArray) {
} }
} }
print(normalize(cursorOrArray)); print(normalizeArray(cursorOrArray));
}
// Run any set-up necessary for a golden jstest.
// This function should be called from the suite definition, so that individual tests don't need
// to remember to call it. This function should not be called from any libs/*.js file, because
// it's surprising if load() has side effects (besides defining JS functions / values).
export function beginGoldenTest() {
_openGoldenData(jsTestName());
} }

View File

@ -22,3 +22,7 @@ globalThis.print = (() => {
return original(...args); return original(...args);
}; };
})(); })();
// Initialize `printGolden` to have the same behavior as `print`. This is needed to utilize markdown
// support (i.e. pretty_md.js) in this golden test suite.
globalThis.printGolden = globalThis.print;

View File

@ -0,0 +1,15 @@
// TODO SERVER-92693: Use only one overrides file (golden_overrides.js) for golden tests.
// Initialize printGolden to output to both stdout and the golden file.
// Note that no other print functions are overriden here, so those won't output to the golden file.
globalThis.printGolden = function(...args) {
let str = args.map(a => a == null ? '[unknown type]' : a).join(' ');
// Make sure each printGolden() call ends in a newline.
if (str.slice(-1) !== '\n') {
str += '\n';
}
_writeGoldenData(str);
print(...args);
};

34
jstests/libs/pretty_md.js Normal file
View File

@ -0,0 +1,34 @@
/**
* Provides helper functions to output content to markdown. This is used for golden testing, using
* `printGolden` to write to the expected output files.
*/
/* eslint-disable no-undef */
let sectionCount = 1;
export function section(msg) {
printGolden(`## ${sectionCount}.`, msg);
sectionCount++;
}
export function subSection(msg) {
printGolden("###", msg);
}
export function line(msg) {
printGolden(msg);
}
export function codeOneLine(msg) {
printGolden("`" + tojsononeline(msg) + "`");
}
export function code(msg, fmt = "json") {
printGolden("```" + fmt);
printGolden(msg);
printGolden("```");
}
export function linebreak() {
printGolden();
}

View File

@ -0,0 +1,8 @@
import {line} from "jstests/libs/pretty_md.js";
line(
`TODO: This is an empty file to allow adding
buildscripts/resmokeconfig/suites/query_golden_sharding.yml without failures, as the roots
selector does not currently match any real tests. This is introduced as part of a partial
backport of SERVER-92467, and will be removed as part of a backport for SERVER-94315 which will
use the introduced query_golden_sharding suite.
`);

View File

@ -0,0 +1,5 @@
TODO: This is an empty file to allow adding
buildscripts/resmokeconfig/suites/query_golden_sharding.yml without failures, as the roots
selector does not currently match any real tests. This is introduced as part of a partial
backport of SERVER-92467, and will be removed as part of a backport for SERVER-94315 which will
use the introduced query_golden_sharding suite.