Files
MultiforaDB/tests/e2e/e2e4.js
Aleksey Shakhmatov d4c9b04f21 rename project to MultiforaDB
Prose and benchmark tables use MultiforaDB; the binary, the CLI usage
line, the log-message prefix and the default database file use
multiforadb.

Two consequences worth noting:

- build.zig.zon's fingerprint is derived from the package name, so it
  had to change with it (Zig refuses to build otherwise). A consumer
  pinning this package by fingerprint needs updating.
- the default --db path is now multiforadb.log, and getCmdLineOpts
  reports it as dbpath. An existing mongo-lite.log has to be passed
  explicitly with --db.

The e2e harness abbreviated the old name as ML_; that is now MFDB_,
including the documented ML_BIN override (MFDB_BIN) and the scratch
file names. MD_ (mongod) is untouched.

compare-run.sh spawned the server by absolute path under a
sandbox/mongo-lite directory that no longer exists; that block already
runs from tests/e2e, so it uses a relative path now.

The archived reports under tests/e2e/results/ keep the old name: they
record what the old binary measured.
2026-08-03 12:35:01 +03:00

139 lines
5.2 KiB
JavaScript

// E2E part 4: TTL indexes through the official Node driver.
// createIndex({expireAfterSeconds}), the option round-tripping through
// listIndexes, background expiry of past-dated documents, and the specs the
// server must reject with CannotCreateIndex (67).
//
// The server must run with a short sweep interval, e.g.
// zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log --ttl-sweep-secs 1
const { MongoClient } = require('mongodb');
const URL = 'mongodb://127.0.0.1:27020';
const results = [];
function check(name, cond, detail = '') {
results.push({ name, ok: !!cond, detail: String(detail) });
if (!cond) console.error(`${name} ${detail}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/// Poll until `fn()` is true or the deadline passes; expiry is a background
/// sweep, so the exact moment a document disappears is not fixed.
async function waitFor(fn, timeoutMs = 15000, stepMs = 250) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await fn()) return true;
await sleep(stepMs);
}
return false;
}
async function expectCode(fn, code, name) {
let err = null;
try {
await fn();
} catch (e) {
err = e;
}
check(name, err && err.code === code, err ? `code ${err.code}: ${err.message}` : 'no error');
}
async function main() {
const client = new MongoClient(URL, { serverSelectionTimeoutMS: 5000 });
await client.connect();
const db = client.db('e2e4');
const coll = db.collection('sessions');
await coll.drop().catch(() => {});
// --- createIndex + listIndexes round-trip ---
await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 });
const idxs = await coll.indexes();
const ttlIdx = idxs.find((i) => i.name === 'expireAt_1');
check('TTL index listed', !!ttlIdx, JSON.stringify(idxs));
check('expireAfterSeconds reported', ttlIdx && Number(ttlIdx.expireAfterSeconds) === 1, JSON.stringify(ttlIdx));
check('_id_ has no expireAfterSeconds', idxs.find((i) => i.name === '_id_').expireAfterSeconds === undefined);
// Idempotent re-create of the same spec.
await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 });
check('idempotent re-create', (await coll.indexes()).filter((i) => i.name === 'expireAt_1').length === 1);
// --- expiry ---
const now = Date.now();
await coll.insertMany([
{ _id: 'past', expireAt: new Date(now - 60_000) },
{ _id: 'future', expireAt: new Date(now + 3_600_000) },
{ _id: 'string', expireAt: 'tomorrow' },
{ _id: 'missing' },
// An array of dates expires on its earliest member.
{ _id: 'array', expireAt: [new Date(now - 60_000), new Date(now + 3_600_000)] },
]);
check('all five inserted', (await coll.countDocuments({})) === 5);
const gone = await waitFor(async () => (await coll.countDocuments({ _id: 'past' })) === 0);
check('past-dated doc expired', gone);
check('array doc expired on earliest date', (await coll.countDocuments({ _id: 'array' })) === 0);
const survivors = (await coll.find({}).toArray()).map((d) => d._id).sort();
check('future/string/missing survive', JSON.stringify(survivors) === '["future","missing","string"]', JSON.stringify(survivors));
// Expiry is a real delete: it holds after the sweeper has run again.
await sleep(1500);
check('expired docs stay deleted', (await coll.countDocuments({})) === 3);
// --- rejected specs ---
const bad = db.collection('bad');
await expectCode(
() => bad.createIndex({ a: 1, b: 1 }, { expireAfterSeconds: 60 }),
67,
'compound TTL rejected with 67',
);
await expectCode(
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: -1 }),
67,
'negative expireAfterSeconds rejected with 67',
);
await expectCode(
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: 'soon' }),
67,
'non-numeric expireAfterSeconds rejected with 67',
);
await expectCode(
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: 2147483648 }),
67,
'expireAfterSeconds past 2147483647 rejected with 67',
);
await expectCode(
() => bad.createIndex({ _id: 1 }, { expireAfterSeconds: 60 }),
197,
'TTL on _id rejected with 197',
);
// Same name and key with a different expiry: IndexOptionsConflict (85),
// exactly as MongoDB does (the change belongs to collMod).
await expectCode(
() => coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 90 }),
85,
'changed expiry gives IndexOptionsConflict',
);
// expireAfterSeconds 0 is legal: expire at exactly the stored instant.
const zero = db.collection('zero');
await zero.drop().catch(() => {});
await zero.createIndex({ at: 1 }, { expireAfterSeconds: 0 });
await zero.insertOne({ _id: 'now', at: new Date(Date.now() - 1000) });
check('expireAfterSeconds 0 accepted', (await zero.indexes()).some((i) => Number(i.expireAfterSeconds) === 0));
check('expireAfterSeconds 0 expires', await waitFor(async () => (await zero.countDocuments({})) === 0));
await client.close();
const failed = results.filter((r) => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
if (failed.length) {
console.log('FAILED:', failed.map((f) => f.name).join(', '));
process.exit(1);
}
console.log('E2E4_OK');
}
main().catch((e) => {
console.error('E2E4_FAIL', e);
process.exit(1);
});