Skip to content

Commit 52fea4d

Browse files
test(NODE-4274): fix match logic in unified spec runner (#3267)
1 parent ee41447 commit 52fea4d

File tree

1 file changed

+164
-72
lines changed

1 file changed

+164
-72
lines changed

test/tools/unified-spec-runner/match.ts

Lines changed: 164 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -134,83 +134,114 @@ TYPE_MAP.set(
134134
actual => (typeof actual === 'number' && Number.isInteger(actual)) || Long.isLong(actual)
135135
);
136136

137+
/**
138+
* resultCheck
139+
*
140+
* @param actual - the actual result
141+
* @param expected - the expected result
142+
* @param entities - the EntitiesMap associated with the test
143+
* @param path - an array of strings representing the 'path' in the document down to the current
144+
* value. For example, given `{ a: { b: { c: 4 } } }`, when evaluating `{ c: 4 }`, the path
145+
* will look like: `['a', 'b']`. Used to print useful error messages when assertions fail.
146+
* @param checkExtraKeys - a boolean value that determines how keys present on the `actual` object but
147+
* not on the `expected` object are treated. When set to `true`, any extra keys on the
148+
* `actual` object will throw an error
149+
*/
137150
export function resultCheck(
138151
actual: Document,
139152
expected: Document | number | string | boolean,
140153
entities: EntitiesMap,
141154
path: string[] = [],
142-
depth = 0
155+
checkExtraKeys = false
143156
): void {
157+
function checkNestedDocuments(key: string, value: any, checkExtraKeys: boolean) {
158+
if (key === 'sort') {
159+
// TODO: This is a workaround that works because all sorts in the specs
160+
// are objects with one key; ideally we'd want to adjust the spec definitions
161+
// to indicate whether order matters for any given key and set general
162+
// expectations accordingly (see NODE-3235)
163+
expect(Object.keys(value)).to.have.lengthOf(1);
164+
expect(actual[key]).to.be.instanceOf(Map);
165+
expect(actual[key].size).to.equal(1);
166+
const expectedSortKey = Object.keys(value)[0];
167+
expect(actual[key]).to.have.all.keys(expectedSortKey);
168+
const objFromActual = { [expectedSortKey]: actual[key].get(expectedSortKey) };
169+
resultCheck(objFromActual, value, entities, path, checkExtraKeys);
170+
} else {
171+
resultCheck(actual[key], value, entities, path, checkExtraKeys);
172+
}
173+
}
174+
144175
if (typeof expected === 'object' && expected) {
145176
// Expected is an object
146177
// either its a special operator or just an object to check equality against
147178

148179
if (isSpecialOperator(expected)) {
149180
// Special operation check is a base condition
150181
// specialCheck may recurse depending upon the check ($$unsetOrMatches)
151-
specialCheck(actual, expected, entities, path, depth);
182+
specialCheck(actual, expected, entities, path, checkExtraKeys);
152183
return;
184+
}
185+
186+
const expectedEntries = Object.entries(expected);
187+
188+
if (Array.isArray(expected)) {
189+
if (!Array.isArray(actual)) {
190+
expect.fail(
191+
`expected value at ${path.join('.')} to be an array, but received ${inspect(actual)}`
192+
);
193+
}
194+
for (const [index, value] of expectedEntries) {
195+
path.push(`[${index}]`);
196+
checkNestedDocuments(index, value, false);
197+
path.pop();
198+
}
153199
} else {
154-
// Just a plain object, however this object can contain special operations
155-
// So we need to recurse over each key,value
156-
const expectedEntries = Object.entries(expected);
200+
for (const [key, value] of expectedEntries) {
201+
path.push(`.${key}`);
202+
checkNestedDocuments(key, value, true);
203+
path.pop();
204+
}
157205

158-
if (depth > 1) {
206+
if (checkExtraKeys) {
159207
expect(actual, `Expected actual to exist at ${path.join('')}`).to.exist;
160208
const actualKeys = Object.keys(actual);
161209
const expectedKeys = Object.keys(expected);
162210
// Don't check for full key set equality because some of the actual keys
163211
// might be e.g. $$unsetOrMatches, which can be omitted.
164-
expect(
165-
actualKeys.filter(key => !expectedKeys.includes(key)),
166-
`[${Object.keys(actual)}] has more than the expected keys: [${Object.keys(expected)}]`
167-
).to.have.lengthOf(0);
168-
}
212+
const extraKeys = actualKeys.filter(key => !expectedKeys.includes(key));
169213

170-
for (const [key, value] of expectedEntries) {
171-
path.push(Array.isArray(expected) ? `[${key}]` : `.${key}`); // record what key we're at
172-
depth += 1;
173-
if (key === 'sort') {
174-
// TODO: This is a workaround that works because all sorts in the specs
175-
// are objects with one key; ideally we'd want to adjust the spec definitions
176-
// to indicate whether order matters for any given key and set general
177-
// expectations accordingly (see NODE-3235)
178-
expect(Object.keys(value)).to.have.lengthOf(1);
179-
expect(actual[key]).to.be.instanceOf(Map);
180-
expect(actual[key].size).to.equal(1);
181-
const expectedSortKey = Object.keys(value)[0];
182-
expect(actual[key]).to.have.all.keys(expectedSortKey);
183-
const objFromActual = { [expectedSortKey]: actual[key].get(expectedSortKey) };
184-
resultCheck(objFromActual, value, entities, path, depth);
185-
} else {
186-
resultCheck(actual[key], value, entities, path, depth);
214+
if (extraKeys.length > 0) {
215+
expect.fail(
216+
`object has more keys than expected. \n\tactual: [${actualKeys}] \n\texpected: [${expectedKeys}]`
217+
);
187218
}
188-
depth -= 1;
189-
path.pop(); // if the recursion was successful we can drop the tested key
190219
}
191220
}
221+
222+
return;
223+
}
224+
225+
// Here's our recursion base case
226+
// expected is: number | Long | string | boolean | null
227+
if (Long.isLong(actual) && typeof expected === 'number') {
228+
// Long requires special equality check
229+
expect(actual.equals(expected)).to.be.true;
230+
} else if (Long.isLong(expected) && typeof actual === 'number') {
231+
// Long requires special equality check
232+
expect(expected.equals(actual)).to.be.true;
233+
} else if (Number.isNaN(actual) && Number.isNaN(expected)) {
234+
// in JS, NaN isn't equal to NaN but we want to not fail if we have two NaN
235+
} else if (
236+
typeof expected === 'number' &&
237+
typeof actual === 'number' &&
238+
expected === 0 &&
239+
actual === 0
240+
) {
241+
// case to handle +0 and -0
242+
expect(Object.is(expected, actual)).to.be.true;
192243
} else {
193-
// Here's our recursion base case
194-
// expected is: number | Long | string | boolean | null
195-
if (Long.isLong(actual) && typeof expected === 'number') {
196-
// Long requires special equality check
197-
expect(actual.equals(expected)).to.be.true;
198-
} else if (Long.isLong(expected) && typeof actual === 'number') {
199-
// Long requires special equality check
200-
expect(expected.equals(actual)).to.be.true;
201-
} else if (Number.isNaN(actual) && Number.isNaN(expected)) {
202-
// in JS, NaN isn't equal to NaN but we want to not fail if we have two NaN
203-
} else if (
204-
typeof expected === 'number' &&
205-
typeof actual === 'number' &&
206-
expected === 0 &&
207-
actual === 0
208-
) {
209-
// case to handle +0 and -0
210-
expect(Object.is(expected, actual)).to.be.true;
211-
} else {
212-
expect(actual).to.equal(expected);
213-
}
244+
expect(actual).to.equal(expected);
214245
}
215246
}
216247

@@ -219,16 +250,12 @@ export function specialCheck(
219250
expected: SpecialOperator,
220251
entities: EntitiesMap,
221252
path: string[] = [],
222-
depth = 0
253+
checkExtraKeys: boolean
223254
): boolean {
224255
if (isUnsetOrMatchesOperator(expected)) {
225-
// $$unsetOrMatches
226256
if (actual === null || actual === undefined) return;
227-
else {
228-
depth += 1;
229-
resultCheck(actual, expected.$$unsetOrMatches, entities, path, depth);
230-
depth -= 1;
231-
}
257+
258+
resultCheck(actual, expected.$$unsetOrMatches, entities, path, checkExtraKeys);
232259
} else if (isMatchesEntityOperator(expected)) {
233260
// $$matchesEntity
234261
const entity = entities.get(expected.$$matchesEntity);
@@ -318,6 +345,60 @@ function failOnMismatchedCount(
318345
);
319346
}
320347

348+
function compareCommandStartedEvents(
349+
actual: CommandStartedEvent,
350+
expected: ExpectedCommandEvent['commandStartedEvent'],
351+
entities: EntitiesMap,
352+
prefix: string
353+
) {
354+
if (expected.command) {
355+
resultCheck(actual.command, expected.command, entities, [`${prefix}.command`]);
356+
}
357+
if (expected.commandName) {
358+
expect(
359+
expected.commandName,
360+
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
361+
).to.equal(actual.commandName);
362+
}
363+
if (expected.databaseName) {
364+
expect(
365+
expected.databaseName,
366+
`expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName}`
367+
).to.equal(actual.databaseName);
368+
}
369+
}
370+
371+
function compareCommandSucceededEvents(
372+
actual: CommandSucceededEvent,
373+
expected: ExpectedCommandEvent['commandSucceededEvent'],
374+
entities: EntitiesMap,
375+
prefix: string
376+
) {
377+
if (expected.reply) {
378+
resultCheck(actual.reply, expected.reply, entities, [prefix]);
379+
}
380+
if (expected.commandName) {
381+
expect(
382+
expected.commandName,
383+
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
384+
).to.equal(actual.commandName);
385+
}
386+
}
387+
388+
function compareCommandFailedEvents(
389+
actual: CommandFailedEvent,
390+
expected: ExpectedCommandEvent['commandFailedEvent'],
391+
entities: EntitiesMap,
392+
prefix: string
393+
) {
394+
if (expected.commandName) {
395+
expect(
396+
expected.commandName,
397+
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
398+
).to.equal(actual.commandName);
399+
}
400+
}
401+
321402
function compareEvents(
322403
actual: CommandEvent[] | CmapEvent[],
323404
expected: (ExpectedCommandEvent & ExpectedCmapEvent)[],
@@ -328,22 +409,31 @@ function compareEvents(
328409
}
329410
for (const [index, actualEvent] of actual.entries()) {
330411
const expectedEvent = expected[index];
412+
const rootPrefix = `events[${index}]`;
331413

332414
if (expectedEvent.commandStartedEvent) {
333-
expect(actualEvent).to.be.instanceOf(CommandStartedEvent);
334-
resultCheck(actualEvent, expectedEvent.commandStartedEvent, entities, [
335-
`events[${index}].commandStartedEvent`
336-
]);
415+
const path = `${rootPrefix}.commandStartedEvent`;
416+
if (!(actualEvent instanceof CommandStartedEvent)) {
417+
expect.fail(`expected ${path} to be instanceof CommandStartedEvent`);
418+
}
419+
compareCommandStartedEvents(actualEvent, expectedEvent.commandStartedEvent, entities, path);
337420
} else if (expectedEvent.commandSucceededEvent) {
338-
expect(actualEvent).to.be.instanceOf(CommandSucceededEvent);
339-
resultCheck(actualEvent, expectedEvent.commandSucceededEvent, entities, [
340-
`events[${index}].commandSucceededEvent`
341-
]);
421+
const path = `${rootPrefix}.commandSucceededEvent`;
422+
if (!(actualEvent instanceof CommandSucceededEvent)) {
423+
expect.fail(`expected ${path} to be instanceof CommandSucceededEvent`);
424+
}
425+
compareCommandSucceededEvents(
426+
actualEvent,
427+
expectedEvent.commandSucceededEvent,
428+
entities,
429+
path
430+
);
342431
} else if (expectedEvent.commandFailedEvent) {
343-
expect(actualEvent).to.be.instanceOf(CommandFailedEvent);
344-
expect(actualEvent)
345-
.to.have.property('commandName')
346-
.that.equals(expectedEvent.commandFailedEvent.commandName);
432+
const path = `${rootPrefix}.commandFailedEvent`;
433+
if (!(actualEvent instanceof CommandFailedEvent)) {
434+
expect.fail(`expected ${path} to be instanceof CommandFailedEvent`);
435+
}
436+
compareCommandFailedEvents(actualEvent, expectedEvent.commandFailedEvent, entities, path);
347437
} else if (expectedEvent.connectionClosedEvent) {
348438
expect(actualEvent).to.be.instanceOf(ConnectionClosedEvent);
349439
if (expectedEvent.connectionClosedEvent.hasServiceId) {
@@ -354,8 +444,10 @@ function compareEvents(
354444
if (expectedEvent.poolClearedEvent.hasServiceId) {
355445
expect(actualEvent).property('serviceId').to.exist;
356446
}
357-
} else if (!validEmptyCmapEvent(expectedEvent, actualEvent)) {
358-
expect.fail(`Events must be one of the known types, got ${inspect(actualEvent)}`);
447+
} else if (validEmptyCmapEvent(expectedEvent, actualEvent)) {
448+
return;
449+
} else {
450+
expect.fail(`Encountered unexpected event - ${inspect(actualEvent)}`);
359451
}
360452
}
361453
}

0 commit comments

Comments
 (0)