mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
192 lines
5.3 KiB
JavaScript
192 lines
5.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
buildIdleEvidenceReport,
|
|
normalizeIdleManifest,
|
|
parseArguments,
|
|
parseProcIo,
|
|
parseProcStat,
|
|
parseProcStatus,
|
|
summarizeSnapshots,
|
|
validateSnapshotSeries,
|
|
} = require('../../scripts/ql3-physical-edge-idle-sampler.cjs');
|
|
|
|
function manifest(overrides = {}) {
|
|
return normalizeIdleManifest({
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_idle_candidate',
|
|
profile: 'edge',
|
|
deviceId: 'router-a1',
|
|
processId: 123,
|
|
expectedExecutable: '/usr/bin/node',
|
|
durationSeconds: 30,
|
|
sampleIntervalMs: 10_000,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function snapshot(index, overrides = {}) {
|
|
return {
|
|
monotonicMs: index * 10_000,
|
|
wallTimeMs: 1_700_000_000_000 + index * 10_000,
|
|
bootId: '019f0000-0000-7000-8000-000000000001',
|
|
executable: '/usr/bin/node',
|
|
commandSha256: 'a'.repeat(64),
|
|
processId: 123,
|
|
state: 'S',
|
|
uid: 1000,
|
|
startTicks: 100,
|
|
minorFaults: 10 + index,
|
|
majorFaults: index,
|
|
userTicks: 20 + index,
|
|
systemTicks: 5 + index,
|
|
threadCount: 4,
|
|
rssPages: 1000 + index,
|
|
rssBytes: (40 + index) * 1024 * 1024,
|
|
voluntaryContextSwitches: 100 + index * 2,
|
|
involuntaryContextSwitches: 5 + index,
|
|
readBytes: 4096 + index * 4096,
|
|
writeBytes: 8192 + index * 8192,
|
|
readSyscalls: 10 + index,
|
|
writeSyscalls: 20 + index,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('normalizes one bounded idle sampling manifest', () => {
|
|
assert.deepEqual(manifest(), {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_idle_candidate',
|
|
profile: 'edge',
|
|
deviceId: 'router-a1',
|
|
processId: 123,
|
|
expectedExecutable: '/usr/bin/node',
|
|
durationSeconds: 30,
|
|
sampleIntervalMs: 10_000,
|
|
});
|
|
assert.throws(
|
|
() => manifest({ durationSeconds: 29 }),
|
|
/durationSeconds must be between 30 and 3600/,
|
|
);
|
|
assert.throws(
|
|
() => manifest({ sampleIntervalMs: 7000 }),
|
|
/sampleIntervalMs must divide the duration/,
|
|
);
|
|
assert.throws(() => manifest({ extra: true }), /keys must be exactly/);
|
|
});
|
|
|
|
test('parses Linux proc stat, status and I/O without exposing command text', () => {
|
|
const stat = parseProcStat(
|
|
'123 (qinglong worker) S 1 123 123 0 -1 4194560 10 0 2 0 20 5 0 0 20 0 4 0 100 1000000 1000 0',
|
|
);
|
|
assert.deepEqual(stat, {
|
|
processId: 123,
|
|
state: 'S',
|
|
minorFaults: 10,
|
|
majorFaults: 2,
|
|
userTicks: 20,
|
|
systemTicks: 5,
|
|
threadCount: 4,
|
|
startTicks: 100,
|
|
rssPages: 1000,
|
|
});
|
|
assert.deepEqual(
|
|
parseProcStatus(
|
|
'Uid:\t1000\t1000\t1000\t1000\nVmRSS:\t40960 kB\nThreads:\t4\nvoluntary_ctxt_switches:\t10\nnonvoluntary_ctxt_switches:\t2\n',
|
|
),
|
|
{
|
|
uid: 1000,
|
|
rssBytes: 40 * 1024 * 1024,
|
|
threadCount: 4,
|
|
voluntaryContextSwitches: 10,
|
|
involuntaryContextSwitches: 2,
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
parseProcIo('syscr: 10\nsyscw: 20\nread_bytes: 4096\nwrite_bytes: 8192\n'),
|
|
{
|
|
readBytes: 4096,
|
|
writeBytes: 8192,
|
|
readSyscalls: 10,
|
|
writeSyscalls: 20,
|
|
},
|
|
);
|
|
});
|
|
|
|
test('summarizes bounded process deltas and RSS percentiles', () => {
|
|
const samples = [0, 1, 2, 3].map((index) => snapshot(index));
|
|
const violations = [];
|
|
assert.deepEqual(summarizeSnapshots(samples, violations), {
|
|
sampleCount: 4,
|
|
actualDurationMs: 30_000,
|
|
rssBytes: {
|
|
minimum: 40 * 1024 * 1024,
|
|
p50: 41 * 1024 * 1024,
|
|
p95: 43 * 1024 * 1024,
|
|
maximum: 43 * 1024 * 1024,
|
|
final: 43 * 1024 * 1024,
|
|
},
|
|
threadCountMaximum: 4,
|
|
cpuTicks: { user: 3, system: 3 },
|
|
faults: { minor: 3, major: 3 },
|
|
contextSwitches: { voluntary: 6, involuntary: 3 },
|
|
io: {
|
|
readBytes: 12_288,
|
|
writeBytes: 24_576,
|
|
readSyscalls: 3,
|
|
writeSyscalls: 3,
|
|
},
|
|
});
|
|
assert.deepEqual(violations, []);
|
|
});
|
|
|
|
test('fails closed when the sampled process or boot generation changes', () => {
|
|
const samples = [
|
|
snapshot(0),
|
|
snapshot(1, { startTicks: 101, commandSha256: 'b'.repeat(64) }),
|
|
snapshot(2, { bootId: 'different', executable: '/tmp/node' }),
|
|
snapshot(3, { state: 'Z' }),
|
|
];
|
|
assert.deepEqual(validateSnapshotSeries(manifest(), samples), [
|
|
'sample 1 process generation changed',
|
|
'sample 1 command identity changed',
|
|
'sample 2 executable changed',
|
|
'sample 2 boot identity changed',
|
|
'sample 3 process was not live',
|
|
]);
|
|
});
|
|
|
|
test('builds digest-bound candidate evidence without overstating wakeups', () => {
|
|
const report = buildIdleEvidenceReport({
|
|
manifest: manifest(),
|
|
snapshots: [0, 1, 2, 3].map((index) => snapshot(index)),
|
|
generatedAt: '2026-07-22T00:00:00.000Z',
|
|
});
|
|
assert.equal(report.supported, false);
|
|
assert.equal(report.qualification.passed, true);
|
|
assert.ok(
|
|
report.qualification.doesNotProve.includes('whole_device_cpu_wakeups'),
|
|
);
|
|
assert.equal(report.sha256.length, 64);
|
|
});
|
|
|
|
test('requires durable absolute manifest and output paths', () => {
|
|
assert.deepEqual(
|
|
parseArguments([
|
|
'--',
|
|
'--manifest=/etc/qinglong/idle.json',
|
|
'--output=/var/lib/qinglong/evidence/idle.json',
|
|
'--json',
|
|
]),
|
|
{
|
|
manifestPath: '/etc/qinglong/idle.json',
|
|
outputPath: '/var/lib/qinglong/evidence/idle.json',
|
|
json: true,
|
|
},
|
|
);
|
|
assert.throws(
|
|
() => parseArguments(['--manifest=idle.json', '--output=/tmp/out.json']),
|
|
/manifestPath must be absolute/,
|
|
);
|
|
});
|