Files
meteor/tools/tool-testing/output-log.js
Jesse Rosenberger d5ff826596 Decompose self-test logic into separate files.
The single file which represented the bulk of the `meteor self-test`
functionality had got a bit heavy and it stood to benefit from some
dissemination.

I embarked on this change originally when looking into replacing
PhantomJS with Chrome Headless (and a new `ChromeClient` class) within
self-test.  Unfortunately, I didn't have time to take this the last
step of actually implementing Chrome, but this should hopefully
facilitate that change in the future by providing what I believe to be
better compartmentalization of this logic.

I apologize for the (likely) difficulty of reviewing this commit.  Due
to heavy intertwining of existing code it was hard to arrange these
changes in an easy-to-review manner.  I believe a reviewer will
find that it's mainly copy and pasting into different files and
careful adjusting of those files (new) module dependencies.
2017-11-14 15:55:11 +02:00

89 lines
2.3 KiB
JavaScript

// Maintains a line-by-line merged log of multiple output channels
// (eg, stdout and stderr).
import TestFailure from './test-failure.js';
const hasOwn = Object.prototype.hasOwnProperty;
export default class OutputLog {
constructor(run) {
// each entry is an object withgit p keys 'channel', 'text', and if it is
// the last entry and there was no newline terminator, 'bare'
this.lines = [];
// map from a channel name to an object representing a partially
// read line of text on that channel. That object has keys 'text'
// (text read), 'offset' (cursor position, equal to text.length
// unless a '\r' has been read).
this.buffers = {};
// a Run, exclusively for inclusion in exceptions
this.run = run;
}
write(channel, text) {
if (!hasOwn.call(this.buffers, 'channel')) {
this.buffers[channel] = { text: '', offset: 0 };
}
const b = this.buffers[channel];
while (text.length) {
const m = text.match(/^[^\n\r]+/);
if (m) {
// A run of non-control characters.
b.text = b.text.substr(0, b.offset) +
m[0] + b.text.substr(b.offset + m[0].length);
b.offset += m[0].length;
text = text.substr(m[0].length);
continue;
}
if (text[0] === '\r') {
b.offset = 0;
text = text.substr(1);
continue;
}
if (text[0] === '\n') {
this.lines.push({ channel, text: b.text });
b.text = '';
b.offset = 0;
text = text.substr(1);
continue;
}
throw new Error("conditions should have been exhaustive?");
}
}
end() {
Object.keys(this.buffers).forEach((channel) => {
if (this.buffers[channel].text.length) {
this.lines.push({
channel,
text: this.buffers[channel].text,
bare: true,
});
this.buffers[channel] = { text: '', offset: 0};
}
});
}
forbid(pattern, channel) {
this.lines.forEach((line) => {
if (channel && channel !== line.channel) {
return;
}
const match = (pattern instanceof RegExp) ?
(line.text.match(pattern)) : (line.text.indexOf(pattern) !== -1);
if (match) {
throw new TestFailure('forbidden-string-present', { run: this.run });
}
});
}
get() {
return this.lines;
}
}