13 changed files with 1579 additions and 1024 deletions
@ -0,0 +1,3 @@ |
|||||||
|
[submodule "test/ttx/fonttools-code"] |
||||||
|
path = test/ttx/fonttools-code |
||||||
|
url = git://git.code.sf.net/p/fonttools/code |
@ -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; |
@ -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); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
@ -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(); |
@ -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() |
|
@ -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); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
@ -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 |
Note: python 2.6 for 32-bit is required to run ttx. |
||||||
|
|
||||||
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'. |
|
@ -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; |
@ -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; |
Loading…
Reference in new issue