lib/report-plot.js
/* eslint-disable no-useless-escape */
var fs = require('fs');
var path = require('path');
var async = require('async');
var _ = require('lodash');
var through = require('through2');
var eos = require('end-of-stream');
var isStream = require('./is-stream.js');
var jsonReport = require('./report-json.js');
var textReport = require('./report-text.js');
var constants = require('./constants.js');
function renderMustaches(text, data) {
_.forEach(data, function(val, key) {
var regex = new RegExp('\{\{' + key + '\}\}', 'g');
text = text.replace(regex, val);
});
return text;
}
function endStream(stream, cb) {
if (isStream.stdio(stream)) {
setImmediate(cb);
return;
}
eos(stream, cb);
stream.end();
}
module.exports = function(opts, done) {
var REPORT_FILE = 'plot.html';
var input = opts.input;
var output = opts.output;
var firstWrite = true;
var keys = [
constants.TEST_FULL_ERR,
constants.TEST_FULL
];
var onFirstWrite = _.noop;
var render = (function() {
return function renderAll(data) {
// let's not modify the original report,
// just in case
var clone = _.cloneDeep(data);
// make sure that existing keys always stay in order,
// and new keys are always added to the end
var unknown = _.difference(_.keys(clone), keys);
keys = keys.concat(unknown);
// make sure all known keys are defined, even if they
// did not originally exist on the report
keys.forEach(function(key) {
clone[key] = clone[key] || {};
});
return _.template(keys.map(function(key) {
return '<%= data["' + key + '"] %>';
}).join(','), {
variable: 'data'
})(_.mapValues(clone, function(val, key) {
return _.isNumber(val.duration) ?
val.duration.toFixed(2) :
'null';
}));
};
}());
function onReport(data) {
// we'll put the comma at the beginning, because it
// is easier to detect the first item than it is
// the last item we are writing
var str = ',[';
if (firstWrite) {
str = '[';
onFirstWrite();
firstWrite = false;
}
// x axis
// from start time, in seconds,
// It might not be great for all custom metrics to have the
// same x-axis, but I am going with it for now.
str += (data.report[constants.TEST_FULL].start / 1000).toFixed(2) + ',';
if (data.report[constants.TEST_FULL].status === 'success') {
data.report[constants.TEST_FULL_ERR] = {};
} else {
data.report[constants.TEST_FULL_ERR] = data.report[constants.TEST_FULL];
data.report[constants.TEST_FULL] = {};
}
str += render(data.report) + ']';
output.write(str);
}
function onJson(data) {
if (data.type === 'report') {
onReport(data);
}
}
var htmlParts;
async.auto({
view: function(next) {
fs.readFile(path.resolve(__dirname, '..', 'views', REPORT_FILE), 'utf8', next);
},
splitHtml: ['view', function(results, next) {
var html = results.view;
htmlParts = html.toString().split('{{data}}');
// This is done to prevent writing to the output
// before we have determined that the input stream
// contains correct data, in order to match all
// of the other reporters.
onFirstWrite = function() {
output.write(htmlParts[0]);
onFirstWrite = _.noop;
};
async.setImmediate(next);
}],
inputs: ['splitHtml', function(results, next) {
var linesStream = through();
var jsonStream = through();
input.on('data', function(chunk) {
jsonStream.write(chunk);
linesStream.write(chunk);
});
input.on('end', function() {
jsonStream.end();
linesStream.end();
});
input.on('error', function(err) {
jsonStream.emit('error', err);
linesStream.emit('error', err);
});
setImmediate(next, undefined, {
lineStream: linesStream,
jsonStream: jsonStream
});
}],
readLines: ['inputs', function(results, next) {
jsonReport.readLines(results.inputs.lineStream, onJson, next);
}],
readJson: ['inputs', function(results, next) {
jsonReport(results.inputs.jsonStream, function(err, statsObj) {
if (err) {
return next(err);
}
var text = textReport(statsObj, {
color: false
});
if (htmlParts[1]) {
htmlParts[1] = renderMustaches(htmlParts[1], {
labels: JSON.stringify(['Start'].concat(keys)),
testname: statsObj.info.name || 'Test',
textStats: text.trim()
});
}
next();
});
}],
writeEnd: ['readLines', 'readJson', function(results, next) {
if (!output.write(htmlParts[1])) {
output.once('drain', function() {
next();
});
} else {
setImmediate(next);
}
}],
endStream: ['writeEnd', function(results, next) {
endStream(output, next);
}]
}, done);
};