nodecat

concatenates files around stdin
const testContent = Buffer.allocUnsafe(256);
for (let i = 0; i < 256; i += 1) {
  testContent[i] = i;
}
const proc = execFile(
  process.execPath,
  [
    binPath,
    testFiles[0],
    '-',
    testFiles[1]
  ],
  {encoding: null},
  (err, stdout, stderr) => {
    assert.ifError(err);
    const expected = Buffer.concat([
      testFileContent[testFiles[0]],
      testContent,
      testFileContent[testFiles[1]]
    ]);
    if (typeof stdout === 'string') {
      // Node 0.10 doesn't support returning Buffer
      deepEqual(stdout, String(expected));
      deepEqual(stderr, '');
    } else {
      deepEqual(stdout, expected);
      deepEqual(stderr, Buffer.alloc(0));
    }
    done();
  }
);
proc.stdin.end(testContent);
exits code 1 with error message for non-existent file
const testContent = Buffer.allocUnsafe(256);
for (let i = 0; i < 256; i += 1) {
  testContent[i] = i;
}
const badFilename = 'nonexistent.txt';
const proc = execFile(
  process.execPath,
  [
    binPath,
    badFilename,
    testFiles[0],
    '-',
    testFiles[1]
  ],
  {encoding: null},
  (err, stdout, stderr) => {
    assert.strictEqual(err.code, 1);
    // stdout has data that can be read
    const expected = Buffer.concat([
      testFileContent[testFiles[0]],
      testContent,
      testFileContent[testFiles[1]]
    ]);
    if (typeof stdout === 'string') {
      // Node 0.10 doesn't support returning Buffer
      deepEqual(stdout, String(expected));
    } else {
      deepEqual(stdout, expected);
    }
    // stderr contains an error message with the problematic file
    const stderrStr = String(stderr);
    assert(
      stderrStr.indexOf(badFilename) >= 0,
      `"${stderrStr}" should contain "${badFilename}"`
    );
    done();
  }
);
proc.stdin.end(testContent);

AggregateError

sets .message from argument
const testMsg = 'test message';
const a = new AggregateError(testMsg);
assert.strictEqual(a.message, testMsg);
can be instantiated without arguments
const a = new AggregateError();
assert(a.message, 'has default message');
behaves like an Array
const a = new AggregateError();
assert.strictEqual(a.length, 0);
const testError = new Error('test');
a.push(testError);
assert.strictEqual(a.length, 1);
assert.strictEqual(a[0], testError);
can be instantiated without new
const testMsg = 'test message';
const a = AggregateError(testMsg);
assert(a instanceof AggregateError);
assert.strictEqual(a.message, testMsg);
inherits from Error
const testMsg = 'test message';
const a = new AggregateError(testMsg);
assert(a instanceof Error);

nodecat command

interprets as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets - as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets file.txt as file.txt with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- file.txt as file.txt with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets file.txt -- as file.txt with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets file.txt -- file.txt as file.txt file.txt with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- - as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- -- as -- with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -u as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -u - as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets - -u - as - - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- -u - as -u - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -u -- - as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -u -- -u - as -u - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -uu as - with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -uu -- -uu as -uu with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
interprets -- -a as -a with match(fileStreams: match(-: typeOf("object")))
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(expectFiles),
    expectOpts,
    match.func
  );
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, sinon.mock().never());
nodecat.verify();
prints error and exits for -a
const outStream = new stream.PassThrough();
const errStream = new stream.PassThrough();
const options = {
  outStream,
  errStream
};
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, options, (err, code) => {
  assert.ifError(err);
  assert.isAtLeast(code, 1);
  assert.strictEqual(outStream.read(), null);
  assert.match(String(errStream.read()), expectErrMsg);
  done();
});
prints error and exits for --unknown
const outStream = new stream.PassThrough();
const errStream = new stream.PassThrough();
const options = {
  outStream,
  errStream
};
const allArgs = RUNTIME_ARGS.concat(args);
nodecatCmd(allArgs, options, (err, code) => {
  assert.ifError(err);
  assert.isAtLeast(code, 1);
  assert.strictEqual(outStream.read(), null);
  assert.match(String(errStream.read()), expectErrMsg);
  done();
});
yields 0 for non-Error nodecat result
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(['-']),
    match.object,
    match.func
  )
  .yields(null);
nodecatCmd([], {}, (err, code) => {
  assert.ifError(err);
  assert.strictEqual(code, 0);
  done();
});
yields non-0 for Error nodecat result
const errTest = new Error('test error');
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(['-']),
    match.object,
    match.func
  )
  .yields(errTest);
nodecatCmd([], {}, (err, code) => {
  // Note:  Error is not propagated, since it is fully handled by nodecat
  assert.ifError(err);
  assert.isAtLeast(code, 1);
  done();
});
throws TypeError for non-function callback
assert.throws(
  () => { nodecatCmd(RUNTIME_ARGS, {}, true); },
  TypeError,
  /\bcallback\b/
);
yields TypeError for non-object options
nodecatCmd([], true, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions\b/);
  done();
});
yields TypeError for non-Readable in
nodecatCmd([], {inStream: {}}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.inStream\b/);
  done();
});
yields TypeError for non-Writable outStream
nodecatCmd([], {outStream: new stream.Readable()}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.outStream\b/);
  done();
});
yields TypeError for non-Writable errStream
nodecatCmd([], {errStream: new stream.Readable()}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.errStream\b/);
  done();
});
returns undefined when called with a function
nodecat = sinon.mock()
  .once()
  .withArgs(
    match(['-']),
    match.object,
    match.func
  );
const result = nodecatCmd(RUNTIME_ARGS, sinon.mock().never());
nodecat.verify();
assert.strictEqual(result, undefined);
returns a Promise when called without a function
nodecat = sinon.stub();
const result = nodecatCmd(RUNTIME_ARGS);
assert(result instanceof global.Promise);
returned Promise is resolved with exit code
nodecat = sinon.stub();
const options = {
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
const result = nodecatCmd(RUNTIME_ARGS, options);
nodecat.yield(null);
return result.then((code) => {
  assert.strictEqual(code, 0);
});
returned Promise is rejected with Error
nodecat = sinon.stub();
const result = nodecatCmd(RUNTIME_ARGS, true);
return result.then(
  sinon.mock().never(),
  (err) => { assert.instanceOf(err, TypeError); }
);

nodecat

concatenates a named file to outStream
const options = {
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
nodecat([filePath], options, (err) => {
  assert.ifError(err);
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), fileContent);
    assert.strictEqual(options.errStream.read(), null);
    done();
  });
});
concatenates two named files to outStream
const options = {
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
nodecat([filePath, filePath], options, (err) => {
  assert.ifError(err);
  options.outStream.end(() => {
    assert.deepEqual(
      options.outStream.read(),
      Buffer.concat([fileContent, fileContent])
    );
    assert.strictEqual(options.errStream.read(), null);
    done();
  });
});
concatenates stdout to outStream
const testData = Buffer.from('Stuff');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
nodecat(['-'], options, (err) => {
  assert.ifError(err);
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), testData);
    assert.strictEqual(options.errStream.read(), null);
    done();
  });
});
inStream.end(testData);
concatenates stdout once when named twice
const testData = Buffer.from('Stuff');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
nodecat(['-', '-'], options, (err) => {
  assert.ifError(err);
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), testData);
    assert.strictEqual(options.errStream.read(), null);
    done();
  });
});
inStream.end(testData);
continues with next file after read error
const errTest = new Error('test read error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
let callCount = 0;
nodecat(['-', filePath], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.strictEqual(err, errTest);
  assert.strictEqual(err.fileName, '-');
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), fileContent);
    assert.match(
      options.errStream.read(),
      /^nodecat: -: .*test read error.*\n$/
    );
    done();
  });
});
inStream.emit('error', errTest);
does not retry stream after read error
const testData = Buffer.from('Stuff');
const errTest = new Error('test read error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
let callCount = 0;
nodecat(['-', filePath, '-'], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.strictEqual(err, errTest);
  assert.strictEqual(err.fileName, '-');
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), fileContent);
    assert.match(
      options.errStream.read(),
      /^nodecat: -: .*test read error.*\n$/
    );
    done();
  });
});
inStream.emit('error', errTest);
inStream.end(testData);
returns AggregateError for multiple read errors
const errTest1 = new Error('test read error 1');
const errTest2 = new Error('test read error 2');
const errTest3 = new Error('test read error 3');
const stream1 = new stream.PassThrough();
const stream2 = new stream.PassThrough();
const stream3 = new stream.PassThrough();
const options = {
  fileStreams: {
    'file1.txt': stream1,
    'file2.txt': stream2,
    'file3.txt': stream3
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
let callCount = 0;
nodecat(['file1.txt', 'file2.txt', 'file3.txt'], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.instanceOf(err, AggregateError);
  assert.strictEqual(err.length, 3);
  assert.strictEqual(err[0], errTest1);
  assert.strictEqual(err[0].fileName, 'file1.txt');
  assert.strictEqual(err[1], errTest2);
  assert.strictEqual(err[1].fileName, 'file2.txt');
  assert.strictEqual(err[2], errTest3);
  assert.strictEqual(err[2].fileName, 'file3.txt');
  // Confirm that AggregateError.toString has contained messages
  const errMsgRE = new RegExp(
    '.*test read error 1.*\\n'
      + '.*test read error 2.*\\n'
      + '.*test read error 3.*\\n.*'
  );
  assert.match(String(err), errMsgRE);
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), null);
    const errText = String(options.errStream.read());
    const errRE
      = new RegExp('^nodecat: file1.txt: .*test read error 1.*\\n'
          + 'nodecat: file2.txt: .*test read error 2.*\\n'
          + 'nodecat: file3.txt: .*test read error 3.*\\n$');
    assert.match(errText, errRE);
    done();
  });
});
stream1.emit('error', errTest1);
process.nextTick(() => {
  stream2.emit('error', errTest2);
  process.nextTick(() => {
    stream3.emit('error', errTest3);
  });
});
returns AggregateError for read and write errors
const errTestRead = new Error('test read error');
const errTestWrite = new Error('test write error');
const stream1 = new stream.PassThrough();
const stream2 = new stream.PassThrough();
const options = {
  fileStreams: {
    'file1.txt': stream1,
    'file2.txt': stream2
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
let callCount = 0;
nodecat(['file1.txt', 'file2.txt'], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.instanceOf(err, AggregateError);
  assert.strictEqual(err.length, 2);
  assert.strictEqual(err[0], errTestRead);
  assert.strictEqual(err[0].fileName, 'file1.txt');
  assert.strictEqual(err[1], errTestWrite);
  assert.strictEqual(err[1].fileName, undefined);
  options.outStream.end(() => {
    assert.deepEqual(options.outStream.read(), null);
    const errText = String(options.errStream.read());
    const errRE
      = new RegExp('^nodecat: file1.txt: .*test read error.*\\n'
          + 'nodecat: .*test write error.*\\n$');
    assert.match(errText, errRE);
    done();
  });
});
stream1.emit('error', errTestRead);
options.outStream.emit('error', errTestWrite);
stops writing after write error
const testData = Buffer.from('Stuff');
const errTest = new Error('test write error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
options.fileStreams[filePath] = {
  pipe: sinon.mock().never()
};
let callCount = 0;
nodecat(['-', filePath], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.strictEqual(err, errTest);
  process.nextTick(() => {
    assert.strictEqual(options.outStream.read(), null);
    assert.match(
      options.errStream.read(),
      /^nodecat: .*test write error.*\n/
    );
    done();
  });
});
options.outStream.emit('error', errTest);
inStream.end(testData);
stops listening for read error after write error
const testData = Buffer.from('Stuff');
const errTestRead = new Error('test read error');
const errTestWrite = new Error('test write error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
options.fileStreams[filePath] = {
  pipe: sinon.mock().never()
};
// Assert would throw without this test listener.
inStream.on('error', function() {
  assert.strictEqual(listenerCount(this, 'error'), 1);
});
let callCount = 0;
nodecat(['-', filePath], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.strictEqual(err, errTestWrite);
  inStream.emit('error', errTestRead);
  setImmediate(() => {
    assert.strictEqual(options.outStream.read(), null);
    assert.match(
      options.errStream.read(),
      /^nodecat: .*test write error.*\n/
    );
    done();
  });
});
options.outStream.emit('error', errTestWrite);
inStream.emit('error', errTestRead);
inStream.end(testData);
stops listening for write error after callback
const testData = Buffer.from('Stuff');
const errTestWrite = new Error('test write error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
// Assert would throw without this test listener.
options.outStream.on('error', function() {
  assert.strictEqual(listenerCount(this, 'error'), 1);
});
let callCount = 0;
nodecat(['-'], options, (err) => {
  callCount += 1;
  assert.strictEqual(callCount, 1);
  assert.ifError(err);
  options.outStream.emit('error', errTestWrite);
  setImmediate(() => {
    assert.deepEqual(options.outStream.read(), testData);
    assert.strictEqual(options.errStream.read(), null);
    done();
  });
});
inStream.end(testData);
throws TypeError for non-function callback
assert.throws(
  () => { nodecat([], {}, true); },
  TypeError,
  /\bcallback\b/
);
yields TypeError for non-Array-like fileNames
nodecat('file.txt', (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\bfileNames\b/);
  done();
});
yields TypeError for non-object options
nodecat([], true, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions\b/);
  done();
});
yields TypeError for non-object options.fileStreams
nodecat([], {fileStreams: true}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.fileStreams\b/);
  done();
});
yields TypeError for non-Writable outStream
nodecat([], {outStream: new stream.Readable()}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.outStream\b/);
  done();
});
yields TypeError for non-Writable errStream
nodecat([], {errStream: new stream.Readable()}, (err) => {
  assert.instanceOf(err, TypeError);
  assert.match(err.message, /\boptions.errStream\b/);
  done();
});
returns undefined when called with a function
const result = nodecat([], done);
assert.strictEqual(result, undefined);
returns a Promise when called without a function
const result = nodecat([]);
assert(result instanceof global.Promise);
returned Promise is resolved after writing
let ended = false;
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
setImmediate(() => {
  ended = true;
  inStream.end();
});
return nodecat(['-'], options).then(() => {
  assert(ended);
});
returned Promise is rejected with argument Error
const result = nodecat([], true);
return result.then(
  sinon.mock().never(),
  (err) => { assert.instanceOf(err, TypeError); }
);
returned Promise is rejected with read Error
const errTest = new Error('test error');
const inStream = new stream.PassThrough();
const options = {
  fileStreams: {
    '-': inStream
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
setImmediate(() => {
  inStream.emit('error', errTest);
});
return nodecat(['-'], options).then(
  sinon.mock().never(),
  (err) => { assert.strictEqual(err, errTest); }
);
returned Promise is rejected with write Error
const errTest = new Error('test error');
const options = {
  fileStreams: {
    '-': new stream.PassThrough()
  },
  outStream: new stream.PassThrough(),
  errStream: new stream.PassThrough()
};
setImmediate(() => {
  options.outStream.emit('error', errTest);
});
return nodecat(['-'], options).then(
  sinon.mock().never(),
  (err) => { assert.strictEqual(err, errTest); }
);