""" run JavaScript unit tests from the Python test runner """ import os import sys import traceback import re import gc from lxml import etree import spidermonkey here = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.join(here, '..', 'static')) def get_exception(): exc, e, tb = sys.exc_info() ret = JSPyError(str(exc), str(e), ''.join(traceback.format_tb(tb))) del tb return ret def format_stack(stack): ret = '' for line in stack: if '@' not in line: ret += line continue info, fpath = line.rsplit('@', 1) fpath = fpath.split('/')[-1] if len(info) > 75: info = info[:35] + '...' + info[-20:] ret += ' %s@%s\n' % (info, fpath) return ret class PyJSError(Exception): def __init__(self, name, message, stack): self.args = (name, message, stack) self.name = name self.message = message self.stack = stack def __str__(self): ret = '%s - %s\n' % (self.name, self.message) ret += format_stack(self.stack) return ret class JSPyError: def __init__(self, name, message, stack): self.args = (name, message, stack) self.name = name self.message = message self.stack = stack.split('\n') class XPathProcessor(object): def __init__(self, expr, xml, nodepath, resolvinfo): self.expr = expr self.xml = xml xml = unicode(xml, 'latin-1') # XXX ?!? self.resolvinfo = resolvinfo or None self.tree = etree.fromstring(xml) self.node = self._resolve_xml_path(nodepath) def paths(self): if self.resolvinfo: r = self.node.xpath(self.expr, namespaces=self.resolvinfo) else: r = self.node.xpath(self.expr) return [self._get_path(node) for node in r] def _get_path(self, node): current = node path = [] if isinstance(node, str) or isinstance(node, unicode): if node.is_text: path = [0] current = current.getparent() else: parent = node.getparent() pparent = parent.getparent() parenti = self._get_node_index(parent, parent.getparent()) path = [parenti + 1] current = parent while True: parent = current.getparent() if parent is None: break i = self._get_node_index(current, parent) path.insert(0, i) current = parent return path def _get_node_index(self, node, parent): i = 0 if parent.text: i = 1 for child in parent: if child == node: break if child.tail: i += 2 else: i += 1 else: raise AssertionError('node not found in tree!') return i def _resolve_xml_path(self, path): xpexpr = ''.join(['/node()[%s]' % (int(i) + 1,) for i in path.split('/')]) nodes = self.tree.xpath(xpexpr) assert len(nodes) == 1 return nodes[0] class XSLTProcessor(object): def __init__(self, stylesheet, xml): self.stylesheet = stylesheet self.xml = xml def process(self): xsltdoc = etree.fromstring(unicode(self.stylesheet, 'utf-8')) xmldoc = etree.fromstring(unicode(self.xml, 'latin-1')) open('/tmp/test.xsl', 'w').write(self.stylesheet) transform = etree.XSLT(xsltdoc) tree = transform(xmldoc) return etree.tostring(tree) class FakeBrowser: def __init__(self, server, webpath='/', xsltransformer=None): self.server = server self.webpath = webpath self.xsltransformer = xsltransformer def _init_runtime(self): self.runtime = spidermonkey.Runtime(maxbytes=10000000) self.context = self.runtime.new_context() self.context.bind_class(FakeBrowser, bind_constructor=False) self.context.bind_class(JSPyError, bind_constructor=True) self.context.bind_object('__fakebrowser__', self) self._xhrs = {} def initialize(self): self._init_runtime() html = self.server.load_url(self.webpath) title = self._get_title(html) self.load(os.path.join(here, 'fakebrowser.js'), True) self.eval('init_fakebrowser(%r, "http://localhost%s", %r);' % ( unicode(html, 'utf-8').encode('utf-8'), self.webpath, title)) htree = etree.fromstring(html) self.resolve_script_tags(htree) self.call_onload(htree) del htree def cleanup(self): del self.runtime del self.context del self._xhrs gc.collect() def _get_title(self, html): match = re.search('(.*)', html, re.M | re.I) if match: title = match.group(1) else: title = 'no title' return title def resolve_script_tags(self, htree): scriptnodes = htree.xpath('//script') for node in scriptnodes: src = node.attrib.get('src') if src: self.load(src, True) elif node.text: self.eval(node.text) def call_onload(self, htree): body = htree.xpath('//body')[0] onload = body.get('onload') if onload: self.eval(onload) def load(self, path, isfs=False): """ load and evaluate a JS file path is a web-path, either relative from the 'current position' (the web page's parent in case this is called directly, the JS file's parent in case it's called from a JS file using load()) or absolute from the web root """ if not isfs: js = self.server.load_url(path) if js is None: # XXX not the right exception imo... raise ValueError( 'could not load path %s from server' % (path,)) else: if not path.startswith('/'): path = os.path.join(here, path) fp = open(path) try: js = fp.read() finally: fp.close() self.eval(js, path) def eval(self, js, scriptname='unknown'): s = (''' try {eval(%r); } catch(e) { var s = e.toString(); if (s.indexOf(': ') > -1) { var namemsg = e.toString().split(': '); new JSPyError(namemsg[0], namemsg[1], e.stack || ''); }; }; ''' % (js,)) try: ret = self.context.eval_script(s, scriptname) except (SystemExit, KeyboardInterrupt), e: raise e except: exc, e, tb = sys.exc_info() print 'an exception occurred in %r' % (scriptname,) print '%s - %s' % (exc, e) print ''.join(traceback.format_tb(tb)) raise exc, e, tb if isinstance(ret, JSPyError): raise PyJSError(ret.name, ret.message, ret.stack) return ret def send_xhr(self, id, data): rdata = {} for var in ('method', 'url', 'async', 'user', 'password', 'headers'): rdata[var] = self.eval('window.__xhrs__[%s]._%s' % (id, var)) # async - perform ready state changes and return in the end aborted = self.eval('''\ var xhr = window.__xhrs__[%s]; for (var i=0; i < 2; i++) { xhr._handle_state_change(i+2); if (xhr._aborted) { break; }; }; xhr._aborted; ''' % (id,)) if not aborted: ret = self.server.handle_request(rdata['method'], rdata['url'], rdata['headers'], data) return ret def process_xpath(self, expr, docxml, nodepath, resolvinfo): # print 'processing XPath on xml', docxml[:100], '...' ret = XPathProcessor(expr, docxml, nodepath, resolvinfo).paths() return ret def process_xsl(self, sheet, xml, stack=None): # print 'processing XSLT on xml', xml[:100], '...' try: ret = XSLTProcessor(sheet, xml).process() except: print 'exception processing XSL, xml: ' + xml if stack is not None: print 'JavaScript stack trace:' print format_stack(stack.split('\n')) raise return ret def raw_input(self, prompt=''): return raw_input(prompt)