M2.5's gate, built before the milestone it gates -- the same order that put
`expectEvents` before the free list in M1 and Tier 0 before everything in M2.
`mongodb/specifications` has no aggregation suite, which is amendment A6's
central finding, so this milestone has to bring its own. The hazard in a corpus
we author is obvious and fatal: it can encode our own bugs as expectations and
then agree with us forever. So the split is enforced by the tooling.
`sources/*.json` holds documents and pipelines and nothing else; `record.js`
asks a real mongod 8.3.7 what each pipeline answers and writes the unified-
format file from the reply. Inputs authored, expectations measured -- the
discipline that corrected three assumptions in M1's session work and every
error code in M2, where the alternative would have shipped both times.
No second runner. `run.js --suite-dir` points the existing one somewhere else,
so the entity model, the matchers, the skip accounting and `expectEvents` come
for free; a second runner would drift from the first exactly where it mattered.
`--scorecard` is refused with `--suite-dir`, because `scorecard.txt` is the crud
corpus's record and the milestones are compared against it -- writing it from an
unrelated run would replace that record silently.
Errors record the code and not the message: message text is mongod's to change
between releases. Group pipelines end in a `$sort`, because group output order
is unspecified and a case depending on it would fail for the wrong reason on
either server.
The first source covers `$group`: nine accumulators including the edge cases
that decide an implementation -- `$avg` over a group whose values are not
numbers, `$min` of a field no document has, `$push` skipping a missing field,
`$first`/`$last` against input order, grouping on an array, a compound `_id`.
Where it starts, run against the M2 tip:
group-accumulators.json 9 pass 10 fail 0 skip
The nine include the four refusals M2 added, which answer with mongod's own
codes -- so the corpus already confirms that half. The ten are the milestone.
The crud corpus is unchanged at 201/90/196.
129 lines
5.2 KiB
JavaScript
129 lines
5.2 KiB
JavaScript
// 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/<name>.json`: documents, and a list of pipelines
|
|
// 2. run this against a real mongod
|
|
// 3. it writes `<name>.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 <n> a running mongod to measure against (default 27099)
|
|
// --only <name> 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 <dir>\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);
|
|
});
|