scribble-math/dockers/Screenshotter/screenshotter.js
Martin von Gagern 2e002ff37a Use jspngopt and pako to create reproducible PNG files for Chrome as well
The combination of jspngopt and pako should eliminate possible causes for
different PNG encodings, although the core reason for #325 remains unknown.
Pako has poorer compression rates than native libz, but optimization can
counter that effect, and actually reduce the size of the screenshots.

The screenshots for LimitControls and UnsupportedCmds on Firefox used to
exhibit subpixel rendering before, for reasons unknown.  The regenerated
versions don't exhibit this.  See #324 for a discussion.
2015-08-30 02:12:55 +02:00

297 lines
8.6 KiB
JavaScript

"use strict";
var childProcess = require("child_process");
var fs = require("fs");
var http = require("http");
var jspngopt = require("jspngopt");
var net = require("net");
var pako = require("pako");
var path = require("path");
var selenium = require("selenium-webdriver");
var app = require("../../server");
var data = require("../../test/screenshotter/ss_data");
var dstDir = path.normalize(
path.join(__dirname, "..", "..", "test", "screenshotter", "images"));
//////////////////////////////////////////////////////////////////////
// Process command line arguments
var opts = require("nomnom")
.option("browser", {
abbr: "b",
"default": "firefox",
help: "Name of the browser to use"
})
.option("container", {
abbr: "c",
type: "string",
help: "Name or ID of a running docker container to contact"
})
.option("seleniumURL", {
full: "selenium-url",
help: "Full URL of the Selenium web driver"
})
.option("seleniumIP", {
full: "selenium-ip",
help: "IP address of the Selenium web driver"
})
.option("seleniumPort", {
full: "selenium-port",
"default": 4444,
help: "Port number of the Selenium web driver"
})
.option("katexURL", {
full: "katex-url",
help: "Full URL of the KaTeX development server"
})
.option("katexIP", {
full: "katex-ip",
"default": "localhost",
help: "Full URL of the KaTeX development server"
})
.option("katexPort", {
full: "katex-port",
help: "Port number of the KaTeX development server"
})
.option("include", {
abbr: "i",
help: "Comma-separated list of test cases to process"
})
.option("exclude", {
abbr: "x",
help: "Comma-separated list of test cases to exclude"
})
.parse();
var listOfCases;
if (opts.include) {
listOfCases = opts.include.split(",");
} else {
listOfCases = Object.keys(data);
}
if (opts.exclude) {
var exclude = opts.exclude.split(",");
listOfCases = listOfCases.filter(function(key) {
return exclude.indexOf(key) === -1;
});
}
var seleniumURL = opts.seleniumURL;
var seleniumIP = opts.seleniumIP;
var seleniumPort = opts.seleniumPort;
var katexURL = opts.katexURL;
var katexIP = opts.katexIP;
var katexPort = opts.katexPort;
//////////////////////////////////////////////////////////////////////
// Work out connection to selenium docker container
function check(err) {
if (!err) {
return;
}
console.error(err);
console.error(err.stack);
process.exit(1);
}
function dockerCmd() {
var args = Array.prototype.slice.call(arguments);
return childProcess.execFileSync(
"docker", args, { encoding: "utf-8" }).replace(/\n$/, "");
}
if (!seleniumURL && opts.container) {
try {
// When using boot2docker, seleniumIP and katexIP are distinct.
seleniumIP = childProcess.execFileSync(
"boot2docker", ["ip"], { encoding: "utf-8" }).replace(/\n$/, "");
var config = childProcess.execFileSync(
"boot2docker", ["config"], { encoding: "utf-8" });
config = (/^HostIP = "(.*)"$/m).exec(config);
if (!config) {
console.error("Failed to find HostIP");
process.exit(2);
}
katexIP = config[1];
} catch(e) {
seleniumIP = katexIP = dockerCmd(
"inspect", "-f", "{{.NetworkSettings.Gateway}}", opts.container);
}
seleniumPort = dockerCmd("port", opts.container, seleniumPort);
seleniumPort = seleniumPort.replace(/^.*:/, "");
}
if (!seleniumURL && seleniumIP) {
seleniumURL = "http://" + seleniumIP + ":" + seleniumPort + "/wd/hub";
}
if (seleniumURL) {
console.log("Selenium driver at " + seleniumURL);
} else {
console.log("Selenium driver in local session");
}
process.nextTick(startServer);
var attempts = 0;
//////////////////////////////////////////////////////////////////////
// Start up development server
var devServer = null;
var minPort = 32768;
var maxPort = 61000;
function startServer() {
if (katexURL || katexPort) {
process.nextTick(tryConnect);
return;
}
var port = Math.floor(Math.random() * (maxPort - minPort)) + minPort;
var server = http.createServer(app).listen(port);
server.once("listening", function() {
devServer = server;
katexPort = port;
attempts = 0;
process.nextTick(tryConnect);
});
server.on("error", function(err) {
if (devServer !== null) { // error after we started listening
throw err;
} else if (++attempts > 50) {
throw new Error("Failed to start up dev server");
} else {
process.nextTick(startServer);
}
});
}
//////////////////////////////////////////////////////////////////////
// Wait for container to become ready
function tryConnect() {
if (!katexURL) {
katexURL = "http://" + katexIP + ":" + katexPort + "/";
console.log("KaTeX URL is " + katexURL);
}
if (!seleniumIP) {
process.nextTick(buildDriver);
return;
}
var sock = net.connect({
host: seleniumIP,
port: +seleniumPort
});
sock.on("connect", function() {
sock.end();
attempts = 0;
process.nextTick(buildDriver);
}).on("error", function() {
if (++attempts > 50) {
throw new Error("Failed to connect selenium server.");
}
setTimeout(tryConnect, 200);
});
}
//////////////////////////////////////////////////////////////////////
// Build the web driver
var driver;
function buildDriver() {
var builder = new selenium.Builder().forBrowser(opts.browser);
if (seleniumURL) {
builder.usingServer(seleniumURL);
}
driver = builder.build();
setSize(targetW, targetH);
}
//////////////////////////////////////////////////////////////////////
// Set the screen size
var targetW = 1024, targetH = 768;
function setSize(reqW, reqH) {
return driver.manage().window().setSize(reqW, reqH).then(function() {
return driver.takeScreenshot();
}).then(function(img) {
img = imageDimensions(img);
var actualW = img.width;
var actualH = img.height;
if (actualW === targetW && actualH === targetH) {
process.nextTick(takeScreenshots);
return;
}
if (++attempts > 5) {
throw new Error("Failed to set window size correctly.");
}
return setSize(targetW + reqW - actualW, targetH + reqH - actualH);
}, check);
}
function imageDimensions(img) {
var buf = new Buffer(img, "base64");
return {
buf: buf,
width: buf.readUInt32BE(16),
height: buf.readUInt32BE(20)
};
}
//////////////////////////////////////////////////////////////////////
// Take the screenshots
var countdown = listOfCases.length;
function takeScreenshots() {
listOfCases.forEach(takeScreenshot);
}
function takeScreenshot(key) {
var itm = data[key];
if (!itm) {
console.error("Test case " + key + " not known!");
return;
}
var url = katexURL + "test/screenshotter/test.html?" + itm.query;
driver.get(url);
driver.takeScreenshot().then(function haveScreenshot(img) {
img = imageDimensions(img);
if (img.width !== targetW || img.height !== targetH) {
throw new Error("Excpected " + targetW + " x " + targetH +
", got " + img.width + "x" + img.height);
}
if (key === "Lap" && opts.browser === "firefox" &&
img.buf[0x32] === 0xf8) {
/* There is some strange non-determinism with this case,
* causing slight vertical shifts. The first difference
* is at offset 0x32, where one file has byte 0xf8 and
* the other has something else. By using a different
* output file name for one of these cases, we accept both.
*/
key += "_alt";
}
var file = path.join(dstDir, key + "-" + opts.browser + ".png");
var deferred = new selenium.promise.Deferred();
var opt = new jspngopt.Optimizer({
pako: pako
});
var buf = opt.bufferSync(img.buf);
fs.writeFile(file, buf, function(err) {
if (err) {
deferred.reject(err);
}
else {
deferred.fulfill();
}
});
return deferred.promise;
}).then(function() {
console.log(key);
if (--countdown === 0) {
// devServer.close(cb) will take too long.
process.exit(0);
}
}, check);
}