meteor/meteor

View on GitHub
tools/tool-testing/output-log.js

Summary

Maintainability
A
1 hr
Test Coverage
// 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) {
    const failure = new TestFailure('forbidden-string-present', {
      run: this.run,
    });

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

  get() {
    return this.lines;
  }
}

import { markThrowingMethods } from "./test-utils.js";
markThrowingMethods(OutputLog.prototype);