Source code for djangojs.tap

# -*- coding: utf-8 -*-
'''
This module provide test runners for JS in Django.
'''
from __future__ import unicode_literals

import re

from django.utils import termcolors
from django.utils.encoding import python_2_unicode_compatible

green = termcolors.make_style(fg='green', opts=('bold',))
red = termcolors.make_style(fg='red', opts=('bold',))

# TAP regex
TAP_MODULE_REGEX = re.compile(r'^(?P<indent>\s*)# module: (?P<name>.*)$')
TAP_TEST_REGEX = re.compile(r'^(?P<indent>\s*)# test: (?P<name>.*)$')
TAP_ASSERTION_REGEX = re.compile(r'^(?P<indent>\s*)(?P<type>(?:not )?ok) (?P<num>\d+)(?: - (?P<details>.*))?$')
TAP_STACK_REGEX = re.compile(r'^(?P<indent>\s*)#\s+at\s+(?P<stack>.*)$')
TAP_END_REGEX = re.compile(r'^(?P<indent>\s*)(?P<start>\d+)\.\.(?P<end>\d+)(?: - (?P<details>.*))?$')
TAP_DETAILS_REGEX = re.compile(
    r'''^(?:(?P<message>(?!expected)(?!got)(?!matcher)(?!source).+?)(?:, )?)?'''
    r'''(?:expected: '(?P<expected>.+)', got: '(?P<got>.+?)'(?:, )?)?'''
    r'''(?:matcher: '(?P<matcher>.+?)'(?:, )?)?'''
    r'''(?:source:\s+at\s+(?P<source>.+?))?$''')


# Output format
INDENT = 4
DEFAULT_ENCODING = 'utf-8'


class TapItem(object):
    parent = None
    parsed_indent = ''

    @property
    def indent(self):
        if self.parent and isinstance(self.parent, TapItem):
            if isinstance(self.parent, TapModule):
                return self.parent.indent + ' ' * INDENT
            else:
                return self.parent.indent
        return ''


@python_2_unicode_compatible
class TapGroup(list, TapItem):
    def __init__(self, name='', parent=None, parsed_indent='', *args, **kwargs):
        super(TapGroup, self).__init__(*args, **kwargs)
        self.name = name
        self.parent = parent
        self.parsed_indent = parsed_indent

    def __str__(self):
        return '# Groupe %s' % self.name

    def __nonzero__(self):
        return True

    def __bool__(self):
        return True

    def append(self, item):
        if isinstance(item, (TapGroup, TapAssertion)) and not item.parent:
            item.parent = self
        super(TapGroup, self).append(item)

    def get_all_failures(self):
        failures = []
        for item in self:
            if isinstance(item, TapGroup):
                failures.extend(item.get_all_failures())
            elif isinstance(item, TapAssertion) and not item.success:
                failures.append(item)
        return failures


@python_2_unicode_compatible
class TapModule(TapGroup):

    def display(self):
        return '%s%s' % (self.indent, self.name)

    def __str__(self):
        return '# module: %s' % self.name

    @classmethod
    def parse(cls, line):
        match = TAP_MODULE_REGEX.match(line.rstrip())
        if match:
            return cls(
                match.group('name').strip(),
                parsed_indent=match.group('indent')
            )
        else:
            return None


@python_2_unicode_compatible
class TapTest(TapGroup):

    def display(self):
        assertions = [result.display(True) for result in self]
        if assertions:
            return '%s%s (%s)' % (self.indent, self.name, ' '.join(assertions))
        else:
            return '%s%s' % (self.indent, self.name)

    def __str__(self):
        return '# test: %s' % self.name

    @classmethod
    def parse(cls, line):
        match = TAP_TEST_REGEX.match(line.rstrip())
        if match:
            return cls(match.group('name').strip(), parsed_indent=match.group('indent'))
        else:
            return None


@python_2_unicode_compatible
class TapAssertion(TapItem):
    def __init__(self, num, success=True, message=None, parsed_indent='', *args, **kwargs):
        super(TapAssertion, self).__init__()
        self.num = num
        self.success = success
        self.message = message
        self.expected = None
        self.got = None
        self.matcher = None
        self.stack = []
        self.parsed_indent = parsed_indent

    def display(self, inline=False):
        if inline:
            return green('ok') if self.success else red('ko')
        else:
            text = self.indent
            if self.success:
                text += green('ok %s' % self.num)
            else:
                text += red('not ok %s' % self.num)
            text = '%s - %s' % (text, self.message) if self.message else text
            if self.expected is not None and self.got is not None:
                text = '\n'.join([text, '# expected: %s' % self.expected, '# got: %s' % self.got])
            if self.stack:
                text = '\n'.join([text] + ['# stack: %s' % line for line in self.stack])
            return text

    def __str__(self):
        return 'ok %s' % self.num if self.success else 'not ok %s' % self.num

    @classmethod
    def parse(cls, line):
        match = TAP_ASSERTION_REGEX.match(line.rstrip())
        if match:
            assertion = cls(
                int(match.group('num')),
                match.group('type') == 'ok',
                parsed_indent=match.group('indent')
            )
            if match.group('details'):
                details_match = TAP_DETAILS_REGEX.match(match.group('details'))
                if details_match and details_match.group('message'):
                    assertion.message = details_match.group('message')
                if details_match and details_match.group('expected'):
                    assertion.expected = details_match.group('expected')
                    assertion.got = details_match.group('got')
                if details_match and details_match.group('matcher'):
                    assertion.matcher = details_match.group('matcher')
                if details_match and details_match.group('source'):
                    assertion.stack = [details_match.group('source')]
            return assertion
        else:
            return None


HIERARCHY = (
    TapModule,
    TapTest,
    TapAssertion,
)


def hierarchy(item):
    if not isinstance(item, TapItem):
        raise ValueError('Item should be a TapItem')
    return HIERARCHY.index(item.__class__)


[docs]class TapParser(object): ''' A TAP parser class reading from iterable TAP lines. ''' def __init__(self, yield_class=TapTest, debug=False): if not issubclass(yield_class, TapItem): raise ValueError('yield class should extend TapItem') self.suites = TapGroup() self.current = self.suites self.yield_class = yield_class self.debug = debug def parse(self, lines): for line in lines: for item in self.parse_line(line): yield item for item in self.get_lasts(): yield item def parse_line(self, line): item = TapModule.parse(line) or TapTest.parse(line) or TapAssertion.parse(line) if item is not None: return self.set_current(item) match = TAP_STACK_REGEX.match(line.rstrip()) if match: self.current.stack.append(match.group('stack')) return [] match = TAP_END_REGEX.match(line.rstrip()) if match and self.debug: print('# end %s-%s' % (match.group('start'), match.group('end'))) return [] if line and self.debug: print('not matched: %s' % line) return [] def set_current(self, item=None): if item and not isinstance(item, TapItem): raise ValueError('Should be a TAP item') ended = [] while self.current.parent and hierarchy(item) < hierarchy(self.current): if isinstance(self.current, self.yield_class): ended.append(self.current) self.current = self.current.parent while self.current.parent \ and hierarchy(item) == hierarchy(self.current) \ and len(item.parsed_indent) <= len(self.current.parsed_indent): if isinstance(self.current, self.yield_class): ended.append(self.current) self.current = self.current.parent self.current.append(item) self.current = item if hierarchy(self.current) < HIERARCHY.index(self.yield_class): ended.append(self.current) return ended def get_lasts(self): lasts = [] while isinstance(self.current, TapItem) and self.current.parent: if isinstance(self.current, self.yield_class): lasts.append(self.current) self.current = self.current.parent return lasts