mirror of https://github.com/mongodb/mongo
SERVER-92467 Add golden tests and utilities for classic & sharded suite (#31391)
GitOrigin-RevId: bbb688b725b4d937d7075ab2f5aa6f03f2343f52
This commit is contained in:
parent
713ba3dbdc
commit
46f88aadc8
|
|
@ -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.*
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -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 #
|
||||||
################################################
|
################################################
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
@ -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());
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
`);
|
||||||
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue