Merge pull request #4515 from yurydelendik/nodetest

Replaces test.py with test.js
This commit is contained in:
Brendan Dahl 2014-03-27 13:44:12 -07:00
commit 19a548ef0d
13 changed files with 1577 additions and 1022 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "test/ttx/fonttools-code"]
path = test/ttx/fonttools-code
url = git://git.code.sf.net/p/fonttools/code

18
make.js
View File

@ -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/'

View File

@ -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"

173
test/downloadutils.js Normal file
View File

@ -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;

View File

@ -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++;

86
test/font/ttxdriver.js Normal file
View File

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

723
test/test.js Normal file
View File

@ -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 ? '<error>' + err + '</error>' : 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();

View File

@ -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("<error>TTX was not found</error>")
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("<error>Output was not generated</error>")
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("<html><frameset cols=*,200><frame name=pdf>" +
"<frame src='" + path + "'></frameset></html>")
return
location = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path))
self.wfile.write("<html><body><h1>PDFs of " + path + "</h1>\n")
for filename in os.listdir(location):
if filename.lower().endswith('.pdf'):
self.wfile.write("<a href='/web/viewer.html?file=" +
urllib.quote_plus(path + filename, '/') + "' target=pdf>" +
filename + "</a><br>\n")
self.wfile.write("</body></html>")
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<<END
tell application "%s"
quit
end tell
END""" % path
os.system(cmd)
class BaseBrowserCommand(object):
def __init__(self, browserRecord):
self.name = browserRecord["name"]
self.path = browserRecord["path"]
self.tempDir = None
self.process = None
if platform.system() == "Darwin" and (self.path.endswith(".app") or self.path.endswith(".app/")):
self._fixupMacPath()
if not os.path.exists(self.path):
raise Exception("Path to browser '%s' does not exist." % self.path)
def setup(self):
self.tempDir = tempfile.mkdtemp()
self.profileDir = os.path.join(self.tempDir, "profile")
self.browserLog = open(BROWSERLOG_FILE, "w")
def teardown(self):
self.process.terminate()
# If the browser is still running, wait up to ten seconds for it to quit
if self.process and self.process.poll() is None:
checks = 0
while self.process.poll() is None and checks < 20:
checks += 1
time.sleep(.5)
# If it's still not dead, try to kill it
if self.process.poll() is None:
print "Process %s is still running. Killing." % self.name
self.process.kill()
self.process.wait()
if self.tempDir is not None and os.path.exists(self.tempDir):
shutil.rmtree(self.tempDir)
self.browserLog.close()
def start(self, url):
raise Exception("Can't start BaseBrowserCommand")
class FirefoxBrowserCommand(BaseBrowserCommand):
def _fixupMacPath(self):
self.path = os.path.join(self.path, "Contents", "MacOS", "firefox-bin")
def setup(self):
super(FirefoxBrowserCommand, self).setup()
shutil.copytree(os.path.join(DOC_ROOT, "test", "resources", "firefox"),
self.profileDir)
def start(self, url):
cmds = [self.path]
if platform.system() == "Darwin":
cmds.append("-foreground")
cmds.extend(["-no-remote", "-profile", self.profileDir, url])
self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog)
class ChromeBrowserCommand(BaseBrowserCommand):
def _fixupMacPath(self):
self.path = os.path.join(self.path, "Contents", "MacOS", "Google Chrome")
def start(self, url):
cmds = [self.path]
cmds.extend(["--user-data-dir=%s" % self.profileDir,
"--no-first-run", "--disable-sync", url])
self.process = subprocess.Popen(cmds, stdout = self.browserLog, stderr = self.browserLog)
def makeBrowserCommand(browser):
path = browser["path"].lower()
name = browser["name"]
if name is not None:
name = name.lower()
types = {"firefox": FirefoxBrowserCommand,
"chrome": ChromeBrowserCommand }
command = None
for key in types.keys():
if (name and name.find(key) > -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()

146
test/testutils.js Normal file
View File

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

View File

@ -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.

@ -0,0 +1 @@
Subproject commit 48ea31215c9886ab2a1c6bfe81c4a6cf308f275b

147
test/webbrowser.js Normal file
View File

@ -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;

256
test/webserver.js Normal file
View File

@ -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('<html><frameset cols=*,200><frame name=pdf>' +
'<frame src=\"' + encodeURI(pathPart) +
'?side\"></frameset></html>', 'utf8');
return;
}
var all = queryPart === 'all';
fs.readdir(dir, function (err, files) {
if (err) {
res.end();
return;
}
res.write('<html><body><h1>PDFs of ' + pathPart + '</h1>\n');
if (pathPart !== '/') {
res.write('<a href=\"..\">..</a><br>\n');
}
files.forEach(function (file) {
var stat = fs.statSync(path.join(dir, file));
var item = pathPart + file;
if (stat.isDirectory()) {
res.write('<a href=\"' + encodeURI(item) + '\">' +
file + '</a><br>\n');
return;
}
var ext = path.extname(file).toLowerCase();
if (ext === '.pdf') {
res.write('<a href=\"/web/viewer.html?file=' +
encodeURI(item) + '\" target=pdf>' +
file + '</a><br>\n');
} else if (all) {
res.write('<a href=\"' + encodeURI(item) + '\">' +
file + '</a><br>\n');
}
});
if (files.length === 0) {
res.write('<p>no files found</p>\n');
}
if (!all && queryPart !== 'side') {
res.write('<hr><p>(only PDF files are shown, ' +
'<a href=\"?all\">show all</a>)</p>\n');
}
res.end('</body></html>');
});
}
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;