Skip to content

Commit bb2ae4c

Browse files
daprahamianmbroadst
authored andcommitted
test: updating transaction test runner to use match function
1 parent 4198d45 commit bb2ae4c

File tree

2 files changed

+172
-99
lines changed

2 files changed

+172
-99
lines changed

test/functional/transactions_tests.js

Lines changed: 13 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ const core = require('mongodb-core');
1010
const sessions = core.Sessions;
1111
const environments = require('../environments');
1212

13-
// mlaunch init --replicaset --arbiter --name rs --hostname localhost --port 31000 --setParameter enableTestCommands=1 --binarypath /Users/mbroadst/Downloads/mongodb-osx-x86_64-enterprise-4.1.0-158-g3d62f3c/bin
14-
1513
chai.use(require('chai-subset'));
14+
chai.use(require('../match_transaction_spec').default);
1615
chai.config.includeStack = true;
1716
chai.config.showDiff = true;
1817
chai.config.truncateThreshold = 0;
@@ -23,55 +22,6 @@ function isPlainObject(value) {
2322

2423
process.on('unhandledRejection', err => console.dir(err));
2524

26-
/**
27-
* Finds placeholder values in a deeply nested object.
28-
*
29-
* NOTE: This also mutates the object, by removing the values for comparison
30-
*
31-
* @param {Object} input the object to find placeholder values in
32-
*/
33-
function findPlaceholders(value, parent) {
34-
return Object.keys(value).reduce((result, key) => {
35-
if (isPlainObject(value[key])) {
36-
return result.concat(
37-
findPlaceholders(value[key], [value, key]).map(x => {
38-
if (x.path.startsWith('$')) {
39-
x.path = key;
40-
} else {
41-
x.path = `${key}.${x.path}`;
42-
}
43-
44-
return x;
45-
})
46-
);
47-
}
48-
49-
if (value[key] === null) {
50-
delete value[key];
51-
result.push({ path: key, type: null });
52-
} else if (value[key] === 42 || value[key] === '42') {
53-
if (key.startsWith('$number')) {
54-
result.push({ path: key, type: 'number' });
55-
} else if (value[key] === 42) {
56-
result.push({ path: key, type: 'exists' });
57-
} else {
58-
result.push({ path: key, type: 'string' });
59-
}
60-
61-
// NOTE: fix this, it just passes the current examples
62-
if (parent == null) {
63-
delete value[key];
64-
} else {
65-
delete parent[0][parent[1]];
66-
}
67-
} else if (value[key] === '') {
68-
result.push({ path: key, type: 'string' });
69-
}
70-
71-
return result;
72-
}, []);
73-
}
74-
7525
function translateClientOptions(options) {
7626
Object.keys(options).forEach(key => {
7727
if (key === 'readConcernLevel') {
@@ -400,18 +350,19 @@ function runTestSuiteTest(configuration, spec, context) {
400350
Object.assign({}, sessionOptions, parseSessionOptions(spec.sessionOptions.session1))
401351
);
402352

353+
const savedSessionData = {
354+
session0: JSON.parse(EJSON.stringify(session0.id)),
355+
session1: JSON.parse(EJSON.stringify(session1.id))
356+
};
357+
403358
// enable to see useful APM debug information at the time of actual test run
404359
// displayCommands = true;
405360

406361
const operationContext = {
407362
database,
408363
session0,
409364
session1,
410-
testRunner: context,
411-
savedSessionData: {
412-
session0: JSON.parse(EJSON.stringify(session0.id)),
413-
session1: JSON.parse(EJSON.stringify(session1.id))
414-
}
365+
testRunner: context
415366
};
416367

417368
let testPromise = Promise.resolve();
@@ -426,7 +377,7 @@ function runTestSuiteTest(configuration, spec, context) {
426377
session0.endSession();
427378
session1.endSession();
428379

429-
return validateExpectations(commandEvents, spec, context, operationContext);
380+
return validateExpectations(commandEvents, spec, savedSessionData);
430381
});
431382
});
432383
}
@@ -448,65 +399,28 @@ function validateOutcome(testData, testContext) {
448399
return Promise.resolve();
449400
}
450401

451-
function validateExpectations(commandEvents, spec, testContext, operationContext) {
402+
function validateExpectations(commandEvents, spec, savedSessionData) {
452403
if (spec.expectations && Array.isArray(spec.expectations) && spec.expectations.length > 0) {
453404
const actualEvents = normalizeCommandShapes(commandEvents);
454-
const rawExpectedEvents = spec.expectations.map(x =>
455-
linkSessionData(x.command_started_event, operationContext.savedSessionData)
456-
);
457-
458-
const expectedEventPlaceholders = rawExpectedEvents.map(event =>
459-
findPlaceholders(event.command)
460-
);
461-
405+
const rawExpectedEvents = spec.expectations.map(x => x.command_started_event);
462406
const expectedEvents = normalizeCommandShapes(rawExpectedEvents);
463407
expect(actualEvents).to.have.length(expectedEvents.length);
464408

465409
expectedEvents.forEach((expected, idx) => {
466410
const actual = actualEvents[idx];
467-
const placeHolders = expectedEventPlaceholders[idx]; // eslint-disable-line
468411

469412
expect(actual.commandName).to.equal(expected.commandName);
470413
expect(actual.databaseName).to.equal(expected.databaseName);
471414

472415
const actualCommand = actual.command;
473416
const expectedCommand = expected.command;
474417

475-
// handle validation of placeholder values
476-
// placeHolders.forEach(placeholder => {
477-
// const parsedActual = EJSON.parse(JSON.stringify(actualCommand), {
478-
// relaxed: true
479-
// });
480-
481-
// if (placeholder.type === null) {
482-
// expect(parsedActual).to.not.have.all.nested.property(placeholder.path);
483-
// } else if (placeholder.type === 'string') {
484-
// expect(parsedActual).nested.property(placeholder.path).to.exist;
485-
// expect(parsedActual)
486-
// .nested.property(placeholder.path)
487-
// .to.have.length.greaterThan(0);
488-
// } else if (placeholder.type === 'number') {
489-
// expect(parsedActual).nested.property(placeholder.path).to.exist;
490-
// expect(parsedActual)
491-
// .nested.property(placeholder.path)
492-
// .to.be.greaterThan(0);
493-
// } else if (placeholder.type === 'exists') {
494-
// expect(parsedActual).nested.property(placeholder.path).to.exist;
495-
// }
496-
// });
497-
498-
// compare the command
499-
expect(actualCommand).to.containSubset(expectedCommand);
418+
expect(actualCommand)
419+
.withSessionData(savedSessionData)
420+
.to.matchTransactionSpec(expectedCommand);
500421
});
501422
}
502423
}
503-
504-
function linkSessionData(command, context) {
505-
const result = Object.assign({}, command);
506-
result.command.lsid = context[command.command.lsid];
507-
return result;
508-
}
509-
510424
function normalizeCommandShapes(commands) {
511425
return commands.map(command =>
512426
JSON.parse(

test/match_transaction_spec.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
'use strict';
2+
3+
const SYMBOL_DOES_NOT_EXIST = Symbol('[[assert does not exist]]');
4+
const SYMBOL_DOES_EXIST = Symbol('[[assert does exist]]');
5+
const SYMBOL_IGNORE = Symbol('[[ignore]]');
6+
7+
const EJSON = require('mongodb-extjson');
8+
9+
function valIs42(input) {
10+
return input === 42 || input === '42';
11+
}
12+
13+
function is42(input) {
14+
if (!input) return false;
15+
return valIs42(input) || valIs42(input.$numberInt) || valIs42(input.$numberLong);
16+
}
17+
18+
function generateMatchAndDiffSpecialCase(key, expectedObj, actualObj, metadata) {
19+
const expected = expectedObj[key];
20+
const actual = actualObj[key];
21+
if (expected === null) {
22+
if (key === 'readConcern') {
23+
// HACK: get around NODE-1889
24+
return {
25+
match: true,
26+
expected: SYMBOL_DOES_NOT_EXIST,
27+
actual: SYMBOL_DOES_NOT_EXIST
28+
};
29+
}
30+
31+
const match = !actualObj.hasOwnProperty(key);
32+
return {
33+
match,
34+
expected: SYMBOL_DOES_NOT_EXIST,
35+
actual: match ? SYMBOL_DOES_NOT_EXIST : actual
36+
};
37+
}
38+
39+
const expectedIs42 = is42(expected);
40+
if (key === 'lsid' && typeof expected === 'string') {
41+
// Case lsid - assert that session matches session in session data
42+
const sessionData = metadata.sessionData;
43+
const lsid = JSON.parse(EJSON.stringify(sessionData[expected] && sessionData[expected].id));
44+
return generateMatchAndDiff(lsid, actual, metadata);
45+
} else if (key === 'getMore' && expectedIs42) {
46+
// cursorid - explicitly ignore 42 values
47+
return {
48+
match: true,
49+
expected: SYMBOL_IGNORE,
50+
actual: SYMBOL_IGNORE
51+
};
52+
} else if (key === 'afterClusterTime' && expectedIs42) {
53+
// afterClusterTime - assert that value exists
54+
const match = actual != null;
55+
return {
56+
match,
57+
expected: match ? actual : SYMBOL_DOES_EXIST,
58+
actual
59+
};
60+
} else if (key === 'recoveryToken' && expectedIs42) {
61+
// recoveryToken - assert that value exists
62+
// TODO: assert that value is BSON
63+
const match = actual != null;
64+
return {
65+
match,
66+
expected: match ? actual : SYMBOL_DOES_EXIST,
67+
actual
68+
};
69+
} else {
70+
// default
71+
return generateMatchAndDiff(expected, actual, metadata);
72+
}
73+
}
74+
75+
function generateMatchAndDiff(expected, actual, metadata) {
76+
const typeOfExpected = typeof expected;
77+
78+
if (typeOfExpected !== typeof actual) {
79+
return { match: false, expected, actual };
80+
}
81+
82+
if (typeOfExpected !== 'object' || expected == null || actual == null) {
83+
return { match: expected === actual, expected, actual };
84+
}
85+
86+
if (expected instanceof Date) {
87+
return {
88+
match: actual instanceof Date ? expected.getTime() === actual.getTime() : false,
89+
expected,
90+
actual
91+
};
92+
}
93+
94+
if (Array.isArray(expected)) {
95+
if (!Array.isArray(actual)) {
96+
return { match: false, expected, actual };
97+
}
98+
99+
return expected.map((val, idx) => generateMatchAndDiff(val, actual[idx], metadata)).reduce(
100+
(ret, value) => {
101+
ret.match = ret.match && value.match;
102+
ret.expected.push(value.expected);
103+
ret.actual.push(value.actual);
104+
return ret;
105+
},
106+
{ match: true, expected: [], actual: [] }
107+
);
108+
}
109+
110+
return Object.keys(expected).reduce(
111+
(ret, key) => {
112+
const check = generateMatchAndDiffSpecialCase(key, expected, actual, metadata);
113+
ret.match = ret.match && check.match;
114+
ret.expected[key] = check.expected;
115+
ret.actual[key] = check.actual;
116+
return ret;
117+
},
118+
{
119+
match: true,
120+
expected: {},
121+
actual: {}
122+
}
123+
);
124+
}
125+
126+
function matchSpec(chai, utils) {
127+
chai.Assertion.addMethod('withSessionData', function(sessionData) {
128+
utils.flag(this, 'transactionTestRunnerSessionData', sessionData);
129+
});
130+
131+
chai.Assertion.addMethod('matchTransactionSpec', function(expected) {
132+
const actual = utils.flag(this, 'object');
133+
134+
const sessionData = utils.flag(this, 'transactionTestRunnerSessionData');
135+
136+
const result = generateMatchAndDiff(expected, actual, { sessionData });
137+
138+
if (!result.match) {
139+
console.log('fizzz');
140+
console.dir(actual, { depth: 6 });
141+
}
142+
143+
chai.Assertion.prototype.assert.call(
144+
this,
145+
result.match,
146+
'expected #{act} to match spec #{exp}',
147+
'expected #{act} to not match spec #{exp}',
148+
result.expected,
149+
result.actual,
150+
chai.config.showDiff
151+
);
152+
});
153+
154+
chai.assert.matchSpec = function(val, exp, msg) {
155+
new chai.Assertion(val, msg).to.matchSpec(exp);
156+
};
157+
}
158+
159+
module.exports.default = matchSpec;

0 commit comments

Comments
 (0)