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
|
||||
!*.md
|
||||
|
||||
# Ignore all golden test output files
|
||||
jstests/*golden*/expected_output/*
|
||||
|
||||
# Ignore all template files
|
||||
# When we eventually enable prettier on javascript these files are invalid and should be ignored
|
||||
**/*.tpl.*
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ executor:
|
|||
crashOnInvalidBSONError: ""
|
||||
objcheck: ""
|
||||
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/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:
|
||||
- class: ValidateCollections
|
||||
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:
|
||||
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 #
|
||||
################################################
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ buildvariants:
|
|||
- name: .multi_shard
|
||||
- name: .query_fuzzer
|
||||
- name: query_golden_classic
|
||||
- name: query_golden_sharding
|
||||
- name: .random_multiversion_ds
|
||||
- name: .read_only
|
||||
- name: .read_write_concern !.large
|
||||
|
|
|
|||
|
|
@ -803,6 +803,7 @@ buildvariants:
|
|||
- name: .multi_shard
|
||||
- name: .query_fuzzer
|
||||
- name: query_golden_classic
|
||||
- name: query_golden_sharding
|
||||
- name: .random_multiversion_ds
|
||||
- name: .read_only
|
||||
- name: .read_write_concern !.large
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ buildvariants:
|
|||
- name: jsCore_txns_large_txns_format
|
||||
- name: json_schema
|
||||
- name: query_golden_classic
|
||||
- name: query_golden_sharding
|
||||
- name: libunwind_tests
|
||||
- name: .multi_shard
|
||||
- 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.
|
||||
const ignoreFields = ["isCached", "indexVersion", "planNodeId"];
|
||||
const ignoreFields = ["isCached", "indexVersion", "filter", "planNodeId"];
|
||||
|
||||
// Iterates over the plan while ignoring the `ignoreFields`, to create flattened stages whenever
|
||||
// `childFields` are encountered.
|
||||
|
|
@ -279,6 +279,115 @@ export function flattenPlan(plan) {
|
|||
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
|
||||
* 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) {
|
||||
let indent = " ";
|
||||
let nolint = true;
|
||||
let depth = undefined;
|
||||
let sortKeys = true;
|
||||
return tojson(x, indent, nolint, depth, sortKeys);
|
||||
return tojson(x, " " /*indent*/, true /*nolint*/, undefined /*depth*/, true /*sortKeys*/);
|
||||
}
|
||||
|
||||
// Takes an array of documents.
|
||||
// - Discards the field ordering, by recursively sorting the fields of each object.
|
||||
// - Discards the result-set ordering by sorting the array of normalized documents.
|
||||
export function tojsonMultiLineSortKeys(x) {
|
||||
return tojson(
|
||||
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.
|
||||
export function normalize(result) {
|
||||
return result.map(d => tojsonOnelineSortKeys(d)).sort().join('\n') + '\n';
|
||||
export function normalizeArray(result, shouldSort = true) {
|
||||
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.
|
||||
|
|
@ -31,13 +39,5 @@ export function show(cursorOrArray) {
|
|||
}
|
||||
}
|
||||
|
||||
print(normalize(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());
|
||||
print(normalizeArray(cursorOrArray));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,7 @@ globalThis.print = (() => {
|
|||
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