From dd18a24062727b61c8f231ace10a9f0d4948ae5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 9 Sep 2026 22:49:00 +0300 Subject: [PATCH] profiler: min-length content compare on size/record mismatch When a declared fileSize/recordLen/numRecords attribute mismatches, compare file contents only over the overlapping (shorter) portion, so a length difference alone doesn't fail the content check when the common bytes match. When sizes match, contents are compared fully as before. Add profilerMatchMin helper + tests. SW cache v43. --- frontend/index.html | 30 ++++++++++++++++++++---------- frontend/sw.js | 2 +- frontend/tests/profiler.test.js | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index ac97481..798da0e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6709,6 +6709,14 @@ function profilerMatch(mode, expected, actual) { return e === a; } +// Like profilerMatch, but compares only the overlapping (shorter) portion. +function profilerMatchMin(mode, expected, actual) { + const e = profilerNormHex(expected); + const a = profilerNormHex(actual); + const n = Math.min(e.length, a.length); + return profilerMatch(mode, e.slice(0, n), a.slice(0, n)); +} + function profilerValidateProfile(obj) { if (!obj || typeof obj !== 'object') return 'Not an object'; if (!obj.name) return 'Missing name'; @@ -7096,6 +7104,7 @@ async function profilerCheck(i) { async function profilerRunRule(r) { const res = { path: r.path, name: null, status: 'pass', checks: [] }; + let sizeMismatch = false; let sel; try { sel = await pysimFetch('/api/select', { path: r.path }); @@ -7118,17 +7127,17 @@ async function profilerRunRule(r) { } if (r.fileSize !== null && r.fileSize !== undefined) { const ok = sel.file_size === r.fileSize; - if (!ok) res.status = 'fail'; + if (!ok) { res.status = 'fail'; sizeMismatch = true; } res.checks.push({ label: 'fileSize', expected: r.fileSize, actual: sel.file_size, ok: ok }); } if (r.recordLen !== null && r.recordLen !== undefined) { const ok = sel.record_len === r.recordLen; - if (!ok) res.status = 'fail'; + if (!ok) { res.status = 'fail'; sizeMismatch = true; } res.checks.push({ label: 'recordLen', expected: r.recordLen, actual: sel.record_len, ok: ok }); } if (r.numRecords !== null && r.numRecords !== undefined) { const ok = sel.num_of_rec === r.numRecords; - if (!ok) res.status = 'fail'; + if (!ok) { res.status = 'fail'; sizeMismatch = true; } res.checks.push({ label: 'numRecords', expected: r.numRecords, actual: sel.num_of_rec, ok: ok }); } if (r.content) { @@ -7146,18 +7155,19 @@ async function profilerRunRule(r) { } else if (r.content.kind === 'record') { const exp = r.content.records || []; const act = rd.records || []; + const n = Math.min(exp.length, act.length); if (exp.length !== act.length) { res.status = 'fail'; res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false }); - } else { - for (let j = 0; j < exp.length; j++) { - const ok = exp[j].num === act[j].num && profilerMatch(r.content.mode, exp[j].data, act[j].data); - if (!ok) res.status = 'fail'; - res.checks.push({ label: 'content.rec' + exp[j].num, expected: exp[j].data, actual: act[j].data, ok: ok }); - } + } + for (let j = 0; j < n; j++) { + const dataOk = sizeMismatch ? profilerMatchMin(r.content.mode, exp[j].data, act[j].data) : profilerMatch(r.content.mode, exp[j].data, act[j].data); + const ok = exp[j].num === act[j].num && dataOk; + if (!ok) res.status = 'fail'; + res.checks.push({ label: 'content.rec' + exp[j].num, expected: exp[j].data, actual: act[j].data, ok: ok }); } } else { - const ok = profilerMatch(r.content.mode, r.content.expected, rd.data); + const ok = sizeMismatch ? profilerMatchMin(r.content.mode, r.content.expected, rd.data) : profilerMatch(r.content.mode, r.content.expected, rd.data); if (!ok) res.status = 'fail'; res.checks.push({ label: 'content', expected: r.content.expected, actual: rd.data, ok: ok }); } diff --git a/frontend/sw.js b/frontend/sw.js index 1a747f1..764b916 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v42'; +const CACHE = 'otaman-v43'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index b70f88b..3dc7eda 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -21,7 +21,7 @@ function extractFunc(src, name) { return src.slice(m.index, i + 1); } -const FNS = ['profilerNormHex', 'profilerMatch', 'profilerValidateProfile']; +const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerValidateProfile']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; eval(code); @@ -54,6 +54,20 @@ test('mask without ? is a prefix match', () => { assert.ok(!profilerMatch('mask', '', '0891')); }); +test('profilerMatchMin compares the shorter overlapping portion', () => { + // shorter expected vs longer actual -> compare prefix + assert.ok(profilerMatchMin('exact', '1122FFFF', '1122FFFFFF')); + assert.ok(!profilerMatchMin('exact', '1122FFFF', '1122FFAA')); + // shorter actual vs longer expected -> compare prefix + assert.ok(profilerMatchMin('exact', '1122FFFFFF', '1122FFFF')); + // equal lengths behave like profilerMatch + assert.ok(profilerMatchMin('exact', '1122FFFF', '1122FFFF')); + assert.ok(!profilerMatchMin('exact', '1122FFFF', '1122FFEE')); + // mask with ? over a shorter actual + assert.ok(profilerMatchMin('mask', '08?91?AA', '081910')); + assert.ok(!profilerMatchMin('mask', '08?91?AA', '091110')); +}); + test('profile validation', () => { assert.strictEqual(profilerValidateProfile(null), 'Not an object'); assert.strictEqual(profilerValidateProfile({ name: 'x' }), 'Missing rules array');