diff --git a/make.js b/make.js index 52e405199..0cc6e5032 100644 --- a/make.js +++ b/make.js @@ -664,7 +664,9 @@ target.test = function() { // target.bottest = function() { target.unittest({}, function() { - target.browsertest({noreftest: true}); + target.fonttest({}, function() { + target.browsertest({noreftest: true}); + }); }); }; @@ -715,6 +717,28 @@ target.unittest = function(options, callback) { PDF_BROWSERS, {async: true}, callback); }; +// +// make fonttest +// +target.fonttest = function(options, callback) { + cd(ROOT_DIR); + echo(); + echo('### Running font tests'); + + var PDF_BROWSERS = env['PDF_BROWSERS'] || + 'resources/browser_manifests/browser_manifest.json'; + + if (!test('-f', 'test/' + PDF_BROWSERS)) { + echo('Browser manifest file test/' + PDF_BROWSERS + ' does not exist.'); + echo('Copy one of the examples in test/resources/browser_manifests/'); + exit(1); + } + callback = callback || function() {}; + cd('test'); + exec(PYTHON_BIN + ' -u test.py --fontTest --browserManifestFile=' + + PDF_BROWSERS, {async: true}, callback); +}; + // // make botmakeref // diff --git a/test/font/font_core_spec.js b/test/font/font_core_spec.js new file mode 100644 index 000000000..6af436dee --- /dev/null +++ b/test/font/font_core_spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('font1', function() { + var font1_1 = decodeFontData('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'); + describe('test harness testing', function() { + it('returns output', function() { + var output; + waitsFor(function() { return output; }, 10000); + ttx(font1_1, function(result) { output = result; }); + runs(function() { + verifyTtxOutput(output); + expect(//.test(output)).toEqual(true); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/font/font_test.html b/test/font/font_test.html new file mode 100644 index 000000000..8470cfedf --- /dev/null +++ b/test/font/font_test.html @@ -0,0 +1,91 @@ + + + + pdf.js unit test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/font/fontutils.js b/test/font/fontutils.js new file mode 100644 index 000000000..f8cd97cef --- /dev/null +++ b/test/font/fontutils.js @@ -0,0 +1,67 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +var base64alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + +function decodeFontData(base64) { + var result = []; + + var bits = 0, bitsLength = 0; + for (var i = 0, ii = base64.length; i < ii; i++) { + var ch = base64[i]; + if (ch <= " ") continue; + var index = base64alphabet.indexOf(ch); + if (index < 0) throw "Invalid character"; + if (index >= 64) break; + bits = (bits << 6) | index; + bitsLength += 6; + if (bitsLength >= 8) { + bitsLength -= 8 + var code = (bits >> bitsLength) & 0xFF; + result.push(code); + } + } + return new Uint8Array(result); +} + +function encodeFontData(data) { + var buffer = ''; + var i, n; + for (i = 0, n = data.length; i < n; i += 3) { + var b1 = data[i] & 0xFF; + var b2 = data[i + 1] & 0xFF; + var b3 = data[i + 2] & 0xFF; + var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); + var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; + var d4 = i + 2 < n ? (b3 & 0x3F) : 64; + buffer += (base64alphabet.charAt(d1) + base64alphabet.charAt(d2) + + base64alphabet.charAt(d3) + base64alphabet.charAt(d4)); + } + return buffer; +} + +function ttx(data, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/ttx'); + + var encodedData = encodeFontData(data); + xhr.setRequestHeader("Content-type", "text/plain"); + xhr.setRequestHeader("Content-length", encodedData.length); + + xhr.onreadystatechange = function getPdfOnreadystatechange(e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + callback(xhr.responseText); + } else { + callback('Transport error: ' + xhr.statusText + ''); + } + } + }; + xhr.send(encodedData); +} + +function verifyTtxOutput(output) { + var m = /^(.*?)<\/error>/.exec(output); + if (m) + throw m[1]; +} diff --git a/test/test.py b/test/test.py index 030bf3aec..4911ec015 100644 --- a/test/test.py +++ b/test/test.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, platform, os, shutil, sys, subprocess, tempfile, threading, time, urllib, urllib2, hashlib +import json, platform, os, shutil, sys, subprocess, tempfile, threading +import time, urllib, urllib2, hashlib, re, base64, uuid, socket, errno from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import SocketServer from optparse import OptionParser @@ -55,6 +56,8 @@ class TestOptions(OptionParser): 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("--ignoreDownloadErrors", action="store_true", dest="ignoreDownloadErrors", @@ -62,8 +65,8 @@ class TestOptions(OptionParser): self.set_usage(USAGE_EXAMPLE) def verifyOptions(self, options): - if options.reftest and options.unitTest: - self.error("--reftest and --unitTest must not be specified at the same time.") + 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: @@ -132,6 +135,14 @@ class TestHandlerBase(BaseHTTPRequestHandler): if VERBOSE: BaseHTTPRequestHandler.log_request(code, size) + def handle_one_request(self): + try: + BaseHTTPRequestHandler.handle_one_request(self) + except socket.error, v: + # Ignoring connection reset by peer exceptions + if v[0] != errno.ECONNRESET: + raise + def sendFile(self, path, ext): self.send_response(200) self.send_header("Content-Type", MIMEs[ext]) @@ -142,6 +153,7 @@ class TestHandlerBase(BaseHTTPRequestHandler): 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)) @@ -157,7 +169,7 @@ class TestHandlerBase(BaseHTTPRequestHandler): return if not (prefix == DOC_ROOT - and os.path.isfile(path) + and os.path.isfile(path) and ext in MIMEs): print path self.send_error(404) @@ -173,15 +185,70 @@ class TestHandlerBase(BaseHTTPRequestHandler): class UnitTestHandler(TestHandlerBase): def sendIndex(self, path, query): print "send index" + + def translateFont(self, base64Data): + self.send_response(200) + self.send_header("Content-Type", "text/xml") + self.end_headers() + + data = base64.b64decode(base64Data) + taskId = str(uuid.uuid4()) + fontPath = 'ttx/' + taskId + '.otf' + resultPath = 'ttx/' + taskId + '.ttx' + with open(fontPath, "wb") as f: + f.write(data) + + # When fontTools used directly, we need to snif ttx file + # to check what version of python is used + ttxPath = '' + for path in os.environ["PATH"].split(os.pathsep): + if os.path.isfile(path + os.sep + "ttx"): + ttxPath = path + os.sep + "ttx" + break + if ttxPath == '': + self.wfile.write("TTX was not found") + return + + ttxRunner = '' + with open(ttxPath, "r") as f: + firstLine = f.readline() + if firstLine[:2] == '#!' and firstLine.find('python') > -1: + ttxRunner = firstLine[2:].strip() + + with open(os.devnull, "w") as fnull: + if ttxRunner != '': + result = subprocess.call([ttxRunner, ttxPath, fontPath], stdout = fnull) + else: + result = subprocess.call([ttxPath, fontPath], stdout = fnull) + + os.remove(fontPath) + + if not os.path.isfile(resultPath): + self.wfile.write("Output was not generated") + return + + with open(resultPath, "rb") as f: + self.wfile.write(f.read()) + + os.remove(resultPath) + + return + def do_POST(self): + 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() - url = urlparse(self.path) - result = json.loads(self.rfile.read(numBytes)) + result = json.loads(content) browser = result['browser'] UnitTestState.lastPost[browser] = int(time.time()) if url.path == "/tellMeToQuit": @@ -563,7 +630,7 @@ def checkEq(task, results, browser, masterMode): eq = (ref == snapshot) if not eq: - print 'TEST-UNEXPECTED-FAIL | ', taskType, taskId, '| in', browser, '| rendering of page', page + 1, '!= reference rendering' + print 'TEST-UNEXPECTED-FAIL |', taskType, taskId, '| in', browser, '| rendering of page', page + 1, '!= reference rendering' if not State.eqLog: State.eqLog = open(EQLOG_FILE, 'w') @@ -592,7 +659,7 @@ def checkEq(task, results, browser, masterMode): of.close() if passed: - print 'TEST-PASS | ', taskType, ' test', task['id'], '| in', browser + print 'TEST-PASS |', taskType, 'test', task['id'], '| in', browser def checkFBF(task, results, browser): round0, round1 = results[0], results[1] @@ -693,10 +760,10 @@ def runTests(options, browsers): print "\nStarting reftest harness to examine %d eq test failures." % State.numEqFailures startReftest(browsers[0], options) -def runUnitTests(options, browsers): +def runUnitTests(options, browsers, url, name): t1 = time.time() try: - startBrowsers(browsers, options, '/test/unit/unit_test.html') + 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: @@ -708,14 +775,14 @@ def runUnitTests(options, browsers): print '' print 'Ran', UnitTestState.numRun, 'tests' if UnitTestState.numErrors > 0: - print 'OHNOES! Some tests failed!' + print 'OHNOES! Some', name, 'tests failed!' print ' ', UnitTestState.numErrors, 'of', UnitTestState.numRun, 'failed' else: - print 'All unit tests passed.' + print 'All', name, 'tests passed.' finally: teardownBrowsers(browsers) t2 = time.time() - print "Unit test Runtime was", int(t2 - t1), "seconds" + print '', name, 'tests runtime was', int(t2 - t1), 'seconds' def main(): optionParser = TestOptions() @@ -724,7 +791,7 @@ def main(): if options == None: sys.exit(1) - if options.unitTest: + 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) @@ -732,7 +799,10 @@ def main(): browsers = setUpUnitTests(options) if len(browsers) > 0: - runUnitTests(options, browsers) + 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 diff --git a/test/ttx/README.md b/test/ttx/README.md new file mode 100644 index 000000000..05a5bad61 --- /dev/null +++ b/test/ttx/README.md @@ -0,0 +1,19 @@ +This folder is a place for temporary files generated by ttx + +# About TTX Installation + +The numpy module is required -- use "easy_install numpy" to install it. + +Download and extract fonttools from http://sourceforge.net/projects/fonttools/ in any folder on your computer. + +From the font tools directory run "python setup.py install" from the command line. + +# TTX for Mac Change + +On Mac OSX, if you are getting error message related to "/Library/Python/2.7/site-packages/FontTools/fontTools/ttLib/macUtils.py", line 18, in MyOpenResFile, use the following patch to change the fonttools + +https://github.com/mcolyer/fonttools/commit/e732bd3ba63c51df9aed903eb2147fa1af1bfdc2 + +# TTX for Windows Change + +On Windows, if ttx generate an exception, it waits for a key to be pressed. Pleaase change "/c/mozilla-build/python/Lib/site-packages/Font-Tools/fontTools/ttx.py" file: replace the waitForKeyPress function body with just 'return'.