diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/test-fs-commands.mjs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/scripts/test-fs-commands.mjs b/scripts/test-fs-commands.mjs new file mode 100644 index 0000000..6f87708 --- /dev/null +++ b/scripts/test-fs-commands.mjs @@ -0,0 +1,236 @@ +#!/usr/bin/env node +// Test harness for newOS4 core FileSystemModule commands (tail, sed, cat/head regression). +// Loads module-fs.js in a vm sandbox with stub terminal/fs — no browser needed. +// Run: node scripts/test-fs-commands.mjs (exit 1 on failure) + +import { readFileSync } from 'node:fs'; +import vm from 'node:vm'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SRC = readFileSync(path.join(ROOT, 'modules/core/module-fs.js'), 'utf8'); + +function makeCtx() { + const ctx = { + setInterval, clearInterval, setTimeout, clearTimeout, console, + __test: { files: new Map(), writes: [] }, + }; + vm.createContext(ctx); + vm.runInContext(SRC + '\n;globalThis.FileSystemModule = FileSystemModule;', ctx, { filename: 'module-fs.js' }); + return ctx; +} + +function makeMod(ctx) { + const out = []; + const files = ctx.__test.files; + const terminal = { + print: (text, type) => out.push({ text: String(text), type: type || '' }), + fs: { + readFile: (p) => (files.has(p) ? files.get(p) : null), + writeFile: (p, c) => { files.set(p, String(c)); ctx.__test.writes.push([p, String(c)]); }, + }, + _pipeInput: null, + _activeFollow: null, + stopFollow(msg) { + if (!this._activeFollow) return false; + clearInterval(this._activeFollow.interval); + this._activeFollow = null; + this.print(msg || '^C — stopped following', 'warning'); + return true; + }, + }; + const mod = new ctx.FileSystemModule(); + mod.terminal = terminal; + mod.registerCommands(); + return { mod, terminal, out }; +} + +function scenario(filesInit) { + const ctx = makeCtx(); + for (const [k, v] of Object.entries(filesInit || {})) ctx.__test.files.set(k, v); + const s = makeMod(ctx); + s.ctx = ctx; + return s; +} + +function runCmd(mod, name, args) { + if (typeof mod.commands[name] !== 'function') { + mod.terminal.print(name + ': command not defined', 'error'); + return; + } + mod.commands[name](args); +} + +let pass = 0, fail = 0; +const check = (name, cond, got) => { + if (cond) { pass++; console.log(' ok ' + name); } + else { fail++; console.log(' FAIL ' + name + (got !== undefined ? ' — got: ' + JSON.stringify(got) : '')); } +}; +const text = (out) => out.map(o => o.text).join('\n'); +const sleep = (ms) => new Promise(r => setTimeout(r, ms)); + +const L12 = ['l1','l2','l3','l4','l5','l6','l7','l8','l9','l10','l11','l12'].join('\n'); + +// ─── regression: cat / head unchanged ─── +{ + const { mod, out } = scenario({ 'a.txt': 'hello' }); + runCmd(mod, 'cat', ['a.txt']); + check('cat regression', text(out) === 'hello', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': '1\n2\n3' }); + runCmd(mod, 'head', ['a.txt', '2']); + check('head regression', text(out) === '1\n2', text(out)); +} + +// ─── tail ─── +{ + const { mod, out } = scenario({ 'log.txt': L12 }); + runCmd(mod, 'tail', ['log.txt']); + check('tail default last 10', text(out) === 'l3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nl12', text(out)); +} +{ + const { mod, out } = scenario({ 'log.txt': L12 }); + runCmd(mod, 'tail', ['log.txt', '2']); + check('tail legacy count arg', text(out) === 'l11\nl12', text(out)); +} +{ + const { mod, out } = scenario({ 'log.txt': L12 }); + runCmd(mod, 'tail', ['-n', '3', 'log.txt']); + check('tail -n 3', text(out) === 'l10\nl11\nl12', text(out)); +} +{ + const { mod, out } = scenario({ 'log.txt': L12 }); + runCmd(mod, 'tail', ['-4', 'log.txt']); + check('tail -4 attached flag', text(out) === 'l9\nl10\nl11\nl12', text(out)); +} +{ + const { mod, out } = scenario({ 'log.txt': L12 }); + runCmd(mod, 'tail', ['-n', 'abc', 'log.txt']); + check('tail -n abc errors', out[0].type === 'error', out); +} +{ + const { mod, out } = scenario({}); + runCmd(mod, 'tail', ['nope.txt']); + check('tail missing file errors', out[0].type === 'error' && text(out).includes('No such file'), text(out)); +} +{ + const { mod, out } = scenario({}); + runCmd(mod, 'tail', []); + check('tail no args usage', out[0].type === 'warning', out); +} +{ + const { mod, terminal, out, ctx } = scenario({ 'log.txt': 'a\nb' }); + runCmd(mod, 'tail', ['-f', 'log.txt']); + check('tail -f prints initial content', out.some(o => o.text === 'a\nb'), out.map(o => o.text)); + check('tail -f registers follow timer', !!(terminal._activeFollow && terminal._activeFollow.file === 'log.txt'), terminal._activeFollow); + ctx.__test.files.set('log.txt', 'a\nb\nc'); + await sleep(1700); + check('tail -f prints appended line', out.some(o => o.text === '\nc'), out.map(o => o.text)); + check('tail -f stopFollow clears', terminal.stopFollow() === true && terminal._activeFollow === null); + ctx.__test.files.set('log.txt', 'a\nb\nc\nd'); + await sleep(1700); + check('no output after stopFollow', !out.some(o => o.text.includes('d')), out.map(o => o.text)); +} +{ + const { mod, terminal, out, ctx } = scenario({ 'log.txt': 'aaaa' }); + runCmd(mod, 'tail', ['-f', 'log.txt']); + ctx.__test.files.set('log.txt', 'xy'); + await sleep(1700); + check('tail -f detects truncation', out.some(o => o.text === 'xy') && out.some(o => o.text.includes('truncated')), out.map(o => o.text)); + terminal.stopFollow(); +} + +// ─── sed ─── +{ + const { mod, out } = scenario({ 'a.txt': 'foo bar\nfoo baz' }); + runCmd(mod, 'sed', ['s/foo/X/', 'a.txt']); + check('sed s/foo/X/ first per line', text(out) === 'X bar\nfoo baz', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'foo bar foo' }); + runCmd(mod, 'sed', ['s/foo/X/g', 'a.txt']); + check('sed global flag', text(out) === 'X bar X', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'Hello HELLO' }); + runCmd(mod, 'sed', ['s/hello/hey/i', 'a.txt']); + check('sed case-insensitive', text(out) === 'hey HELLO', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'ab' }); + runCmd(mod, 'sed', ['s/a/$&/', 'a.txt']); + check('sed $& replacement', text(out) === 'ab', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'a/b path' }); + runCmd(mod, 'sed', ['s/a\\/b/X/', 'a.txt']); + check('sed escaped slash', text(out) === 'X path', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'keep\ndelete me\nkeep2' }); + runCmd(mod, 'sed', ['/delete/d', 'a.txt']); + check('sed /pat/d', text(out) === 'keep\nkeep2', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': '1\n2\n3' }); + runCmd(mod, 'sed', ['2d', 'a.txt']); + check('sed 2d', text(out) === '1\n3', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': '1\n2\n3' }); + runCmd(mod, 'sed', ['$d', 'a.txt']); + check('sed $d', text(out) === '1\n2', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'foo\nbar\nfoo' }); + runCmd(mod, 'sed', ['-n', 's/foo/X/p', 'a.txt']); + check('sed -n s///p prints only changed', text(out) === 'X\nX', text(out)); +} +{ + const { mod, out, ctx } = scenario({ 'a.txt': 'old line' }); + runCmd(mod, 'sed', ['-i', 's/old/new/', 'a.txt']); + check('sed -i writes file', ctx.__test.files.get('a.txt') === 'new line', ctx.__test.files.get('a.txt')); + check('sed -i success msg', out.some(o => o.type === 'success'), out); +} +{ + const { mod, ctx } = scenario({ 'a.txt': '1\n2\n' }); + runCmd(mod, 'sed', ['-i', '2d', 'a.txt']); + check('sed -i keeps trailing newline', ctx.__test.files.get('a.txt') === '1\n', JSON.stringify(ctx.__test.files.get('a.txt'))); +} +{ + const { mod, out, terminal } = scenario({}); + terminal._pipeInput = 'x1\nx2'; + runCmd(mod, 'sed', ['s/x/y/']); + check('sed reads pipe input', text(out) === 'y1\nx2', text(out)); +} +{ + const { mod, out } = scenario({}); + runCmd(mod, 'sed', []); + check('sed no args usage', out[0].type === 'warning', out); +} +{ + const { mod, out } = scenario({ 'a.txt': 'x' }); + runCmd(mod, 'sed', ['s/a', 'a.txt']); + check('sed malformed expr errors', out[0].type === 'error', text(out)); +} +{ + const { mod, out } = scenario({ 'a.txt': 'x' }); + runCmd(mod, 'sed', ['s/[/y/', 'a.txt']); + check('sed invalid regex errors', out[0].type === 'error', text(out)); +} +{ + const { mod, out } = scenario({}); + runCmd(mod, 'sed', ['s/a/b/', 'missing.txt']); + check('sed missing file errors', out[0].type === 'error', text(out)); +} +{ + const { mod, out } = scenario({}); + runCmd(mod, 'sed', ['s/a/b/']); + check('sed no operand errors', out[0].type === 'error', text(out)); +} + +console.log(''); +console.log(`${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0);
\ No newline at end of file |