mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e1-qa-readiness' into german/navara-refactor
# Conflicts: # scripts/qa-harness-baseline.json # scripts/qa-strict-harness.mjs
This commit is contained in:
commit
18da76e8d8
47 files changed
+1789
-314
No files matched your search
+85
-1
@@ -16,10 +16,89 @@
|
||||
// following() { label, native } of engine.camera.following()
|
||||
// movers() engine.movers.list()
|
||||
// framesDrawn() frames drawn since the probe started
|
||||
// settled({ frames? }) readiness conditions over drawn frames
|
||||
|
||||
const plain = (position) =>
|
||||
position ? { x: position.x, y: position.y, z: position.z } : null;
|
||||
|
||||
/** Frame-based readiness, also serialized by the production QA harness. */
|
||||
export function createSettledState(
|
||||
engine,
|
||||
getRestore = () =>
|
||||
globalThis.window?.__godsEyeView?.styleManager?.initialRestorePromise,
|
||||
) {
|
||||
let drawn = 0;
|
||||
let stable = 0;
|
||||
let previous = null;
|
||||
let restore;
|
||||
let restored = false;
|
||||
let restoreError = null;
|
||||
const observeRestore = () => {
|
||||
const next = getRestore();
|
||||
if (!next || next === restore) return;
|
||||
restore = next;
|
||||
restored = false;
|
||||
restoreError = null;
|
||||
Promise.resolve(next).then(
|
||||
() => {
|
||||
if (restore === next) restored = true;
|
||||
},
|
||||
(error) => {
|
||||
if (restore === next) restoreError = String(error);
|
||||
},
|
||||
);
|
||||
};
|
||||
const pose = () => {
|
||||
const value = engine.camera.pose();
|
||||
return [
|
||||
...['position', 'direction', 'up'].flatMap((key) =>
|
||||
['x', 'y', 'z'].map((axis) => value[key][axis]),
|
||||
),
|
||||
value.fovy,
|
||||
value.aspectRatio,
|
||||
];
|
||||
};
|
||||
const same = (a, b) =>
|
||||
a && a.every((n, i) => Math.abs(n - b[i]) <= (i < 3 ? 1e-6 : 1e-10));
|
||||
const remove = engine.frames.postRender.addEventListener(() => {
|
||||
observeRestore();
|
||||
drawn++;
|
||||
const current = pose();
|
||||
stable =
|
||||
same(previous, current) &&
|
||||
!engine.camera.flightActive() &&
|
||||
engine.input.cameraInputsEnabled()
|
||||
? stable + 1
|
||||
: 0;
|
||||
previous = current;
|
||||
});
|
||||
observeRestore();
|
||||
return {
|
||||
settled({ frames = 3 } = {}) {
|
||||
observeRestore();
|
||||
const started = !engine.isDestroyed();
|
||||
const conditions = {
|
||||
engineStarted: started,
|
||||
initialViewRestored: restored,
|
||||
frameDrawn: drawn > 0,
|
||||
noFlight: started && !engine.camera.flightActive(),
|
||||
cameraInputsEnabled: started && engine.input.cameraInputsEnabled(),
|
||||
cameraPoseStable: started && stable >= frames && same(previous, pose()),
|
||||
};
|
||||
const unmet = Object.keys(conditions).filter((key) => !conditions[key]);
|
||||
return {
|
||||
settled: !unmet.length,
|
||||
conditions,
|
||||
unmet,
|
||||
framesDrawn: drawn,
|
||||
stableFrames: stable,
|
||||
restoreError,
|
||||
};
|
||||
},
|
||||
destroy: remove,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* engine: import('../globe/contract.js').GlobeEngine,
|
||||
@@ -27,6 +106,7 @@ const plain = (position) =>
|
||||
* }} options
|
||||
*/
|
||||
export function createQaProbe({ engine, dataManager }) {
|
||||
const readiness = createSettledState(engine);
|
||||
let frames = 0;
|
||||
const removeFrames = engine.frames.postRender.addEventListener(() => {
|
||||
frames++;
|
||||
@@ -84,6 +164,10 @@ export function createQaProbe({ engine, dataManager }) {
|
||||
},
|
||||
movers: () => engine.movers.list(),
|
||||
framesDrawn: () => frames,
|
||||
destroy: () => removeFrames(),
|
||||
settled: (options) => readiness.settled(options),
|
||||
destroy() {
|
||||
removeFrames();
|
||||
readiness.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/** Observe a started engine without importing runtime or SDK modules. */
|
||||
export function createContractMonitor(engine, report = () => {}) {
|
||||
const records = [];
|
||||
const flights = [];
|
||||
const removers = [];
|
||||
const restorers = [];
|
||||
let destroyed = false;
|
||||
let moving = false;
|
||||
let frame = 0;
|
||||
let inFrame = false;
|
||||
let requestedInFrame = null;
|
||||
const record = (invariant, detail = {}) => {
|
||||
const row = { invariant, frame, ...detail };
|
||||
records.push(row);
|
||||
report(row);
|
||||
};
|
||||
const wrap = (owner, name, decorate) => {
|
||||
const original = owner[name];
|
||||
const wrapped = decorate(original);
|
||||
owner[name] = wrapped;
|
||||
restorers.push(() => {
|
||||
if (owner[name] === wrapped) owner[name] = original;
|
||||
});
|
||||
};
|
||||
const listen = (event, name, callback = () => {}) => {
|
||||
if (!event) return;
|
||||
removers.push(
|
||||
event.addEventListener(() => {
|
||||
if (destroyed) record('event-after-destroy', { event: name });
|
||||
else callback();
|
||||
}),
|
||||
);
|
||||
};
|
||||
for (const name of ['changed', 'commanded', 'followChanged'])
|
||||
listen(engine.camera[name], `camera.${name}`);
|
||||
listen(engine.camera.moveStart, 'camera.moveStart', () => {
|
||||
if (moving) record('move-alternation', { event: 'moveStart' });
|
||||
moving = true;
|
||||
});
|
||||
listen(engine.camera.moveEnd, 'camera.moveEnd', () => {
|
||||
if (!moving) record('move-alternation', { event: 'moveEnd' });
|
||||
moving = false;
|
||||
});
|
||||
listen(engine.frames.preUpdate, 'frames.preUpdate', () => {
|
||||
frame++;
|
||||
inFrame = true;
|
||||
// Request-mode engines can update without drawing a frame.
|
||||
queueMicrotask(() => {
|
||||
inFrame = false;
|
||||
});
|
||||
});
|
||||
listen(engine.frames.preRender, 'frames.preRender', () => {
|
||||
inFrame = true;
|
||||
});
|
||||
listen(engine.frames.postRender, 'frames.postRender', () => {
|
||||
if (requestedInFrame !== null && frame > requestedInFrame)
|
||||
requestedInFrame = null;
|
||||
// Include requests made by listeners registered after the monitor.
|
||||
queueMicrotask(() => {
|
||||
inFrame = false;
|
||||
});
|
||||
});
|
||||
listen(engine.maps.errors, 'maps.errors');
|
||||
for (const type of ['click', 'doubleClick', 'move', 'down', 'up']) {
|
||||
removers.push(
|
||||
engine.input.on(type, () => {
|
||||
if (destroyed)
|
||||
record('event-after-destroy', { event: `input.${type}` });
|
||||
}),
|
||||
);
|
||||
}
|
||||
wrap(
|
||||
engine.render,
|
||||
'request',
|
||||
(original) =>
|
||||
function (...args) {
|
||||
if (inFrame) requestedInFrame = frame;
|
||||
return original.apply(this, args);
|
||||
},
|
||||
);
|
||||
const missing = () => {
|
||||
for (const flight of flights) {
|
||||
if (!flight.ends && !flight.reported) {
|
||||
flight.reported = true;
|
||||
record('flight-terminal', { flight: flight.id, ends: 0 });
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const name of ['flyTo', 'flyToSphere']) {
|
||||
wrap(
|
||||
engine.camera,
|
||||
name,
|
||||
(original) =>
|
||||
function (...args) {
|
||||
const index = name === 'flyTo' ? 0 : 1;
|
||||
const options = args[index] ?? {};
|
||||
const flight = { id: flights.length + 1, ends: 0 };
|
||||
flights.push(flight);
|
||||
const observed = { ...options };
|
||||
for (const outcome of ['complete', 'cancel']) {
|
||||
observed[outcome] = function (...values) {
|
||||
flight.ends++;
|
||||
if (flight.ends !== 1)
|
||||
record('flight-terminal', {
|
||||
flight: flight.id,
|
||||
ends: flight.ends,
|
||||
outcome,
|
||||
});
|
||||
if (outcome === 'complete' && engine.camera.flightActive())
|
||||
record('flight-active', { flight: flight.id, at: 'complete' });
|
||||
return options[outcome]?.apply(this, values);
|
||||
};
|
||||
}
|
||||
args[index] = observed;
|
||||
try {
|
||||
return original.apply(this, args);
|
||||
} catch (error) {
|
||||
// Invalid commands do not promise a flight or a terminal callback.
|
||||
flights.splice(flights.indexOf(flight), 1);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
for (const name of ['cancelFlight', 'completeFlight']) {
|
||||
wrap(
|
||||
engine.camera,
|
||||
name,
|
||||
(original) =>
|
||||
function (...args) {
|
||||
const result = original.apply(this, args);
|
||||
if (engine.camera.flightActive())
|
||||
record('flight-active', { at: name });
|
||||
missing();
|
||||
return result;
|
||||
},
|
||||
);
|
||||
}
|
||||
wrap(
|
||||
engine,
|
||||
'destroy',
|
||||
(original) =>
|
||||
function (...args) {
|
||||
const result = original.apply(this, args);
|
||||
destroyed = true;
|
||||
inFrame = false;
|
||||
requestedInFrame = null;
|
||||
missing();
|
||||
return result;
|
||||
},
|
||||
);
|
||||
return {
|
||||
records,
|
||||
async finish({ active = true } = {}) {
|
||||
missing();
|
||||
if (!destroyed && active) {
|
||||
if (!engine.input.cameraInputsEnabled())
|
||||
record('final-inputs-disabled');
|
||||
if (engine.camera.flightActive()) record('final-flight-active');
|
||||
if (requestedInFrame !== null) {
|
||||
const requested = requestedInFrame;
|
||||
// A request on the last observed frame still deserves its next frame.
|
||||
await new Promise((resolve) => {
|
||||
const remove = engine.frames.postRender.addEventListener(() => {
|
||||
if (frame <= requested) return;
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
resolve();
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
remove();
|
||||
if (!destroyed)
|
||||
record('render-request-lost', { requestedInFrame: requested });
|
||||
resolve();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
}
|
||||
return records;
|
||||
},
|
||||
dispose() {
|
||||
for (const remove of removers) remove();
|
||||
for (const restore of restorers.reverse()) restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createFakeEngine } from './fakeEngine.js';
|
||||
import { createContractMonitor } from './contractMonitor.js';
|
||||
|
||||
const destination = { x: 7000000, y: 1000000, z: 0 };
|
||||
const violations = (monitor) => monitor.records.map((row) => row.invariant);
|
||||
|
||||
test('normal flights, moves, in-frame requests and destruction preserve the contract', async () => {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.camera.flyTo({ destination, duration: 0 });
|
||||
engine.camera.flyTo({ destination, duration: 1 });
|
||||
engine.camera.cancelFlight();
|
||||
engine.camera.flyToSphere({ center: destination, radius: 100 });
|
||||
engine.camera.completeFlight();
|
||||
let remove = engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
engine.render.request();
|
||||
});
|
||||
engine.fake.frames(3);
|
||||
await Promise.resolve();
|
||||
await monitor.finish();
|
||||
engine.destroy();
|
||||
assert.deepEqual(monitor.records, []);
|
||||
monitor.dispose();
|
||||
});
|
||||
|
||||
test('terminal callbacks are counted even without caller callbacks', () => {
|
||||
for (const endings of [
|
||||
[],
|
||||
['complete', 'cancel'],
|
||||
['complete', 'complete'],
|
||||
]) {
|
||||
const engine = createFakeEngine();
|
||||
engine.camera.flyTo = (options) =>
|
||||
endings.forEach((name) => options[name]());
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.camera.flyTo({ destination });
|
||||
monitor.finish();
|
||||
assert.ok(violations(monitor).includes('flight-terminal'));
|
||||
monitor.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('complete callbacks and explicit flight termination must see an inactive flight', () => {
|
||||
for (const at of ['complete', 'cancelFlight', 'completeFlight']) {
|
||||
const engine = createFakeEngine();
|
||||
if (at === 'complete')
|
||||
engine.camera.flyTo = (options) => options.complete();
|
||||
else engine.camera[at] = () => {};
|
||||
engine.camera.flightActive = () => true;
|
||||
const monitor = createContractMonitor(engine);
|
||||
if (at === 'complete') engine.camera.flyTo({ destination });
|
||||
else engine.camera[at]();
|
||||
assert.ok(
|
||||
monitor.records.some(
|
||||
(row) => row.invariant === 'flight-active' && row.at === at,
|
||||
),
|
||||
);
|
||||
monitor.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('a cancelled flight that loses its callback is reported at cancellation', () => {
|
||||
const engine = createFakeEngine();
|
||||
engine.camera.flyTo = () => {};
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.camera.flyTo({ destination });
|
||||
engine.camera.cancelFlight();
|
||||
assert.ok(violations(monitor).includes('flight-terminal'));
|
||||
});
|
||||
|
||||
test('moveStart and moveEnd must alternate', () => {
|
||||
for (const events of [
|
||||
['moveEnd'],
|
||||
['moveStart', 'moveStart'],
|
||||
['moveStart', 'moveEnd', 'moveEnd'],
|
||||
]) {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
for (const name of events) engine.fake.emit(`camera.${name}`);
|
||||
assert.ok(violations(monitor).includes('move-alternation'));
|
||||
}
|
||||
});
|
||||
|
||||
test('a swallowed in-frame render request leaves a missing next frame', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const engine = createFakeEngine();
|
||||
engine.render.request = () => {};
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.frames.postRender.addEventListener(() => engine.render.request());
|
||||
engine.fake.frame();
|
||||
const finish = monitor.finish();
|
||||
t.mock.timers.tick(2000);
|
||||
await finish;
|
||||
assert.ok(violations(monitor).includes('render-request-lost'));
|
||||
});
|
||||
|
||||
test('a request in the next frame must also draw a later frame', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const engine = createFakeEngine();
|
||||
engine.render.request = () => {};
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.frames.preRender.addEventListener(() => engine.render.request());
|
||||
engine.fake.frames(2);
|
||||
const finish = monitor.finish();
|
||||
t.mock.timers.tick(2000);
|
||||
await finish;
|
||||
assert.ok(violations(monitor).includes('render-request-lost'));
|
||||
});
|
||||
|
||||
test('an update without a draw does not mark later requests as in-frame', async () => {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.fake.emit('frames.preUpdate');
|
||||
await Promise.resolve();
|
||||
engine.render.request();
|
||||
await monitor.finish();
|
||||
assert.deepEqual(monitor.records, []);
|
||||
});
|
||||
|
||||
test('the active engine must finish with inputs enabled and without a flight', () => {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.input.suspendCameraInputs();
|
||||
engine.camera.flyTo({ destination });
|
||||
monitor.finish();
|
||||
assert.ok(violations(monitor).includes('final-inputs-disabled'));
|
||||
assert.ok(violations(monitor).includes('final-flight-active'));
|
||||
});
|
||||
|
||||
test('events emitted after destroy remain observable until disposal', () => {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
engine.destroy();
|
||||
engine.fake.emit('camera.changed');
|
||||
engine.fake.emit('frames.postRender');
|
||||
assert.deepEqual(violations(monitor), [
|
||||
'event-after-destroy',
|
||||
'event-after-destroy',
|
||||
]);
|
||||
monitor.dispose();
|
||||
engine.fake.emit('camera.changed');
|
||||
assert.equal(monitor.records.length, 2);
|
||||
});
|
||||
|
||||
test('wrappers preserve callback arguments, receivers, returns, and thrown commands', () => {
|
||||
const engine = createFakeEngine();
|
||||
const receiver = {};
|
||||
engine.camera.flyTo = (options) => options.complete.call(receiver, 7);
|
||||
const original = engine.camera.flyTo;
|
||||
const monitor = createContractMonitor(engine);
|
||||
assert.equal(
|
||||
engine.camera.flyTo({
|
||||
complete(value) {
|
||||
assert.equal(this, receiver);
|
||||
return value;
|
||||
},
|
||||
}),
|
||||
7,
|
||||
);
|
||||
monitor.dispose();
|
||||
assert.equal(engine.camera.flyTo, original);
|
||||
const real = createFakeEngine();
|
||||
const other = createContractMonitor(real);
|
||||
assert.throws(() => real.camera.flyTo({}), /destination/);
|
||||
other.finish();
|
||||
assert.deepEqual(other.records, []);
|
||||
});
|
||||
|
||||
test('a final in-frame request may draw its next frame before evidence closes', async () => {
|
||||
const engine = createFakeEngine();
|
||||
const monitor = createContractMonitor(engine);
|
||||
let remove = engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
engine.render.request();
|
||||
});
|
||||
engine.fake.frame();
|
||||
const finish = monitor.finish();
|
||||
engine.fake.frame();
|
||||
await finish;
|
||||
assert.deepEqual(monitor.records, []);
|
||||
});
|
||||
@@ -121,21 +121,33 @@ export function waitForFirstFrame(
|
||||
clearTimer = clearTimeout,
|
||||
} = {},
|
||||
) {
|
||||
if (document?.hidden) return Promise.resolve(false);
|
||||
const outcome = (path) => {
|
||||
if (engine.host?.canvas?.dataset)
|
||||
engine.host.canvas.dataset.firstFrameOutcome = path;
|
||||
};
|
||||
if (document?.hidden) {
|
||||
outcome('hidden');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
outcome('pending');
|
||||
return new Promise((resolve) => {
|
||||
let finished = false;
|
||||
let timer = null;
|
||||
let remove = () => {};
|
||||
const finish = (drawn) => {
|
||||
const finish = (path) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
outcome(path);
|
||||
remove();
|
||||
clearTimer(timer);
|
||||
document?.removeEventListener?.('visibilitychange', hidden);
|
||||
resolve(drawn);
|
||||
resolve(path === 'frame');
|
||||
};
|
||||
function hidden() {
|
||||
if (document.hidden) finish(false);
|
||||
if (document.hidden) finish('hidden');
|
||||
}
|
||||
remove = engine.frames.postRender.addEventListener(() => finish(true));
|
||||
timer = setTimer(() => finish(false), timeoutMs);
|
||||
remove = engine.frames.postRender.addEventListener(() => finish('frame'));
|
||||
timer = setTimer(() => finish('timeout'), timeoutMs);
|
||||
document?.addEventListener?.('visibilitychange', hidden);
|
||||
engine.render.request('engine-switch');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user