From 5d155c75db298687a216bb079bdf7588af0dc52a Mon Sep 17 00:00:00 2001 From: Martin von Gagern Date: Mon, 22 Jun 2015 19:30:26 +0200 Subject: [PATCH] Switch from own docker image to standard selenium images Since the Selenium images are available for download, and downloading them is usually faster than building them from scratch, this makes taking screenshots easier. Furthermore, since the Selenium image is not specific to KaTeX, it could as well be used for other purposes, thus saving space since a single image can be used in multiple projects. This change also deals with the non-determinism in the Lap screenshot: We detect the one known (and accepted) alternate rendering and change the output file name to Lap_alt in this case. So either Lap or Lap_alt gets saved to, and if the image is different from both, then one of these files will show a modification. On the other hand, if it is equal to either of these, then the matching one will get overwritten, showing no change. --- dockers/Screenshotter/Dockerfile | 14 -- dockers/Screenshotter/README.md | 76 ++++-- dockers/Screenshotter/screenshotter.js | 237 ++++++++++++++++++ dockers/Screenshotter/screenshotter.py | 107 -------- dockers/Screenshotter/screenshotter.sh | 28 +++ package.json | 2 + test/screenshotter/images/Lap_alt-firefox.png | Bin 0 -> 12683 bytes test/screenshotter/test.html | 3 + 8 files changed, 326 insertions(+), 141 deletions(-) delete mode 100644 dockers/Screenshotter/Dockerfile create mode 100644 dockers/Screenshotter/screenshotter.js delete mode 100755 dockers/Screenshotter/screenshotter.py create mode 100755 dockers/Screenshotter/screenshotter.sh create mode 100644 test/screenshotter/images/Lap_alt-firefox.png diff --git a/dockers/Screenshotter/Dockerfile b/dockers/Screenshotter/Dockerfile deleted file mode 100644 index 2234d5d..0000000 --- a/dockers/Screenshotter/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER xymostech -RUN apt-get -qq update -RUN apt-get -qqy install default-jre=2:1.7-51 firefox=28.0+build2-0ubuntu2 xvfb=2:1.15.1-0ubuntu2 wget=1.15-1ubuntu1 python=2.7.5-5ubuntu3 python-pip=1.5.4-1 nodejs=0.10.25~dfsg2-2ubuntu1 || true -RUN wget http://selenium-release.storage.googleapis.com/2.43/selenium-server-standalone-2.43.0.jar -RUN ln -s /usr/bin/nodejs /usr/bin/node -RUN pip install selenium pypng -ENV DISPLAY :1 -CMD /bin/bash ~/run.sh -RUN echo "java -jar /selenium-server-standalone-2.43.0.jar > /dev/null &" >> ~/run.sh -RUN echo "Xvfb :1 2> /dev/null &" >> ~/run.sh -RUN echo "make -C /KaTeX serve > /dev/null &" >> ~/run.sh -RUN echo "sleep 2" >> ~/run.sh -RUN echo "/KaTeX/dockers/Screenshotter/screenshotter.py /KaTeX/test/screenshotter/ss_data.json" >> ~/run.sh \ No newline at end of file diff --git a/dockers/Screenshotter/README.md b/dockers/Screenshotter/README.md index 581917d..bdf7d5a 100644 --- a/dockers/Screenshotter/README.md +++ b/dockers/Screenshotter/README.md @@ -1,29 +1,65 @@ -### How to generate screenshotter images ----------------------------------------- +# How to generate screenshotter images + +## Automatic generation of screen shots Now you too can generate screenshots from your own computer, and (hopefully) -have them look mostly the same as the current ones! To start, make a docker -image from the included Dockerfile using a command like +have them look mostly the same as the current ones! Make sure you have docker +installed and running. Also make sure that the development server is running, +or start it by running - docker build --tag=ss . + node server.js -from within this directory (note you need to have docker installed and running -for this to work). This will build a docker image with the `ss` tag, which you -can then use to run dockers based on it. +in the top level directory of the source tree. If all you want is (re)create +all the snapshots for all the browsers, then you can do so by running the +`screenshotter.sh` script: -This Dockerfile is set up such that it will run everything and generate all the -screenshots when the docker is run, so no interactive input is required. All -that you need to do is mount the KaTeX directory you want to test into the -`/KaTeX` directory in the docker, and run the `ss` docker, like so: + dockers/Screenshotter/screenshotter.sh - docker run --volume=/your/KaTeX/:/KaTeX ss +It will fetch all required selenium docker images, and use them to +take screenshots. -The `--volume=/your/KaTeX:/KaTeX` switch mounts your KaTeX directory into the -docker. Note this is a read-write mounting, so the new screenshots will be -directly placed into your KaTeX directory. +## Manual generation -Since this docker is very self-contained, there should be no need to do -interactive management of the docker, but if you feel the need, you can read the -General Docker Help section of the MathJaxFonts docker readme. +If you are creating screenshots on a regular basis, you can keep the +docker containers with the selenium setups running. Essentially you +are encouraged to reproduce the steps from `screenshotter.sh` +manually. Example run for Firefox: -That's it! + container=$(docker run -d -P selenium/standalone-firefox:2.46.0) + node dockers/Screenshotter/screenshotter.js -b firefox -c ${container} + # possibly repeat the above command as often as you need, then eventually + docker stop ${container} + docker rm ${container} + +## Use without docker + +It is possible to run `screenshotter.js` without the use of Docker: + + npm install selenium-webdriver + node dockers/Screenshotter/screenshotter.js + +This will generate screenshots using the Firefox installed on your system. +Browsers other than Firefox can be targeted using the `--browser` option. +For a complete list of options pass `--help` as an argument to +`screenshotter.js`. Using these it should be possible to have the script +connect to almost any Selenium web driver you might have access to. + +Note that screenshots taken without Docker are very likely to disagree +from the ones stored in the repository, due to different versions of +various software components being used. The screenshots taken in this +fashion are well suited for visual inspection, but for exact binary +comparisons it would be neccessary to carefully set up the environment +to match the one used by the Docker approach. + +## Choosing the list of test cases + +Both `screenshotter.js` and `screenshotter.sh` will accept +an `--include` option (short `-i`) which can be used to specify +a list of test cases to be processed, as a comma separated list. +Conversely, the `--exclude` option (short `-x`) can be used +to specify a list of cases which are not being processed. + +Examples: + + node dockers/Screenshotter/screenshotter.js -i Sqrt,SqrtRoot + dockers/Screenshotter/screenshotter.sh --exclude=GreekLetters diff --git a/dockers/Screenshotter/screenshotter.js b/dockers/Screenshotter/screenshotter.js new file mode 100644 index 0000000..186ba36 --- /dev/null +++ b/dockers/Screenshotter/screenshotter.js @@ -0,0 +1,237 @@ +"use strict"; + +var childProcess = require("child_process"); +var fs = require("fs"); +var path = require("path"); +var net = require("net"); +var selenium = require("selenium-webdriver"); + +var data = require("../../test/screenshotter/ss_data.json"); + +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", + "default": 7936, + 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 katexURL = opts.katexURL; +var seleniumIP = opts.seleniumIP; +var seleniumPort = opts.seleniumPort; +var katexIP = opts.katexIP; + +////////////////////////////////////////////////////////////////////// +// 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"); +} + +if (!katexURL) { + katexURL = "http://" + katexIP + ":" + opts.katexPort + "/"; +} +var toStrip = "http://localhost:7936/"; // remove this from testcase URLs + +////////////////////////////////////////////////////////////////////// +// Wait for container to become ready + +var attempts = 0; +process.nextTick(seleniumIP ? tryConnect : buildDriver); +function tryConnect() { + var sock = net.connect({ + host: seleniumIP, + port: +seleniumPort + }); + sock.on("connect", function() { + sock.end(); + attempts = 0; + setTimeout(buildDriver, 0); + }).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 + +function takeScreenshots() { + listOfCases.forEach(takeScreenshot); +} + +function takeScreenshot(key) { + var url = data[key]; + if (!url) { + console.error("Test case " + key + " not known!"); + return; + } + url = katexURL + url.substr(toStrip.length); + 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"); + fs.writeFile(file, img.buf, check); + console.log(key); + }, check); +} diff --git a/dockers/Screenshotter/screenshotter.py b/dockers/Screenshotter/screenshotter.py deleted file mode 100755 index 6ddfb24..0000000 --- a/dockers/Screenshotter/screenshotter.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python2 - -import argparse -import json -import os -import png -import StringIO -import sys - -from selenium import webdriver - - -def get_png_size(png_data): - w, h, _, _ = png.Reader(file=StringIO.StringIO(png_data)).read() - return (w, h) - - -def set_driver_size(driver, width, height): - """Correctly sets the size of the driver window so screenshots end up the - provided size""" - driver.set_window_size(width, height) - - screenshot_size = get_png_size(driver.get_screenshot_as_png()) - attempts = 0 - while (width, height) != screenshot_size: - attempts += 1 - if attempts > 5: - print "Tried 5 times to size screen correctly, bailing out" - exit(1) - - ss_width, ss_height = screenshot_size - driver.set_window_size( - width + (width - ss_width), - height + (height - ss_height)) - screenshot_size = get_png_size(driver.get_screenshot_as_png()) - - -def main(): - parser = argparse.ArgumentParser( - description='Take screenshots of webpages', add_help=False) - parser.add_argument('file', metavar='file.json') - parser.add_argument('-t', '--tests', metavar='test', nargs='*') - parser.add_argument('-w', '--width', metavar='width', default=1024, - type=int) - parser.add_argument('-h', '--height', metavar='height', default=768, - type=int) - parser.add_argument('-b', '--browser', metavar='browser', - choices=['firefox'], default='firefox') - - args = parser.parse_args() - - data = None - with open(args.file) as f: - try: - data = json.load(f) - except ValueError: - print "Invalid json in input file:", args.file - exit(1) - - tests = [] - - if args.tests is None: - tests = data.keys() - else: - data_tests = data.keys() - for test in args.tests: - if test not in data_tests: - print "Unknown test:", test - exit(1) - - tests = args.tests - - print "Starting up" - sys.stdout.flush() - - driver = None - if args.browser == 'firefox': - driver = webdriver.Firefox() - else: - print "Unknown browser:", args.browser - exit(1) - - set_driver_size(driver, args.width, args.height) - - data_dir = os.path.join( - os.path.dirname(os.path.realpath(args.file)), "images") - - try: - os.mkdir(data_dir) - except OSError: - pass - - for test, url in data.iteritems(): - if test in tests: - filename = os.path.join( - data_dir, '%s-%s.png' % (test, args.browser)) - - print "Running:", test - sys.stdout.flush() - - driver.get(url) - driver.get_screenshot_as_file(filename) - - print "Done" - -if __name__ == '__main__': - main() diff --git a/dockers/Screenshotter/screenshotter.sh b/dockers/Screenshotter/screenshotter.sh new file mode 100755 index 0000000..93287ae --- /dev/null +++ b/dockers/Screenshotter/screenshotter.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# This script does a one-shot creation of screenshots, creating needed +# docker containers and removing them afterwards. During development, +# it might be desirable to avoid the overhead for starting and +# stopping the containers. Developers are encouraged to manage +# suitable containers themselves, calling the screenshotter.js script +# directly. + +status=0 +for browserTag in firefox:2.46.0; do + browser=${browserTag%:*} + image=selenium/standalone-${browserTag} + echo "Starting container for ${image}" + container=$(docker run -d -P ${image}) + [[ ${container} ]] || continue + echo "Container ${container:0:12} started, creating screenshots..." + if node "$(dirname "$0")"/screenshotter.js \ + --browser="${browser}" --container="${container}" "$@"; then + res=Done + else + res=Failed + status=1 + fi + echo "${res} taking screenshots, stopping and removing ${container:0:12}" + docker stop ${container} >/dev/null && docker rm ${container} >/dev/null +done +exit ${status} diff --git a/package.json b/package.json index 2802a34..f4ac836 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "jasmine-node": "2.0.0-beta4", "jshint": "^2.5.6", "less": "~1.7.5", + "selenium-webdriver": "^2.46.1", + "nomnom": "^1.8.1", "uglify-js": "~2.4.15" }, "bin": "cli.js", diff --git a/test/screenshotter/images/Lap_alt-firefox.png b/test/screenshotter/images/Lap_alt-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..cb73349706df3f7c5ceb89d4ff8aadbbe58d3f70 GIT binary patch literal 12683 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 literal 0 HcmV?d00001 diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html index 2b8b4c9..f03df4d 100644 --- a/test/screenshotter/test.html +++ b/test/screenshotter/test.html @@ -8,6 +8,9 @@ #math, #pre, #post { font-size: 4em; } + body { + font-family: "DejaVu Serif",serif; + }