UNPKG

testarmada-magellan

Version:

Massively parallel automated testing

192 lines (163 loc) 6.14 kB
"use strict"; var fs = require("fs"); var glob = require("glob"); var request = require("request"); var Q = require("q"); var slugify = require("slugify"); var settings = require("./settings"); var path = require("path"); var async = require("async"); var BaseReporter = require("../reporter"); var util = require("util"); var MAX_CONCURRENT_UPLOADS = 2; function ScreenshotAggregator() { // This is an URL where we've stored screenshots at for this entire build (regardless of subtests) // If we successfully upload any screenshots, this value will be assigned. this.buildURL = null; this.q = async.queue(this._uploadImage.bind(this), MAX_CONCURRENT_UPLOADS); this.q.drain = this.onQueueDrained.bind(this); // If this property is set, then onQueueDrained must resolve this deferred's promise. this.deferFlush = null; this.counter = 0; } util.inherits(ScreenshotAggregator, BaseReporter); ScreenshotAggregator.prototype.initialize = function () { var deferred = Q.defer(); if (!settings.aggregatorURL) { deferred.reject(new Error("ScreenshotAggregator is missing an aggregatorURL" + " in its configuration")); } else { deferred.resolve(); } return deferred.promise; }; ScreenshotAggregator.prototype.listenTo = function (testRun, test, source) { source.addListener("message", this._handleMessage.bind(this, testRun, test)); }; ScreenshotAggregator.prototype._uploadImage = function (image, callback) { this.counter++; var formData = { /*eslint-disable camelcase */ build_id: image.buildId, child_build_id: image.childBuildId, imagefile: { value: fs.createReadStream(image.localFilePath), options: { filename: image.intendedFilename, contentType: "image/png" } } }; request.post({ url: settings.aggregatorURL, formData: formData }, function (err, httpResponse, body) { var result; try { result = JSON.parse(body); } catch (e) { // NOTE: For the moment, we eat and ignore upload errors. err = e; } if (!err && result && result.status === "success") { this.buildURL = result.buildURL; } else { console.error("Error uploading screenshot to screenshot service. ", err); } callback(); }.bind(this)); }; ScreenshotAggregator.prototype._getScreenshots = function (tempDir) { return glob.sync(path.resolve(tempDir) + "/*.png").concat( glob.sync(path.resolve(tempDir) + "/*.PNG")); }; ScreenshotAggregator.prototype._deleteScreenshots = function (tempDir) { // I couldn't figure out how to make nocase: true work -- it just produces empty results this._getScreenshots(tempDir).forEach(function (screenshotPath) { fs.unlinkSync(screenshotPath); }); }; // Collect screenshots and queue them for uploading to a remote screenshot storage service. ScreenshotAggregator.prototype._collectScreenshots = function (tempDir, buildId, testName, browserId) { // // // TODO: resolve apparent ambiguity when the same browserId is used // multiple times in parallel but with different resolutions // and/or orientations // TODO: consider shifting slug generation over to TestRun class to avoid specializing // in disambiguating here. // // var childBuildId = slugify(testName + "_" + browserId); var shots = this._getScreenshots(tempDir); shots.forEach(function (filePath) { var fullPath = path.resolve(filePath); var intendedFilename = fullPath; /*eslint-disable no-magic-numbers */ if (fullPath.indexOf("/") > -1) { intendedFilename = fullPath.split("/").pop(); } this.q.push({ localFilePath: fullPath, intendedFilename: intendedFilename, buildId: slugify(buildId), childBuildId: childBuildId }); }.bind(this)); }; ScreenshotAggregator.prototype.onQueueDrained = function () { // if deferFlush has been set, it means we tried to call flush() while the upload queue // was still running. If this is the case, onQueueDrained has been called while an external // test runner is paused, waiting for if (this.deferFlush) { this.deferFlush.resolve(); } }; // Summarize our results to the screen or optionally promise that we will, since summarizing // might require pending screenshots to finish uploading. ScreenshotAggregator.prototype.flush = function () { console.log(""); var showSummary = function () { if (this.counter > 0) { console.log("There " + (this.counter > 1 ? "are " : "is ") + this.counter + " screenshot" + (this.counter > 1 ? "s" : "") + " of this build available at " + this.buildURL); } else { console.log("Screenshot aggregator enabled, but no screenshots were uploaded."); } }.bind(this); if (this.q.idle()) { return showSummary(); } else { var awaitedUploads = this.q.length() + this.q.running(); console.log("Screenshot aggregator is waiting for " + (awaitedUploads > 1 ? awaitedUploads + " screenshots" : " screenshot") + " to finish uploading.."); var deferSummary = Q.defer(); // Set up a deferred for this.deferFlush = Q.defer(); this.deferFlush.promise.then(function () { showSummary(); deferSummary.resolve(); }); // return a promise that we'll show a summary once uploads have completed return deferSummary.promise; } }; ScreenshotAggregator.prototype._handleMessage = function (testRun, test, message) { if (message.type === "worker-status") { if (message.status === "finished") { var tempDir = testRun.tempAssetPath; if (message.passed || test.attempts === test.maxAttempts - 1) { // Is this our last attempt ever? Sweep up screenshots from this test run. this._collectScreenshots(tempDir, testRun.buildId, message.name, test.browser.slug()); } else { // We've failed a test and we're going to retry it again in the future. // Delete screenshots generated by this run, we don't care about // intermediate results. this._deleteScreenshots(tempDir); } } } }; module.exports = ScreenshotAggregator;