// Record an aggregation corpus by asking a real mongod what the answer is. // // `mongodb/specifications` has no aggregation suite (see PLAN amendment A6), so // M2.5 has to bring its own. The one thing a corpus we author must not do is // encode our own bugs as expectations -- so the *inputs* are authored here and // the *expectations* are measured, the same discipline that corrected three // assumptions in M1's session work and every error code in M2. // // 1. author `sources/.json`: documents, and a list of pipelines // 2. run this against a real mongod // 3. it writes `.json` in the unified format, expectations filled in // 4. `node tests/spec/run.js --suite-dir tests/spec/aggregate` runs it // // Generated files are committed: they are the corpus, and regenerating them is // how a disagreement with mongod gets re-measured rather than argued about. // // node tests/spec/aggregate/record.js --mongod-port 27099 // // Options: // --mongod-port a running mongod to measure against (default 27099) // --only record just one source file const fs = require('fs'); const path = require('path'); // The same pinned driver `run.js` uses, resolved the same way: there is one // lockfile in this repo and it lives with the e2e suites. const { MongoClient } = require(path.join(__dirname, '..', '..', 'e2e', 'node_modules', 'mongodb')); const argv = process.argv.slice(2); function opt(name, dflt) { const i = argv.indexOf('--' + name); if (i < 0) return dflt; const v = argv[i + 1]; return v === undefined || v.startsWith('--') ? true : v; } const PORT = parseInt(opt('mongod-port', '27099'), 10); const ONLY = opt('only', null); const SRC_DIR = path.join(__dirname, 'sources'); const DB_NAME = 'aggregate-corpus'; // The corpus is run against a server that reports 4.4.0, so a case whose answer // depends on a later server would be recorded from mongod 8.x and then judged // against a 4.4 answer. Pinned here rather than per file: every case in this // corpus is expected to be version-independent, and one that is not should be // left out rather than annotated. const SCHEMA_VERSION = '1.4'; async function main() { if (!fs.existsSync(SRC_DIR)) { console.error(`missing ${SRC_DIR}`); process.exit(2); } const client = new MongoClient(`mongodb://127.0.0.1:${PORT}`, { serverSelectionTimeoutMS: 3000 }); try { await client.connect(); } catch (e) { console.error(`no mongod on :${PORT} -- start one first:\n` + ` mongod --port ${PORT} --dbpath \n${e.message}`); process.exit(2); } const build = await client.db('admin').command({ buildInfo: 1 }); console.log(`recording against mongod ${build.version} on :${PORT}`); const sources = fs.readdirSync(SRC_DIR).filter((f) => f.endsWith('.json')) .filter((f) => !ONLY || f === ONLY || f === ONLY + '.json') .sort(); if (!sources.length) { console.error('no source files'); process.exit(2); } for (const file of sources) { const src = JSON.parse(fs.readFileSync(path.join(SRC_DIR, file), 'utf8')); const name = path.basename(file, '.json'); const out = await record(client, name, src); const target = path.join(__dirname, `${name}.json`); fs.writeFileSync(target, JSON.stringify(out, null, 2) + '\n'); const errs = out.tests.filter((t) => t.operations[0].expectError).length; console.log(` ${name}: ${out.tests.length} cases, ${errs} of them errors`); } await client.close(); console.log('RECORDED'); } async function record(client, name, src) { const db = client.db(DB_NAME); const coll = db.collection('coll'); const tests = []; for (const c of src.cases) { await coll.drop().catch(() => {}); await coll.insertMany(structuredClone(src.documents)); const op = { object: 'collection0', name: 'aggregate', arguments: { pipeline: c.pipeline } }; try { const got = await coll.aggregate(structuredClone(c.pipeline)).toArray(); op.expectResult = got; } catch (e) { // The code, not the message: message text is mongod's to change // between releases, and a corpus that pins it would fail for the // wrong reason. `isError` plus the code is what the unified format // asserts anyway. op.expectError = { isError: true, errorCode: e.code }; } tests.push({ description: c.description, operations: [op] }); } await coll.drop().catch(() => {}); return { description: name, schemaVersion: SCHEMA_VERSION, // Recorded, not authored. Regenerate with tests/spec/aggregate/record.js. createEntities: [ { client: { id: 'client0' } }, { database: { id: 'database0', client: 'client0', databaseName: DB_NAME } }, { collection: { id: 'collection0', database: 'database0', collectionName: 'coll' } }, ], initialData: [ { collectionName: 'coll', databaseName: DB_NAME, documents: src.documents }, ], tests, }; } main().catch((e) => { console.error('RECORD_FAIL', e); process.exit(1); });