Skip to content

Commit 8e2ad06

Browse files
committed
revert runner changes
1 parent 713056f commit 8e2ad06

File tree

2 files changed

+385
-382
lines changed

2 files changed

+385
-382
lines changed
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
'use strict';
2+
const fs = require('fs');
3+
const path = require('path');
4+
const { Topology } = require('../../../src/sdam/topology');
5+
const { TopologyType } = require('../../../src/sdam/common');
6+
const { Server } = require('../../../src/sdam/server');
7+
const { ServerDescription } = require('../../../src/sdam/server_description');
8+
const sdamEvents = require('../../../src/sdam/events');
9+
const { parseOptions } = require('../../../src/connection_string');
10+
const sinon = require('sinon');
11+
const { EJSON } = require('bson');
12+
const { ConnectionPool } = require('../../../src/cmap/connection_pool');
13+
const {
14+
MongoNetworkError,
15+
MongoNetworkTimeoutError,
16+
MongoServerError
17+
} = require('../../../src/error');
18+
const { eachAsyncSeries, ns } = require('../../../src/utils');
19+
const { expect } = require('chai');
20+
21+
const specDir = path.resolve(__dirname, '../../spec/server-discovery-and-monitoring');
22+
function collectTests() {
23+
const testTypes = fs
24+
.readdirSync(specDir)
25+
.filter(d => fs.statSync(path.resolve(specDir, d)).isDirectory())
26+
.filter(d => d !== 'integration');
27+
28+
const tests = {};
29+
testTypes.forEach(testType => {
30+
tests[testType] = fs
31+
.readdirSync(path.join(specDir, testType))
32+
.filter(f => path.extname(f) === '.json')
33+
.map(f => {
34+
const result = EJSON.parse(fs.readFileSync(path.join(specDir, testType, f)), {
35+
relaxed: true
36+
});
37+
38+
result.type = testType;
39+
return result;
40+
});
41+
});
42+
43+
return tests;
44+
}
45+
46+
describe('Server Discovery and Monitoring (spec)', function () {
47+
let serverConnect;
48+
before(() => {
49+
serverConnect = sinon.stub(Server.prototype, 'connect').callsFake(function () {
50+
this.s.state = 'connected';
51+
this.emit('connect');
52+
});
53+
});
54+
55+
after(() => {
56+
serverConnect.restore();
57+
});
58+
59+
const shouldSkip = desc => {
60+
const descriptions = [
61+
// placeholder for potential skips
62+
];
63+
return descriptions.includes(desc);
64+
};
65+
66+
const specTests = collectTests();
67+
Object.keys(specTests).forEach(specTestName => {
68+
describe(specTestName, () => {
69+
specTests[specTestName].forEach(testData => {
70+
const skip = shouldSkip(testData.description);
71+
const type = skip ? it.skip : it;
72+
type(testData.description, function (done) {
73+
executeSDAMTest(testData, done);
74+
});
75+
});
76+
});
77+
});
78+
});
79+
80+
const OUTCOME_TRANSLATIONS = new Map();
81+
OUTCOME_TRANSLATIONS.set('topologyType', 'type');
82+
83+
function translateOutcomeKey(key) {
84+
if (OUTCOME_TRANSLATIONS.has(key)) {
85+
return OUTCOME_TRANSLATIONS.get(key);
86+
}
87+
88+
return key;
89+
}
90+
91+
function convertOutcomeEvents(events) {
92+
return events.map(event => {
93+
const eventType = Object.keys(event)[0];
94+
const args = [];
95+
Object.keys(event[eventType]).forEach(key => {
96+
let argument = event[eventType][key];
97+
if (argument.servers) {
98+
argument.servers = argument.servers.reduce((result, server) => {
99+
result[server.address] = normalizeServerDescription(server);
100+
return result;
101+
}, {});
102+
}
103+
104+
Object.keys(argument).forEach(key => {
105+
if (OUTCOME_TRANSLATIONS.has(key)) {
106+
argument[OUTCOME_TRANSLATIONS.get(key)] = argument[key];
107+
delete argument[key];
108+
}
109+
});
110+
111+
args.push(argument);
112+
});
113+
114+
// convert snake case to camelCase with capital first letter
115+
let eventClass = eventType.replace(/_\w/g, c => c[1].toUpperCase());
116+
eventClass = eventClass.charAt(0).toUpperCase() + eventClass.slice(1);
117+
args.unshift(null);
118+
const eventConstructor = sdamEvents[eventClass];
119+
const eventInstance = new (Function.prototype.bind.apply(eventConstructor, args))();
120+
return eventInstance;
121+
});
122+
}
123+
124+
// iterates through expectation building a path of keys that should not exist (null), and
125+
// removes them from the expectation (NOTE: this mutates the expectation)
126+
function findOmittedFields(expected) {
127+
const result = [];
128+
Object.keys(expected).forEach(key => {
129+
if (expected[key] == null) {
130+
result.push(key);
131+
delete expected[key];
132+
}
133+
});
134+
135+
return result;
136+
}
137+
138+
function normalizeServerDescription(serverDescription) {
139+
if (serverDescription.type === 'PossiblePrimary') {
140+
// Some single-threaded drivers care a lot about ordering potential primary
141+
// servers, in order to speed up selection. We don't care, so we'll just mark
142+
// it as `Unknown`.
143+
serverDescription.type = 'Unknown';
144+
}
145+
146+
return serverDescription;
147+
}
148+
149+
function cloneMap(map) {
150+
const result = Object.create(null);
151+
for (let key of map.keys()) {
152+
result[key] = JSON.parse(JSON.stringify(map.get(key)));
153+
}
154+
155+
return result;
156+
}
157+
158+
function cloneForCompare(event) {
159+
const result = JSON.parse(JSON.stringify(event));
160+
['previousDescription', 'newDescription'].forEach(key => {
161+
if (event[key] != null && event[key].servers != null) {
162+
result[key].servers = cloneMap(event[key].servers);
163+
}
164+
});
165+
166+
return result;
167+
}
168+
169+
function executeSDAMTest(testData, testDone) {
170+
const options = parseOptions(testData.uri);
171+
// create the topology
172+
const topology = new Topology(options.hosts, options);
173+
// Each test will attempt to connect by doing server selection. We want to make the first
174+
// call to `selectServers` call a fake, and then immediately restore the original behavior.
175+
let topologySelectServers = sinon
176+
.stub(Topology.prototype, 'selectServer')
177+
.callsFake(function (selector, options, callback) {
178+
topologySelectServers.restore();
179+
180+
const fakeServer = { s: { state: 'connected' }, removeListener: () => {} };
181+
callback(undefined, fakeServer);
182+
});
183+
// listen for SDAM monitoring events
184+
let events = [];
185+
[
186+
'serverOpening',
187+
'serverClosed',
188+
'serverDescriptionChanged',
189+
'topologyOpening',
190+
'topologyClosed',
191+
'topologyDescriptionChanged',
192+
'serverHeartbeatStarted',
193+
'serverHeartbeatSucceeded',
194+
'serverHeartbeatFailed'
195+
].forEach(eventName => {
196+
topology.on(eventName, event => events.push(event));
197+
});
198+
199+
function done(err) {
200+
topology.close(e => testDone(e || err));
201+
}
202+
203+
const incompatibilityHandler = err => {
204+
if (err.message.match(/but this version of the driver/)) return;
205+
throw err;
206+
};
207+
208+
// connect the topology
209+
topology.connect(options, err => {
210+
expect(err).to.not.exist;
211+
212+
eachAsyncSeries(
213+
testData.phases,
214+
(phase, cb) => {
215+
function phaseDone() {
216+
if (phase.outcome) {
217+
assertOutcomeExpectations(topology, events, phase.outcome);
218+
}
219+
220+
// remove error handler
221+
topology.removeListener('error', incompatibilityHandler);
222+
// reset the captured events for each phase
223+
events = [];
224+
cb();
225+
}
226+
227+
const incompatibilityExpected = phase.outcome ? !phase.outcome.compatible : false;
228+
if (incompatibilityExpected) {
229+
topology.on('error', incompatibilityHandler);
230+
}
231+
232+
// if (phase.description) {
233+
// console.log(`[phase] ${phase.description}`);
234+
// }
235+
236+
if (phase.responses) {
237+
// simulate each hello response
238+
phase.responses.forEach(response =>
239+
topology.serverUpdateHandler(new ServerDescription(response[0], response[1]))
240+
);
241+
phaseDone();
242+
} else if (phase.applicationErrors) {
243+
eachAsyncSeries(
244+
phase.applicationErrors,
245+
(appError, phaseCb) => {
246+
let withConnectionStub = sinon
247+
.stub(ConnectionPool.prototype, 'withConnection')
248+
.callsFake(withConnectionStubImpl(appError));
249+
250+
const server = topology.s.servers.get(appError.address);
251+
server.command(ns('admin.$cmd'), { ping: 1 }, undefined, err => {
252+
expect(err).to.exist;
253+
withConnectionStub.restore();
254+
255+
phaseCb();
256+
});
257+
},
258+
err => {
259+
expect(err).to.not.exist;
260+
phaseDone();
261+
}
262+
);
263+
} else {
264+
phaseDone();
265+
}
266+
},
267+
err => {
268+
expect(err).to.not.exist;
269+
done();
270+
}
271+
);
272+
});
273+
}
274+
275+
function withConnectionStubImpl(appError) {
276+
return function (conn, fn, callback) {
277+
const connectionPool = this; // we are stubbing `withConnection` on the `ConnectionPool` class
278+
const fakeConnection = {
279+
generation:
280+
typeof appError.generation === 'number' ? appError.generation : connectionPool.generation,
281+
282+
command: (ns, cmd, options, callback) => {
283+
if (appError.type === 'network') {
284+
callback(new MongoNetworkError('test generated'));
285+
} else if (appError.type === 'timeout') {
286+
callback(
287+
new MongoNetworkTimeoutError('xxx timed out', {
288+
beforeHandshake: appError.when === 'beforeHandshakeCompletes'
289+
})
290+
);
291+
} else {
292+
callback(new MongoServerError(appError.response));
293+
}
294+
}
295+
};
296+
297+
fn(undefined, fakeConnection, (fnErr, result) => {
298+
if (typeof callback === 'function') {
299+
if (fnErr) {
300+
callback(fnErr);
301+
} else {
302+
callback(undefined, result);
303+
}
304+
}
305+
});
306+
};
307+
}
308+
309+
function assertOutcomeExpectations(topology, events, outcome) {
310+
// then verify the resulting outcome
311+
const description = topology.description;
312+
Object.keys(outcome).forEach(key => {
313+
const outcomeValue = outcome[key];
314+
const translatedKey = translateOutcomeKey(key);
315+
316+
if (key === 'servers') {
317+
expect(description).to.include.keys(translatedKey);
318+
const expectedServers = outcomeValue;
319+
const actualServers = description[translatedKey];
320+
321+
Object.keys(expectedServers).forEach(serverName => {
322+
expect(actualServers).to.include.keys(serverName);
323+
324+
// TODO: clean all this up, always operate directly on `Server` not `ServerDescription`
325+
if (expectedServers[serverName].pool) {
326+
const expectedPool = expectedServers[serverName].pool;
327+
delete expectedServers[serverName].pool;
328+
const actualPoolGeneration = topology.s.servers.get(serverName).s.pool.generation;
329+
expect(actualPoolGeneration).to.equal(expectedPool.generation);
330+
}
331+
332+
const expectedServer = normalizeServerDescription(expectedServers[serverName]);
333+
const omittedFields = findOmittedFields(expectedServer);
334+
335+
const actualServer = actualServers.get(serverName);
336+
expect(actualServer).to.matchMongoSpec(expectedServer);
337+
338+
if (omittedFields.length) {
339+
expect(actualServer).to.not.have.all.keys(omittedFields);
340+
}
341+
});
342+
343+
return;
344+
}
345+
346+
// Load balancer mode has no monitor hello response and
347+
// only expects address and compatible to be set in the
348+
// server description.
349+
if (description.type === TopologyType.LoadBalanced) {
350+
if (key !== 'address' || key !== 'compatible') {
351+
return;
352+
}
353+
}
354+
355+
if (key === 'events') {
356+
const expectedEvents = convertOutcomeEvents(outcomeValue);
357+
expect(events).to.have.length(expectedEvents.length);
358+
for (let i = 0; i < events.length; ++i) {
359+
const expectedEvent = expectedEvents[i];
360+
const actualEvent = cloneForCompare(events[i]);
361+
expect(actualEvent).to.matchMongoSpec(expectedEvent);
362+
}
363+
364+
return;
365+
}
366+
367+
if (key === 'compatible' || key === 'setName') {
368+
if (outcomeValue == null) {
369+
expect(topology.description[key]).to.not.exist;
370+
} else {
371+
expect(topology.description).property(key).to.equal(outcomeValue);
372+
}
373+
374+
return;
375+
}
376+
377+
expect(description).to.include.keys(translatedKey);
378+
379+
if (outcomeValue == null) {
380+
expect(description[translatedKey]).to.not.exist;
381+
} else {
382+
expect(description).property(translatedKey).to.eql(outcomeValue, `(key="${translatedKey}")`);
383+
}
384+
});
385+
}

0 commit comments

Comments
 (0)