Added tests for ANSIString, and fixed some bugs for it along the way.

This commit is contained in:
Kelketek Rritaa 2014-03-01 14:20:39 -06:00 committed by Griatch
parent ef8755581c
commit f0130d8384
4 changed files with 156 additions and 15 deletions

View file

@ -515,6 +515,7 @@ INSTALLED_APPS = (
'src.comms', 'src.comms',
'src.help', 'src.help',
'src.scripts', 'src.scripts',
'src.utils',
'src.web.news', 'src.web.news',
'src.web.website',) 'src.web.website',)
# The user profile extends the User object with more functionality; # The user profile extends the User object with more functionality;

View file

@ -166,6 +166,12 @@ class ANSIParser(object):
else: else:
return ANSI_NORMAL + ANSI_BLUE return ANSI_NORMAL + ANSI_BLUE
def strip_ansi(self, string):
"""
Strips raw ANSI codes from a string.
"""
return self.ansi_regex.sub("", string)
def parse_ansi(self, string, strip_ansi=False, xterm256=False): def parse_ansi(self, string, strip_ansi=False, xterm256=False):
""" """
Parses a string, subbing color codes according to Parses a string, subbing color codes according to
@ -199,7 +205,7 @@ class ANSIParser(object):
if strip_ansi: if strip_ansi:
# remove all ansi codes (including those manually # remove all ansi codes (including those manually
# inserted in string) # inserted in string)
parsed_string = self.ansi_regex.sub("", parsed_string) return self.strip_ansi(string)
# cache and crop old cache # cache and crop old cache
_PARSE_CACHE[cachekey] = parsed_string _PARSE_CACHE[cachekey] = parsed_string
@ -378,7 +384,7 @@ def _on_raw(func_name):
args.insert(0, string) args.insert(0, string)
except IndexError: except IndexError:
pass pass
result = _query_super(func_name)(self, *args, **kwargs) result = getattr(self._raw_string, func_name)(*args, **kwargs)
if isinstance(result, basestring): if isinstance(result, basestring):
return ANSIString(result, decoded=True) return ANSIString(result, decoded=True)
return result return result
@ -416,7 +422,7 @@ class ANSIMeta(type):
'rfind', 'rindex', '__len__']: 'rfind', 'rindex', '__len__']:
setattr(cls, func_name, _query_super(func_name)) setattr(cls, func_name, _query_super(func_name))
for func_name in [ for func_name in [
'__mul__', '__mod__', 'expandtabs', '__rmul__', 'join', '__mul__', '__mod__', 'expandtabs', '__rmul__',
'decode', 'replace', 'format', 'encode']: 'decode', 'replace', 'format', 'encode']:
setattr(cls, func_name, _on_raw(func_name)) setattr(cls, func_name, _on_raw(func_name))
for func_name in [ for func_name in [
@ -456,13 +462,17 @@ class ANSIString(unicode):
parser = kwargs.get('parser', ANSI_PARSER) parser = kwargs.get('parser', ANSI_PARSER)
decoded = kwargs.get('decoded', False) or hasattr(string, '_raw_string') decoded = kwargs.get('decoded', False) or hasattr(string, '_raw_string')
if not decoded: if not decoded:
# Completely new ANSI String
clean_string = unicode(parser.parse_ansi(string, strip_ansi=True))
string = parser.parse_ansi(string) string = parser.parse_ansi(string)
if hasattr(string, '_clean_string'): elif hasattr(string, '_clean_string'):
# It's already an ANSIString
clean_string = string._clean_string clean_string = string._clean_string
string = string._raw_string string = string._raw_string
else: else:
clean_string = unicode(parser.parse_ansi( # It's a string that has been pre-ansi decoded.
string, strip_ansi=True)) clean_string = parser.strip_ansi(string)
if not isinstance(string, unicode): if not isinstance(string, unicode):
string = string.decode('utf-8') string = string.decode('utf-8')
else: else:
@ -491,7 +501,7 @@ class ANSIString(unicode):
""" """
return "ANSIString(%s, decoded=True)" % repr(self._raw_string) return "ANSIString(%s, decoded=True)" % repr(self._raw_string)
def __init__(self, text="", parser=ANSI_PARSER, **kwargs): def __init__(self, *_, **kwargs):
""" """
When the ANSIString is first initialized, a few internal variables When the ANSIString is first initialized, a few internal variables
have to be set. have to be set.
@ -517,8 +527,8 @@ class ANSIString(unicode):
tables for which characters in the raw string are related to ANSI tables for which characters in the raw string are related to ANSI
escapes, and which are for the readable text. escapes, and which are for the readable text.
""" """
self.parser = parser self.parser = kwargs.pop('parser', ANSI_PARSER)
super(ANSIString, self).__init__(text) super(ANSIString, self).__init__()
self._code_indexes, self._char_indexes = self._get_indexes() self._code_indexes, self._char_indexes = self._get_indexes()
def __add__(self, other): def __add__(self, other):
@ -583,8 +593,8 @@ class ANSIString(unicode):
string += self._raw_string[i] string += self._raw_string[i]
except IndexError: except IndexError:
pass pass
if slc.stop is not None: if i is not None:
append_tail = self._get_interleving(slc.stop) append_tail = self._get_interleving(self._char_indexes.index(i) + 1)
else: else:
append_tail = '' append_tail = ''
return ANSIString(string + append_tail, decoded=True) return ANSIString(string + append_tail, decoded=True)
@ -721,11 +731,11 @@ class ANSIString(unicode):
if next < 0: if next < 0:
break break
# Get character codes after the index as well. # Get character codes after the index as well.
res.append(self[start:next] + self._get_interleving(next)) res.append(self[start:next])
start = next + bylen start = next + bylen
maxsplit -= 1 # NB. if it's already < 0, it stays < 0 maxsplit -= 1 # NB. if it's already < 0, it stays < 0
res.append(self[start:len(self)] + self._get_interleving(len(self))) res.append(self[start:len(self)])
return res return res
def rsplit(self, by, maxsplit=-1): def rsplit(self, by, maxsplit=-1):
@ -747,14 +757,28 @@ class ANSIString(unicode):
if next < 0: if next < 0:
break break
# Get character codes after the index as well. # Get character codes after the index as well.
res.append(self[next+bylen:end] + self._get_interleving(end)) res.append(self[next+bylen:end])
end = next end = next
maxsplit -= 1 # NB. if it's already < 0, it stays < 0 maxsplit -= 1 # NB. if it's already < 0, it stays < 0
res.append(self[:end] + self._get_interleving(end)) res.append(self[:end])
res.reverse() res.reverse()
return res return res
def join(self, iterable):
"""
Joins together strings in an iterable.
"""
result = ANSIString('')
last_item = None
for item in iterable:
if last_item is not None:
result += self
result += item
last_item = item
return result
@_spacing_preflight @_spacing_preflight
def center(self, width, fillchar, difference): def center(self, width, fillchar, difference):
""" """

4
src/utils/models.py Normal file
View file

@ -0,0 +1,4 @@
"""
Dummy models.py file to allow us to add utils to the apps list so Django's
test runner will recognize it.
"""

112
src/utils/tests.py Normal file
View file

@ -0,0 +1,112 @@
import re
try:
from django.utils.unittest import TestCase
except ImportError:
from django.test import TestCase
from ansi import ANSIString
class ANSIStringTestCase(TestCase):
def checker(self, ansi, raw, clean):
"""
Verifies the raw and clean strings of an ANSIString match expected
output.
"""
self.assertEqual(unicode(ansi.clean()), clean)
self.assertEqual(unicode(ansi.raw()), raw)
def table_check(self, ansi, char, code):
"""
Verifies the indexes in an ANSIString match what they should.
"""
self.assertEqual(ansi._char_indexes, char)
self.assertEqual(ansi._code_indexes, code)
def test_instance(self):
"""
Make sure the ANSIString is always constructed correctly.
"""
clean = u'This isA{r testTest'
encoded = u'\x1b[1m\x1b[32mThis is\x1b[1m\x1b[31mA{r test\x1b[0mTest\x1b[0m'
target = ANSIString(r'{gThis is{rA{{r test{nTest{n')
char_table = [9, 10, 11, 12, 13, 14, 15, 25, 26, 27, 28, 29, 30, 31,
32, 37, 38, 39, 40]
code_table = [0, 1, 2, 3, 4, 5, 6, 7, 8, 16, 17, 18, 19, 20, 21, 22,
23, 24, 33, 34, 35, 36, 41, 42, 43, 44]
self.checker(target, encoded, clean)
self.table_check(target, char_table, code_table)
self.checker(ANSIString(target), encoded, clean)
self.table_check(ANSIString(target), char_table, code_table)
self.checker(ANSIString(encoded, decoded=True), encoded, clean)
self.table_check(ANSIString(encoded, decoded=True), char_table,
code_table)
self.checker(ANSIString('Test'), u'Test', u'Test')
self.table_check(ANSIString('Test'), [0, 1, 2, 3], [])
self.checker(ANSIString(''), u'', u'')
def test_slice(self):
"""
Verifies that slicing an ANSIString results in expected color code
distribution.
"""
target = ANSIString(r'{gTest{rTest{n')
result = target[:3]
self.checker(result, u'\x1b[1m\x1b[32mTes', u'Tes')
result = target[:4]
self.checker(result, u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[31m', u'Test')
result = target[:]
self.checker(
result,
u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[31mTest\x1b[0m',
u'TestTest')
result = target[:-1]
self.checker(
result,
u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[31mTes',
u'TestTes')
result = target[0:0]
self.checker(
result,
u'',
u'')
def test_split(self):
"""
Verifies that re.split and .split behave similarly and that color
codes end up where they should.
"""
target = ANSIString("{gThis is {nA split string{g")
first = (u'\x1b[1m\x1b[32mThis is \x1b[0m', u'This is ')
second = (u'\x1b[1m\x1b[32m\x1b[0m split string\x1b[1m\x1b[32m',
u' split string')
re_split = re.split('A', target)
normal_split = target.split('A')
self.assertEqual(re_split, normal_split)
self.assertEqual(len(normal_split), 2)
self.checker(normal_split[0], *first)
self.checker(normal_split[1], *second)
def test_join(self):
"""
Verify that joining a set of ANSIStrings works.
"""
# This isn't the desired behavior, but the expected one. Python
# concatinates the in-memory representation with the built-in string's
# join.
l = [ANSIString("{gTest{r") for s in range(0, 3)]
# Force the generator to be evaluated.
result = "".join(l)
self.assertEqual(unicode(result), u'TestTestTest')
result = ANSIString("").join(l)
self.checker(result, u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[31m\x1b[1m\x1b'
u'[32mTest\x1b[1m\x1b[31m\x1b[1m\x1b[32mTest'
u'\x1b[1m\x1b[31m', u'TestTestTest')
def test_len(self):
"""
Make sure that length reporting on ANSIStrings does not include
ANSI codes.
"""
self.assertEqual(len(ANSIString('{gTest{n')), 4)