directives.test.js (6568B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 4 import { createDirectives, createScanner } from '../src/worker/directives.js'; 5 6 function scan(chunks, options = {}) { 7 const problems = []; 8 const masked = []; 9 const directives = createDirectives({ 10 onProblem: (m) => problems.push(m), 11 onMask: (v) => masked.push(v), 12 ...options, 13 }); 14 const scanner = createScanner(directives); 15 const out = []; 16 for (const chunk of chunks) out.push(scanner.push(Buffer.from(chunk))); 17 out.push(scanner.end()); 18 return { log: Buffer.concat(out).toString('utf8'), directives, problems, masked }; 19 } 20 21 test('set-output is taken off the log and defaults to masked', () => { 22 const { log, directives, masked } = scan(['hello\n::set-output token=abcdef\nbye\n']); 23 assert.equal(log, 'hello\nbye\n'); 24 assert.deepEqual(directives.outputs, [{ name: 'token', value: 'abcdef', masked: true }]); 25 assert.deepEqual(masked, ['abcdef']); 26 }); 27 28 test('public outputs are not masked', () => { 29 const { directives, masked } = scan(['::set-output tag=v1.2.3::public\n']); 30 assert.deepEqual(directives.outputs, [{ name: 'tag', value: 'v1.2.3', masked: false }]); 31 assert.deepEqual(masked, []); 32 }); 33 34 test('masked may be stated explicitly', () => { 35 const { directives } = scan(['::set-output tag=v1::masked\n']); 36 assert.equal(directives.outputs[0].masked, true); 37 }); 38 39 test('a directive split across chunks is still recognised', () => { 40 const { log, directives } = scan(['out\n::set-out', 'put to', 'ken=abcdef\nmore\n']); 41 assert.equal(log, 'out\nmore\n'); 42 assert.deepEqual(directives.outputs, [{ name: 'token', value: 'abcdef', masked: true }]); 43 }); 44 45 test('a directive split one byte at a time is still recognised', () => { 46 const { log, directives } = scan([...'::set-output a=bcdef\ntail\n']); 47 assert.equal(log, 'tail\n'); 48 assert.equal(directives.outputs[0].value, 'bcdef'); 49 }); 50 51 test('ordinary output is not held back waiting for a newline', () => { 52 const problems = []; 53 const directives = createDirectives({ onProblem: (m) => problems.push(m) }); 54 const scanner = createScanner(directives); 55 assert.equal(scanner.push(Buffer.from('progress...')).toString(), 'progress...'); 56 assert.equal(scanner.push(Buffer.from(' done\n')).toString(), ' done\n'); 57 }); 58 59 test('a trailing line with no newline still parses', () => { 60 const { log, directives } = scan(['::set-state failed']); 61 assert.equal(log, ''); 62 assert.equal(directives.state, 'failed'); 63 }); 64 65 test('CRLF line endings are tolerated', () => { 66 const { log, directives } = scan(['a\r\n::set-state skipped\r\nb\r\n']); 67 assert.equal(log, 'a\r\nb\r\n'); 68 assert.equal(directives.state, 'skipped'); 69 }); 70 71 test('last one wins for state and for a repeated output', () => { 72 const { directives } = scan([ 73 '::set-state failed\n::set-output a=first\n::set-state success\n::set-output a=second\n', 74 ]); 75 assert.equal(directives.state, 'success'); 76 assert.deepEqual(directives.outputs, [{ name: 'a', value: 'second', masked: true }]); 77 }); 78 79 test('a line that is not a directive is left alone', () => { 80 const { log, problems } = scan(['::not-a-command whatever\n:: spaced\nplain\n']); 81 assert.equal(log, '::not-a-command whatever\n:: spaced\nplain\n'); 82 assert.deepEqual(problems, []); 83 }); 84 85 test('a malformed set-output is reported and still stripped', () => { 86 const { log, problems, directives } = scan(['::set-output token=abcdef::typo\n']); 87 assert.equal(log, '', 'the value must not reach the log'); 88 assert.equal(directives.outputs.length, 0); 89 assert.match(problems[0], /unknown visibility/); 90 }); 91 92 test('an output name may carry dots and dashes', () => { 93 const { directives, problems } = scan([ 94 '::set-output myname-x86_64=/pkg/a.tar::public\n::set-output pizza.calzone=tasty::public\n', 95 ]); 96 assert.deepEqual(problems, []); 97 assert.deepEqual(directives.outputs, [ 98 { name: 'myname-x86_64', value: '/pkg/a.tar', masked: false }, 99 { name: 'pizza.calzone', value: 'tasty', masked: false }, 100 ]); 101 }); 102 103 test('a bad output name is reported and stripped', () => { 104 for (const name of ['9lives', 'pizza..calzone', 'pizza.', '.pizza', 'a b']) { 105 const { log, problems } = scan([`::set-output ${name}=x\n`]); 106 assert.equal(log, '', `${name} must not reach the log`); 107 assert.match(problems[0], /is not a valid name/, `${name} should be rejected`); 108 } 109 }); 110 111 test('set-output without name=value is reported', () => { 112 const { problems } = scan(['::set-output nope\n']); 113 assert.match(problems[0], /wants name=value/); 114 }); 115 116 test('an unknown state is reported and stripped', () => { 117 const { log, problems, directives } = scan(['::set-state banana\n']); 118 assert.equal(log, ''); 119 assert.equal(directives.state, null); 120 assert.match(problems[0], /unknown state/); 121 }); 122 123 test('a value may contain an equals sign', () => { 124 const { directives } = scan(['::set-output data=a=b=c\n']); 125 assert.equal(directives.outputs[0].value, 'a=b=c'); 126 }); 127 128 test('a directive must start at column 0', () => { 129 const { log, directives } = scan([' ::set-output a=b\n']); 130 assert.equal(log, ' ::set-output a=b\n'); 131 assert.equal(directives.outputs.length, 0); 132 }); 133 134 test('trailing whitespace after a directive is tolerated', () => { 135 const { directives } = scan(['::set-state success \n']); 136 assert.equal(directives.state, 'success'); 137 }); 138 139 test('binary output passes through untouched', () => { 140 const bytes = Buffer.from([0x00, 0xff, 0xfe, 0x0a, 0x01, 0x02]); 141 const directives = createDirectives(); 142 const scanner = createScanner(directives); 143 const out = Buffer.concat([scanner.push(bytes), scanner.end()]); 144 assert.deepEqual(out, bytes); 145 }); 146 147 test('outputs are capped and the excess is dropped without leaking', () => { 148 const lines = []; 149 for (let i = 0; i < 70; i += 1) lines.push(`::set-output n${i}=value${i}\n`); 150 const { log, directives, problems } = scan([lines.join('')]); 151 assert.equal(log, ''); 152 assert.equal(directives.outputs.length, 64); 153 assert.match(problems[0], /more than 64 outputs/); 154 }); 155 156 test('stdout and stderr scanners do not corrupt each other', () => { 157 const directives = createDirectives(); 158 const out = createScanner(directives); 159 const err = createScanner(directives); 160 // Interleaved halves of two different directives. 161 out.push(Buffer.from('::set-out')); 162 err.push(Buffer.from('::set-sta')); 163 out.push(Buffer.from('put a=bcdef\n')); 164 err.push(Buffer.from('te success\n')); 165 assert.equal(directives.state, 'success'); 166 assert.deepEqual(directives.outputs, [{ name: 'a', value: 'bcdef', masked: true }]); 167 });