mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
Exercise every catalog layer on both planets and verify live switch continuity
This commit is contained in:
1 parent
05b646daa3
commit
62296a833c
6 files changed
+224
-15
No files matched your search
@@ -0,0 +1,119 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { APPLICATION_LAYER_DIRECTORY } from '../src/app/layers/directory.js';
|
||||||
|
|
||||||
|
const base = new URL(process.argv[2]);
|
||||||
|
if (
|
||||||
|
!['127.0.0.1', 'localhost'].includes(base.hostname) ||
|
||||||
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
|
base.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw Error('Supply an unreserved keyless local preview URL');
|
||||||
|
const output = process.argv[3]
|
||||||
|
? resolve(process.argv[3])
|
||||||
|
: mkdtempSync(join(tmpdir(), 'gev-layer-switch-'));
|
||||||
|
mkdirSync(output, { recursive: true });
|
||||||
|
const runs = [
|
||||||
|
['engine-switch', ['earthquakes']],
|
||||||
|
['director-engine-switch', []],
|
||||||
|
['layer-handoff', ['flights']],
|
||||||
|
['layer-handoff', ['military'], '--military'],
|
||||||
|
[
|
||||||
|
'ground-layer-switch',
|
||||||
|
['ais-live-vessels', 'air-quality', 'gnss-integrity', 'fire-perimeters'],
|
||||||
|
],
|
||||||
|
['contact-switch', ['military-awareness', 'military-installations']],
|
||||||
|
['local-adsb-engine-switch', ['local-adsb']],
|
||||||
|
['ghostmaps-switch', ['s2-ghostmaps']],
|
||||||
|
[
|
||||||
|
'weather-engine-switch',
|
||||||
|
[
|
||||||
|
'weather-radar',
|
||||||
|
'weather-satellite',
|
||||||
|
'weather-lightning',
|
||||||
|
'weather-cyclones',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['wind-engine-switch', ['wind']],
|
||||||
|
['infrastructure-engine-switch', ['local-dams', 'local-datacenters']],
|
||||||
|
['submarine-cables-engine-switch', ['telegeography-submarine-cables']],
|
||||||
|
['directions-engine-switch', ['directions']],
|
||||||
|
['nepal-locator-switch', ['bhote-koshi-locator']],
|
||||||
|
['nepal-event-switch', ['bhote-koshi-2026']],
|
||||||
|
['traffic-engine-switch', ['traffic']],
|
||||||
|
['traffic-engine-switch', ['traffic'], '--simulation'],
|
||||||
|
['street-level-engine-switch', ['street-level']],
|
||||||
|
['street-level-engine-switch', ['street-level'], '--panoramax'],
|
||||||
|
['radio-engine-switch', ['radio']],
|
||||||
|
['w2-imagery', ['recent-imagery']],
|
||||||
|
['fire-camera-switch', ['alpr-cameras']],
|
||||||
|
['fire-camera-switch', ['local-firms'], '--fires'],
|
||||||
|
['urban-engine-switch', ['bikeshare']],
|
||||||
|
['urban-engine-switch', ['transit'], '--transit'],
|
||||||
|
['cctv-engine-switch', ['cctv']],
|
||||||
|
['space-engine-switch', ['satellites']],
|
||||||
|
['space-engine-switch', ['rocket-launches'], '--missions'],
|
||||||
|
];
|
||||||
|
const ids = [...new Set(runs.flatMap(([, ids]) => ids))].sort();
|
||||||
|
assert.deepEqual(
|
||||||
|
ids,
|
||||||
|
APPLICATION_LAYER_DIRECTORY.map((l) => l.id).sort(),
|
||||||
|
'Every catalog layer must have a browser scenario',
|
||||||
|
);
|
||||||
|
const report = [];
|
||||||
|
for (const [name, layers, flag] of runs) {
|
||||||
|
const label = name + (flag ?? '');
|
||||||
|
const args =
|
||||||
|
name === 'w2-imagery'
|
||||||
|
? [
|
||||||
|
'--url',
|
||||||
|
base.origin + '/?engine=navara',
|
||||||
|
'--report',
|
||||||
|
join(output, 'imagery.json'),
|
||||||
|
'--switch',
|
||||||
|
'--preview',
|
||||||
|
]
|
||||||
|
: [base.origin, ...(flag ? [flag] : [])];
|
||||||
|
const start = Date.now();
|
||||||
|
const run = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[fileURLToPath(new URL('./qa-' + name + '.mjs', import.meta.url)), ...args],
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 240000,
|
||||||
|
maxBuffer: 8388608,
|
||||||
|
env: { ...process.env, GOOGLE_MAPS_API_KEY: '', CESIUM_ION_TOKEN: '' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const ok = run.status === 0;
|
||||||
|
writeFileSync(
|
||||||
|
join(output, label + '.log'),
|
||||||
|
(run.stdout || '') + (run.stderr || '') + (run.error?.message || ''),
|
||||||
|
);
|
||||||
|
report.push({
|
||||||
|
label,
|
||||||
|
layers,
|
||||||
|
ok,
|
||||||
|
seconds: Math.round((Date.now() - start) / 1000),
|
||||||
|
});
|
||||||
|
writeFileSync(
|
||||||
|
join(output, 'summary.json'),
|
||||||
|
JSON.stringify({ ids, report }, null, 2) + '\n',
|
||||||
|
);
|
||||||
|
console.log(ok ? 'PASS' : 'FAIL', label, report.at(-1).seconds);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
ids.length +
|
||||||
|
' layers, ' +
|
||||||
|
report.filter((row) => row.ok).length +
|
||||||
|
'/' +
|
||||||
|
runs.length +
|
||||||
|
' browser scenarios. ' +
|
||||||
|
output,
|
||||||
|
);
|
||||||
|
if (report.some((row) => !row.ok)) process.exitCode = 1;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
import sharp from 'sharp';
|
||||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
import { installTileCache } from './qa/tileCache.mjs';
|
import { installTileCache } from './qa/tileCache.mjs';
|
||||||
|
|
||||||
@@ -29,7 +30,14 @@ try {
|
|||||||
window.fetch = (url, options) =>
|
window.fetch = (url, options) =>
|
||||||
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
|
String(url).startsWith('https://earthquake.usgs.gov/earthquakes/feed/')
|
||||||
? Promise.resolve(
|
? Promise.resolve(
|
||||||
Response.json({ type: 'FeatureCollection', features: [] }),
|
Response.json({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [{
|
||||||
|
type: 'Feature', id: 'qa-boston',
|
||||||
|
geometry: { type: 'Point', coordinates: [-71.0605, 42.3554, 8] },
|
||||||
|
properties: { mag: 4, time: Date.now(), place: 'QA Boston' },
|
||||||
|
}],
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
: fetch(url, options);
|
: fetch(url, options);
|
||||||
});
|
});
|
||||||
@@ -53,6 +61,24 @@ try {
|
|||||||
});
|
});
|
||||||
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
|
assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind);
|
||||||
}
|
}
|
||||||
|
async function verifyQuakeSurface(kind) {
|
||||||
|
const meanRed = async (enabled) => {
|
||||||
|
await page.evaluate(async (enabled) => {
|
||||||
|
const a = window.__godsEyeView;
|
||||||
|
await a.dataManager.setEnabled('earthquakes', enabled, { origin: 'user' });
|
||||||
|
a.engine.render.request('qa-quake-surface');
|
||||||
|
}, enabled);
|
||||||
|
if (enabled) await page.waitForFunction(() =>
|
||||||
|
window.__godsEyeView.dataManager.layers.get('earthquakes').module.getAnalystRecords().length === 1);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1800));
|
||||||
|
const bytes = await page.screenshot({ clip: { x: 675, y: 403, width: 20, height: 20 } });
|
||||||
|
const { channels } = await sharp(bytes).stats();
|
||||||
|
return channels[0].mean - channels[1].mean;
|
||||||
|
};
|
||||||
|
const off = await meanRed(false), on = await meanRed(true);
|
||||||
|
assert.ok(on > off + 8, kind + ' must draw the shallow red earthquake footprint: ' + off + ' -> ' + on);
|
||||||
|
console.log('PASS', kind, 'earthquake footprint red minus green:', off, '->', on);
|
||||||
|
}
|
||||||
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
|
const stored = () => page.evaluate(() => localStorage.getItem('gev.engine'));
|
||||||
async function switchTo(kind) {
|
async function switchTo(kind) {
|
||||||
if (
|
if (
|
||||||
@@ -151,6 +177,7 @@ try {
|
|||||||
}
|
}
|
||||||
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
|
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
|
||||||
await ready('cesium');
|
await ready('cesium');
|
||||||
|
await verifyQuakeSurface('cesium');
|
||||||
const sharpen = () =>
|
const sharpen = () =>
|
||||||
page.evaluate(
|
page.evaluate(
|
||||||
() => window.__godsEyeView.styleManager.getControlState().sharpen,
|
() => window.__godsEyeView.styleManager.getControlState().sharpen,
|
||||||
@@ -162,6 +189,7 @@ try {
|
|||||||
});
|
});
|
||||||
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
|
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 49 });
|
||||||
await switchTo('navara');
|
await switchTo('navara');
|
||||||
|
await verifyQuakeSurface('navara');
|
||||||
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
|
assert.deepEqual(await sharpen(), { enabled: true, intensityPct: 80 });
|
||||||
assert.ok(
|
assert.ok(
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
|||||||
import { switchGlobe } from './qa-switch-globe.mjs';
|
import { switchGlobe } from './qa-switch-globe.mjs';
|
||||||
const url = new URL(process.argv[2]);
|
const url = new URL(process.argv[2]);
|
||||||
const soak = process.argv.includes('--soak');
|
const soak = process.argv.includes('--soak');
|
||||||
|
const requested = process.argv.find((arg) => arg.startsWith('--swaps='));
|
||||||
|
const swaps = requested ? Number(requested.slice(8)) : soak ? 50 : 2;
|
||||||
|
if (requested && (!soak || !Number.isInteger(swaps) || swaps % 2 || swaps < 4 || swaps > 1000))
|
||||||
|
throw Error('Use --soak --swaps=N with an even count from 4 to 1000');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
||||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
@@ -78,7 +83,7 @@ try {
|
|||||||
window.qaModels = new Map([...dataManager.layers].map(([id, entry]) => [id, entry.module]));
|
window.qaModels = new Map([...dataManager.layers].map(([id, entry]) => [id, entry.module]));
|
||||||
window.qaEngines = new Map([[engine.kind, engine]]);
|
window.qaEngines = new Map([[engine.kind, engine]]);
|
||||||
});
|
});
|
||||||
const swaps = soak ? 50 : 2, measurements = [], memory = [];
|
const measurements = [], memory = [];
|
||||||
let contextListeners;
|
let contextListeners;
|
||||||
for (let i = 1; i <= swaps; i++) {
|
for (let i = 1; i <= swaps; i++) {
|
||||||
const kind = i % 2 ? 'cesium' : 'navara';
|
const kind = i % 2 ? 'cesium' : 'navara';
|
||||||
@@ -96,7 +101,7 @@ try {
|
|||||||
window.qaModels.size === 33 && window.qaQuakeReads >= 1 &&
|
window.qaModels.size === 33 && window.qaQuakeReads >= 1 &&
|
||||||
dataManager.getEnabledLayerIds().has('earthquakes');
|
dataManager.getEnabledLayerIds().has('earthquakes');
|
||||||
}));
|
}));
|
||||||
if ([2, 10, 50].includes(i)) {
|
if ([2, 10, 50, 100, 250, swaps].includes(i)) {
|
||||||
const { result } = await client.send('Runtime.evaluate', {
|
const { result } = await client.send('Runtime.evaluate', {
|
||||||
includeCommandLineAPI: true,
|
includeCommandLineAPI: true,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {switchGlobe} from './qa-switch-globe.mjs';
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { installTileCache } from './qa/tileCache.mjs';
|
||||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
|
import { isExpectedPreviewSetupError, redact } from './qa-browserEvidence.mjs';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ try {
|
|||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
|
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
|
||||||
await blockPaidQaMaps(page);
|
await blockPaidQaMaps(page);
|
||||||
|
await installTileCache(page);
|
||||||
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
|
page.on('pageerror', (e) => report.errors.push(redact(e.message)));
|
||||||
page.on('console', (message) => {
|
page.on('console', (message) => {
|
||||||
if (message.type() !== 'error') return;
|
if (message.type() !== 'error') return;
|
||||||
@@ -64,7 +66,10 @@ try {
|
|||||||
((/\b503\b/.test(text) &&
|
((/\b503\b/.test(text) &&
|
||||||
String(message.location()?.url).includes('HLS_L30')) ||
|
String(message.location()?.url).includes('HLS_L30')) ||
|
||||||
(failLandsat && /Failed to obtain image tile/.test(text))));
|
(failLandsat && /Failed to obtain image tile/.test(text))));
|
||||||
(expected ? report.expectedErrors : report.errors).push(text);
|
const source = message.location()?.url;
|
||||||
|
(expected ? report.expectedErrors : report.errors).push(
|
||||||
|
source ? text + ' (' + new URL(source, url).hostname + ')' : text,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
const session = await page.createCDPSession();
|
const session = await page.createCDPSession();
|
||||||
await session.send('Fetch.enable', {
|
await session.send('Fetch.enable', {
|
||||||
|
|||||||
@@ -266,14 +266,41 @@ try {
|
|||||||
await manager.setEnabled('weather-cyclones', true, {origin:'user'});
|
await manager.setEnabled('weather-cyclones', true, {origin:'user'});
|
||||||
window.qaStorm = manager.layers.get('weather-cyclones').module;
|
window.qaStorm = manager.layers.get('weather-cyclones').module;
|
||||||
},ids);
|
},ids);
|
||||||
const click = async (lon,lat) => {
|
const click = async (lon, lat, insideCone) => {
|
||||||
const at = await p.evaluate(position => {
|
const hit = await p.waitForFunction(
|
||||||
const {engine} = window.__godsEyeView;
|
({ lon, lat, base, insideCone }) => {
|
||||||
const xy = engine.projector.worldToWindow(position,{});
|
const { engine } = window.__godsEyeView;
|
||||||
const rect = engine.host.canvas.getBoundingClientRect();
|
const longitude = (lon * Math.PI) / 180,
|
||||||
return [rect.left+xy.x,rect.top+xy.y];
|
latitude = (lat * Math.PI) / 180;
|
||||||
},fromDegrees(lon,lat));
|
const height =
|
||||||
await p.mouse.click(...at);
|
engine.surface.terrainHeight({ longitude, latitude, height: 0 }) ?? 0;
|
||||||
|
const xy = engine.projector.worldToWindow(
|
||||||
|
{
|
||||||
|
x: base.x + height * Math.cos(latitude) * Math.cos(longitude),
|
||||||
|
y: base.y + height * Math.cos(latitude) * Math.sin(longitude),
|
||||||
|
z: base.z + height * Math.sin(latitude),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
if (!xy) return false;
|
||||||
|
const canvas = engine.host.canvas,
|
||||||
|
rect = canvas.getBoundingClientRect();
|
||||||
|
const x = rect.left + xy.x,
|
||||||
|
y = rect.top + xy.y;
|
||||||
|
if (document.elementFromPoint(x, y) !== canvas) return false;
|
||||||
|
const id = engine.picker.pick(xy)?.id;
|
||||||
|
if (
|
||||||
|
String(id?.id ?? id).startsWith('cyclone:al012026:cone:') !==
|
||||||
|
insideCone
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return [x, y];
|
||||||
|
},
|
||||||
|
{ timeout: 30000 },
|
||||||
|
{ lon, lat, base: fromDegrees(lon, lat), insideCone },
|
||||||
|
);
|
||||||
|
await p.mouse.click(...(await hit.jsonValue()));
|
||||||
|
await hit.dispose();
|
||||||
};
|
};
|
||||||
const before = cycloneReads;
|
const before = cycloneReads;
|
||||||
for (const kind of ['navara','cesium','navara']) {
|
for (const kind of ['navara','cesium','navara']) {
|
||||||
@@ -285,10 +312,9 @@ try {
|
|||||||
window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
||||||
const d = await p.evaluate(()=>window.qaStorm.getDiagnostics());
|
const d = await p.evaluate(()=>window.qaStorm.getDiagnostics());
|
||||||
assert.deepEqual([d.storms,d.tracks,d.cones,d.forecastPoints],[1,1,1,1],kind);
|
assert.deepEqual([d.storms,d.tracks,d.cones,d.forecastPoints],[1,1,1,1],kind);
|
||||||
await new Promise(r => setTimeout(r,1200));
|
await click(-79.43,43.59,false);
|
||||||
await click(-79.43,43.59);
|
|
||||||
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === null);
|
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === null);
|
||||||
await click(-79.30,43.643);
|
await click(-79.30,43.643,true);
|
||||||
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
await p.waitForFunction(()=>window.qaStorm.getDiagnostics().selectedId === 'al012026');
|
||||||
assert.equal(cycloneReads,before);
|
assert.equal(cycloneReads,before);
|
||||||
console.log('PASS',kind,'pick advisory cone and retain selected storm');
|
console.log('PASS',kind,'pick advisory cone and retain selected storm');
|
||||||
|
|||||||
@@ -245,6 +245,32 @@ test('the catalog per engine: Cesium as before, Navara only what it can show', a
|
|||||||
assert.equal(subset.get('military'), undefined);
|
assert.equal(subset.get('military'), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('both production registries attach to the same 33 session models', async (t) => {
|
||||||
|
const { registerNavaraLayerRenderers } = await import('./navaraMode.js');
|
||||||
|
const lifetime = new AbortController();
|
||||||
|
t.after(() => lifetime.abort());
|
||||||
|
const options = {
|
||||||
|
layerSession: { lifetime },
|
||||||
|
sources: createStandaloneLayerSources(),
|
||||||
|
signal: lifetime.signal,
|
||||||
|
surface: fixtureSurface(lifetime.signal),
|
||||||
|
};
|
||||||
|
const cesium = createApplicationCatalog(options);
|
||||||
|
await registerNavaraLayerRenderers();
|
||||||
|
const navara = createApplicationCatalog({ ...options, engineKind: 'navara' });
|
||||||
|
assert.equal(cesium.layers.length, 33);
|
||||||
|
assert.deepEqual(
|
||||||
|
navara.layers.map(({ id }) => id),
|
||||||
|
cesium.layers.map(({ id }) => id),
|
||||||
|
);
|
||||||
|
assert.deepEqual(navara.metadata, cesium.metadata);
|
||||||
|
for (const layer of cesium.layers) {
|
||||||
|
assert.equal(navara.get(layer.id), layer, layer.id);
|
||||||
|
assert.equal(typeof layer.attachRenderer, 'function', layer.id);
|
||||||
|
assert.equal(typeof layer.detachRenderer, 'function', layer.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('a GNSS integrity source can set the layer evidence window', (t) => {
|
test('a GNSS integrity source can set the layer evidence window', (t) => {
|
||||||
const lifetime = new AbortController();
|
const lifetime = new AbortController();
|
||||||
t.after(() => lifetime.abort());
|
t.after(() => lifetime.abort());
|
||||||
|
|||||||
Reference in new issue
Block a user