test(calcUnionAsync): pin the worker plumbing and the event-loop guarantee
Runs the real worker against the real @turf modules, because what broke twice was never the geometry: the worker script and the turf packages sit in sibling directories of the bundle that a bare require() cannot reach (3249362), and the first fix doubled the programs/server prefix so nothing resolved (570cb49). The last test is the reason the worker exists at all: timers keep firing while a 120-circle union computes. Revertfbb746fand it fails.
This commit is contained in:
parent
6e448bda55
commit
755afccf90
1 changed files with 123 additions and 0 deletions
123
test/server/calcUnionAsync.test.js
Normal file
123
test/server/calcUnionAsync.test.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/* eslint-env mocha */
|
||||
/* eslint-disable func-names, prefer-arrow-callback */
|
||||
/* eslint-disable import/no-absolute-path */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { expect } from 'chai';
|
||||
import calcUnionAsync from '/imports/startup/server/calcUnionAsync';
|
||||
|
||||
// These run the real worker thread against the real @turf modules. The point is
|
||||
// not the geometry (turf is not ours to test) but the plumbing that broke twice
|
||||
// in production: the worker script and the turf packages live in sibling
|
||||
// directories of the built bundle that a bare require() can never find
|
||||
// (3249362), and one attempt at fixing that doubled the programs/server prefix
|
||||
// so nothing resolved at all (570cb49).
|
||||
|
||||
const subAt = (lat, lon, distance = 10) => ({ location: { lat, lon }, distance });
|
||||
|
||||
describe('calcUnionAsync worker plumbing', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
it('resolves the worker script and the turf packages from the bundle layout', function () {
|
||||
const worker = path.resolve(process.cwd(), 'assets/app/workers/unionWorker.js');
|
||||
expect(fs.existsSync(worker), `worker script missing at ${worker}`).to.equal(true);
|
||||
|
||||
['circle', 'union', 'truncate'].forEach((mod) => {
|
||||
const turf = path.resolve(process.cwd(), 'npm/node_modules/@turf', mod);
|
||||
expect(fs.existsSync(turf), `@turf/${mod} missing at ${turf}`).to.equal(true);
|
||||
// The doubled-prefix regression: this path must NOT be under programs/server
|
||||
// twice, which is what silently produced "Cannot find module" at runtime.
|
||||
expect(turf).to.not.match(/programs\/server\/programs\/server/);
|
||||
});
|
||||
});
|
||||
|
||||
it('computes a union of one circle', async function () {
|
||||
const union = await calcUnionAsync([subAt(43.5, -5.9)]);
|
||||
expect(union).to.be.an('object');
|
||||
expect(union.geometry.type).to.equal('Polygon');
|
||||
expect(union.geometry.coordinates[0].length).to.be.above(3);
|
||||
});
|
||||
|
||||
it('returns null when there is nothing to unite', async function () {
|
||||
expect(await calcUnionAsync([])).to.equal(null);
|
||||
});
|
||||
|
||||
it('merges two overlapping circles into a single polygon', async function () {
|
||||
const union = await calcUnionAsync([subAt(43.5, -5.9, 20), subAt(43.55, -5.9, 20)]);
|
||||
expect(union.geometry.type).to.equal('Polygon');
|
||||
});
|
||||
|
||||
it('keeps two far-apart circles as a MultiPolygon', async function () {
|
||||
const union = await calcUnionAsync([subAt(43.5, -5.9, 10), subAt(28.1, -15.4, 10)]);
|
||||
expect(union.geometry.type).to.equal('MultiPolygon');
|
||||
expect(union.geometry.coordinates).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('merges into a given base union instead of starting over', async function () {
|
||||
const base = await calcUnionAsync([subAt(43.5, -5.9, 10)]);
|
||||
const merged = await calcUnionAsync([subAt(28.1, -15.4, 10)], base);
|
||||
|
||||
expect(merged.geometry.type).to.equal('MultiPolygon');
|
||||
expect(merged.geometry.coordinates).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('truncates coordinates to 6 decimals so the stored union stays small', async function () {
|
||||
const union = await calcUnionAsync([subAt(43.5, -5.9)]);
|
||||
union.geometry.coordinates[0].forEach(([lon, lat]) => {
|
||||
expect(String(lon).split('.')[1] || '').to.have.length.at.most(6);
|
||||
expect(String(lat).split('.')[1] || '').to.have.length.at.most(6);
|
||||
});
|
||||
});
|
||||
|
||||
// Regression 50ca9cc's other half: the caller must be able to tell a failure
|
||||
// apart from an empty union, so a worker error has to reject, never resolve.
|
||||
it('rejects when the worker throws instead of resolving to null', async function () {
|
||||
let rejected = false;
|
||||
try {
|
||||
// A subscription with no location makes tcircle throw inside the worker.
|
||||
await calcUnionAsync([{ distance: 10 }]);
|
||||
} catch (e) {
|
||||
rejected = true;
|
||||
expect(e).to.be.an('error');
|
||||
}
|
||||
expect(rejected, 'calcUnionAsync resolved instead of rejecting').to.equal(true);
|
||||
});
|
||||
|
||||
it('rejects when the worker script itself cannot be loaded', async function () {
|
||||
let rejected = false;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const worker = new Worker(path.resolve(process.cwd(), 'assets/app/workers/doesNotExist.js'));
|
||||
worker.once('message', resolve);
|
||||
worker.once('error', reject);
|
||||
});
|
||||
} catch (e) {
|
||||
rejected = true;
|
||||
}
|
||||
expect(rejected).to.equal(true);
|
||||
});
|
||||
|
||||
// The whole reason the worker exists (b4e5511 / fbb746f): a union big enough to
|
||||
// take real CPU time must not stop the event loop from running timers, because
|
||||
// that is what froze DDP and disconnected every browser.
|
||||
it('leaves the event loop free while it computes', async function () {
|
||||
const subs = [];
|
||||
for (let i = 0; i < 120; i += 1) subs.push(subAt(40 + (i * 0.02), -4 + (i * 0.02), 25));
|
||||
|
||||
let ticks = 0;
|
||||
const timer = setInterval(() => { ticks += 1; }, 10);
|
||||
const started = Date.now();
|
||||
try {
|
||||
await calcUnionAsync(subs);
|
||||
} finally {
|
||||
clearInterval(timer);
|
||||
}
|
||||
const elapsed = Date.now() - started;
|
||||
|
||||
// If the union ran on the main thread the interval would barely fire at all.
|
||||
// Half the theoretical ticks is a wide margin against a loaded CI runner.
|
||||
expect(ticks, `only ${ticks} timer ticks in ${elapsed}ms`).to.be.at.least(Math.floor(elapsed / 10 / 2));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue