conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 });