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 da7624ef2..7be7e0c59 100644 --- a/make.js +++ b/make.js @@ -1267,9 +1267,7 @@ target.lint = function() { 'external/crlfchecker/', 'src/', 'web/', - 'test/driver.js', - 'test/reporter.js', - 'test/webserver.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..081896929 --- /dev/null +++ b/test/downloadutils.js @@ -0,0 +1,149 @@ +/* -*- 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) { + var completed = false; + var protocol = /^https:\/\//.test(url) ? https : http; + protocol.get(url, function (response) { + 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) { + 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/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/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