feat: CAP memory estimation with a confirm step before install (v3.5.8)
Selecting a .cap in the RAM installer or the SCP81 "Install from .cap" template now runs a read-only analysis (POST /api/cap-info) before any APDU is built: the archive is validated structurally (a corrupt or wrong-format file fails here) and the bundled capmem analyzer estimates the code size and the persistent (NVRAM) / volatile (RAM) requirements, with the tool's suggested C6/C7/C8 quotas shown as information. The form's action button (Execute / Generate) stays disabled until the analysis succeeds - pressing it is the user's confirmation to continue. - pysim_simple_server/capmem.py: bundled analyzer (component parsers + JCVM opcode table + method-bytecode allocation scan), adapted to take the CAP archive as bytes and return report/memory dicts; output verified byte-identical to the workspace tool on 21 real CAPs. - _cap_info_body + POST /api/cap-info (read-only; the install endpoints stay unchanged and self-sufficient). - PWA: shared capAnalyzeFile/capMemHtml/capGateOk helpers, estimate box under both CAP inputs (reusing the idle #ram-cap-info div, new #scripts-cap-info), data-cap-gate gating in pysimApplyAvailability, stale-response guard, Retry, EN/RU strings. - Tests: tests/test_cap_memory.py (synthetic CAPs: new/newarray/ makeTransientByteArray/static fields/unknown-opcode warnings/corrupt input), frontend capmem.test.js (renderer, gate, analyze flow). - help EN/RU, docs/api.md, AGENTS; version 3.5.8; sw cache simple-v261. 567 frontend / 429 Python green.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractBlock(startMarker, endMarker) {
|
||||
const start = html.indexOf(startMarker);
|
||||
const end = html.indexOf(endMarker, start);
|
||||
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
|
||||
return html.slice(start, end);
|
||||
}
|
||||
|
||||
function extractFunc(src, name, asyncFn) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
// Rewrite the top-level const so it leaks out of sloppy-mode eval.
|
||||
eval(extractBlock('const _capAnalysis = {', 'function capFileKey').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'capFileKey'));
|
||||
eval(extractFunc(html, 'capGateOk'));
|
||||
eval(extractFunc(html, 'capMemBytes'));
|
||||
eval(extractFunc(html, 'capMemHtml'));
|
||||
eval(extractFunc(html, 'capAnalyzeFile', true));
|
||||
|
||||
globalThis.esc = s => s;
|
||||
globalThis.t = s => s;
|
||||
globalThis.capRenderAnalysis = () => {};
|
||||
globalThis.pysimApplyAvailability = () => {};
|
||||
|
||||
function resetState() {
|
||||
_capAnalysis.ram = { key: null, status: 'idle', memory: null, error: null, token: 0 };
|
||||
_capAnalysis.scripts = { key: null, status: 'idle', memory: null, error: null, token: 0 };
|
||||
}
|
||||
|
||||
function memFixture() {
|
||||
return {
|
||||
code: { method_component: 2048 },
|
||||
nvram: { static_image: 12, array_init: 4, install_objects: 100, header_overhead: 12, ref_storage: 8, total: 136, runtime: 0 },
|
||||
ram: { transient_arrays: 16, runtime_transient: 0, peak_frame: 8, total: 24 },
|
||||
suggested: { c6: 2048, c7: 272, c8: 136 },
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
test('capMemBytes formats bytes and kilobytes', () => {
|
||||
assert.strictEqual(capMemBytes(0), '0 B');
|
||||
assert.strictEqual(capMemBytes(1023), '1023 B');
|
||||
assert.strictEqual(capMemBytes(1024), '1.0 kB');
|
||||
assert.strictEqual(capMemBytes(1587), '1.5 kB');
|
||||
assert.strictEqual(capMemBytes(null), '0 B');
|
||||
});
|
||||
|
||||
test('capMemHtml renders the summary, breakdown and warnings', () => {
|
||||
const mem = memFixture();
|
||||
mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped'];
|
||||
const out = capMemHtml(mem);
|
||||
assert.ok(out.includes('CAP requirements (estimate)'), out);
|
||||
assert.ok(out.includes('Code (Method.cap): 2.0 kB'), out);
|
||||
assert.ok(out.includes('Persistent (NVRAM): 136 B'), out);
|
||||
assert.ok(out.includes('RAM (volatile): 24 B'), out);
|
||||
assert.ok(out.includes('Static image: 12 B'), out);
|
||||
assert.ok(out.includes('Reference storage: 8 B'), out);
|
||||
assert.ok(out.includes('Peak method frame: 8 B'), out);
|
||||
assert.ok(out.includes('C6=2048 C7=0x0110 C8=0x0088'), out);
|
||||
assert.ok(out.includes('Estimate only'), out);
|
||||
assert.ok(out.includes('unknown opcode 0xAA'), out);
|
||||
assert.strictEqual(capMemHtml(null), '');
|
||||
});
|
||||
|
||||
test('capGateOk gates the RAM install op and the scripts form', () => {
|
||||
resetState();
|
||||
globalThis.document = { getElementById: id => (id === 'ram-op' ? { value: 'install-cap' } : null) };
|
||||
assert.strictEqual(capGateOk('ram'), false);
|
||||
_capAnalysis.ram.status = 'ok';
|
||||
assert.strictEqual(capGateOk('ram'), true);
|
||||
_capAnalysis.scripts.status = 'error';
|
||||
assert.strictEqual(capGateOk('scripts'), false);
|
||||
// other RAM ops (explore/delete) are not gated
|
||||
globalThis.document = { getElementById: id => (id === 'ram-op' ? { value: 'explore' } : null) };
|
||||
assert.strictEqual(capGateOk('ram'), true);
|
||||
});
|
||||
|
||||
test('capAnalyzeFile stores the estimate and ignores stale responses', async () => {
|
||||
resetState();
|
||||
const pending = [];
|
||||
globalThis.ramReadFileHex = async f => f.name + 'hex';
|
||||
globalThis.pysimFetch = (url, body) => new Promise(resolve => pending.push(resolve));
|
||||
const f1 = { name: 'a.cap', size: 1, lastModified: 1 };
|
||||
const f2 = { name: 'b.cap', size: 2, lastModified: 2 };
|
||||
const p1 = capAnalyzeFile(f1, 'ram');
|
||||
const p2 = capAnalyzeFile(f2, 'ram');
|
||||
assert.strictEqual(_capAnalysis.ram.status, 'analyzing');
|
||||
// let both file reads settle so the fetches are in flight
|
||||
for (let i = 0; i < 10 && pending.length < 2; i++) await Promise.resolve();
|
||||
// resolve the first (stale) response after the second selection
|
||||
pending[0]({ ok: true, memory: { code: { method_component: 111 } } });
|
||||
pending[1]({ ok: true, memory: { code: { method_component: 222 } } });
|
||||
await Promise.all([p1, p2]);
|
||||
assert.strictEqual(_capAnalysis.ram.status, 'ok');
|
||||
assert.strictEqual(_capAnalysis.ram.memory.code.method_component, 222);
|
||||
assert.strictEqual(_capAnalysis.ram.key, capFileKey(f2));
|
||||
globalThis.pysimFetch = undefined;
|
||||
});
|
||||
|
||||
test('capAnalyzeFile records server and network failures', async () => {
|
||||
resetState();
|
||||
globalThis.ramReadFileHex = async () => '00';
|
||||
globalThis.pysimFetch = async () => ({ ok: false, error: 'cap parse failed: File is not a zip file' });
|
||||
await capAnalyzeFile({ name: 'bad.cap', size: 1, lastModified: 1 }, 'scripts');
|
||||
assert.strictEqual(_capAnalysis.scripts.status, 'error');
|
||||
assert.ok(_capAnalysis.scripts.error.includes('not a zip file'), _capAnalysis.scripts.error);
|
||||
globalThis.pysimFetch = async () => { throw new Error('boom'); };
|
||||
await capAnalyzeFile({ name: 'net.cap', size: 1, lastModified: 1 }, 'scripts');
|
||||
assert.strictEqual(_capAnalysis.scripts.status, 'error');
|
||||
assert.strictEqual(_capAnalysis.scripts.error, 'boom');
|
||||
});
|
||||
|
||||
test('the CAP inputs and their action buttons are wired', () => {
|
||||
assert.match(html, /id="ram-cap-file"[^>]*onchange="ramCapFileChanged\(\)"/);
|
||||
assert.match(html, /id="scripts-cap-file"[^>]*onchange="scriptsCapFileChanged\(\)"/);
|
||||
assert.match(html, /data-cap-gate="ram"/);
|
||||
assert.match(html, /data-cap-gate="scripts"/);
|
||||
assert.match(html, /id="ram-cap-info"/);
|
||||
assert.match(html, /id="scripts-cap-info"/);
|
||||
assert.match(html, /\/api\/cap-info/);
|
||||
});
|
||||
Reference in New Issue
Block a user