SERVER-94643 Add a shell helper to support fuzzer comparison fast path (#33728)

GitOrigin-RevId: d4d0ce00ba835dac89c1acc95ec0c0ad3f760b02
This commit is contained in:
Henri Nikku 2025-03-20 17:33:51 +00:00 committed by MongoDB Bot
parent 8a9d615058
commit e47324d0c9
4 changed files with 113 additions and 49 deletions

View File

@ -124,6 +124,7 @@ export default [
_openGoldenData: true,
_rand: true,
_replMonitorStats: true,
_resultSetsEqualNormalized: true,
_resultSetsEqualUnordered: true,
_setShellFailPoint: true,
_srand: true,

View File

@ -1,39 +1,67 @@
/*
* Tests `_resultSetsEqualUnordered`, which compares two sets of results (order of documents is
* disregarded) for equality. Field order inside an object is ignored, but array ordering and
* everything else is required for equality.
* Tests `_resultSetsEqualUnordered` and `_resultSetsEqualNormalized`, which compare two sets of
* results for equality. In `_resultSetsEqualUnordered`, field order inside an object is ignored,
* but array ordering and everything else is required for equality. In `_resultSetsEqualNormalized`,
* array ordering is also ignored, as well as some differences regarding numeric types and
* floating point closeness.
*/
const currentDate = new Date();
// We should throw for invalid input. This function expects both arguments to be a list of objects.
assert.throwsWithCode(() => _resultSetsEqualUnordered({}, []), 9193201);
assert.throwsWithCode(() => _resultSetsEqualUnordered([], 5), 9193201);
assert.throwsWithCode(() => _resultSetsEqualUnordered([4, 1], []), 9193202);
assert.throwsWithCode(() => _resultSetsEqualUnordered([], ["abc"]), 9193203);
assert.throwsWithCode(() => _resultSetsEqualUnordered([[]], [{a: 1}]), 9193202);
assert.throwsWithCode(() => _resultSetsEqualUnordered([], undefined), 9193201);
assert.throwsWithCode(() => _resultSetsEqualUnordered([], null), 9193201);
assert.throwsWithCode(() => _resultSetsEqualUnordered([null], []), 9193202);
const comparisonFunctions = [_resultSetsEqualUnordered, _resultSetsEqualNormalized];
// We should throw for invalid input. These functions expect both arguments to be a list of objects.
comparisonFunctions.forEach(comparisonFn => {
assert.throwsWithCode(() => comparisonFn({}, []), 9193201);
assert.throwsWithCode(() => comparisonFn([], 5), 9193201);
assert.throwsWithCode(() => comparisonFn([4, 1], []), 9193202);
assert.throwsWithCode(() => comparisonFn([], ["abc"]), 9193203);
assert.throwsWithCode(() => comparisonFn([[]], [{a: 1}]), 9193202);
assert.throwsWithCode(() => comparisonFn([], undefined), 9193201);
assert.throwsWithCode(() => comparisonFn([], null), 9193201);
assert.throwsWithCode(() => comparisonFn([null], []), 9193202);
});
// Some base cases.
assert(_resultSetsEqualUnordered([], []));
assert(_resultSetsEqualUnordered([{a: 1}], [{a: 1}]));
assert(_resultSetsEqualUnordered([{a: 1}, {a: 1}], [{a: 1}, {a: 1}]));
assert(_resultSetsEqualUnordered([{a: 1}, {b: 1}], [{b: 1}, {a: 1}]));
assert(_resultSetsEqualUnordered([{a: null}], [{a: null}]));
assert(!_resultSetsEqualUnordered([], [{a: 1}]));
assert(!_resultSetsEqualUnordered([{a: 1}], []));
// Different types should fail the comparison.
assert(!_resultSetsEqualUnordered([{a: 1}], [{a: '1'}]));
comparisonFunctions.forEach(comparisonFn => {
assert(comparisonFn([], []));
assert(comparisonFn([{a: 1}], [{a: 1}]));
assert(comparisonFn([{a: 1}, {a: 1}], [{a: 1}, {a: 1}]));
assert(comparisonFn([{a: 1}, {b: 1}], [{b: 1}, {a: 1}]));
assert(comparisonFn([{a: null}], [{a: null}]));
assert(!comparisonFn([], [{a: 1}]));
assert(!comparisonFn([{a: 1}], []));
});
// Different non-numeric types should fail both comparisons.
comparisonFunctions.forEach(comparisonFn => {
assert(!comparisonFn([{a: 1}], [{a: '1'}]));
assert(!comparisonFn([{a: null}], [{}]));
assert(!comparisonFn([{a: null}], [{b: null}]));
assert(!comparisonFn([{a: null}], [{a: undefined}]));
assert(!comparisonFn([{}], [{a: undefined}]));
});
// Different numeric types should fail the unordered comparison.
assert(!_resultSetsEqualUnordered([{a: 1}], [{a: NumberLong(1)}]));
assert(!_resultSetsEqualUnordered([{a: 1}], [{a: NumberDecimal(1)}]));
assert(!_resultSetsEqualUnordered([{a: NumberInt(1)}], [{a: NumberDecimal(1)}]));
assert(!_resultSetsEqualUnordered([{a: NumberInt(1)}], [{a: NumberLong(1)}]));
assert(!_resultSetsEqualUnordered([{a: null}], [{}]));
assert(!_resultSetsEqualUnordered([{a: null}], [{b: null}]));
assert(!_resultSetsEqualUnordered([{a: null}], [{a: undefined}]));
assert(!_resultSetsEqualUnordered([{}], [{a: undefined}]));
// However, they should pass the normalized comparison.
assert(_resultSetsEqualNormalized([{a: 1}], [{a: NumberLong(1)}]));
assert(_resultSetsEqualNormalized([{a: 1}], [{a: NumberDecimal(1)}]));
assert(_resultSetsEqualNormalized([{a: NumberInt(1)}], [{a: NumberDecimal(1)}]));
assert(_resultSetsEqualNormalized([{a: NumberInt(1)}], [{a: NumberLong(1)}]));
// Unordered comparison requires all values to be exactly equal.
assert(!_resultSetsEqualUnordered([{a: 0.14285714285714285}], [{a: 0.14285714285714288}]));
// Normalized comparison rounds doubles to 15 decimal places.
assert(_resultSetsEqualNormalized([{a: 0.14285714285714285}], [{a: 0.14285714285714288}]));
// Normalized comparison is sensitive to differences before the 15th decimal place.
assert(!_resultSetsEqualNormalized([{a: 0.142857142856}], [{a: 0.142857142855}]));
// Normalized comparison doesn't currently round decimals.
assert(!_resultSetsEqualNormalized([{a: NumberDecimal('0.14285714285714285')}],
[{a: NumberDecimal('0.14285714285714288')}]));
/*
* Given two sets of results - `equalResults` and `differentResults`, we test that all pairs of
@ -45,6 +73,9 @@ function assertExpectedOutputs(equalResults, differentResults) {
for (const result2 of equalResults) {
assert(_resultSetsEqualUnordered(result1, result2), {result1, result2});
assert(_resultSetsEqualUnordered(result2, result1), {result1, result2});
// Additional normalizations shouldn't make the comparison more strict.
assert(_resultSetsEqualNormalized(result1, result2), {result1, result2});
assert(_resultSetsEqualNormalized(result2, result1), {result1, result2});
}
}
for (const result1 of equalResults) {

View File

@ -859,8 +859,7 @@ void sortBSONObjectInternallyHelper(const BSONObj& input,
* Returns a new BSON with the same field/value pairings, but is recursively sorted by the fields.
* By default, arrays are not sorted unless NormalizationOptsSet has the kSortArrays bit set.
*/
BSONObj sortBSONObjectInternally(const BSONObj& input,
NormalizationOptsSet opts = NormalizationOpts::kSortBSON) {
BSONObj sortBSONObjectInternally(const BSONObj& input, NormalizationOptsSet opts) {
BSONObjBuilder bob(input.objsize());
sortBSONObjectInternallyHelper(input, bob, opts);
return bob.obj();
@ -1048,20 +1047,14 @@ BSONObj normalizeBSONObj(const BSONObj& input, NormalizationOptsSet opts) {
return result;
}
/*
* Takes two arrays of documents, and returns whether they contain the same set of BSON Objects. The
* BSON do not need to be in the same order for this to return true. Has no special logic for
* handling double/NumberDecimal closeness.
*/
BSONObj _resultSetsEqualUnordered(const BSONObj& input, void*) {
bool compareNormalizedResultSets(const BSONObj& input, NormalizationOptsSet opts) {
BSONObjIterator i(input);
uassert(9422901, "_resultSetsEqualUnordered expects two arguments", i.more());
uassert(9422901, "expected two arguments", i.more());
auto first = i.next();
uassert(9422902, "_resultSetsEqualUnordered expects two arguments", i.more());
uassert(9422902, "expected two arguments", i.more());
auto second = i.next();
uassert(9193201,
str::stream() << "_resultSetsEqualUnordered expects two arrays of containing objects "
"as input received "
str::stream() << "expected two arrays containing objects as input received "
<< first.type() << " and " << second.type(),
first.type() == BSONType::Array && second.type() == BSONType::Array);
@ -1070,45 +1063,70 @@ BSONObj _resultSetsEqualUnordered(const BSONObj& input, void*) {
for (const auto& el : firstAsBson) {
uassert(9193202,
str::stream() << "_resultSetsEqualUnordered expects all elements of input arrays "
"to be objects, received "
str::stream() << "expected all elements of input arrays to be objects, received "
<< el.type(),
el.type() == BSONType::Object);
}
for (const auto& el : secondAsBson) {
uassert(9193203,
str::stream() << "_resultSetsEqualUnordered expects all elements of input arrays "
"to be objects, received "
str::stream() << "expected all elements of input arrays to be objects, received "
<< el.type(),
el.type() == BSONType::Object);
}
if (firstAsBson.size() != secondAsBson.size()) {
return BSON("" << false);
return false;
}
// Optimistically assume they're already in the same order.
if (first.binaryEqualValues(second)) {
return BSON("" << true);
return true;
}
std::vector<BSONObj> firstSorted;
std::vector<BSONObj> secondSorted;
for (size_t i = 0; i < firstAsBson.size(); i++) {
firstSorted.push_back(sortBSONObjectInternally(firstAsBson[i].Obj()));
secondSorted.push_back(sortBSONObjectInternally(secondAsBson[i].Obj()));
firstSorted.push_back(normalizeBSONObj(firstAsBson[i].Obj(), opts));
secondSorted.push_back(normalizeBSONObj(secondAsBson[i].Obj(), opts));
}
sortQueryResults(firstSorted);
sortQueryResults(secondSorted);
if (isSet(opts, NormalizationOpts::kSortResults)) {
sortQueryResults(firstSorted);
sortQueryResults(secondSorted);
}
for (size_t i = 0; i < firstSorted.size(); i++) {
if (!firstSorted[i].binaryEqual(secondSorted[i])) {
return BSON("" << false);
return false;
}
}
return BSON("" << true);
return true;
}
/*
* Takes two arrays of documents, and returns whether they contain the same set of BSON Objects.
* Applies more normalizations than '_resultSetsEqualUnordered()'. Used by the fuzzer comparator.
* The following edge cases are currently accepted by the fuzzer but rejected by this function:
* - String comparison with collaction.
* - Numeric string vs other numeric types.
* - Rounding numeric types down to <15 digits. The fuzzer rounds to 6 or 10, depending on the type.
*/
BSONObj _resultSetsEqualNormalized(const BSONObj& input, void*) {
auto opts = NormalizationOpts::kSortResults | NormalizationOpts::kSortBSON |
NormalizationOpts::kSortArrays | NormalizationOpts::kNormalizeNumerics |
NormalizationOpts::kRoundFloatingPointNumerics;
return BSON("" << compareNormalizedResultSets(input, opts));
}
/*
* Takes two arrays of documents, and returns whether they contain the same set of BSON Objects. The
* BSON do not need to be in the same order for this to return true. Has no special logic for
* handling double/NumberDecimal closeness. Used in property based jstests.
*/
BSONObj _resultSetsEqualUnordered(const BSONObj& input, void*) {
auto opts = NormalizationOpts::kSortResults | NormalizationOpts::kSortBSON;
return BSON("" << compareNormalizedResultSets(input, opts));
}
/*
@ -1170,6 +1188,7 @@ void installShellUtils(Scope& scope) {
scope.injectNative("_buildBsonObj", _buildBsonObj);
scope.injectNative("_fnvHashToHexString", _fnvHashToHexString);
scope.injectNative("_resultSetsEqualUnordered", _resultSetsEqualUnordered);
scope.injectNative("_resultSetsEqualNormalized", _resultSetsEqualNormalized);
scope.injectNative("_compareStringsWithCollation", _compareStringsWithCollation);
installShellUtilsLauncher(scope);

View File

@ -10,6 +10,19 @@ declare function _isWindows()
declare function _openGoldenData()
declare function _rand()
declare function _replMonitorStats()
/**
* Compares two result sets after applying some normalizations. This function should only
* be used in the fuzzer.
*
* @param a First result set.
* @param b Second result set.
*
* @throws {Error} If the size of the BSON representation of 'a' and 'b' exceeds the BSON size limit
* (~16mb).
*
* @returns True if the result sets compare equal and false otherwise.
*/
declare function _resultSetsEqualNormalized(a: object[], b: object[]): boolean
declare function _resultSetsEqualUnordered()
declare function _setShellFailPoint()
declare function _srand()