aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-vfs-size-bytes.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test/test-vfs-size-bytes.js')
-rw-r--r--modules/test/test-vfs-size-bytes.js216
1 files changed, 216 insertions, 0 deletions
diff --git a/modules/test/test-vfs-size-bytes.js b/modules/test/test-vfs-size-bytes.js
new file mode 100644
index 0000000..109c58e
--- /dev/null
+++ b/modules/test/test-vfs-size-bytes.js
@@ -0,0 +1,216 @@
+// node modules/test/test-vfs-size-bytes.js
+// test-vfs-size-bytes — `vfs-size` must report REAL UTF-8 STORED BYTES, one unit, everywhere.
+//
+// RED on the pre-patch command (v4.6.159): it printed three different units —
+// total = JSON.stringify(fs.root).length / 1MB
+// per file = child.content.length (UTF-16 code units)
+// system dir = JSON.stringify(readDir(dir)).length (a list of entries, not bytes)
+// The total also carried every structural character of the serialisation, so a text
+// tree reported ~2x its real stored size, while 'é' and '☃' were under-counted.
+//
+// This test pins the NUMBERS THE COMMAND PRINTS, never a helper:
+// 1. unit facts on one-file trees: 'abcd' 4 B, 'é' 2 B, '☃' 3 B, 'é☃' 5 B,
+// 1000 ASCII chars 1000 B, '' 0 B;
+// 2. a 12-file nested tree: total == sum of the per-file rows == sum of the
+// top-directory rows (each file counted once, no structure overhead);
+// 3. every system-dir line == a manual sum of that dir's files;
+// 4. a tree of directories with no files at all neither throws nor prints NaN.
+//
+// Set FS_MODULE_SRC=<path> to point the harness at another copy of module-fs.js
+// (used to prove the test is RED against the pre-fix / mutated file).
+const fsMod = require('fs');
+const path = require('path');
+const SRC = process.env.FS_MODULE_SRC || path.join(__dirname, '..', 'core', 'module-fs.js');
+
+// ── harness ────────────────────────────────────────────────────────────────
+let pass = 0;
+const fails = [];
+function ok(name, cond, detail) {
+ if (cond) { pass++; console.log(' ok ' + name); }
+ else { fails.push(name + (detail ? ' — ' + detail : '')); console.log(' FAIL ' + name + (detail ? ' — ' + detail : '')); }
+}
+function eq(name, actual, expected) {
+ ok(name, actual === expected, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected));
+}
+
+// ── load the real class out of the real file (no re-implementation) ────────
+const src = fsMod.readFileSync(SRC, 'utf8');
+const FileSystemModule = new Function(src + '\nreturn FileSystemModule;')();
+
+// ── tree builders ──────────────────────────────────────────────────────────
+function dir(children) { return { type: 'dir', content: children }; }
+function file(text) { return { type: 'file', content: text }; }
+const B = (s) => Buffer.byteLength(s, 'utf8'); // independent of Blob
+
+// ── drive the shipped command and capture what it prints ───────────────────
+function run(tree) {
+ const out = [];
+ const mod = new FileSystemModule();
+ mod.terminal = { fs: { root: tree }, print: (text) => out.push(String(text)) };
+ mod.registerCommands();
+ mod.commands['vfs-size'](); // a throw escapes here and fails the test loudly
+ return out;
+}
+
+const UNITS = { B: 1, KB: 1024, MB: 1024 * 1024 };
+function toBytes(human) {
+ const m = /^([\d.]+) (B|KB|MB)$/.exec(human);
+ if (!m) return null;
+ return Math.round(parseFloat(m[1]) * UNITS[m[2]]);
+}
+function parse(out) {
+ const r = { totalHuman: null, totalExact: null, files: null, rows: [], rest: null, dirs: [], sys: [] };
+ for (const line of out) {
+ let m;
+ if ((m = /^ {2}Total: ([\d.]+ (?:B|KB|MB)) \((\d+) bytes\) across (\d+) files?$/.exec(line))) {
+ r.totalHuman = toBytes(m[1]); r.totalExact = Number(m[2]); r.files = Number(m[3]);
+ } else if ((m = /^ {4}\d+\. (\S+) — ([\d.]+ (?:B|KB|MB))$/.exec(line))) {
+ r.rows.push({ path: m[1], bytes: toBytes(m[2]) });
+ } else if ((m = /^ {4}\(\+(\d+) more files? — ([\d.]+ (?:B|KB|MB))\)$/.exec(line))) {
+ r.rest = { count: Number(m[1]), bytes: toBytes(m[2]) };
+ } else if ((m = /^ {4}(.+?) — ([\d.]+ (?:B|KB|MB)) \((\d+) files?\)$/.exec(line))) {
+ r.dirs.push({ name: m[1], bytes: toBytes(m[2]), files: Number(m[3]) });
+ } else if ((m = /^ {2}(\/[\w.]+): (\d+) files?, ([\d.]+ (?:B|KB|MB))$/.exec(line))) {
+ r.sys.push({ name: m[1], files: Number(m[2]), bytes: toBytes(m[3]) });
+ }
+ }
+ return r;
+}
+const sum = (a) => a.reduce((x, y) => x + y, 0);
+
+console.log('module: ' + SRC);
+console.log('node ' + process.version + ', Blob is a global: ' + (typeof Blob === 'function'));
+
+// ── 1. unit facts, one file per tree (always row 1, never truncated) ───────
+console.log('\n── 1. one-file trees: bytes actually counted ──');
+const CASES = [
+ ['abcd', 'abcd', 4, 'a file containing "abcd" counts 4 bytes'],
+ ['é', 'é', 2, '"é" counts 2 bytes (1 UTF-16 unit, 2 UTF-8 bytes)'],
+ ['☃', '☃', 3, '"☃" counts 3 bytes'],
+ ['é☃', 'é☃', 5, '"é☃" counts 5 bytes'],
+ ['x'.repeat(1000), '1000 ASCII chars', 1000, 'a 1000-char ASCII file counts 1000 bytes'],
+ ['', '(empty)', 0, 'an empty file counts 0 bytes'],
+];
+for (const [content, label, want, name] of CASES) {
+ const p = parse(run(dir({ 'f.txt': file(content) })));
+ console.log(' ' + String(label).slice(0, 24).padEnd(26) + String(p.totalExact).padStart(6) + ' B row1=' + (p.rows[0] ? p.rows[0].bytes : 'MISSING'));
+ eq(name, p.totalExact, want);
+ eq(' row 1 for ' + label + ' is the same number', p.rows[0] && p.rows[0].bytes, want);
+ eq(' file count for ' + label, p.files, 1);
+}
+
+// ── 2. a 12-file nested tree ──────────────────────────────────────────────
+const tree = dir({
+ 'a.txt': file('abcd'), // 4 (root-level file -> own group row)
+ 'dirA': dir({
+ 'e.txt': file('é'), // 2
+ 'sub': dir({ 'c.txt': file('☃') }), // 3 (three levels deep)
+ }),
+ 'dirB': dir({
+ 'f.txt': file('x'.repeat(1000)), // 1000
+ 'deep': dir({ 'g.txt': file('é☃') }), // 5
+ }),
+ 'dirC': dir({ 'onlySub': dir({ 'deeper': dir({}) }) }), // 0 (no files at any depth)
+ '_snapshots': dir({ 'cp1.json': file('{"a":1}'), 'cp2.json': file('{"b":"é"}') }), // 7 + 11
+ '.trash': dir({ 'old.txt': file('☃☃') }), // 6
+ '_mods': dir({ 'calc.js': file('é'), 'nested': dir({ 'x.js': file('ab☃') }) }), // 2 + 5
+ '.semantic_index': dir({ 'index.json': file('[]') }), // 2
+ 'empty.txt': file(''), // 0
+ // '_sessions' deliberately absent -> line skipped, must not throw
+});
+const EXPECT = B('abcd') + B('é') + B('☃') + B('x'.repeat(1000)) + B('é☃') + B('')
+ + B('{"a":1}') + B('{"b":"é"}') + B('☃☃') + B('é') + B('ab☃') + B('[]');
+const EXPECT_FILES = 12;
+
+let out, p;
+try { out = run(tree); p = parse(out); }
+catch (e) { console.log(' FAIL vfs-size threw on the nested tree: ' + e.message); process.exit(1); }
+const text = out.join('\n');
+const rowSum = sum(p.rows.map(r => r.bytes)) + (p.rest ? p.rest.bytes : 0);
+const dirSum = sum(p.dirs.map(d => d.bytes));
+
+console.log('\n── 2. nested tree totals ──');
+console.log(' printed total (exact) ' + p.totalExact + ' B');
+console.log(' printed total (human) ' + p.totalHuman + ' B (1-decimal, rounded from the exact field)');
+console.log(' per-file rows (' + p.rows.length + ') + remainder ' + (p.rest ? p.rest.bytes : 0) + ' = ' + rowSum);
+console.log(' top-dir rows ' + dirSum);
+console.log(' independent expectation ' + EXPECT + ' B over ' + EXPECT_FILES + ' files');
+console.log(' old metric JSON.stringify(root).length = ' + JSON.stringify(tree).length + ' (would have been printed as MB)');
+eq('file count', p.files, EXPECT_FILES);
+eq('printed total is the UTF-8 byte sum', p.totalExact, EXPECT);
+ok('human total is the same number (1-decimal KB tolerance)',
+ Math.abs(p.totalHuman - p.totalExact) <= 51, 'human=' + p.totalHuman + ' exact=' + p.totalExact);
+eq('per-file rows + remainder sum to the printed total', rowSum, p.totalExact);
+eq('top-directory rows sum to the printed total', dirSum, p.totalExact);
+ok('printed total is NOT the old JSON.stringify(root).length',
+ p.totalExact !== JSON.stringify(tree).length,
+ 'old metric = ' + JSON.stringify(tree).length);
+
+console.log('\n── 2b. per-file table ──');
+for (const r of p.rows) console.log(' ' + r.path.padEnd(22) + String(r.bytes).padStart(6) + ' B');
+if (p.rest) console.log(' (+' + p.rest.count + ' more files — ' + p.rest.bytes + ' B)');
+const rowFor = (path) => (p.rows.find(r => r.path === path) || {}).bytes;
+eq('largest file wins', rowFor('/dirB/f.txt'), 1000);
+eq('nested file is listed once at its own path', rowFor('/dirA/sub/c.txt'), 3);
+eq('truncated tail is one row, not silently dropped', p.rest && p.rest.count, EXPECT_FILES - p.rows.length);
+
+console.log('\n── 2c. top directories ──');
+for (const d of p.dirs) console.log(' ' + d.name.padEnd(22) + String(d.bytes).padStart(6) + ' B ' + d.files + ' files');
+const dirFor = (n) => p.dirs.find(d => d.name === n) || {};
+eq('/dirA counts its subdir once (2 + 3)', dirFor('/dirA').bytes, 5);
+eq('/dirB counts deep/g.txt once (1000 + 5)', dirFor('/dirB').bytes, 1005);
+eq('/dirB file count is 2, not 3', dirFor('/dirB').files, 2);
+eq('root-level files get their own row', dirFor('/(root files)').bytes, 4);
+eq('directory with only subdirs is 0 bytes', dirFor('/dirC').bytes, 0);
+eq('every top-level dir appears (7 dirs + the root-files row)', p.dirs.length, 8);
+
+// ── 3. system dirs: real bytes, equal to a manual sum ─────────────────────
+console.log('\n── 3. system dirs ──');
+for (const s of p.sys) console.log(' ' + s.name.padEnd(22) + String(s.bytes).padStart(6) + ' B ' + s.files + ' files');
+const sysFor = (n) => p.sys.find(s => s.name === n) || {};
+const SYS_EXPECT = {
+ '/_snapshots': { files: 2, bytes: B('{"a":1}') + B('{"b":"é"}') },
+ '/.trash': { files: 1, bytes: B('☃☃') },
+ '/_mods': { files: 2, bytes: B('é') + B('ab☃') },
+ '/.semantic_index': { files: 1, bytes: B('[]') },
+};
+for (const [name, want] of Object.entries(SYS_EXPECT)) {
+ eq(name + ' bytes == manual sum', sysFor(name).bytes, want.bytes);
+ eq(name + ' file count (recursive)', sysFor(name).files, want.files);
+}
+ok('missing /_sessions prints no line instead of throwing', !sysFor('/_sessions').name);
+eq('exactly the four present system dirs printed', p.sys.length, 4);
+ok('_mods and .semantic_index are covered (the two dirs that dominate a checkpoint)',
+ !!sysFor('/_mods').name && !!sysFor('/.semantic_index').name);
+
+// ── 4. a tree of directories with no files ────────────────────────────────
+console.log('\n── 4. directories only, no files ──');
+let out2;
+try {
+ out2 = run(dir({ 'a': dir({ 'b': dir({ 'c': dir({}) }) }), 'd': dir({}) }));
+ ok('does not throw', true);
+} catch (e) {
+ ok('does not throw', false, e.message);
+ out2 = [];
+}
+const text2 = out2.join('\n');
+const p2 = parse(out2);
+eq('total is 0 bytes', p2.totalExact, 0);
+eq('file count is 0', p2.files, 0);
+ok('no "Largest:" block when there are no files', !/Largest:/.test(text2));
+eq('dir rows still sum to the total', sum(p2.dirs.map(d => d.bytes)), 0);
+
+// ── 5. no undefined / NaN anywhere ────────────────────────────────────────
+ok('main output has no "undefined"', !/undefined/.test(text));
+ok('main output has no "NaN"', !/NaN/.test(text));
+ok('dirs-only output has no "undefined"', !/undefined/.test(text2));
+ok('dirs-only output has no "NaN"', !/NaN/.test(text2));
+
+// ── verdict ───────────────────────────────────────────────────────────────
+console.log('\n' + (pass + fails.length) + ' assertions, ' + pass + ' passed, ' + fails.length + ' failed');
+if (fails.length) {
+ fails.forEach(f => console.log(' FAILED: ' + f));
+ console.log('RESULT: FAIL');
+ process.exit(1);
+}
+console.log('RESULT: PASS — vfs-size reports UTF-8 stored bytes in one unit everywhere');