|
| 1 | +import { Document, EJSON } from 'bson'; |
| 2 | +import { expect } from 'chai'; |
| 3 | +import { readdirSync, readFileSync, statSync } from 'fs'; |
| 4 | +import { basename, extname, join } from 'path'; |
| 5 | + |
| 6 | +import { |
| 7 | + ReadPreference, |
| 8 | + ReadPreferenceMode, |
| 9 | + ReadPreferenceOptions |
| 10 | +} from '../../../src/read_preference'; |
| 11 | +import { ServerType, TopologyType } from '../../../src/sdam/common'; |
| 12 | +import { ServerDescription, TagSet } from '../../../src/sdam/server_description'; |
| 13 | +import { |
| 14 | + readPreferenceServerSelector, |
| 15 | + writableServerSelector |
| 16 | +} from '../../../src/sdam/server_selection'; |
| 17 | +import { TopologyDescription } from '../../../src/sdam/topology_description'; |
| 18 | +import { serverDescriptionFromDefinition } from './server_selection_spec_helper'; |
| 19 | + |
| 20 | +interface ServerSelectionLogicTestServer { |
| 21 | + address: string; |
| 22 | + avg_rtt_ms: number; |
| 23 | + type: ServerType; |
| 24 | + tags?: TagSet; |
| 25 | +} |
| 26 | +interface ServerSelectionLogicTest { |
| 27 | + topology_description: { |
| 28 | + type: TopologyType; |
| 29 | + servers: ServerSelectionLogicTestServer[]; |
| 30 | + }; |
| 31 | + operation: 'read' | 'write'; |
| 32 | + read_preference: { |
| 33 | + mode: ReadPreferenceMode; |
| 34 | + tag_sets?: TagSet[]; |
| 35 | + }; |
| 36 | + /** |
| 37 | + * The spec says we should confirm the list of suitable servers in addition to the list of |
| 38 | + * servers in the latency window, if possible. We apply the latency window inside the |
| 39 | + * selector so for Node this is not possible. |
| 40 | + * https://github.com/mongodb/specifications/tree/master/source/server-selection/tests#server-selection-logic-tests |
| 41 | + */ |
| 42 | + suitable_servers: never; |
| 43 | + in_latency_window: ServerSelectionLogicTestServer[]; |
| 44 | +} |
| 45 | + |
| 46 | +function readPreferenceFromDefinition(definition) { |
| 47 | + const mode = definition.mode |
| 48 | + ? definition.mode.charAt(0).toLowerCase() + definition.mode.slice(1) |
| 49 | + : 'primary'; |
| 50 | + |
| 51 | + const options: ReadPreferenceOptions = {}; |
| 52 | + if (typeof definition.maxStalenessSeconds !== 'undefined') |
| 53 | + options.maxStalenessSeconds = definition.maxStalenessSeconds; |
| 54 | + const tags = definition.tag_sets ?? []; |
| 55 | + |
| 56 | + return new ReadPreference(mode, tags, options); |
| 57 | +} |
| 58 | + |
| 59 | +/** |
| 60 | + * Compares two server descriptions and compares all fields that are present |
| 61 | + * in the yaml spec tests. |
| 62 | + */ |
| 63 | +function compareServerDescriptions(s1: ServerDescription, s2: ServerDescription) { |
| 64 | + expect(s1.address).to.equal(s2.address); |
| 65 | + expect(s1.roundTripTime).to.equal(s2.roundTripTime); |
| 66 | + expect(s1.type).to.equal(s2.type); |
| 67 | + expect(s1.tags).to.deep.equal(s2.tags); |
| 68 | +} |
| 69 | + |
| 70 | +function serverDescriptionsToMap( |
| 71 | + descriptions: ServerDescription[] |
| 72 | +): Map<string, ServerDescription> { |
| 73 | + const descriptionMap = new Map<string, ServerDescription>(); |
| 74 | + |
| 75 | + for (const description of descriptions) { |
| 76 | + descriptionMap.set(description.address, description); |
| 77 | + } |
| 78 | + |
| 79 | + return descriptionMap; |
| 80 | +} |
| 81 | + |
| 82 | +/** |
| 83 | + * Executes a server selection logic test |
| 84 | + * @see https://github.com/mongodb/specifications/tree/master/source/server-selection/tests#server-selection-logic-tests |
| 85 | + */ |
| 86 | +export function runServerSelectionLogicTest(testDefinition: ServerSelectionLogicTest) { |
| 87 | + const allHosts = testDefinition.topology_description.servers.map(({ address }) => address); |
| 88 | + const serversInTopology = testDefinition.topology_description.servers.map(s => |
| 89 | + serverDescriptionFromDefinition(s, allHosts) |
| 90 | + ); |
| 91 | + const serverDescriptions = serverDescriptionsToMap(serversInTopology); |
| 92 | + const topologyDescription = new TopologyDescription( |
| 93 | + testDefinition.topology_description.type, |
| 94 | + serverDescriptions |
| 95 | + ); |
| 96 | + const expectedServers = serverDescriptionsToMap( |
| 97 | + testDefinition.in_latency_window.map(s => serverDescriptionFromDefinition(s)) |
| 98 | + ); |
| 99 | + |
| 100 | + let selector; |
| 101 | + if (testDefinition.operation === 'write') { |
| 102 | + selector = writableServerSelector(); |
| 103 | + } else if (testDefinition.operation === 'read' || testDefinition.read_preference) { |
| 104 | + const readPreference = readPreferenceFromDefinition(testDefinition.read_preference); |
| 105 | + selector = readPreferenceServerSelector(readPreference); |
| 106 | + } else { |
| 107 | + expect.fail('test operation was neither read nor write, and no read preference was provided.'); |
| 108 | + } |
| 109 | + |
| 110 | + const result = selector(topologyDescription, serversInTopology); |
| 111 | + |
| 112 | + expect(result.length).to.equal(expectedServers.size); |
| 113 | + |
| 114 | + for (const server of result) { |
| 115 | + const expectedServer = expectedServers.get(server.address); |
| 116 | + expect(expectedServer).to.exist; |
| 117 | + compareServerDescriptions(server, expectedServer); |
| 118 | + expectedServers.delete(server.address); |
| 119 | + } |
| 120 | + |
| 121 | + expect(expectedServers.size).to.equal(0); |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * reads in the server selection logic tests from the provided directory |
| 126 | + */ |
| 127 | +export function collectServerSelectionLogicTests(specDir) { |
| 128 | + const testTypes = readdirSync(specDir).filter(d => statSync(join(specDir, d)).isDirectory()); |
| 129 | + |
| 130 | + const tests = {}; |
| 131 | + for (const testType of testTypes) { |
| 132 | + const testsOfType = readdirSync(join(specDir, testType)).filter(d => |
| 133 | + statSync(join(specDir, testType, d)).isDirectory() |
| 134 | + ); |
| 135 | + const result = {}; |
| 136 | + for (const subType of testsOfType) { |
| 137 | + result[subType] = readdirSync(join(specDir, testType, subType)) |
| 138 | + .filter(f => extname(f) === '.json') |
| 139 | + .map(f => { |
| 140 | + const fileContents = readFileSync(join(specDir, testType, subType, f), { |
| 141 | + encoding: 'utf-8' |
| 142 | + }); |
| 143 | + const test = EJSON.parse(fileContents, { relaxed: true }) as unknown as Document; |
| 144 | + test.name = basename(f, '.json'); |
| 145 | + test.type = testType; |
| 146 | + test.subType = subType; |
| 147 | + return test; |
| 148 | + }); |
| 149 | + } |
| 150 | + |
| 151 | + tests[testType] = result; |
| 152 | + } |
| 153 | + |
| 154 | + return tests; |
| 155 | +} |
0 commit comments