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