aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-shell-stdin.js
blob: 1b0fbca546b5c0d2d410a8659c647747ff25ab59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// modules/test/test-shell-stdin.js — run: node
// Pipes: wc/head/tail must read stdin when no file operand is given, and must still read the file
// when one is (ignoring a stale pipe buffer). The channel is terminal._pipeInput — the property
// the pipe wrapper in modules/ext/shell.js sets before dispatching each stage.
const fsMod = require('fs'), path = require('path');
const ROOT = path.join(__dirname, '..');
const src = fsMod.readFileSync(path.join(ROOT, 'core/module-fs.js'), 'utf8');
const FileSystemModule = new Function(src + '\nreturn FileSystemModule;')();

let fails = 0, passes = 0;
const ok = (name, cond, detail) => { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); };

// A fake terminal whose fs knows only the files it is handed: there is no __pipebuf__ magic here,
// so a handler that quietly searched the cwd instead of reading stdin would fail, not pass.
const mk = (files, pipeInput) => {
    const out = [];
    const term = {
        fs: {
            readFile: (p) => (files && Object.prototype.hasOwnProperty.call(files, p)) ? files[p] : null,
            getCurrentDir: () => ({ name: 'fake', content: {} }),
        },
        _pipeInput: pipeInput === undefined ? null : pipeInput,
        print: (msg) => out.push(String(msg)),
    };
    const m = new FileSystemModule();
    m.terminal = term;
    m.registerCommands();
    return { m, out };
};
const last = (out) => out.length ? out[out.length - 1] : '<nothing printed>';

// (g) grep over a pipe must search the piped text, never the working directory.
// Measured 2026-10-27 on v4.6.115: `cat /gdir/in.txt | grep zzz` answered
// "other.txt:1: zzz in a cwd file" — a file that was never piped.
{
    const decoy = {
        readFile: () => null,
        getCurrentDir: () => ({ name: 'gdir', content: {
            'other.txt': { type: 'file', content: 'zzz in a cwd file' },
            'in.txt': { type: 'file', content: 'aaa\nbbb\nccc' },
        } }),
    };
    const out = [];
    const term = { fs: decoy, _pipeInput: 'aaa\nbbb\nccc', print: (m) => out.push(String(m)) };
    const m = new FileSystemModule();
    m.terminal = term; m.registerCommands();
    m.commands.grep(['zzz']);
    ok('G1 grep over a pipe does not search the cwd', !out.join('|').includes('other.txt'), JSON.stringify(out));
    ok('G2 a non-match over a pipe reports no matches', out.join('|').includes('(no matches)'), JSON.stringify(out));

    out.length = 0;
    m.commands.grep(['bbb']);
    ok('G3 a match prints the line number without a filename', out.length === 1 && out[0] === '2: bbb', JSON.stringify(out));

    out.length = 0;
    m.commands.grep(['-c', 'b']);
    ok('G4 grep -c over a pipe counts stdin lines', out.length === 1 && out[0] === '1', JSON.stringify(out));
}
// (a) wc -l over 3 lines of stdin
{
    const { m, out } = mk({}, 'alpha\nbeta\ngamma');
    m.commands.wc(['-l']);
    ok('A1 wc -l on 3 piped lines prints 3', out.length === 1 && out[0].trim() === '3', JSON.stringify(out));
}
// (b) wc -c counts characters, not lines
{
    const { m, out } = mk({}, 'abcdef');
    m.commands.wc(['-c']);
    ok('B1 wc -c on "abcdef" piped prints 6', out.length === 1 && out[0].trim() === '6', JSON.stringify(out));

    const w = mk({}, 'abcdef');
    w.m.commands.wc(['-w']);
    ok('B2 wc -w on one piped word prints 1', w.out.length === 1 && w.out[0].trim() === '1', JSON.stringify(w.out));

    const l = mk({}, 'abcdef');
    l.m.commands.wc(['-l']);
    ok('B3 wc -l on the same piped text still prints 1 (so -c is not counting lines)', l.out.length === 1 && l.out[0].trim() === '1', JSON.stringify(l.out));
}
// (c) head -n 2 returns the first 2 piped lines
{
    const { m, out } = mk({}, 'alpha\nbeta\ngamma');
    m.commands.head(['-n', '2']);
    ok('C1 head -n 2 on piped stdin prints 2 lines', out.length === 1 && out[0] === 'alpha\nbeta', JSON.stringify(out));

    const d = mk({}, 'alpha\nbeta\ngamma');
    d.m.commands.head([]);
    ok('C2 head with no operand/number defaults to 10 piped lines', d.out.length === 1 && d.out[0] === 'alpha\nbeta\ngamma', JSON.stringify(d.out));
}
// (d) tail -n 1 returns the last piped line
{
    const { m, out } = mk({}, 'alpha\nbeta\ngamma');
    m.commands.tail(['-n', '1']);
    ok('D1 tail -n 1 on piped stdin prints the last line', out.length === 1 && out[0] === 'gamma', JSON.stringify(out));

    const t = mk({}, 'alpha\nbeta\ngamma');
    t.m.commands.tail(['-2']);
    ok('D2 tail -2 on piped stdin prints the last 2 lines', t.out.length === 1 && t.out[0] === 'beta\ngamma', JSON.stringify(t.out));

    const nl = mk({}, 'alpha\nbeta\ngamma\n');
    nl.m.commands.tail(['-n', '1']);
    ok('D3 tail -n 1 ignores a trailing newline (no blank line)', nl.out.length === 1 && nl.out[0] === 'gamma', JSON.stringify(nl.out));
}
// (e) a real file operand still wins: the file is read and the pipe buffer is ignored
{
    const files = { '/probe.txt': 'one\ntwo\nthree\nfour' };
    const { m, out } = mk(files, 'PIPED\nJUNK\nHERE');
    m.commands.wc(['-l', '/probe.txt']);
    ok('E1 wc -l <file> counts the file (4), not stdin', out.length === 1 && out[0].trim() === '4 /probe.txt', JSON.stringify(out));

    const h = mk(files, 'PIPED\nJUNK\nHERE');
    h.m.commands.head(['-n', '1', '/probe.txt']);
    ok('E2 head -n 1 <file> prints the file\'s first line', h.out.length === 1 && h.out[0] === 'one', JSON.stringify(h.out));

    const t = mk(files, 'PIPED\nJUNK\nHERE');
    t.m.commands.tail(['-n', '1', '/probe.txt']);
    ok('E3 tail -n 1 <file> prints the file\'s last line', t.out.length === 1 && t.out[0] === 'four', JSON.stringify(t.out));

    const d = mk(files, 'PIPED\nJUNK\nHERE');
    d.m.commands.wc(['/probe.txt']);
    // Real behaviour (verified identical on the pre-change file): with no flag and a single
    // file, wc prints the bare counts and no name — the name is only appended when there are
    // several rows or when a flag was given.
    ok('E4 wc <file> with no flags keeps its long format (l w c, no name for one file)',
        d.out.length === 1 && d.out[0] === '4 4 18' && d.out[0].indexOf('PIPED') === -1, JSON.stringify(d.out));
}
// (f) no operand and no pipe must still be usage, not a crash or a silent empty read
{
    const w = mk({}, null);
    w.m.commands.wc(['-l']);
    ok('F1 wc with no file and no stdin prints usage', /^Usage: wc/.test(w.out[0] || ''), JSON.stringify(w.out));

    const h = mk({});
    h.m.commands.head(['-n', '2']);
    ok('F2 head with no file and no stdin prints usage', /^Usage: head/.test(h.out[0] || ''), JSON.stringify(h.out));

    const t = mk({}, null);
    t.m.commands.tail(['-n', '1']);
    ok('F3 tail with no file and no stdin prints usage', /^Usage: tail/.test(t.out[0] || ''), JSON.stringify(t.out));
}

console.log(passes + ' passed, ' + fails + ' failed');
if (fails) process.exit(1);