diff --git a/evennia/utils/evform.py b/evennia/utils/evform.py index b601c8bfc5..55fa0ec9e2 100644 --- a/evennia/utils/evform.py +++ b/evennia/utils/evform.py @@ -429,7 +429,7 @@ class EvForm(object): def _test(): "test evform. This is used by the unittest system." - form = EvForm("evennia.utils.evform_test") + form = EvForm("evennia.utils.tests.data.evform_example") # add data to each tagged form cell form.map(cells={"AA": "|gTom the Bouncer", diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index f6785a2f1f..b4124b7219 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -35,7 +35,7 @@ class EvenniaTest(TestCase): self.room1.db.desc = "room_desc" settings.DEFAULT_HOME = "#%i" % self.room1.id # we must have a default home # Set up fake prototype module for allowing tests to use named prototypes. - settings.PROTOTYPE_MODULES = "evennia.utils.test_prototypes" + settings.PROTOTYPE_MODULES = "evennia.utils.tests.data.prototypes_example" self.room2 = create.create_object(self.room_typeclass, key="Room2") self.exit = create.create_object(self.exit_typeclass, key='out', location=self.room1, destination=self.room2) self.obj1 = create.create_object(self.object_typeclass, key="Obj", location=self.room1, home=self.room1) diff --git a/evennia/utils/tests.py b/evennia/utils/tests.py deleted file mode 100644 index 78ceb45a78..0000000000 --- a/evennia/utils/tests.py +++ /dev/null @@ -1,532 +0,0 @@ -from builtins import range - -import re - -try: - from django.utils.unittest import TestCase -except ImportError: - from django.test import TestCase - -from .ansi import ANSIString -from evennia import utils - -from django.conf import settings - - -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 - # concatenates the in-memory representation with the built-in string's - # join. - l = [ANSIString("|gTest|r") for _ 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) - - def test_capitalize(self): - """ - Make sure that capitalization works. This is the simplest of the - _transform functions. - """ - target = ANSIString('|gtest|n') - result = u'\x1b[1m\x1b[32mTest\x1b[0m' - self.checker(target.capitalize(), result, u'Test') - - def test_mxp_agnostic(self): - """ - Make sure MXP tags are not treated like ANSI codes, but normal text. - """ - mxp1 = "|lclook|ltat|le" - mxp2 = "Start to |lclook here|ltclick somewhere here|le first" - self.assertEqual(15, len(ANSIString(mxp1))) - self.assertEqual(53, len(ANSIString(mxp2))) - # These would indicate an issue with the tables. - self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0])) - self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0])) - self.assertEqual(mxp1, ANSIString(mxp1)) - self.assertEqual(mxp2, unicode(ANSIString(mxp2))) - - def test_add(self): - """ - Verify concatenation works correctly. - """ - a = ANSIString("|gTest") - b = ANSIString("|cString|n") - c = a + b - result = u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[36mString\x1b[0m' - self.checker(c, result, u'TestString') - char_table = [9, 10, 11, 12, 22, 23, 24, 25, 26, 27] - code_table = [0, 1, 2, 3, 4, 5, 6, 7, 8, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31] - self.table_check(c, char_table, code_table) - - def test_strip(self): - """ - Test the ansi-aware .strip() methods - """ - a = ANSIString(" |r Test of stuff |b with spaces |n ") - b = ANSIString("|r|b") - self.assertEqual(a.strip(), ANSIString("|rTest of stuff |b with spaces|n")) - self.assertEqual(a.lstrip(), ANSIString("|rTest of stuff |b with spaces |n ")) - self.assertEqual(a.rstrip(), ANSIString(" |r Test of stuff |b with spaces|n")) - self.assertEqual(b.strip(), b) - - -class TestIsIter(TestCase): - def test_is_iter(self): - self.assertEqual(True, utils.is_iter([1, 2, 3, 4])) - self.assertEqual(False, utils.is_iter("This is not an iterable")) - - -class TestCrop(TestCase): - def test_crop(self): - # No text, return no text - self.assertEqual("", utils.crop("", width=10, suffix="[...]")) - # Input length equal to max width, no crop - self.assertEqual("0123456789", utils.crop("0123456789", width=10, suffix="[...]")) - # Input length greater than max width, crop (suffix included in width) - self.assertEqual("0123[...]", utils.crop("0123456789", width=9, suffix="[...]")) - # Input length less than desired width, no crop - self.assertEqual("0123", utils.crop("0123", width=9, suffix="[...]")) - # Width too small or equal to width of suffix - self.assertEqual("012", utils.crop("0123", width=3, suffix="[...]")) - self.assertEqual("01234", utils.crop("0123456", width=5, suffix="[...]")) - - -class TestDedent(TestCase): - def test_dedent(self): - # Empty string, return empty string - self.assertEqual("", utils.dedent("")) - # No leading whitespace - self.assertEqual("TestDedent", utils.dedent("TestDedent")) - # Leading whitespace, single line - self.assertEqual("TestDedent", utils.dedent(" TestDedent")) - # Leading whitespace, multi line - input_string = " hello\n world" - expected_string = "hello\nworld" - self.assertEqual(expected_string, utils.dedent(input_string)) - - -class TestListToString(TestCase): - """ - Default function header from utils.py: - list_to_string(inlist, endsep="and", addquote=False) - - Examples: - no endsep: - [1,2,3] -> '1, 2, 3' - with endsep=='and': - [1,2,3] -> '1, 2 and 3' - with addquote and endsep - [1,2,3] -> '"1", "2" and "3"' - """ - - def test_list_to_string(self): - self.assertEqual('1, 2, 3', utils.list_to_string([1, 2, 3], endsep="")) - self.assertEqual('"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep="", addquote=True)) - self.assertEqual('1, 2 and 3', utils.list_to_string([1, 2, 3])) - self.assertEqual('"1", "2" and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)) - - -class TestMLen(TestCase): - """ - Verifies that m_len behaves like len in all situations except those - where MXP may be involved. - """ - - def test_non_mxp_string(self): - self.assertEqual(utils.m_len('Test_string'), 11) - - def test_mxp_string(self): - self.assertEqual(utils.m_len('|lclook|ltat|le'), 2) - - def test_mxp_ansi_string(self): - self.assertEqual(utils.m_len(ANSIString('|lcl|gook|ltat|le|n')), 2) - - def test_non_mxp_ansi_string(self): - self.assertEqual(utils.m_len(ANSIString('|gHello|n')), 5) - - def test_list(self): - self.assertEqual(utils.m_len([None, None]), 2) - - def test_dict(self): - self.assertEqual(utils.m_len({'hello': True, 'Goodbye': False}), 2) - - -from .text2html import TextToHTMLparser - - -class TestTextToHTMLparser(TestCase): - def setUp(self): - self.parser = TextToHTMLparser() - - def tearDown(self): - del self.parser - - def test_url_scheme_ftp(self): - self.assertEqual(self.parser.convert_urls('ftp.example.com'), - 'ftp.example.com') - - def test_url_scheme_www(self): - self.assertEqual(self.parser.convert_urls('www.example.com'), - 'www.example.com') - - def test_url_scheme_ftpproto(self): - self.assertEqual(self.parser.convert_urls('ftp://ftp.example.com'), - 'ftp://ftp.example.com') - - def test_url_scheme_http(self): - self.assertEqual(self.parser.convert_urls('http://example.com'), - 'http://example.com') - - def test_url_scheme_https(self): - self.assertEqual(self.parser.convert_urls('https://example.com'), - 'https://example.com') - - def test_url_chars_slash(self): - self.assertEqual(self.parser.convert_urls('www.example.com/homedir'), - 'www.example.com/homedir') - - def test_url_chars_colon(self): - self.assertEqual(self.parser.convert_urls('https://example.com:8000/login/'), - '' - 'https://example.com:8000/login/') - - def test_url_chars_querystring(self): - self.assertEqual(self.parser.convert_urls('https://example.com/submitform?field1=val1+val3&field2=val2'), - '' - 'https://example.com/submitform?field1=val1+val3&field2=val2') - - def test_url_chars_anchor(self): - self.assertEqual(self.parser.convert_urls('http://www.example.com/menu#section_1'), - '' - 'http://www.example.com/menu#section_1') - - def test_url_chars_exclam(self): - self.assertEqual(self.parser.convert_urls('https://groups.google.com/forum/' - '?fromgroups#!categories/evennia/ainneve'), - 'https://groups.google.com/forum/?fromgroups#!categories/evennia/ainneve') - - def test_url_edge_leadingw(self): - self.assertEqual(self.parser.convert_urls('wwww.example.com'), - 'wwww.example.com') - - def test_url_edge_following_period_eol(self): - self.assertEqual(self.parser.convert_urls('www.example.com.'), - 'www.example.com.') - - def test_url_edge_following_period(self): - self.assertEqual(self.parser.convert_urls('see www.example.com. '), - 'see www.example.com. ') - - def test_url_edge_brackets(self): - self.assertEqual(self.parser.convert_urls('[http://example.com/]'), - '[http://example.com/]') - - def test_url_edge_multiline(self): - self.assertEqual(self.parser.convert_urls(' * http://example.com/info\n * bullet'), - ' * ' - 'http://example.com/info\n * bullet') - - def test_url_edge_following_htmlentity(self): - self.assertEqual(self.parser.convert_urls('http://example.com/info<span>'), - 'http://example.com/info<span>') - - def test_url_edge_surrounded_spans(self): - self.assertEqual(self.parser.convert_urls('http://example.com/'), - '' - 'http://example.com/') - - -from evennia.utils import evmenu -from mock import Mock - - -class TestEvMenu(TestCase): - "Run the EvMenu test." - - def setUp(self): - self.caller = Mock() - self.caller.msg = Mock() - self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node", - persistent=True, cmdset_mergetype="Replace", testval="val", testval2="val2") - - def test_kwargsave(self): - self.assertTrue(hasattr(self.menu, "testval")) - self.assertTrue(hasattr(self.menu, "testval2")) - - -from evennia.utils import inlinefuncs - - -class TestInlineFuncs(TestCase): - """Test the nested inlinefunc module""" - - def test_nofunc(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - "as$382ewrw w we w werw,|44943}"), - "as$382ewrw w we w werw,|44943}") - - def test_incomplete(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - "testing $blah{without an ending."), - "testing $blah{without an ending.") - - def test_single_func(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - "this is a test with $pad(centered, 20) text in it."), - "this is a test with centered text in it.") - - def test_nested(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - "this $crop(is a test with $pad(padded, 20) text in $pad(pad2, 10) a crop, 80)"), - "this is a test with padded text in pad2 a crop") - - def test_escaped(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - "this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"), - "this should be escaped, and instead, cropped with text. ") - - def test_escaped2(self): - self.assertEqual(inlinefuncs.parse_inlinefunc( - 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'), - "this should be escaped, and instead, cropped with text. ") - - -from evennia.utils import evform - - -class TestEvForm(TestCase): - def test_form(self): - self.maxDiff = None - self.assertEqual(evform._test(), - u'.------------------------------------------------.\n' - u'| |\n' - u'| Name: \x1b[0m\x1b[1m\x1b[32mTom\x1b[1m\x1b[32m \x1b' - u'[1m\x1b[32mthe\x1b[1m\x1b[32m \x1b[0m \x1b[0m ' - u'Account: \x1b[0m\x1b[1m\x1b[33mGriatch ' - u'\x1b[0m\x1b[0m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[0m\x1b[0m ' - u'|\n' - u'| \x1b[0m\x1b[1m\x1b[32mBouncer\x1b[0m \x1b[0m |\n' - u'| |\n' - u' >----------------------------------------------<\n' - u'| |\n' - u'| Desc: \x1b[0mA sturdy \x1b[0m \x1b[0m' - u' STR: \x1b[0m12 \x1b[0m\x1b[0m\x1b[0m\x1b[0m' - u' DEX: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| \x1b[0mfellow\x1b[0m \x1b[0m' - u' INT: \x1b[0m5 \x1b[0m\x1b[0m\x1b[0m\x1b[0m' - u' STA: \x1b[0m18 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| \x1b[0m \x1b[0m' - u' LUC: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m' - u' MAG: \x1b[0m3 \x1b[0m\x1b[0m\x1b[0m |\n' - u'| |\n' - u' >----------.-----------------------------------<\n' - u'| | |\n' - u'| \x1b[0mHP\x1b[0m|\x1b[0mMV \x1b[0m|\x1b[0mMP\x1b[0m ' - u'| \x1b[0mSkill \x1b[0m|\x1b[0mValue \x1b[0m' - u'|\x1b[0mExp \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| ~~+~~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~+~~~~~~~~~~~ |\n' - u'| \x1b[0m**\x1b[0m|\x1b[0m***\x1b[0m\x1b[0m|\x1b[0m**\x1b[0m\x1b[0m ' - u'| \x1b[0mShooting \x1b[0m|\x1b[0m12 \x1b[0m' - u'|\x1b[0m550/1200 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| \x1b[0m \x1b[0m|\x1b[0m**\x1b[0m \x1b[0m|\x1b[0m*\x1b[0m \x1b[0m ' - u'| \x1b[0mHerbalism \x1b[0m|\x1b[0m14 \x1b[0m' - u'|\x1b[0m990/1400 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| \x1b[0m \x1b[0m|\x1b[0m \x1b[0m|\x1b[0m \x1b[0m ' - u'| \x1b[0mSmithing \x1b[0m|\x1b[0m9 \x1b[0m' - u'|\x1b[0m205/900 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' - u'| | |\n' - u' -----------`-------------------------------------\n') - - def test_ansi_escape(self): - # note that in a msg() call, the result would be the correct |-----, - # in a print, ansi only gets called once, so ||----- is the result - self.assertEqual(unicode(evform.EvForm(form={"FORM": "\n||-----"})), "||-----") - - -class TestTimeformat(TestCase): - """ - Default function header from utils.py: - time_format(seconds, style=0) - - """ - - def test_style_0(self): - """Test the style 0 of time_format.""" - self.assertEqual(utils.time_format(0, 0), "00:00") - self.assertEqual(utils.time_format(28, 0), "00:00") - self.assertEqual(utils.time_format(92, 0), "00:01") - self.assertEqual(utils.time_format(300, 0), "00:05") - self.assertEqual(utils.time_format(660, 0), "00:11") - self.assertEqual(utils.time_format(3600, 0), "01:00") - self.assertEqual(utils.time_format(3725, 0), "01:02") - self.assertEqual(utils.time_format(86350, 0), "23:59") - self.assertEqual(utils.time_format(86800, 0), "1d 00:06") - self.assertEqual(utils.time_format(130800, 0), "1d 12:20") - self.assertEqual(utils.time_format(530800, 0), "6d 03:26") - - def test_style_1(self): - """Test the style 1 of time_format.""" - self.assertEqual(utils.time_format(0, 1), "0s") - self.assertEqual(utils.time_format(28, 1), "28s") - self.assertEqual(utils.time_format(92, 1), "1m") - self.assertEqual(utils.time_format(300, 1), "5m") - self.assertEqual(utils.time_format(660, 1), "11m") - self.assertEqual(utils.time_format(3600, 1), "1h") - self.assertEqual(utils.time_format(3725, 1), "1h") - self.assertEqual(utils.time_format(86350, 1), "23h") - self.assertEqual(utils.time_format(86800, 1), "1d") - self.assertEqual(utils.time_format(130800, 1), "1d") - self.assertEqual(utils.time_format(530800, 1), "6d") - - def test_style_2(self): - """Test the style 2 of time_format.""" - self.assertEqual(utils.time_format(0, 2), "0 minutes") - self.assertEqual(utils.time_format(28, 2), "0 minutes") - self.assertEqual(utils.time_format(92, 2), "1 minute") - self.assertEqual(utils.time_format(300, 2), "5 minutes") - self.assertEqual(utils.time_format(660, 2), "11 minutes") - self.assertEqual(utils.time_format(3600, 2), "1 hour, 0 minutes") - self.assertEqual(utils.time_format(3725, 2), "1 hour, 2 minutes") - self.assertEqual(utils.time_format(86350, 2), "23 hours, 59 minutes") - self.assertEqual(utils.time_format(86800, 2), - "1 day, 0 hours, 6 minutes") - self.assertEqual(utils.time_format(130800, 2), - "1 day, 12 hours, 20 minutes") - self.assertEqual(utils.time_format(530800, 2), - "6 days, 3 hours, 26 minutes") - - def test_style_3(self): - """Test the style 3 of time_format.""" - self.assertEqual(utils.time_format(0, 3), "") - self.assertEqual(utils.time_format(28, 3), "28 seconds") - self.assertEqual(utils.time_format(92, 3), "1 minute 32 seconds") - self.assertEqual(utils.time_format(300, 3), "5 minutes 0 seconds") - self.assertEqual(utils.time_format(660, 3), "11 minutes 0 seconds") - self.assertEqual(utils.time_format(3600, 3), - "1 hour, 0 minutes") - self.assertEqual(utils.time_format(3725, 3), - "1 hour, 2 minutes 5 seconds") - self.assertEqual(utils.time_format(86350, 3), - "23 hours, 59 minutes 10 seconds") - self.assertEqual(utils.time_format(86800, 3), - "1 day, 0 hours, 6 minutes 40 seconds") - self.assertEqual(utils.time_format(130800, 3), - "1 day, 12 hours, 20 minutes 0 seconds") - self.assertEqual(utils.time_format(530800, 3), - "6 days, 3 hours, 26 minutes 40 seconds") - - def test_style_4(self): - """Test the style 4 of time_format.""" - self.assertEqual(utils.time_format(0, 4), "0 seconds") - self.assertEqual(utils.time_format(28, 4), "28 seconds") - self.assertEqual(utils.time_format(92, 4), "a minute") - self.assertEqual(utils.time_format(300, 4), "5 minutes") - self.assertEqual(utils.time_format(660, 4), "11 minutes") - self.assertEqual(utils.time_format(3600, 4), "an hour") - self.assertEqual(utils.time_format(3725, 4), "an hour") - self.assertEqual(utils.time_format(86350, 4), "23 hours") - self.assertEqual(utils.time_format(86800, 4), "a day") - self.assertEqual(utils.time_format(130800, 4), "a day") - self.assertEqual(utils.time_format(530800, 4), "6 days") - self.assertEqual(utils.time_format(3030800, 4), "a month") - self.assertEqual(utils.time_format(7030800, 4), "2 months") - self.assertEqual(utils.time_format(40030800, 4), "a year") - self.assertEqual(utils.time_format(90030800, 4), "2 years") - - def test_unknown_format(self): - """Test that unknown formats raise exceptions.""" - self.assertRaises(ValueError, utils.time_format, 0, 5) - self.assertRaises(ValueError, utils.time_format, 0, "u") diff --git a/evennia/utils/tests/__init__.py b/evennia/utils/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/utils/tests/data/__init__.py b/evennia/utils/tests/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/utils/evform_test.py b/evennia/utils/tests/data/evform_example.py similarity index 100% rename from evennia/utils/evform_test.py rename to evennia/utils/tests/data/evform_example.py diff --git a/evennia/utils/test_prototypes.py b/evennia/utils/tests/data/prototypes_example.py similarity index 100% rename from evennia/utils/test_prototypes.py rename to evennia/utils/tests/data/prototypes_example.py diff --git a/evennia/utils/tests/test_evform.py b/evennia/utils/tests/test_evform.py new file mode 100644 index 0000000000..e6a0d26049 --- /dev/null +++ b/evennia/utils/tests/test_evform.py @@ -0,0 +1,55 @@ +""" +Unit tests for the EvForm text form generator + +""" +from django.test import TestCase +from evennia.utils import evform + + +class TestEvForm(TestCase): + def test_form(self): + self.maxDiff = None + self.assertEqual(evform._test(), + u'.------------------------------------------------.\n' + u'| |\n' + u'| Name: \x1b[0m\x1b[1m\x1b[32mTom\x1b[1m\x1b[32m \x1b' + u'[1m\x1b[32mthe\x1b[1m\x1b[32m \x1b[0m \x1b[0m ' + u'Account: \x1b[0m\x1b[1m\x1b[33mGriatch ' + u'\x1b[0m\x1b[0m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[1m\x1b[32m\x1b[0m\x1b[0m ' + u'|\n' + u'| \x1b[0m\x1b[1m\x1b[32mBouncer\x1b[0m \x1b[0m |\n' + u'| |\n' + u' >----------------------------------------------<\n' + u'| |\n' + u'| Desc: \x1b[0mA sturdy \x1b[0m \x1b[0m' + u' STR: \x1b[0m12 \x1b[0m\x1b[0m\x1b[0m\x1b[0m' + u' DEX: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| \x1b[0mfellow\x1b[0m \x1b[0m' + u' INT: \x1b[0m5 \x1b[0m\x1b[0m\x1b[0m\x1b[0m' + u' STA: \x1b[0m18 \x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| \x1b[0m \x1b[0m' + u' LUC: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m' + u' MAG: \x1b[0m3 \x1b[0m\x1b[0m\x1b[0m |\n' + u'| |\n' + u' >----------.-----------------------------------<\n' + u'| | |\n' + u'| \x1b[0mHP\x1b[0m|\x1b[0mMV \x1b[0m|\x1b[0mMP\x1b[0m ' + u'| \x1b[0mSkill \x1b[0m|\x1b[0mValue \x1b[0m' + u'|\x1b[0mExp \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| ~~+~~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~+~~~~~~~~~~~ |\n' + u'| \x1b[0m**\x1b[0m|\x1b[0m***\x1b[0m\x1b[0m|\x1b[0m**\x1b[0m\x1b[0m ' + u'| \x1b[0mShooting \x1b[0m|\x1b[0m12 \x1b[0m' + u'|\x1b[0m550/1200 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| \x1b[0m \x1b[0m|\x1b[0m**\x1b[0m \x1b[0m|\x1b[0m*\x1b[0m \x1b[0m ' + u'| \x1b[0mHerbalism \x1b[0m|\x1b[0m14 \x1b[0m' + u'|\x1b[0m990/1400 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| \x1b[0m \x1b[0m|\x1b[0m \x1b[0m|\x1b[0m \x1b[0m ' + u'| \x1b[0mSmithing \x1b[0m|\x1b[0m9 \x1b[0m' + u'|\x1b[0m205/900 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n' + u'| | |\n' + u' -----------`-------------------------------------\n') + + def test_ansi_escape(self): + # note that in a msg() call, the result would be the correct |-----, + # in a print, ansi only gets called once, so ||----- is the result + self.assertEqual(unicode(evform.EvForm(form={"FORM": "\n||-----"})), "||-----") diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py new file mode 100644 index 0000000000..4436fdccd6 --- /dev/null +++ b/evennia/utils/tests/test_evmenu.py @@ -0,0 +1,25 @@ +""" +Unit tests for the EvMenu system + +TODO: This need expansion. + +""" + +from django.test import TestCase +from evennia.utils import evmenu +from mock import Mock + + +class TestEvMenu(TestCase): + "Run the EvMenu testing." + + def setUp(self): + self.caller = Mock() + self.caller.msg = Mock() + self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node", + persistent=True, cmdset_mergetype="Replace", testval="val", + testval2="val2") + + def test_kwargsave(self): + self.assertTrue(hasattr(self.menu, "testval")) + self.assertTrue(hasattr(self.menu, "testval2")) diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py new file mode 100644 index 0000000000..a2f07af204 --- /dev/null +++ b/evennia/utils/tests/test_tagparsing.py @@ -0,0 +1,277 @@ +""" +Unit tests for all sorts of inline text-tag parsing, like ANSI, html conversion, inlinefuncs etc + +""" +import re +from django.test import TestCase +from evennia.utils.ansi import ANSIString +from evennia.utils.text2html import TextToHTMLparser +from evennia.utils import inlinefuncs + + +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 + # concatenates the in-memory representation with the built-in string's + # join. + l = [ANSIString("|gTest|r") for _ 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) + + def test_capitalize(self): + """ + Make sure that capitalization works. This is the simplest of the + _transform functions. + """ + target = ANSIString('|gtest|n') + result = u'\x1b[1m\x1b[32mTest\x1b[0m' + self.checker(target.capitalize(), result, u'Test') + + def test_mxp_agnostic(self): + """ + Make sure MXP tags are not treated like ANSI codes, but normal text. + """ + mxp1 = "|lclook|ltat|le" + mxp2 = "Start to |lclook here|ltclick somewhere here|le first" + self.assertEqual(15, len(ANSIString(mxp1))) + self.assertEqual(53, len(ANSIString(mxp2))) + # These would indicate an issue with the tables. + self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0])) + self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0])) + self.assertEqual(mxp1, ANSIString(mxp1)) + self.assertEqual(mxp2, unicode(ANSIString(mxp2))) + + def test_add(self): + """ + Verify concatenation works correctly. + """ + a = ANSIString("|gTest") + b = ANSIString("|cString|n") + c = a + b + result = u'\x1b[1m\x1b[32mTest\x1b[1m\x1b[36mString\x1b[0m' + self.checker(c, result, u'TestString') + char_table = [9, 10, 11, 12, 22, 23, 24, 25, 26, 27] + code_table = [0, 1, 2, 3, 4, 5, 6, 7, 8, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31] + self.table_check(c, char_table, code_table) + + def test_strip(self): + """ + Test the ansi-aware .strip() methods + """ + a = ANSIString(" |r Test of stuff |b with spaces |n ") + b = ANSIString("|r|b") + self.assertEqual(a.strip(), ANSIString("|rTest of stuff |b with spaces|n")) + self.assertEqual(a.lstrip(), ANSIString("|rTest of stuff |b with spaces |n ")) + self.assertEqual(a.rstrip(), ANSIString(" |r Test of stuff |b with spaces|n")) + self.assertEqual(b.strip(), b) + + +class TestTextToHTMLparser(TestCase): + def setUp(self): + self.parser = TextToHTMLparser() + + def tearDown(self): + del self.parser + + def test_url_scheme_ftp(self): + self.assertEqual(self.parser.convert_urls('ftp.example.com'), + 'ftp.example.com') + + def test_url_scheme_www(self): + self.assertEqual(self.parser.convert_urls('www.example.com'), + 'www.example.com') + + def test_url_scheme_ftpproto(self): + self.assertEqual(self.parser.convert_urls('ftp://ftp.example.com'), + 'ftp://ftp.example.com') + + def test_url_scheme_http(self): + self.assertEqual(self.parser.convert_urls('http://example.com'), + 'http://example.com') + + def test_url_scheme_https(self): + self.assertEqual(self.parser.convert_urls('https://example.com'), + 'https://example.com') + + def test_url_chars_slash(self): + self.assertEqual(self.parser.convert_urls('www.example.com/homedir'), + 'www.example.com/homedir') + + def test_url_chars_colon(self): + self.assertEqual(self.parser.convert_urls('https://example.com:8000/login/'), + '' + 'https://example.com:8000/login/') + + def test_url_chars_querystring(self): + self.assertEqual(self.parser.convert_urls('https://example.com/submitform?field1=val1+val3&field2=val2'), + '' + 'https://example.com/submitform?field1=val1+val3&field2=val2') + + def test_url_chars_anchor(self): + self.assertEqual(self.parser.convert_urls('http://www.example.com/menu#section_1'), + '' + 'http://www.example.com/menu#section_1') + + def test_url_chars_exclam(self): + self.assertEqual(self.parser.convert_urls('https://groups.google.com/forum/' + '?fromgroups#!categories/evennia/ainneve'), + 'https://groups.google.com/forum/?fromgroups#!categories/evennia/ainneve') + + def test_url_edge_leadingw(self): + self.assertEqual(self.parser.convert_urls('wwww.example.com'), + 'wwww.example.com') + + def test_url_edge_following_period_eol(self): + self.assertEqual(self.parser.convert_urls('www.example.com.'), + 'www.example.com.') + + def test_url_edge_following_period(self): + self.assertEqual(self.parser.convert_urls('see www.example.com. '), + 'see www.example.com. ') + + def test_url_edge_brackets(self): + self.assertEqual(self.parser.convert_urls('[http://example.com/]'), + '[http://example.com/]') + + def test_url_edge_multiline(self): + self.assertEqual(self.parser.convert_urls(' * http://example.com/info\n * bullet'), + ' * ' + 'http://example.com/info\n * bullet') + + def test_url_edge_following_htmlentity(self): + self.assertEqual(self.parser.convert_urls('http://example.com/info<span>'), + 'http://example.com/info<span>') + + def test_url_edge_surrounded_spans(self): + self.assertEqual(self.parser.convert_urls('http://example.com/'), + '' + 'http://example.com/') + + +class TestInlineFuncs(TestCase): + """Test the nested inlinefunc module""" + + def test_nofunc(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + "as$382ewrw w we w werw,|44943}"), + "as$382ewrw w we w werw,|44943}") + + def test_incomplete(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + "testing $blah{without an ending."), + "testing $blah{without an ending.") + + def test_single_func(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + "this is a test with $pad(centered, 20) text in it."), + "this is a test with centered text in it.") + + def test_nested(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + "this $crop(is a test with $pad(padded, 20) text in $pad(pad2, 10) a crop, 80)"), + "this is a test with padded text in pad2 a crop") + + def test_escaped(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + "this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"), + "this should be escaped, and instead, cropped with text. ") + + def test_escaped2(self): + self.assertEqual(inlinefuncs.parse_inlinefunc( + 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'), + "this should be escaped, and instead, cropped with text. ") + + diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py new file mode 100644 index 0000000000..d2e42c4169 --- /dev/null +++ b/evennia/utils/tests/test_utils.py @@ -0,0 +1,188 @@ +""" +Unit tests for the utilities of the evennia.utils.utils module. + +TODO: Not nearly all utilities are covered yet. + +""" + +from django.test import TestCase + +from evennia.utils.ansi import ANSIString +from evennia.utils import utils + + +class TestIsIter(TestCase): + def test_is_iter(self): + self.assertEqual(True, utils.is_iter([1, 2, 3, 4])) + self.assertEqual(False, utils.is_iter("This is not an iterable")) + + +class TestCrop(TestCase): + def test_crop(self): + # No text, return no text + self.assertEqual("", utils.crop("", width=10, suffix="[...]")) + # Input length equal to max width, no crop + self.assertEqual("0123456789", utils.crop("0123456789", width=10, suffix="[...]")) + # Input length greater than max width, crop (suffix included in width) + self.assertEqual("0123[...]", utils.crop("0123456789", width=9, suffix="[...]")) + # Input length less than desired width, no crop + self.assertEqual("0123", utils.crop("0123", width=9, suffix="[...]")) + # Width too small or equal to width of suffix + self.assertEqual("012", utils.crop("0123", width=3, suffix="[...]")) + self.assertEqual("01234", utils.crop("0123456", width=5, suffix="[...]")) + + +class TestDedent(TestCase): + def test_dedent(self): + # Empty string, return empty string + self.assertEqual("", utils.dedent("")) + # No leading whitespace + self.assertEqual("TestDedent", utils.dedent("TestDedent")) + # Leading whitespace, single line + self.assertEqual("TestDedent", utils.dedent(" TestDedent")) + # Leading whitespace, multi line + input_string = " hello\n world" + expected_string = "hello\nworld" + self.assertEqual(expected_string, utils.dedent(input_string)) + + +class TestListToString(TestCase): + """ + Default function header from utils.py: + list_to_string(inlist, endsep="and", addquote=False) + + Examples: + no endsep: + [1,2,3] -> '1, 2, 3' + with endsep=='and': + [1,2,3] -> '1, 2 and 3' + with addquote and endsep + [1,2,3] -> '"1", "2" and "3"' + """ + + def test_list_to_string(self): + self.assertEqual('1, 2, 3', utils.list_to_string([1, 2, 3], endsep="")) + self.assertEqual('"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep="", addquote=True)) + self.assertEqual('1, 2 and 3', utils.list_to_string([1, 2, 3])) + self.assertEqual('"1", "2" and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)) + + +class TestMLen(TestCase): + """ + Verifies that m_len behaves like len in all situations except those + where MXP may be involved. + """ + + def test_non_mxp_string(self): + self.assertEqual(utils.m_len('Test_string'), 11) + + def test_mxp_string(self): + self.assertEqual(utils.m_len('|lclook|ltat|le'), 2) + + def test_mxp_ansi_string(self): + self.assertEqual(utils.m_len(ANSIString('|lcl|gook|ltat|le|n')), 2) + + def test_non_mxp_ansi_string(self): + self.assertEqual(utils.m_len(ANSIString('|gHello|n')), 5) + + def test_list(self): + self.assertEqual(utils.m_len([None, None]), 2) + + def test_dict(self): + self.assertEqual(utils.m_len({'hello': True, 'Goodbye': False}), 2) + + +class TestTimeformat(TestCase): + """ + Default function header from utils.py: + time_format(seconds, style=0) + + """ + + def test_style_0(self): + """Test the style 0 of time_format.""" + self.assertEqual(utils.time_format(0, 0), "00:00") + self.assertEqual(utils.time_format(28, 0), "00:00") + self.assertEqual(utils.time_format(92, 0), "00:01") + self.assertEqual(utils.time_format(300, 0), "00:05") + self.assertEqual(utils.time_format(660, 0), "00:11") + self.assertEqual(utils.time_format(3600, 0), "01:00") + self.assertEqual(utils.time_format(3725, 0), "01:02") + self.assertEqual(utils.time_format(86350, 0), "23:59") + self.assertEqual(utils.time_format(86800, 0), "1d 00:06") + self.assertEqual(utils.time_format(130800, 0), "1d 12:20") + self.assertEqual(utils.time_format(530800, 0), "6d 03:26") + + def test_style_1(self): + """Test the style 1 of time_format.""" + self.assertEqual(utils.time_format(0, 1), "0s") + self.assertEqual(utils.time_format(28, 1), "28s") + self.assertEqual(utils.time_format(92, 1), "1m") + self.assertEqual(utils.time_format(300, 1), "5m") + self.assertEqual(utils.time_format(660, 1), "11m") + self.assertEqual(utils.time_format(3600, 1), "1h") + self.assertEqual(utils.time_format(3725, 1), "1h") + self.assertEqual(utils.time_format(86350, 1), "23h") + self.assertEqual(utils.time_format(86800, 1), "1d") + self.assertEqual(utils.time_format(130800, 1), "1d") + self.assertEqual(utils.time_format(530800, 1), "6d") + + def test_style_2(self): + """Test the style 2 of time_format.""" + self.assertEqual(utils.time_format(0, 2), "0 minutes") + self.assertEqual(utils.time_format(28, 2), "0 minutes") + self.assertEqual(utils.time_format(92, 2), "1 minute") + self.assertEqual(utils.time_format(300, 2), "5 minutes") + self.assertEqual(utils.time_format(660, 2), "11 minutes") + self.assertEqual(utils.time_format(3600, 2), "1 hour, 0 minutes") + self.assertEqual(utils.time_format(3725, 2), "1 hour, 2 minutes") + self.assertEqual(utils.time_format(86350, 2), "23 hours, 59 minutes") + self.assertEqual(utils.time_format(86800, 2), + "1 day, 0 hours, 6 minutes") + self.assertEqual(utils.time_format(130800, 2), + "1 day, 12 hours, 20 minutes") + self.assertEqual(utils.time_format(530800, 2), + "6 days, 3 hours, 26 minutes") + + def test_style_3(self): + """Test the style 3 of time_format.""" + self.assertEqual(utils.time_format(0, 3), "") + self.assertEqual(utils.time_format(28, 3), "28 seconds") + self.assertEqual(utils.time_format(92, 3), "1 minute 32 seconds") + self.assertEqual(utils.time_format(300, 3), "5 minutes 0 seconds") + self.assertEqual(utils.time_format(660, 3), "11 minutes 0 seconds") + self.assertEqual(utils.time_format(3600, 3), + "1 hour, 0 minutes") + self.assertEqual(utils.time_format(3725, 3), + "1 hour, 2 minutes 5 seconds") + self.assertEqual(utils.time_format(86350, 3), + "23 hours, 59 minutes 10 seconds") + self.assertEqual(utils.time_format(86800, 3), + "1 day, 0 hours, 6 minutes 40 seconds") + self.assertEqual(utils.time_format(130800, 3), + "1 day, 12 hours, 20 minutes 0 seconds") + self.assertEqual(utils.time_format(530800, 3), + "6 days, 3 hours, 26 minutes 40 seconds") + + def test_style_4(self): + """Test the style 4 of time_format.""" + self.assertEqual(utils.time_format(0, 4), "0 seconds") + self.assertEqual(utils.time_format(28, 4), "28 seconds") + self.assertEqual(utils.time_format(92, 4), "a minute") + self.assertEqual(utils.time_format(300, 4), "5 minutes") + self.assertEqual(utils.time_format(660, 4), "11 minutes") + self.assertEqual(utils.time_format(3600, 4), "an hour") + self.assertEqual(utils.time_format(3725, 4), "an hour") + self.assertEqual(utils.time_format(86350, 4), "23 hours") + self.assertEqual(utils.time_format(86800, 4), "a day") + self.assertEqual(utils.time_format(130800, 4), "a day") + self.assertEqual(utils.time_format(530800, 4), "6 days") + self.assertEqual(utils.time_format(3030800, 4), "a month") + self.assertEqual(utils.time_format(7030800, 4), "2 months") + self.assertEqual(utils.time_format(40030800, 4), "a year") + self.assertEqual(utils.time_format(90030800, 4), "2 years") + + def test_unknown_format(self): + """Test that unknown formats raise exceptions.""" + self.assertRaises(ValueError, utils.time_format, 0, 5) + self.assertRaises(ValueError, utils.time_format, 0, "u")