diff --git a/dockers/Screenshotter/Dockerfile b/dockers/Screenshotter/Dockerfile deleted file mode 100644 index 2234d5df1..000000000 --- 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 581917d80..bdf7d5ab8 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 000000000..186ba361b --- /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 6ddfb24a9..000000000 --- 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 000000000..93287aec7 --- /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 2802a349e..f4ac836cb 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 000000000..cb7334970 Binary files /dev/null and b/test/screenshotter/images/Lap_alt-firefox.png differ diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html index 2b8b4c9fb..f03df4d73 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; + }