diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..8ac3e93e2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/ttx/fonttools-code"] + path = test/ttx/fonttools-code + url = git://git.code.sf.net/p/fonttools/code diff --git a/make.js b/make.js index 94a2d1f08..0b7ee6e00 100644 --- a/make.js +++ b/make.js @@ -40,7 +40,6 @@ var ROOT_DIR = __dirname + '/', // absolute path to project's root GENERIC_DIR = BUILD_DIR + 'generic/', MINIFIED_DIR = BUILD_DIR + 'minified/', REPO = 'git@github.com:mozilla/pdf.js.git', - PYTHON_BIN = 'python2.7', MOZCENTRAL_PREF_PREFIX = 'pdfjs', FIREFOX_PREF_PREFIX = 'extensions.uriloader@pdf.js', MOZCENTRAL_STREAM_CONVERTER_ID = 'd0c5195d-e798-49d4-b1d3-9324328b2291', @@ -1005,7 +1004,7 @@ target.browsertest = function(options) { var reftest = (options && options.noreftest) ? '' : '--reftest'; cd('test'); - exec(PYTHON_BIN + ' -u test.py ' + reftest + ' --browserManifestFile=' + + exec('node test.js ' + reftest + ' --browserManifestFile=' + PDF_BROWSERS + ' --manifestFile=' + PDF_TEST, {async: true}); }; @@ -1027,7 +1026,7 @@ target.unittest = function(options, callback) { } callback = callback || function() {}; cd('test'); - exec(PYTHON_BIN + ' -u test.py --unitTest --browserManifestFile=' + + exec('node test.js --unitTest --browserManifestFile=' + PDF_BROWSERS, {async: true}, callback); }; @@ -1049,7 +1048,7 @@ target.fonttest = function(options, callback) { } callback = callback || function() {}; cd('test'); - exec(PYTHON_BIN + ' -u test.py --fontTest --browserManifestFile=' + + exec('node test.js --fontTest --browserManifestFile=' + PDF_BROWSERS, {async: true}, callback); }; @@ -1072,7 +1071,7 @@ target.botmakeref = function() { } cd('test'); - exec(PYTHON_BIN + ' -u test.py --masterMode --noPrompts ' + + exec('node test.js --masterMode --noPrompts ' + '--browserManifestFile=' + PDF_BROWSERS, {async: true}); }; @@ -1251,8 +1250,10 @@ target.server = function() { echo(); echo('### Starting local server'); - cd('test'); - exec(PYTHON_BIN + ' -u test.py --port=8888 --noDownload', {async: true}); + var WebServer = require('./test/webserver.js').WebServer; + var server = new WebServer(); + server.port = 8888; + server.start(); }; // @@ -1268,8 +1269,7 @@ target.lint = function() { 'external/crlfchecker/', 'src/', 'web/', - 'test/driver.js', - 'test/reporter.js', + 'test/*.js', 'test/unit/', 'extensions/firefox/', 'extensions/chromium/' diff --git a/package.json b/package.json index 179e1a5b2..1dcdf8271 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "pdf.js", "version": "0.8.0", "dependencies": { - "jshint": "2.4.x" + "jshint": "2.4.x", + "yargs": "~1.2.1" }, "scripts": { "test": "node make lint" diff --git a/test/downloadutils.js b/test/downloadutils.js new file mode 100644 index 000000000..483734e7a --- /dev/null +++ b/test/downloadutils.js @@ -0,0 +1,173 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var fs = require('fs'); +var crypto = require('crypto'); +var http = require('http'); +var https = require('https'); + +function downloadFile(file, url, callback, redirects) { + var completed = false; + var protocol = /^https:\/\//.test(url) ? https : http; + protocol.get(url, function (response) { + if (response.statusCode === 301 || response.statusCode === 302 || + response.statusCode === 307 || response.statusCode === 308) { + if (redirects > 10) { + callback('Too many redirects'); + } + var redirectTo = response.headers.location; + redirectTo = require('url').resolve(url, redirectTo); + downloadFile(file, redirectTo, callback, (redirects || 0) + 1); + return; + } + if (response.statusCode === 404 && url.indexOf('web.archive.org') < 0) { + // trying waybackmachine + var redirectTo = 'http://web.archive.org/web/' + url; + downloadFile(file, redirectTo, callback, (redirects || 0) + 1); + return; + } + + if (response.statusCode !== 200) { + if (!completed) { + completed = true; + callback('HTTP ' + response.statusCode); + } + return; + } + var stream = fs.createWriteStream(file); + stream.on('error', function (err) { + if (!completed) { + completed = true; + callback(err); + } + }); + response.pipe(stream); + stream.on('finish', function() { + stream.close(); + if (!completed) { + completed = true; + callback(); + } + }); + }).on('error', function (err) { + if (!completed) { + if (typeof err === 'object' && err.errno === 'ENOTFOUND' && + url.indexOf('web.archive.org') < 0) { + // trying waybackmachine + var redirectTo = 'http://web.archive.org/web/' + url; + downloadFile(file, redirectTo, callback, (redirects || 0) + 1); + return; + } + completed = true; + callback(err); + } + }); +} + +function downloadManifestFiles(manifest, callback) { + function downloadNext() { + if (i >= links.length) { + callback(); + return; + } + var file = links[i].file; + var url = links[i].url; + console.log('Downloading ' + url + ' to ' + file + '...'); + downloadFile(file, url, function (err) { + if (err) { + console.error('Error during downloading of ' + url + ': ' + err); + fs.writeFileSync(file, ''); // making it empty file + fs.writeFileSync(file + '.error', err); + } + i++; + downloadNext(); + }); + } + + var links = manifest.filter(function (item) { + return item.link && !fs.existsSync(item.file); + }).map(function (item) { + var file = item.file; + var linkfile = file + '.link'; + var url = fs.readFileSync(linkfile).toString(); + url = url.replace(/\s+$/, ''); + return {file: file, url: url}; + }); + + var i = 0; + downloadNext(); +} + +function calculateMD5(file, callback) { + var hash = crypto.createHash('md5'); + var stream = fs.createReadStream(file); + stream.on('data', function (data) { + hash.update(data); + }); + stream.on('error', function (err) { + callback(err); + }); + stream.on('end', function() { + var result = hash.digest('hex'); + callback(null, result); + }); +} + +function verifyManifestFiles(manifest, callback) { + function verifyNext() { + if (i >= manifest.length) { + callback(error); + return; + } + var item = manifest[i]; + if (fs.existsSync(item.file + '.error')) { + console.error('WARNING: File was not downloaded. See "' + + item.file + '.error" file.'); + error = true; + i++; + verifyNext(); + return; + } + calculateMD5(item.file, function (err, md5) { + if (err) { + console.log('WARNING: Unable to open file for reading "' + err + '".'); + error = true; + } else if (!item.md5) { + console.error('WARNING: Missing md5 for file "' + item.file + '". ' + + 'Hash for current file is "' + md5 + '"'); + error = true; + } else if (md5 !== item.md5) { + console.error('WARNING: MD5 of file "' + item.file + + '" does not match file. Expected "' + + item.md5 + '" computed "' + md5 + '"'); + error = true; + } + i++; + verifyNext(); + }); + } + var i = 0; + var error = false; + verifyNext(); +} + +exports.downloadManifestFiles = downloadManifestFiles; +exports.verifyManifestFiles = verifyManifestFiles; diff --git a/test/driver.js b/test/driver.js index 0c41988d5..6602f14ba 100644 --- a/test/driver.js +++ b/test/driver.js @@ -232,10 +232,11 @@ function nextPage(task, loadError) { var failure = loadError || ''; if (!task.pdfDoc) { - sendTaskResult(canvasToDataURL(), task, failure); - log('done' + (failure ? ' (failed !: ' + failure + ')' : '') + '\n'); - ++currentTaskIdx; - nextTask(); + sendTaskResult(canvasToDataURL(), task, failure, function () { + log('done' + (failure ? ' (failed !: ' + failure + ')' : '') + '\n'); + ++currentTaskIdx; + nextTask(); + }); return; } @@ -332,18 +333,12 @@ function nextPage(task, loadError) { function snapshotCurrentPage(task, failure) { log('done, snapshotting... '); - sendTaskResult(canvasToDataURL(), task, failure); - log('done' + (failure ? ' (failed !: ' + failure + ')' : '') + '\n'); + sendTaskResult(canvasToDataURL(), task, failure, function () { + log('done' + (failure ? ' (failed !: ' + failure + ')' : '') + '\n'); - // Set up the next request - var backoff = (inFlightRequests > 0) ? inFlightRequests * 10 : 0; - setTimeout( - function snapshotCurrentPageSetTimeout() { - ++task.pageNum; - nextPage(task); - }, - backoff - ); + ++task.pageNum; + nextPage(task); + }); } function sendQuitRequest() { @@ -373,28 +368,25 @@ function done() { } } -function sendTaskResult(snapshot, task, failure, result) { - // Optional result argument is for retrying XHR requests - see below - if (!result) { - result = JSON.stringify({ - browser: browser, - id: task.id, - numPages: task.pdfDoc ? - (task.lastPage || task.pdfDoc.numPages) : 0, - lastPageNum: getLastPageNum(task), - failure: failure, - file: task.file, - round: task.round, - page: task.pageNum, - snapshot: snapshot, - stats: task.stats.times - }); - } +function sendTaskResult(snapshot, task, failure, callback) { + var result = JSON.stringify({ + browser: browser, + id: task.id, + numPages: task.pdfDoc ? + (task.lastPage || task.pdfDoc.numPages) : 0, + lastPageNum: getLastPageNum(task), + failure: failure, + file: task.file, + round: task.round, + page: task.pageNum, + snapshot: snapshot, + stats: task.stats.times + }); - send('/submit_task_results', result); + send('/submit_task_results', result, callback); } -function send(url, message) { +function send(url, message, callback) { var r = new XMLHttpRequest(); // (The POST URI is ignored atm.) r.open('POST', url, true); @@ -408,6 +400,9 @@ function send(url, message) { send(url, message); }); } + if (callback) { + callback(); + } } }; document.getElementById('inFlightCount').innerHTML = inFlightRequests++; diff --git a/test/font/ttxdriver.js b/test/font/ttxdriver.js new file mode 100644 index 000000000..c9e95a7b1 --- /dev/null +++ b/test/font/ttxdriver.js @@ -0,0 +1,86 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var spawn = require('child_process').spawn; + +var ttxResourcesHome = path.join(__dirname, '..', 'ttx'); + +var nextTTXTaskId = Date.now(); + +function runTtx(ttxResourcesHome, fontPath, registerOnCancel, callback) { + fs.realpath(ttxResourcesHome, function (err, ttxResourcesHome) { + var fontToolsHome = path.join(ttxResourcesHome, 'fonttools-code'); + fs.realpath(fontPath, function (err, fontPath) { + var ttxPath = path.join('Tools', 'ttx'); + if (!fs.existsSync(path.join(fontToolsHome, ttxPath))) { + callback('TTX was not found, please checkout PDF.js submodules'); + return; + } + var ttxEnv = { + 'PYTHONPATH': path.join(fontToolsHome, 'Lib'), + 'PYTHONDONTWRITEBYTECODE': true + }; + var ttxStdioMode = 'ignore'; + var ttx = spawn('python', [ttxPath, fontPath], + {cwd: fontToolsHome, stdio: ttxStdioMode, env: ttxEnv}); + var ttxRunError; + registerOnCancel(function (reason) { + ttxRunError = reason; + callback(reason); + ttx.kill(); + }); + ttx.on('error', function (err) { + ttxRunError = err; + callback('Unable to execute ttx'); + }); + ttx.on('close', function (code) { + if (ttxRunError) { + return; + } + callback(); + }); + }); + }); +} + +exports.translateFont = function translateFont(content, registerOnCancel, + callback) { + var buffer = new Buffer(content, 'base64'); + var taskId = (nextTTXTaskId++).toString(); + var fontPath = path.join(ttxResourcesHome, taskId + '.otf'); + var resultPath = path.join(ttxResourcesHome, taskId + '.ttx'); + + fs.writeFileSync(fontPath, buffer); + runTtx(ttxResourcesHome, fontPath, registerOnCancel, function (err) { + fs.unlink(fontPath); + if (err) { + console.error(err); + callback(err); + } else if (!fs.existsSync(resultPath)) { + callback('Output was not generated'); + } else { + callback(null, fs.readFileSync(resultPath)); + fs.unlink(resultPath); + } + }); +}; diff --git a/test/test.js b/test/test.js new file mode 100644 index 000000000..9f06f6369 --- /dev/null +++ b/test/test.js @@ -0,0 +1,723 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var WebServer = require('./webserver.js').WebServer; +var WebBrowser = require('./webbrowser.js').WebBrowser; +var path = require('path'); +var fs = require('fs'); +var os = require('os'); +var url = require('url'); +var spawn = require('child_process').spawn; +var testUtils = require('./testutils.js'); + +function parseOptions() { + function describeCheck(fn, text) { + fn.toString = function () { + return text; + }; + return fn; + } + + var yargs = require('yargs') + .usage('Usage: $0') + .boolean(['help', 'masterMode', 'reftest', 'unitTest', 'fontTest', + 'noPrompts', 'noDownload']) + .string(['manifestFile', 'browser', 'browserManifestFile', + 'port', 'statsFile', 'statsDelay']) + .alias('browser', 'b').alias('help', 'h').alias('masterMode', 'm') + .describe('help', 'Show this help message') + .describe('masterMode', 'Run the script in master mode.') + .describe('noPrompts', + 'Uses default answers (intended for CLOUD TESTS only!).') + .describe('manifestFile', + 'A path to JSON file in the form of test_manifest.json') + .default('manifestFile', 'test_manifest.json') + .describe('browser', 'The path to a single browser ') + .describe('browserManifestFile', 'A path to JSON file in the form of ' + + 'those found in resources/browser_manifests/') + .describe('reftest', 'Automatically start reftest showing comparison ' + + 'test failures, if there are any.') + .describe('port', 'The port the HTTP server should listen on.') + .default('port', 8000) + .describe('unitTest', 'Run the unit tests.') + .describe('fontTest', 'Run the font tests.') + .describe('noDownload', 'Skips test PDFs downloading.') + .describe('statsFile', 'The file where to store stats.') + .describe('statsDelay', 'The amount of time in milliseconds the browser ' + + 'should wait before starting stats.') + .default('statsDelay', 0) + .check(describeCheck(function (argv) { + return +argv.reftest + argv.unitTest + argv.fontTest + + argv.masterMode <= 1; + }, '--reftest, --unitTest, --fontTest and --masterMode must not be ' + + 'specified at the same time.')) + .check(describeCheck(function (argv) { + return !argv.masterMode || argv.manifestFile === 'test_manifest.json'; + }, 'when --masterMode is specified --manifestFile shall be equal ' + + 'test_manifest.json')) + .check(describeCheck(function (argv) { + return !argv.browser || !argv.browserManifestFile; + }, '--browser and --browserManifestFile must not be specified at the ' +'' + + 'same time.')); + var result = yargs.argv; + if (result.help) { + yargs.showHelp(); + process.exit(0); + } + return result; +} + +var refsTmpDir = 'tmp'; +var testResultDir = 'test_snapshots'; +var refsDir = 'ref'; +var eqLog = 'eq.log'; +var browserTimeout = 120; + +function monitorBrowserTimeout(session, onTimeout) { + if (session.timeoutMonitor) { + clearTimeout(session.timeoutMonitor); + } + if (!onTimeout) { + session.timeoutMonitor = null; + return; + } + session.timeoutMonitor = setTimeout(function () { + onTimeout(session); + }, browserTimeout * 1000); +} + +function updateRefImages() { + function sync(removeTmp) { + console.log(' Updating ref/ ... '); + testUtils.copySubtreeSync(refsTmpDir, refsDir); + if (removeTmp) { + testUtils.removeDirSync(refsTmpDir); + } + console.log('done'); + } + + if (options.noPrompts) { + sync(false); // don't remove tmp/ for botio + return; + } + testUtils.confirm('Would you like to update the master copy in ref/? [yn] ', + function (confirmed) { + if (confirmed) { + sync(true); + } else { + console.log(' OK, not updating.'); + } + }); +} + +function examineRefImages() { + startServer(); + var startUrl = 'http://' + server.host + ':' + server.port + + '/test/resources/reftest-analyzer.html#web=/test/eq.log'; + var browser = WebBrowser.create(sessions[0].config); + browser.start(startUrl); +} + +function startRefTest(masterMode, showRefImages) { + function finalize() { + stopServer(); + var numErrors = 0; + var numFBFFailures = 0; + var numEqFailures = 0; + var numEqNoSnapshot = 0; + sessions.forEach(function (session) { + numErrors += session.numErrors; + numFBFFailures += session.numFBFFailures; + numEqFailures += session.numEqFailures; + numEqNoSnapshot += session.numEqNoSnapshot; + }); + var numFatalFailures = numErrors + numFBFFailures; + console.log(); + if (numFatalFailures + numEqFailures > 0) { + console.log('OHNOES! Some tests failed!'); + if (numErrors > 0) { + console.log(' errors: ' + numErrors); + } + if (numEqFailures > 0) { + console.log(' different ref/snapshot: ' + numEqFailures); + } + if (numFBFFailures > 0) { + console.log(' different first/second rendering: ' + numFBFFailures); + } + } else { + console.log('All regression tests passed.'); + } + var runtime = (Date.now() - startTime) / 1000; + console.log('Runtime was ' + runtime.toFixed(1) + ' seconds'); + + if (options.statsFile) { + fs.writeFileSync(options.statsFile, JSON.stringify(stats, null, 2)); + } + if (masterMode) { + if (numEqFailures + numEqNoSnapshot > 0) { + console.log(); + console.log('Some eq tests failed or didn\'t have snapshots.'); + console.log('Checking to see if master references can be updated...'); + if (numFatalFailures > 0) { + console.log(' No. Some non-eq tests failed.'); + } else { + console.log( + ' Yes! The references in tmp/ can be synced with ref/.'); + updateRefImages(); + } + } + } else if (showRefImages && numEqFailures > 0) { + console.log(); + console.log('Starting reftest harness to examine ' + numEqFailures + + ' eq test failures.'); + examineRefImages(numEqFailures); + } + } + + function setup() { + if (fs.existsSync(refsTmpDir)) { + console.error('tmp/ exists -- unable to proceed with testing'); + process.exit(1); + } + + if (fs.existsSync(eqLog)) { + fs.unlink(eqLog); + } + if (fs.existsSync(testResultDir)) { + testUtils.removeDirSync(testResultDir); + } + + startTime = Date.now(); + startServer(); + server.hooks['POST'].push(refTestPostHandler); + onAllSessionsClosed = finalize; + + startBrowsers('/test/test_slave.html', function (session) { + session.masterMode = masterMode; + session.taskResults = {}; + session.tasks = {}; + session.remaining = manifest.length; + manifest.forEach(function (item) { + var rounds = item.rounds || 1; + var roundsResults = []; + roundsResults.length = rounds; + session.taskResults[item.id] = roundsResults; + session.tasks[item.id] = item; + }); + session.numErrors = 0; + session.numFBFFailures = 0; + session.numEqNoSnapshot = 0; + session.numEqFailures = 0; + monitorBrowserTimeout(session, handleSessionTimeout); + }); + } + function checkRefsTmp() { + if (masterMode && fs.existsSync(refsTmpDir)) { + if (options.noPrompt) { + testUtils.removeDirSync(refsTmpDir); + setup(); + return; + } + console.log('Temporary snapshot dir tmp/ is still around.'); + console.log('tmp/ can be removed if it has nothing you need.'); + testUtils.confirm('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY [yn] ', + function (confirmed) { + if (confirmed) { + testUtils.removeDirSync(refsTmpDir); + } + setup(); + }); + } else { + setup(); + } + } + + var startTime; + var manifest = JSON.parse(fs.readFileSync(options.manifestFile)); + if (options.noDownload) { + checkRefsTmp(); + } else { + ensurePDFsDownloaded(checkRefsTmp); + } +} + +function handleSessionTimeout(session) { + if (session.closed) { + return; + } + var browser = session.name; + console.log('TEST-UNEXPECTED-FAIL | test failed ' + browser + + ' has not responded in ' + browserTimeout + 's'); + session.numErrors += session.remaining; + session.remaining = 0; + closeSession(browser); +} + +function checkEq(task, results, browser, masterMode) { + var taskId = task.id; + var refSnapshotDir = path.join(refsDir, os.platform(), browser, taskId); + var testSnapshotDir = path.join(testResultDir, os.platform(), browser, + taskId); + + var pageResults = results[0]; + var taskType = task.type; + var numEqNoSnapshot = 0; + var numEqFailures = 0; + for (var page = 0; page < pageResults.length; page++) { + if (!pageResults[page]) { + continue; + } + var testSnapshot = pageResults[page].snapshot; + if (testSnapshot && testSnapshot.indexOf('data:image/png;base64,') === 0) { + testSnapshot = new Buffer(testSnapshot.substring(22), 'base64'); + } else { + console.error('Valid snapshot was not found.'); + } + + var refSnapshot = null; + var eq = false; + var refPath = path.join(refSnapshotDir, (page + 1) + '.png'); + if (!fs.existsSync(refPath)) { + numEqNoSnapshot++; + if (!masterMode) { + console.log('WARNING: no reference snapshot ' + refPath); + } + } else { + refSnapshot = fs.readFileSync(refPath); + eq = (refSnapshot.toString('hex') === testSnapshot.toString('hex')); + if (!eq) { + console.log('TEST-UNEXPECTED-FAIL | ' + taskType + ' ' + taskId + + ' | in ' + browser + ' | rendering of page ' + (page + 1) + + ' != reference rendering'); + + testUtils.ensureDirSync(testSnapshotDir); + fs.writeFileSync(path.join(testSnapshotDir, (page + 1) + '.png'), + testSnapshot); + fs.writeFileSync(path.join(testSnapshotDir, (page + 1) + '_ref.png'), + refSnapshot); + + // NB: this follows the format of Mozilla reftest output so that + // we can reuse its reftest-analyzer script + fs.appendFileSync(eqLog, 'REFTEST TEST-UNEXPECTED-FAIL | ' + browser + + '-' + taskId + '-page' + (page + 1) + ' | image comparison (==)\n' + + 'REFTEST IMAGE 1 (TEST): ' + + path.join(testSnapshotDir, (page + 1) + '.png') + '\n' + + 'REFTEST IMAGE 2 (REFERENCE): ' + + path.join(testSnapshotDir, (page + 1) + '_ref.png') + '\n'); + numEqFailures++; + } + } + if (masterMode && (!refSnapshot || !eq)) { + var tmpSnapshotDir = path.join(refsTmpDir, os.platform(), browser, + taskId); + testUtils.ensureDirSync(tmpSnapshotDir); + fs.writeFileSync(path.join(tmpSnapshotDir, (page + 1) + '.png'), + testSnapshot); + } + } + + var session = getSession(browser); + session.numEqNoSnapshot += numEqNoSnapshot; + if (numEqFailures > 0) { + session.numEqFailures += numEqFailures; + } else { + console.log('TEST-PASS | ' + taskType + ' test ' + taskId + ' | in ' + + browser); + } +} + +function checkFBF(task, results, browser) { + var numFBFFailures = 0; + var round0 = results[0], round1 = results[1]; + if (round0.length !== round1.length) { + console.error('round 1 and 2 sizes are different'); + } + + for (var page = 0; page < round1.length; page++) { + var r0Page = round0[page], r1Page = round1[page]; + if (!r0Page) { + continue; + } + if (r0Page.snapshot !== r1Page.snapshot) { + console.log('TEST-UNEXPECTED-FAIL | forward-back-forward test ' + + task.id + ' | in ' + browser + ' | first rendering of page ' + + (page + 1) + ' != second'); + numFBFFailures++; + } + } + + if (numFBFFailures > 0) { + getSession(browser).numFBFFailures += numFBFFailures; + } else { + console.log('TEST-PASS | forward-back-forward test ' + task.id + + ' | in ' + browser); + } +} + +function checkLoad(task, results, browser) { + // Load just checks for absence of failure, so if we got here the + // test has passed + console.log('TEST-PASS | load test ' + task.id + ' | in ' + browser); +} + +function checkRefTestResults(browser, id, results) { + var failed = false; + var session = getSession(browser); + var task = session.tasks[id]; + results.forEach(function (roundResults, round) { + roundResults.forEach(function (pageResult, page) { + if (!pageResult) { + return; // no results + } + if (pageResult.failure) { + failed = true; + if (fs.existsSync(task.file + '.error')) { + console.log('TEST-SKIPPED | PDF was not downloaded ' + id + ' | in ' + + browser + ' | page' + (page + 1) + ' round ' + + (round + 1) + ' | ' + pageResult.failure); + } else { + session.numErrors++; + console.log('TEST-UNEXPECTED-FAIL | test failed ' + id + ' | in ' + + browser + ' | page' + (page + 1) + ' round ' + + (round + 1) + ' | ' + pageResult.failure); + } + } + }); + }); + if (failed) { + return; + } + switch (task.type) { + case 'eq': + case 'text': + checkEq(task, results, browser, session.masterMode); + break; + case 'fbf': + checkFBF(task, results, browser); + break; + case 'load': + checkLoad(task, results, browser); + break; + default: + throw new Error('Unknown test type'); + } +} + +function refTestPostHandler(req, res) { + var parsedUrl = url.parse(req.url, true); + var pathname = parsedUrl.pathname; + if (pathname !== '/tellMeToQuit' && + pathname !== '/info' && + pathname !== '/submit_task_results') { + return false; + } + + var body = ''; + req.on('data', function (data) { + body += data; + }); + req.on('end', function () { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(); + + if (pathname === '/tellMeToQuit') { + // finding by path + var browserPath = parsedUrl.query.path; + var session = sessions.filter(function (session) { + return session.config.path === browserPath; + })[0]; + monitorBrowserTimeout(session, null); + closeSession(session.name); + return; + } + + var data = JSON.parse(body); + if (pathname === '/info') { + console.log(data.message); + return; + } + + var browser = data.browser; + var round = data.round; + var id = data.id; + var page = data.page - 1; + var failure = data.failure; + var snapshot = data.snapshot; + var lastPageNum = data.lastPageNum; + + var session = getSession(browser); + monitorBrowserTimeout(session, handleSessionTimeout); + + var taskResults = session.taskResults[id]; + if (!taskResults[round]) { + taskResults[round] = []; + } + + if (taskResults[round][page]) { + console.error('Results for ' + browser + ':' + id + ':' + round + + ':' + page + ' were already submitted'); + // TODO abort testing here? + } + + taskResults[round][page] = { + failure: failure, + snapshot: snapshot + }; + if (stats) { + stats.push({ + 'browser': browser, + 'pdf': id, + 'page': page, + 'round': round, + 'stats': data.stats + }); + } + + var isDone = taskResults[taskResults.length - 1] && + taskResults[taskResults.length - 1][lastPageNum - 1]; + if (isDone) { + checkRefTestResults(browser, id, taskResults); + session.remaining--; + } + }); + return true; +} + +function startUnitTest(url, name) { + var startTime = Date.now(); + startServer(); + server.hooks['POST'].push(unitTestPostHandler); + onAllSessionsClosed = function () { + stopServer(); + var numRuns = 0, numErrors = 0; + sessions.forEach(function (session) { + numRuns += session.numRuns; + numErrors += session.numErrors; + }); + console.log(); + console.log('Run ' + numRuns + ' tests'); + if (numErrors > 0) { + console.log('OHNOES! Some ' + name + ' tests failed!'); + console.log(' ' + numErrors + ' of ' + numRuns + ' failed'); + } else { + console.log('All ' + name + ' tests passed.'); + } + var runtime = (Date.now() - startTime) / 1000; + console.log(name + ' tests runtime was ' + runtime.toFixed(1) + ' seconds'); + }; + startBrowsers(url, function (session) { + session.numRuns = 0; + session.numErrors = 0; + }); +} + +function unitTestPostHandler(req, res) { + var parsedUrl = url.parse(req.url); + var pathname = parsedUrl.pathname; + if (pathname !== '/tellMeToQuit' && + pathname !== '/info' && + pathname !== '/ttx' && + pathname !== '/submit_task_results') { + return false; + } + + var body = ''; + req.on('data', function (data) { + body += data; + }); + req.on('end', function () { + if (pathname === '/ttx') { + var translateFont = require('./font/ttxdriver.js').translateFont; + var onCancel = null, ttxTimeout = 10000; + var timeoutId = setTimeout(function () { + if (onCancel) { + onCancel('TTX timeout'); + } + }, ttxTimeout); + translateFont(body, function (fn) { + onCancel = fn; + }, function (err, xml) { + clearTimeout(timeoutId); + res.writeHead(200, {'Content-Type': 'text/xml'}); + res.end(err ? '' + err + '' : xml); + }); + return; + } + + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(); + + var data = JSON.parse(body); + if (pathname === '/tellMeToQuit') { + closeSession(data.browser); + return; + } + if (pathname === '/info') { + console.log(data.message); + return; + } + var session = getSession(data.browser); + session.numRuns++; + var message = data.status + ' | ' + data.description; + if (data.status === 'TEST-UNEXPECTED-FAIL') { + session.numErrors++; + } + if (data.error) { + message += ' | ' + data.error; + } + console.log(message); + }); + return true; +} + +function startBrowsers(url, initSessionCallback) { + var browsers; + if (options.browserManifestFile) { + browsers = JSON.parse(fs.readFileSync(options.browserManifestFile)); + } else if (options.browser) { + var browserPath = options.browser; + var name = path.basename(browserPath, path.extname(browserPath)); + browsers = [{name: name, path: browserPath}]; + } else { + console.error('Specify either browser or browserManifestFile.'); + process.exit(1); + } + sessions = []; + browsers.forEach(function (b) { + var browser = WebBrowser.create(b); + var startUrl = getServerBaseAddress() + url + + '?browser=' + encodeURIComponent(b.name) + + '&manifestFile=' + encodeURIComponent('/test/' + options.manifestFile) + + '&path=' + encodeURIComponent(b.path) + + '&delay=' + options.statsDelay + + '&masterMode=' + options.masterMode; + browser.start(startUrl); + var session = { + name: b.name, + config: b, + browser: browser, + closed: false + }; + if (initSessionCallback) { + initSessionCallback(session); + } + sessions.push(session); + }); +} + +function stopBrowsers(callback) { + var count = sessions.length; + sessions.forEach(function (session) { + if (session.closed) { + return; + } + session.browser.stop(function () { + session.closed = true; + count--; + if (count === 0 && callback) { + callback(); + } + }); + }); +} + +function getServerBaseAddress() { + return 'http://' + host + ':' + server.port; +} + +function startServer() { + server = new WebServer(); + server.host = host; + server.port = options.port; + server.root = '..'; + server.start(); +} + +function stopServer() { + server.stop(); +} + +function getSession(browser) { + return sessions.filter(function (session) { + return session.name === browser; + })[0]; +} + +function closeSession(browser) { + var i = 0; + while (i < sessions.length && sessions[i].name !== browser) { + i++; + } + if (i < sessions.length) { + var session = sessions[i]; + session.browser.stop(function () { + session.closed = true; + var allClosed = sessions.every(function (s) { + return s.closed; + }); + if (allClosed && onAllSessionsClosed) { + onAllSessionsClosed(); + } + }); + } +} + +function ensurePDFsDownloaded(callback) { + var downloadUtils = require('./downloadutils.js'); + var downloadManifestFiles = downloadUtils.downloadManifestFiles; + var manifest = JSON.parse(fs.readFileSync(options.manifestFile)); + downloadUtils.downloadManifestFiles(manifest, function () { + downloadUtils.verifyManifestFiles(manifest, function (hasErrors) { + if (hasErrors) { + console.log('Unable to verify the checksum for the files that are ' + + 'used for testing.'); + console.log('Please re-download the files, or adjust the MD5 ' + + 'checksum in the manifest for the files listed above.\n'); + } + callback(); + }); + }); +} + +function main() { + if (options.statsFile) { + stats = []; + } + + if (!options.browser && !options.browserManifestFile) { + startServer(); + } else if (options.unitTest) { + startUnitTest('/test/unit/unit_test.html', 'unit'); + } else if (options.fontTest) { + startUnitTest('/test/font/font_test.html', 'font'); + } else { + startRefTest(options.masterMode, options.reftest); + } +} + +var server; +var sessions; +var onAllSessionsClosed; +var host = '127.0.0.1'; +var options = parseOptions(); +var stats; + +main(); \ No newline at end of file diff --git a/test/test.py b/test/test.py deleted file mode 100644 index 4d51a7ce4..000000000 --- a/test/test.py +++ /dev/null @@ -1,960 +0,0 @@ -# Copyright 2012 Mozilla Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json, platform, os, shutil, sys, subprocess, tempfile, threading -import time, urllib, urllib2, hashlib, re, base64, uuid, socket, errno -import traceback -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from SocketServer import ThreadingMixIn -from optparse import OptionParser -from urlparse import urlparse, parse_qs -from threading import Lock - -USAGE_EXAMPLE = "%prog" - -# The local web server uses the git repo as the document root. -DOC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),"..")) - -GIT_CLONE_CHECK = True -DEFAULT_MANIFEST_FILE = 'test_manifest.json' -EQLOG_FILE = 'eq.log' -BROWSERLOG_FILE = 'browser.log' -REFDIR = 'ref' -TEST_SNAPSHOTS = 'test_snapshots' -TMPDIR = 'tmp' -VERBOSE = False -BROWSER_TIMEOUT = 120 - -SERVER_HOST = "localhost" - -lock = Lock() - -class TestOptions(OptionParser): - def __init__(self, **kwargs): - OptionParser.__init__(self, **kwargs) - self.add_option("-m", "--masterMode", action="store_true", dest="masterMode", - help="Run the script in master mode.", default=False) - self.add_option("--noPrompts", action="store_true", dest="noPrompts", - help="Uses default answers (intended for CLOUD TESTS only!).", default=False) - self.add_option("--manifestFile", action="store", type="string", dest="manifestFile", - help="A JSON file in the form of test_manifest.json (the default).") - self.add_option("-b", "--browser", action="store", type="string", dest="browser", - help="The path to a single browser (right now, only Firefox is supported).") - self.add_option("--browserManifestFile", action="store", type="string", - dest="browserManifestFile", - help="A JSON file in the form of those found in resources/browser_manifests") - self.add_option("--reftest", action="store_true", dest="reftest", - help="Automatically start reftest showing comparison test failures, if there are any.", - default=False) - self.add_option("--port", action="store", dest="port", type="int", - help="The port the HTTP server should listen on.", default=8080) - self.add_option("--unitTest", action="store_true", dest="unitTest", - help="Run the unit tests.", default=False) - self.add_option("--fontTest", action="store_true", dest="fontTest", - help="Run the font tests.", default=False) - self.add_option("--noDownload", action="store_true", dest="noDownload", - help="Skips test PDFs downloading.", default=False) - self.add_option("--statsFile", action="store", dest="statsFile", type="string", - help="The file where to store stats.", default=None) - self.add_option("--statsDelay", action="store", dest="statsDelay", type="int", - help="The amount of time in milliseconds the browser should wait before starting stats.", default=10000) - self.set_usage(USAGE_EXAMPLE) - - def verifyOptions(self, options): - if options.reftest and (options.unitTest or options.fontTest): - self.error("--reftest and --unitTest/--fontTest must not be specified at the same time.") - if options.masterMode and options.manifestFile: - self.error("--masterMode and --manifestFile must not be specified at the same time.") - if not options.manifestFile: - options.manifestFile = DEFAULT_MANIFEST_FILE - if options.browser and options.browserManifestFile: - print "Warning: ignoring browser argument since manifest file was also supplied" - if not options.browser and not options.browserManifestFile: - print "Starting server on port %s." % options.port - if not options.statsFile: - options.statsDelay = 0 - - return options - - -def prompt(question): - '''Return True iff the user answered "yes" to |question|.''' - inp = raw_input(question +' [yes/no] > ') - return inp == 'yes' - -MIMEs = { - '.css': 'text/css', - '.html': 'text/html', - '.js': 'application/javascript', - '.json': 'application/json', - '.svg': 'image/svg+xml', - '.pdf': 'application/pdf', - '.xhtml': 'application/xhtml+xml', - '.gif': 'image/gif', - '.ico': 'image/x-icon', - '.png': 'image/png', - '.log': 'text/plain', - '.bcmap': 'application/octet-stream', - '.properties': 'text/plain' -} - -class State: - browsers = [ ] - manifest = { } - taskResults = { } - remaining = { } - results = { } - done = False - numErrors = 0 - numEqFailures = 0 - numEqNoSnapshot = 0 - numFBFFailures = 0 - numLoadFailures = 0 - eqLog = None - saveStats = False - stats = [ ] - lastPost = { } - -class UnitTestState: - browsers = [ ] - browsersRunning = 0 - lastPost = { } - numErrors = 0 - numRun = 0 - -class Result: - def __init__(self, snapshot, failure, page): - self.snapshot = snapshot - self.failure = failure - self.page = page - -class TestServer(ThreadingMixIn, HTTPServer): - pass - -class TestHandlerBase(BaseHTTPRequestHandler): - # Disable annoying noise by default - def log_request(code=0, size=0): - if VERBOSE: - BaseHTTPRequestHandler.log_request(code, size) - - def handle_one_request(self): - try: - BaseHTTPRequestHandler.handle_one_request(self) - except socket.error, v: - if v[0] == errno.ECONNRESET: - # Ignoring connection reset by peer exceptions - if VERBOSE: - print 'Detected connection reset' - elif v[0] == errno.EPIPE: - if VERBOSE: - print 'Detected remote peer disconnected' - elif v[0] == 10053: - if VERBOSE: - print 'An established connection was aborted by the' \ - ' software in your host machine' - else: - raise - - def finish(self,*args,**kw): - # From http://stackoverflow.com/a/14355079/1834797 - try: - if not self.wfile.closed: - self.wfile.flush() - self.wfile.close() - except socket.error: - pass - self.rfile.close() - - def sendFile(self, path, ext): - self.send_response(200) - self.send_header("Accept-Ranges", "bytes") - self.send_header("Content-Type", MIMEs[ext]) - self.send_header("Content-Length", os.path.getsize(path)) - self.end_headers() - with open(path, "rb") as f: - self.wfile.write(f.read()) - - def sendFileRange(self, path, ext, start, end): - file_len = os.path.getsize(path) - if (end is None) or (file_len < end): - end = file_len - if (file_len < start) or (end <= start): - self.send_error(416) - return - chunk_len = end - start - time.sleep(chunk_len / 1000000.0) - self.send_response(206) - self.send_header("Accept-Ranges", "bytes") - self.send_header("Content-Type", MIMEs[ext]) - self.send_header("Content-Length", chunk_len) - self.send_header("Content-Range", 'bytes ' + str(start) + '-' + str(end - 1) + '/' + str(file_len)) - self.end_headers() - with open(path, "rb") as f: - f.seek(start) - self.wfile.write(f.read(chunk_len)) - - def do_GET(self): - url = urlparse(self.path) - - # Ignore query string - path, _ = urllib.unquote_plus(url.path), url.query - path = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path)) - prefix = os.path.commonprefix(( path, DOC_ROOT )) - _, ext = os.path.splitext(path.lower()) - - if url.path == "/favicon.ico": - self.sendFile(os.path.join(DOC_ROOT, "test", "resources", "favicon.ico"), ext) - return - - if os.path.isdir(path): - self.sendIndex(url.path, url.query) - return - - pieces = path.split(os.sep); - if pieces[len(pieces) - 2] == 'cmaps': - self.sendFile(path, '.properties'); - return - - if not (prefix == DOC_ROOT - and os.path.isfile(path) - and ext in MIMEs): - print path - self.send_error(404) - return - - if 'Range' in self.headers: - range_re = re.compile(r"^bytes=(\d+)\-(\d+)?") - parsed_range = range_re.search(self.headers.getheader("Range")) - if parsed_range is None: - self.send_error(501) - return - if VERBOSE: - print 'Range requested %s - %s: %s' % ( - parsed_range.group(1), parsed_range.group(2)) - start = int(parsed_range.group(1)) - if parsed_range.group(2) is None: - self.sendFileRange(path, ext, start, None) - else: - end = int(parsed_range.group(2)) + 1 - self.sendFileRange(path, ext, start, end) - return - - self.sendFile(path, ext) - -class UnitTestHandler(TestHandlerBase): - def sendIndex(self, path, query): - print "send index" - - def translateFont(self, base64Data): - self.send_response(200) - self.send_header("Content-Type", "text/xml") - self.end_headers() - - data = base64.b64decode(base64Data) - taskId = str(uuid.uuid4()) - fontPath = 'ttx/' + taskId + '.otf' - resultPath = 'ttx/' + taskId + '.ttx' - with open(fontPath, "wb") as f: - f.write(data) - - # When fontTools used directly, we need to snif ttx file - # to check what version of python is used - ttxPath = '' - for path in os.environ["PATH"].split(os.pathsep): - if os.path.isfile(path + os.sep + "ttx"): - ttxPath = path + os.sep + "ttx" - break - if ttxPath == '': - self.wfile.write("TTX was not found") - return - - ttxRunner = '' - with open(ttxPath, "r") as f: - firstLine = f.readline() - if firstLine[:2] == '#!' and firstLine.find('python') > -1: - ttxRunner = firstLine[2:].strip() - - with open(os.devnull, "w") as fnull: - if ttxRunner != '': - result = subprocess.call([ttxRunner, ttxPath, fontPath], stdout = fnull) - else: - result = subprocess.call([ttxPath, fontPath], stdout = fnull) - - os.remove(fontPath) - - if not os.path.isfile(resultPath): - self.wfile.write("Output was not generated") - return - - with open(resultPath, "rb") as f: - self.wfile.write(f.read()) - - os.remove(resultPath) - - return - - def do_POST(self): - with lock: - url = urlparse(self.path) - numBytes = int(self.headers['Content-Length']) - content = self.rfile.read(numBytes) - - # Process special utility requests - if url.path == '/ttx': - self.translateFont(content) - return - - self.send_response(200) - self.send_header('Content-Type', 'text/plain') - self.end_headers() - - result = json.loads(content) - browser = result['browser'] - UnitTestState.lastPost[browser] = int(time.time()) - if url.path == "/tellMeToQuit": - tellAppToQuit(url.path, url.query) - UnitTestState.browsersRunning -= 1 - UnitTestState.lastPost[browser] = None - return - elif url.path == '/info': - print result['message'] - elif url.path == '/submit_task_results': - status, description = result['status'], result['description'] - UnitTestState.numRun += 1 - if status == 'TEST-UNEXPECTED-FAIL': - UnitTestState.numErrors += 1 - message = status + ' | ' + description + ' | in ' + browser - if 'error' in result: - message += ' | ' + result['error'] - print message - else: - print 'Error: uknown action' + url.path - -class PDFTestHandler(TestHandlerBase): - - def sendIndex(self, path, query): - if not path.endswith("/"): - # we need trailing slash - self.send_response(301) - redirectLocation = path + "/" - if query: - redirectLocation += "?" + query - self.send_header("Location", redirectLocation) - self.end_headers() - return - - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - if query == "frame": - self.wfile.write("" + - "") - return - - location = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path)) - self.wfile.write("

PDFs of " + path + "

\n") - for filename in os.listdir(location): - if filename.lower().endswith('.pdf'): - self.wfile.write("" + - filename + "
\n") - self.wfile.write("") - - - def do_POST(self): - with lock: - numBytes = int(self.headers['Content-Length']) - - self.send_response(200) - self.send_header('Content-Type', 'text/plain') - self.end_headers() - - url = urlparse(self.path) - if url.path == "/tellMeToQuit": - tellAppToQuit(url.path, url.query) - return - - result = json.loads(self.rfile.read(numBytes)) - browser = result['browser'] - State.lastPost[browser] = int(time.time()) - if url.path == "/info": - print result['message'] - return - - id = result['id'] - failure = result['failure'] - round = result['round'] - page = result['page'] - snapshot = result['snapshot'] - - taskResults = State.taskResults[browser][id] - taskResults[round].append(Result(snapshot, failure, page)) - if State.saveStats: - stat = { - 'browser': browser, - 'pdf': id, - 'page': page, - 'round': round, - 'stats': result['stats'] - } - State.stats.append(stat) - - def isTaskDone(): - last_page_num = result['lastPageNum'] - rounds = State.manifest[id]['rounds'] - for round in range(0,rounds): - if not taskResults[round]: - return False - latest_page = taskResults[round][-1] - if not latest_page.page == last_page_num: - return False - return True - - if isTaskDone(): - # sort the results since they sometimes come in out of order - for results in taskResults: - results.sort(key=lambda result: result.page) - check(State.manifest[id], taskResults, browser, - self.server.masterMode) - # Please oh please GC this ... - del State.taskResults[browser][id] - State.remaining[browser] -= 1 - - checkIfDone() - -def checkIfDone(): - State.done = True - for key in State.remaining: - if State.remaining[key] != 0: - State.done = False - return - -# Applescript hack to quit Chrome on Mac -def tellAppToQuit(path, query): - if platform.system() != "Darwin": - return - d = parse_qs(query) - path = d['path'][0] - cmd = """osascript< -1) or path.find(key) > -1: - command = types[key](browser) - command.name = command.name or key - break - - if command is None: - raise Exception("Unrecognized browser: %s" % browser) - - return command - -def makeBrowserCommands(browserManifestFile): - with open(browserManifestFile) as bmf: - browsers = [makeBrowserCommand(browser) for browser in json.load(bmf)] - return browsers - -def downloadLinkedPDF(f): - linkFile = open(f +'.link') - link = linkFile.read() - linkFile.close() - - sys.stdout.write('Downloading '+ link +' to '+ f +' ...') - sys.stdout.flush() - response = urllib2.urlopen(link) - - with open(f, 'wb') as out: - out.write(response.read()) - - print 'done' - -def downloadLinkedPDFs(manifestList): - for item in manifestList: - f, isLink = item['file'], item.get('link', False) - if isLink and not os.access(f, os.R_OK): - try: - downloadLinkedPDF(f) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print 'ERROR: Unable to download file "' + f + '".' - open(f, 'wb').close() - with open(f + '.error', 'w') as out: - out.write('\n'.join(traceback.format_exception(exc_type, - exc_value, - exc_traceback))) - -def verifyPDFs(manifestList): - error = False - for item in manifestList: - f = item['file'] - if os.path.isfile(f + '.error'): - print 'WARNING: File was not downloaded. See "' + f + '.error" file.' - error = True - elif os.access(f, os.R_OK): - fileMd5 = hashlib.md5(open(f, 'rb').read()).hexdigest() - if 'md5' not in item: - print 'WARNING: Missing md5 for file "' + f + '".', - print 'Hash for current file is "' + fileMd5 + '"' - error = True - continue - md5 = item['md5'] - if fileMd5 != md5: - print 'WARNING: MD5 of file "' + f + '" does not match file.', - print 'Expected "' + md5 + '" computed "' + fileMd5 + '"' - error = True - continue - else: - print 'WARNING: Unable to open file for reading "' + f + '".' - error = True - return not error - -def getTestBrowsers(options): - testBrowsers = [] - if options.browserManifestFile: - testBrowsers = makeBrowserCommands(options.browserManifestFile) - elif options.browser: - testBrowsers = [makeBrowserCommand({"path":options.browser, "name":None})] - - if options.browserManifestFile or options.browser: - assert len(testBrowsers) > 0 - return testBrowsers - -def setUp(options): - # Only serve files from a pdf.js clone - assert not GIT_CLONE_CHECK or os.path.isfile('../src/pdf.js') and os.path.isdir('../.git') - - if options.masterMode and os.path.isdir(TMPDIR): - print 'Temporary snapshot dir tmp/ is still around.' - print 'tmp/ can be removed if it has nothing you need.' - if options.noPrompts or prompt('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY'): - subprocess.call(( 'rm', '-rf', 'tmp' )) - - assert not os.path.isdir(TMPDIR) - - testBrowsers = getTestBrowsers(options) - - with open(options.manifestFile) as mf: - manifestList = json.load(mf) - - if not options.noDownload: - downloadLinkedPDFs(manifestList) - - if not verifyPDFs(manifestList): - print 'Unable to verify the checksum for the files that are used for testing.' - print 'Please re-download the files, or adjust the MD5 checksum in the manifest for the files listed above.\n' - - for b in testBrowsers: - State.taskResults[b.name] = { } - State.remaining[b.name] = len(manifestList) - State.lastPost[b.name] = int(time.time()) - for item in manifestList: - id, rounds = item['id'], int(item['rounds']) - State.manifest[id] = item - taskResults = [ ] - for r in xrange(rounds): - taskResults.append([ ]) - State.taskResults[b.name][id] = taskResults - - if options.statsFile != None: - State.saveStats = True - return testBrowsers - -def setUpUnitTests(options): - # Only serve files from a pdf.js clone - assert not GIT_CLONE_CHECK or os.path.isfile('../src/pdf.js') and os.path.isdir('../.git') - - testBrowsers = getTestBrowsers(options) - - UnitTestState.browsersRunning = len(testBrowsers) - for b in testBrowsers: - UnitTestState.lastPost[b.name] = int(time.time()) - return testBrowsers - -def startBrowsers(browsers, options, path): - for b in browsers: - b.setup() - print 'Launching', b.name - host = 'http://%s:%s' % (SERVER_HOST, options.port) - qs = '?browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile) - qs += '&path=' + b.path - qs += '&delay=' + str(options.statsDelay) - qs += '&masterMode=' + str(options.masterMode) - b.start(host + path + qs) - -def teardownBrowsers(browsers): - for b in browsers: - try: - b.teardown() - except: - print "Error cleaning up after browser at ", b.path - print "Temp dir was ", b.tempDir - print "Error:", sys.exc_info()[0] - -def check(task, results, browser, masterMode): - failed = False - for r in xrange(len(results)): - pageResults = results[r] - for p in xrange(len(pageResults)): - pageResult = pageResults[p] - if pageResult is None: - continue - failure = pageResult.failure - if failure: - failed = True - if os.path.isfile(task['file'] + '.error'): - print 'TEST-SKIPPED | PDF was not downloaded', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure - else: - State.numErrors += 1 - print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure - - if failed: - return - - kind = task['type'] - if 'eq' == kind or 'text' == kind: - checkEq(task, results, browser, masterMode) - elif 'fbf' == kind: - checkFBF(task, results, browser) - elif 'load' == kind: - checkLoad(task, results, browser) - else: - assert 0 and 'Unknown test type' - -def createDir(dir): - try: - os.makedirs(dir) - except OSError, e: - if e.errno != 17: # file exists - print >>sys.stderr, 'Creating', dir, 'failed!' - - -def readDataUri(data): - metadata, encoded = data.rsplit(",", 1) - return base64.b64decode(encoded) - -def checkEq(task, results, browser, masterMode): - pfx = os.path.join(REFDIR, sys.platform, browser, task['id']) - testSnapshotDir = os.path.join(TEST_SNAPSHOTS, sys.platform, browser, task['id']) - results = results[0] - taskId = task['id'] - taskType = task['type'] - - passed = True - for result in results: - page = result.page - snapshot = readDataUri(result.snapshot) - ref = None - eq = True - - path = os.path.join(pfx, str(page) + '.png') - if not os.access(path, os.R_OK): - State.numEqNoSnapshot += 1 - if not masterMode: - print 'WARNING: no reference snapshot', path - else: - f = open(path, 'rb') - ref = f.read() - f.close() - - eq = (ref == snapshot) - if not eq: - print 'TEST-UNEXPECTED-FAIL |', taskType, taskId, '| in', browser, '| rendering of page', page, '!= reference rendering' - - if not State.eqLog: - State.eqLog = open(EQLOG_FILE, 'w') - eqLog = State.eqLog - - createDir(testSnapshotDir) - testSnapshotPath = os.path.join(testSnapshotDir, str(page) + '.png') - handle = open(testSnapshotPath, 'wb') - handle.write(snapshot) - handle.close() - - refSnapshotPath = os.path.join(testSnapshotDir, str(page) + '_ref.png') - handle = open(refSnapshotPath, 'wb') - handle.write(ref) - handle.close() - - # NB: this follows the format of Mozilla reftest - # output so that we can reuse its reftest-analyzer - # script - eqLog.write('REFTEST TEST-UNEXPECTED-FAIL | ' + browser +'-'+ taskId +'-page'+ str(page) + ' | image comparison (==)\n') - eqLog.write('REFTEST IMAGE 1 (TEST): ' + testSnapshotPath + '\n') - eqLog.write('REFTEST IMAGE 2 (REFERENCE): ' + refSnapshotPath + '\n') - - passed = False - State.numEqFailures += 1 - - if masterMode and (ref is None or not eq): - tmpTaskDir = os.path.join(TMPDIR, sys.platform, browser, task['id']) - createDir(tmpTaskDir) - - handle = open(os.path.join(tmpTaskDir, str(page)) + '.png', 'wb') - handle.write(snapshot) - handle.close() - - if passed: - print 'TEST-PASS |', taskType, 'test', task['id'], '| in', browser - -def checkFBF(task, results, browser): - round0, round1 = results[0], results[1] - assert len(round0) == len(round1) - - passed = True - for page in xrange(len(round1)): - r0Page, r1Page = round0[page], round1[page] - if r0Page is None: - break - if r0Page.snapshot != r1Page.snapshot: - print 'TEST-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second' - passed = False - State.numFBFFailures += 1 - if passed: - print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser - - -def checkLoad(task, results, browser): - # Load just checks for absence of failure, so if we got here the - # test has passed - print 'TEST-PASS | load test', task['id'], '| in', browser - - -def processResults(options): - print '' - numFatalFailures = (State.numErrors + State.numFBFFailures) - if 0 == State.numEqFailures and 0 == numFatalFailures: - print 'All regression tests passed.' - else: - print 'OHNOES! Some tests failed!' - if 0 < State.numErrors: - print ' errors:', State.numErrors - if 0 < State.numEqFailures: - print ' different ref/snapshot:', State.numEqFailures - if 0 < State.numFBFFailures: - print ' different first/second rendering:', State.numFBFFailures - if options.statsFile != None: - with open(options.statsFile, 'w') as sf: - sf.write(json.dumps(State.stats, sort_keys=True, indent=4)) - print 'Wrote stats file: ' + options.statsFile - - -def maybeUpdateRefImages(options, browser): - if options.masterMode and (0 < State.numEqFailures or 0 < State.numEqNoSnapshot): - print "Some eq tests failed or didn't have snapshots." - print 'Checking to see if master references can be updated...' - numFatalFailures = (State.numErrors + State.numFBFFailures) - if 0 < numFatalFailures: - print ' No. Some non-eq tests failed.' - else: - print ' Yes! The references in tmp/ can be synced with ref/.' - if options.reftest: - startReftest(browser, options) - if options.noPrompts or prompt('Would you like to update the master copy in ref/?'): - sys.stdout.write(' Updating ref/ ... ') - - if not os.path.exists('ref'): - subprocess.check_call('mkdir ref', shell = True) - subprocess.check_call('cp -Rf tmp/* ref/', shell = True) - - print 'done' - else: - print ' OK, not updating.' - -def startReftest(browser, options): - url = "http://%s:%s" % (SERVER_HOST, options.port) - url += "/test/resources/reftest-analyzer.html" - url += "#web=/test/eq.log" - try: - browser.setup() - browser.start(url) - print "Waiting for browser..." - browser.process.wait() - finally: - teardownBrowsers([browser]) - print "Completed reftest usage." - -def runTests(options, browsers): - try: - shutil.rmtree(TEST_SNAPSHOTS); - except OSError, e: - if e.errno != 2: # folder doesn't exist - print >>sys.stderr, 'Deleting', dir, 'failed!' - t1 = time.time() - try: - startBrowsers(browsers, options, '/test/test_slave.html') - while not State.done: - for b in State.lastPost: - if State.remaining[b] > 0 and int(time.time()) - State.lastPost[b] > BROWSER_TIMEOUT: - print 'TEST-UNEXPECTED-FAIL | test failed', b, "has not responded in", BROWSER_TIMEOUT, "s" - State.numErrors += State.remaining[b] - State.remaining[b] = 0 - checkIfDone() - time.sleep(1) - processResults(options) - finally: - teardownBrowsers(browsers) - t2 = time.time() - print "Runtime was", int(t2 - t1), "seconds" - if State.eqLog: - State.eqLog.close(); - if options.masterMode: - maybeUpdateRefImages(options, browsers[0]) - elif options.reftest and State.numEqFailures > 0: - print "\nStarting reftest harness to examine %d eq test failures." % State.numEqFailures - startReftest(browsers[0], options) - -def runUnitTests(options, browsers, url, name): - t1 = time.time() - try: - startBrowsers(browsers, options, url) - while UnitTestState.browsersRunning > 0: - for b in UnitTestState.lastPost: - if UnitTestState.lastPost[b] != None and int(time.time()) - UnitTestState.lastPost[b] > BROWSER_TIMEOUT: - print 'TEST-UNEXPECTED-FAIL | test failed', b, "has not responded in", BROWSER_TIMEOUT, "s" - UnitTestState.lastPost[b] = None - UnitTestState.browsersRunning -= 1 - UnitTestState.numErrors += 1 - time.sleep(1) - print '' - print 'Ran', UnitTestState.numRun, 'tests' - if UnitTestState.numErrors > 0: - print 'OHNOES! Some', name, 'tests failed!' - print ' ', UnitTestState.numErrors, 'of', UnitTestState.numRun, 'failed' - else: - print 'All', name, 'tests passed.' - finally: - teardownBrowsers(browsers) - t2 = time.time() - print '', name, 'tests runtime was', int(t2 - t1), 'seconds' - -def main(): - optionParser = TestOptions() - options, args = optionParser.parse_args() - options = optionParser.verifyOptions(options) - if options == None: - sys.exit(1) - - if options.unitTest or options.fontTest: - httpd = TestServer((SERVER_HOST, options.port), UnitTestHandler) - httpd_thread = threading.Thread(target=httpd.serve_forever) - httpd_thread.setDaemon(True) - httpd_thread.start() - - browsers = setUpUnitTests(options) - if len(browsers) > 0: - if options.unitTest: - runUnitTests(options, browsers, '/test/unit/unit_test.html', 'unit') - if options.fontTest: - runUnitTests(options, browsers, '/test/font/font_test.html', 'font') - else: - httpd = TestServer((SERVER_HOST, options.port), PDFTestHandler) - httpd.masterMode = options.masterMode - httpd_thread = threading.Thread(target=httpd.serve_forever) - httpd_thread.setDaemon(True) - httpd_thread.start() - - browsers = setUp(options) - if len(browsers) > 0: - runTests(options, browsers) - else: - # just run the server - print "Running HTTP server. Press Ctrl-C to quit." - try: - while True: - time.sleep(1) - except (KeyboardInterrupt): - print "\nExiting." - -if __name__ == '__main__': - main() diff --git a/test/testutils.js b/test/testutils.js new file mode 100644 index 000000000..9a9d1bb9a --- /dev/null +++ b/test/testutils.js @@ -0,0 +1,146 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +exports.removeDirSync = function removeDirSync(dir) { + var files = fs.readdirSync(dir); + files.forEach(function (filename) { + var file = path.join(dir, filename); + var stats = fs.statSync(file); + if (stats.isDirectory()) { + removeDirSync(file); + } else { + fs.unlinkSync(file); + } + }); + fs.rmdirSync(dir); +}; + +exports.copySubtreeSync = function copySubtreeSync(src, dest) { + var files = fs.readdirSync(src); + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + files.forEach(function (filename) { + var srcFile = path.join(src, filename); + var file = path.join(dest, filename); + var stats = fs.statSync(srcFile); + if (stats.isDirectory()) { + copySubtreeSync(srcFile, file); + } else { + fs.writeFileSync(file, fs.readFileSync(srcFile)); + } + }); +}; + +exports.ensureDirSync = function ensureDirSync(dir) { + if (fs.existsSync(dir)) { + return; + } + var parts = dir.split(path.sep), i = parts.length; + while (i > 1 && !fs.existsSync(parts.slice(0, i - 1).join(path.sep))) { + i--; + } + if (i < 0 || (i === 0 && parts[0])) { + throw new Error(); + } + + while (i <= parts.length) { + fs.mkdirSync(parts.slice(0, i).join(path.sep)); + i++; + } +}; + +var stdinBuffer = '', endOfStdin = false, stdinInitialized = false; +var stdinOnLineCallbacks = []; + +function handleStdinBuffer() { + if (endOfStdin) { + if (stdinBuffer && stdinOnLineCallbacks.length > 0) { + var callback = stdinOnLineCallbacks.shift(); + callback(stdinBuffer); + stdinBuffer = null; + } + while (stdinOnLineCallbacks.length > 0) { + var callback = stdinOnLineCallbacks.shift(); + callback(); + } + return; + } + while (stdinOnLineCallbacks.length > 0) { + var i = stdinBuffer.indexOf('\n'); + if (i < 0) { + return; + } + var callback = stdinOnLineCallbacks.shift(); + var result = stdinBuffer.substring(0, i + 1); + stdinBuffer = stdinBuffer.substring(i + 1); + callback(result); + } + // all callbacks handled, stop stdin processing + process.stdin.pause(); +} + +function initStdin() { + process.stdin.setEncoding('utf8'); + + process.stdin.on('data', function(chunk) { + stdinBuffer += chunk; + handleStdinBuffer(); + }); + + process.stdin.on('end', function() { + endOfStdin = true; + handleStdinBuffer(); + }); +} + +exports.prompt = function prompt(message, callback) { + if (!stdinInitialized) { + process.stdin.resume(); + initStdin(); + stdinInitialized = true; + } else if (stdinOnLineCallbacks.length === 0) { + process.stdin.resume(); + } + + process.stdout.write(message); + stdinOnLineCallbacks.push(callback); + handleStdinBuffer(); +}; + +exports.confirm = function confirm(message, callback) { + exports.prompt(message, function (answer) { + if (answer === undefined) { + callback(); + return; + } + if (answer[0].toLowerCase() === 'y') { + callback(true); + } else if (answer[0].toLowerCase() === 'n') { + callback(false); + } else { + confirm(message, callback); + } + }); +}; \ No newline at end of file diff --git a/test/ttx/README.md b/test/ttx/README.md index 05a5bad61..655e7c332 100644 --- a/test/ttx/README.md +++ b/test/ttx/README.md @@ -1,19 +1,3 @@ -This folder is a place for temporary files generated by ttx +If `git clone --recursive` was not used, please run `git submodile init; git submodule update` to pull fonttools code. -# About TTX Installation - -The numpy module is required -- use "easy_install numpy" to install it. - -Download and extract fonttools from http://sourceforge.net/projects/fonttools/ in any folder on your computer. - -From the font tools directory run "python setup.py install" from the command line. - -# TTX for Mac Change - -On Mac OSX, if you are getting error message related to "/Library/Python/2.7/site-packages/FontTools/fontTools/ttLib/macUtils.py", line 18, in MyOpenResFile, use the following patch to change the fonttools - -https://github.com/mcolyer/fonttools/commit/e732bd3ba63c51df9aed903eb2147fa1af1bfdc2 - -# TTX for Windows Change - -On Windows, if ttx generate an exception, it waits for a key to be pressed. Pleaase change "/c/mozilla-build/python/Lib/site-packages/Font-Tools/fontTools/ttx.py" file: replace the waitForKeyPress function body with just 'return'. +Note: python 2.6 for 32-bit is required to run ttx. \ No newline at end of file diff --git a/test/ttx/fonttools-code b/test/ttx/fonttools-code new file mode 160000 index 000000000..48ea31215 --- /dev/null +++ b/test/ttx/fonttools-code @@ -0,0 +1 @@ +Subproject commit 48ea31215c9886ab2a1c6bfe81c4a6cf308f275b diff --git a/test/webbrowser.js b/test/webbrowser.js new file mode 100644 index 000000000..ea7fff80b --- /dev/null +++ b/test/webbrowser.js @@ -0,0 +1,147 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var os = require('os'); +var fs = require('fs'); +var path = require('path'); +var spawn = require('child_process').spawn; +var testUtils = require('./testutils.js'); + +var tempDirPrefix = 'pdfjs_'; + +function WebBrowser(name, path) { + this.name = name; + this.path = path; + this.tmpDir = null; + this.profileDir = null; + this.process = null; + this.finished = false; + this.callback = null; +} +WebBrowser.prototype = { + start: function (url) { + this.tmpDir = path.join(os.tmpdir(), tempDirPrefix + this.name); + if (!fs.existsSync(this.tmpDir)) { + fs.mkdirSync(this.tmpDir); + } + this.process = this.startProcess(url); + }, + getProfileDir: function () { + if (!this.profileDir) { + var profileDir = path.join(this.tmpDir, 'profile'); + if (fs.existsSync(profileDir)) { + testUtils.removeDirSync(profileDir); + } + fs.mkdirSync(profileDir); + this.profileDir = profileDir; + this.setupProfileDir(profileDir); + } + return this.profileDir; + }, + buildArguments: function (url) { + return [url]; + }, + setupProfileDir: function (dir) { + }, + startProcess: function (url) { + var args = this.buildArguments(url); + var proc = spawn(this.path, args); + proc.on('close', function (code) { + this.finished = true; + if (this.callback) { + this.callback.call(null, code); + } + this.cleanup(); + }.bind(this)); + return proc; + }, + cleanup: function () { + testUtils.removeDirSync(this.tmpDir); + }, + stop: function (callback) { + if (this.finished) { + if (callback) { + callback(); + } + } else { + this.callback = callback; + } + + this.process.kill(); + this.process = null; + } +}; + +var firefoxResourceDir = path.join(__dirname, 'resources', 'firefox'); + +function FirefoxBrowser(name, path) { + if (os.platform() === 'darwin') { + var m = /([^.\/]+)\.app(\/?)$/.exec(path); + if (m) { + path += (m[2] ? '' : '/') + 'Contents/MacOS/firefox'; + } + } + WebBrowser.call(this, name, path); +} +FirefoxBrowser.prototype = Object.create(WebBrowser.prototype); +FirefoxBrowser.prototype.buildArguments = function (url) { + var profileDir = this.getProfileDir(); + var args = []; + if (os.platform() === 'darwin') { + args.push('-foreground'); + } + args.push('-no-remote', '-profile', profileDir, url); + return args; +}; +FirefoxBrowser.prototype.setupProfileDir = function (dir) { + testUtils.copySubtreeSync(firefoxResourceDir, dir); +}; + +function ChromiumBrowser(name, path) { + if (os.platform() === 'darwin') { + var m = /([^.\/]+)\.app(\/?)$/.exec(path); + if (m) { + path += (m[2] ? '' : '/') + 'Contents/MacOS/' + m[1]; + console.log(path); + } + } + WebBrowser.call(this, name, path); +} +ChromiumBrowser.prototype = Object.create(WebBrowser.prototype); +ChromiumBrowser.prototype.buildArguments = function (url) { + var profileDir = this.getProfileDir(); + return ['--user-data-dir=' + profileDir, + '--no-first-run', '--disable-sync', url]; +}; + +WebBrowser.create = function (desc) { + var name = desc.name; + if (/firefox/i.test(name)) { + return new FirefoxBrowser(desc.name, desc.path); + } + if (/(chrome|chromium)/i.test(name)) { + return new ChromiumBrowser(desc.name, desc.path); + } + return new WebBrowser(desc.name, desc.path); +}; + + +exports.WebBrowser = WebBrowser; \ No newline at end of file diff --git a/test/webserver.js b/test/webserver.js new file mode 100644 index 000000000..b854619ee --- /dev/null +++ b/test/webserver.js @@ -0,0 +1,256 @@ +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* + * Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jslint node: true */ + +'use strict'; + +var http = require('http'); +var path = require('path'); +var fs = require('fs'); + +var mimeTypes = { + '.css': 'text/css', + '.html': 'text/html', + '.js': 'application/javascript', + '.json': 'application/json', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.xhtml': 'application/xhtml+xml', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.png': 'image/png', + '.log': 'text/plain', + '.bcmap': 'application/octet-stream', + '.properties': 'text/plain' +}; + +var defaultMimeType = 'application/octet-stream'; + +function WebServer() { + this.root = '.'; + this.host = 'localhost'; + this.port = 8000; + this.server = null; + this.verbose = false; + this.hooks = { + 'GET': [], + 'POST': [] + }; +} +WebServer.prototype = { + start: function (callback) { + this.server = http.createServer(this._handler.bind(this)); + this.server.listen(this.port, this.host, callback); + console.log( + 'Server running at http://' + this.host + ':' + this.port + '/'); + }, + stop: function (callback) { + this.server.close(callback); + this.server = null; + }, + _handler: function (req, res) { + var agent = req.headers['user-agent']; + var url = req.url; + var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url); + var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3]; + var verbose = this.verbose; + + var methodHooks = this.hooks[req.method]; + if (!methodHooks) { + res.writeHead(405); + res.end('Unsupported request method', 'utf8'); + return; + } + var handled = methodHooks.some(function (hook) { + return hook(req, res); + }); + if (handled) { + return; + } + + if (pathPart === '/favicon.ico') { + fs.realpath(path.join(this.root, 'test/resources/favicon.ico'), + checkFile); + return; + } + + // disables range requests for chrome windows -- locks during testing + var disableRangeRequests = /Windows.*?Chrom/i.test(agent); + + var filePath; + fs.realpath(path.join(this.root, pathPart), checkFile); + + function checkFile(err, file) { + if (err) { + res.writeHead(404); + res.end(); + if (verbose) { + console.error(url + ': not found'); + } + return; + } + filePath = file; + fs.stat(filePath, statFile); + } + + var fileSize; + + function statFile(err, stats) { + if (err) { + res.writeHead(500); + res.end(); + return; + } + + fileSize = stats.size; + var isDir = stats.isDirectory(); + if (isDir && !/\/$/.test(pathPart)) { + res.setHeader('Location', pathPart + '/' + urlParts[2]); + res.writeHead(301); + res.end('Redirected', 'utf8'); + return; + } + if (isDir) { + serveDirectoryIndex(filePath); + return; + } + + var range = req.headers['range']; + if (range && !disableRangeRequests) { + var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range); + if (!rangesMatches) { + res.writeHead(501); + res.end('Bad range', 'utf8'); + if (verbose) { + console.error(url + ': bad range: "' + range + '"'); + } + return; + } + var start = +rangesMatches[1]; + var end = +rangesMatches[2]; + if (verbose) { + console.log(url + ': range ' + start + ' - ' + end); + } + serveRequestedFileRange(filePath, + start, + isNaN(end) ? fileSize : (end + 1)); + return; + } + if (verbose) { + console.log(url); + } + serveRequestedFile(filePath); + } + + function serveDirectoryIndex(dir) { + res.setHeader('Content-Type', 'text/html'); + res.writeHead(200); + + var content = ''; + if (queryPart === 'frame') { + res.end('' + + '', 'utf8'); + return; + } + var all = queryPart === 'all'; + fs.readdir(dir, function (err, files) { + if (err) { + res.end(); + return; + } + res.write('

PDFs of ' + pathPart + '

\n'); + if (pathPart !== '/') { + res.write('..
\n'); + } + files.forEach(function (file) { + var stat = fs.statSync(path.join(dir, file)); + var item = pathPart + file; + if (stat.isDirectory()) { + res.write('' + + file + '
\n'); + return; + } + var ext = path.extname(file).toLowerCase(); + if (ext === '.pdf') { + res.write('' + + file + '
\n'); + } else if (all) { + res.write('' + + file + '
\n'); + } + }); + if (files.length === 0) { + res.write('

no files found

\n'); + } + if (!all && queryPart !== 'side') { + res.write('

(only PDF files are shown, ' + + 'show all)

\n'); + } + res.end(''); + }); + } + + function serveRequestedFile(filePath) { + var stream = fs.createReadStream(filePath, {flags: 'rs'}); + + stream.on('error', function (error) { + res.writeHead(500); + res.end(); + }); + + var ext = path.extname(filePath).toLowerCase(); + var contentType = mimeTypes[ext] || defaultMimeType; + + if (!disableRangeRequests) { + res.setHeader('Accept-Ranges', 'bytes'); + } + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Length', fileSize); + res.writeHead(200); + + stream.pipe(res); + } + + function serveRequestedFileRange(filePath, start, end) { + var stream = fs.createReadStream(filePath, { + flags: 'rs', start: start, end: end - 1}); + + stream.on('error', function (error) { + res.writeHead(500); + res.end(); + }); + + var ext = path.extname(filePath).toLowerCase(); + var contentType = mimeTypes[ext] || defaultMimeType; + + res.setHeader('Accept-Ranges', 'bytes'); + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Length', (end - start)); + res.setHeader('Content-Range', + 'bytes ' + start + '-' + (end - 1) + '/' + fileSize); + res.writeHead(206); + + stream.pipe(res); + } + + } +}; + +exports.WebServer = WebServer;