1. Marc-Alexandre Chan
  2. DailyPromptBot

Commits

Marc-Alexandre Chan  committed 95e7caa

Added unit tests for util.py and config.py

  • Participants
  • Parent commits 44ac079
  • Branches dev-minibot

Comments (0)

Files changed (6)

File minibot/DailyPrompt-minibot.psproj

View file
 Count=4
 
 [Project\ChildNodes\Node0\ChildNodes\Node2]
+ClassName=TProjectFolderNode
+Name=test
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node0]
+ClassName=TProjectFolderNode
+Name=mock
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node1]
+ClassName=TProjectFileNode
+FileName=$[Project-Path]test\__init__.py
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node2]
+ClassName=TProjectFileNode
+FileName=$[Project-Path]test\blank.ini
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node3]
+ClassName=TProjectFileNode
+FileName=$[Project-Path]test\config.ini
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node4]
+ClassName=TProjectFileNode
+FileName=$[Project-Path]test\config.py
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes\Node5]
+ClassName=TProjectFileNode
+FileName=$[Project-Path]test\util.py
+
+[Project\ChildNodes\Node0\ChildNodes\Node2\ChildNodes]
+Count=6
+
+[Project\ChildNodes\Node0\ChildNodes\Node3]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]../minibot.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node3]
+[Project\ChildNodes\Node0\ChildNodes\Node4]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]__init__.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node4]
+[Project\ChildNodes\Node0\ChildNodes\Node5]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]config.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node5]
+[Project\ChildNodes\Node0\ChildNodes\Node6]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]db.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node6]
+[Project\ChildNodes\Node0\ChildNodes\Node7]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]errors.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node7]
+[Project\ChildNodes\Node0\ChildNodes\Node8]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]events.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node8]
+[Project\ChildNodes\Node0\ChildNodes\Node9]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]eventscheduler.py
 
-[Project\ChildNodes\Node0\ChildNodes\Node9]
+[Project\ChildNodes\Node0\ChildNodes\Node10]
 ClassName=TProjectFileNode
 FileName=$[Project-Path]util.py
 
 [Project\ChildNodes\Node0\ChildNodes]
-Count=10
+Count=11
 
 [Project\ChildNodes\Node1]
 ClassName=TProjectRunConfiguationsNode

File minibot/test/__init__.py

View file
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# The Daily Prompt Mini-Bot - A Shut Up and Write Project
+# Unit Tests
+# Author: Marc-Alexandre Chan <laogeodritt at arenthil.net>
+#-------------------------------------------------------------------------------
+#
+# Copyright (c) 2012 Marc-Alexandre Chan. Licensed under the GNU GPL version 3
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#-------------------------------------------------------------------------------

File minibot/test/blank.ini

View file
+# required to avoid errors
+[reddit]
+target = spacedicks
+user = myusername
+password = mypassword

File minibot/test/config.ini

View file
+[minibot]
+salt = Write a little every day!
+user_agent = DailyPromptMinibot/TestConfig
+refresh_rate = 2
+queue_rate = 32
+msg_rate     = 4
+msg_chunk    = 42
+default_time = 16:15:14
+suggestions_day = 3
+suggestions_time = 18:17:16
+pidfile_path = piddly.pid
+pidfile_timeout = 60
+
+[log]
+file = minibot.log
+db_file = minibot-db.log
+level = error
+db_level = on
+
+format = potato potato potato
+date_format = %Y-%m-%d
+
+[sqlite]
+file = minibot.sqlite
+tableprefix = kwilly
+
+[reddit]
+target = spacedicks
+user = myusername
+password = mypassword
+
+[users]
+bob=100
+jane=100
+ashley=9001
+keren=52

File minibot/test/config.py

View file
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# The Daily Prompt Mini-Bot - A Shut Up and Write Project
+# Unit Tests
+# Author: Marc-Alexandre Chan <laogeodritt at arenthil.net>
+#-------------------------------------------------------------------------------
+#
+# Copyright (c) 2012 Marc-Alexandre Chan. Licensed under the GNU GPL version 3
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#-------------------------------------------------------------------------------
+
+# testing framework
+import unittest
+from mock import MagicMock, patch
+
+# mocks
+
+
+# module under test
+import minibot.config
+
+# modules necessary for patching or testing results
+from minibot.db import Prompt
+from minibot.errors import ConfigNameError, ConfigValueError
+
+# modules used in test code
+from datetime import time as dt_time
+from os.path import dirname, abspath
+import os
+import logging
+import sys
+
+class TestSection(unittest.TestCase):
+    def setUp(self):
+        self.default = zip(['a', 'b', 'c'], [1, 2, 3])
+        self.values  = zip(['a', 'b', 'd', 'e'], [11, 22, 44, 55])
+        self.allowed = ['a', 'b', 'c', 'd']
+        self.expect = {'a': 11, 'b': 22, 'c' : 3, 'd': 44}
+
+    def tearDown(self):
+        pass
+
+    def runTest(self):
+        self.obj = minibot.config.Section(
+                    self.values, self.default, self.allowed)
+
+        self.assertEqual(dict(self.obj.items()), self.expect,
+            'Incorrect loaded options reported via items()')
+        self.assertEqual(dict(self.obj.iteritems()), self.expect,
+            'Incorrect loaded options reported via iteritems()')
+        self.obj.set('d', '127')
+        self.assertEqual(self.obj.get('d'), '127',
+            "Failed to set and retrieve config value 'd' to '127'")
+        self.assertEqual(self.obj.get('d', int), 127,
+            "Failed to retrieve config value 'd' as int")
+        self.assertRaises(ConfigNameError, self.obj.set, 'invalid', 'asdf')
+
+class TestConfig(unittest.TestCase):
+    expected = {
+        'minibot' : {
+            'salt' : 'Write a little every day!',
+            'user_agent' : 'DailyPromptMinibot/TestConfig',
+            'refresh_rate' : 2,
+            'queue_rate' : 32,
+            'msg_rate' : 4,
+            'msg_chunk' : 42,
+            'default_time' : dt_time(16, 15, 14),
+            'suggestions_time' : dt_time(18, 17, 16),
+            'suggestions_day' : 3,
+            'pidfile_path' : 'piddly.pid',
+            'pidfile_timeout' : 60},
+        'log' : {
+            'file' : 'minibot.log',
+            'db_file' : 'minibot-db.log',
+            'level' : logging.ERROR,
+            'db_level' : logging.INFO,
+            'format' : 'potato potato potato',
+            'date_format' : '%Y-%m-%d'},
+        'sqlite' : {
+            'file' : 'minibot.sqlite',
+            'tableprefix' : 'kwilly'},
+        'reddit' : {
+            'target' : 'spacedicks',
+            'user' : 'myusername',
+            'password' : 'mypassword'},
+        'users' : {
+            'bob' : 100,
+            'jane' : 100,
+            'ashley' : 9001,
+            'keren' : 52}
+        }
+
+    def setUp(self):
+        self.Config = minibot.config.Config
+        self.blank = self.Config(abspath(dirname(__file__) + '/blank.ini'))
+
+    def tearDown(self):
+        pass
+
+    def testDefaults(self):
+        self.blank.verify()
+
+    def testConfig(self):
+        filename = abspath(dirname(__file__) + '/config.ini')
+        test_dir = abspath(dirname(__file__) + '/temp')
+        self.config = self.Config(filename)
+        self.assertEqual(self.config.get_filename(), filename,
+                        'filename mismatch')
+        self.assertTrue(self.config.verify(),
+                        'validity check of test data failed')
+
+        # check data
+        self.__check_expected_data(self.config, self.expected, 'Read test')
+
+        # quick check of items()/iteritems()
+        self.assertDictEqual(
+                dict(self.config.reddit.items()),
+                self.expected['reddit'])
+        self.assertDictEqual(
+                dict(self.config.reddit.iteritems()),
+                self.expected['reddit'])
+
+        # check file write
+        cwd = os.getcwd()
+        try:
+            try:
+                os.mkdir(test_dir)
+            except OSError: # already exists
+                pass
+            os.chdir(test_dir)
+
+            # check that write() calls writefp() - assumption made by test
+            with patch('minibot.config.Config.writefp'):
+                self.config.write('test-config-write.ini')
+                self.assertEqual(minibot.config.Config.writefp.call_count, 1,
+                    "write() not implemented using writefp(); update the test")
+
+            # and now check the actual output by writing and then reading
+            self.config.write('test-config-write.ini')
+            config_new = self.Config('test-config-write.ini')
+            self.__check_expected_data(config_new, self.expected, 'Write test')
+        finally: # clean up the test
+            try:
+                os.remove('test-config-write.ini')
+            except OSError:
+                pass
+            os.chdir(cwd)
+
+    def __check_expected_data(self, config, expect_dict, prefix=None):
+        for sname, esection in expect_dict.iteritems():
+            csection = getattr(config, sname)
+            for key, evalue in esection.iteritems():
+                if sname != 'users':
+                    cvalue = getattr(csection, key)
+                else:
+                    cvalue = csection.get_level(key)
+                self.assertEqual(cvalue, evalue,
+                    "{}Key {}.{} does not have expected value '{}': '{}' found".\
+                    format(prefix + ': ' if prefix else '',
+                           sname, key, evalue, cvalue))
+
+def suite():
+    doLoad = unittest.TestLoader().loadTestsFromTestCase
+    suites = [doLoad(TestSection), doLoad(TestConfig)]
+    return unittest.TestSuite(suites)
+
+def run(ostream=sys.stdout):
+    unittest.TextTestRunner(stream=ostream, verbosity=2).run(suite())

File minibot/test/util.py

View file
+# -*- coding: utf-8 -*-
+#-------------------------------------------------------------------------------
+# The Daily Prompt Mini-Bot - A Shut Up and Write Project
+# Unit Tests
+# Author: Marc-Alexandre Chan <laogeodritt at arenthil.net>
+#-------------------------------------------------------------------------------
+#
+# Copyright (c) 2012 Marc-Alexandre Chan. Licensed under the GNU GPL version 3
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#-------------------------------------------------------------------------------
+
+# testing framework
+import unittest
+from mock import MagicMock, patch
+
+# mocks
+
+
+# module under test
+import minibot.util
+
+# modules necessary for patching or testing results
+from minibot.db import Prompt
+from minibot.errors import CommandParameterError
+from datetime import datetime, time as dt_time, date as dt_date
+import time
+
+# modules used in test code
+import re
+import sys
+
+class TestMarkdownMixin(unittest.TestCase):
+    text = ('word', 'one line', 'some punctuation!',
+            'oh wow\nthis is two lines', '   leading space',
+            'trailing space     ', '    two line\n  leading space',
+            'two line    \ntrailing space    ',
+            'OMG!\n3 lines!\ntrailing newline!\n')
+    text_list = ['hello', 'goodbye', 'this is a good test\nno?',
+                 'one paragraph\n\ntwo paragraph\n\nthree paragraph']
+    text_bold = "**word**"
+    re_it = re.compile(r'([*_])word\1$')
+    text_code = "`word`"
+    text_codeblock = ('    word', '    one line', '    some punctuation!',
+                      '    oh wow\n    this is two lines',
+                      '       leading space', '    trailing space     ',
+                      '        two line\n      leading space',
+                      '    two line    \n    trailing space    ',
+                      '    OMG!\n    3 lines!\n    trailing newline!\n')
+    re_bullet = re.compile(r'([*+-]) +hello\n'
+                            r'\1 +goodbye\n'
+                            r'\1 +this is a good test\n'
+                            r' *no\?\n'
+                            r'\1 +one paragraph\n'
+                            r'\n'
+                            r'    two paragraph\n'
+                            r'\n'
+                            r'    three paragraph\n')
+    re_numbered = re.compile(r'\d+\. +hello\n'
+                            r'\d+\. +goodbye\n'
+                            r'\d+\. +this is a good test\n'
+                            r' *no\?\n'
+                            r'\d+\. +one paragraph\n'
+                            r'\n'
+                            r'    two paragraph\n'
+                            r'\n'
+                            r'    three paragraph\n')
+    text_quote = ('> word', '> one line', '> some punctuation!',
+                      '> oh wow\n> this is two lines',
+                      '>    leading space', '> trailing space     ',
+                      '>     two line\n>   leading space',
+                      '> two line    \n> trailing space    ',
+                      '> OMG!\n> 3 lines!\n> trailing newline!\n')
+
+    args_link = ("Go to Google!", "http://google.com")
+    text_link = "[Go to Google!](http://google.com)"
+
+    def setUp(self):
+        self.obj = minibot.util.MarkdownMixin()
+
+    def tearDown(self):
+        self.obj = None
+
+    def testmd_bold(self):
+        self.assertEqual(self.obj.md_bold(self.text[0]), self.text_bold)
+
+    def testmd_italic(self):
+        self.assertIsNotNone(self.re_it.match(self.obj.md_italic(self.text[0])))
+
+    def testmd_code(self):
+        self.assertEqual(self.obj.md_code(self.text[0]), self.text_code)
+
+    def testmd_link(self):
+        text_link = self.obj.md_link(*self.args_link)
+        self.assertEqual(text_link, self.text_link)
+
+    def testmd_codeblock(self):
+        for text, expected in zip(self.text, self.text_codeblock):
+            self.assertEqual(self.obj.md_codeblock(text), expected)
+
+    def testmd_quote(self):
+        for text, expected in zip(self.text, self.text_quote):
+            self.assertEqual(self.obj.md_quote(text), expected)
+
+    def testmd_bullet(self):
+        output = self.obj.md_bullet(self.text_list)
+        self.assertIsNotNone(self.re_bullet.match(output),
+            'returned value does not match bullet list format')
+
+    def testmd_numbered(self):
+        output = self.obj.md_numbered(self.text_list)
+        self.assertIsNotNone(self.re_numbered.match(output),
+            'returned value does not match numbered list format')
+
+class TestDateParseMixin(unittest.TestCase):
+    est_tzoffset = 18000
+    edt_tzoffset = 14400
+
+    def setUp(self):
+        self.obj = minibot.util.DateParseMixin()
+        self.utctime = datetime(2012, 8, 4,  1, 12, 8)
+        self.esttime = datetime(2012, 8, 3, 20, 12, 8)
+        self.edttime = datetime(2012, 8, 3, 21, 12, 8)
+
+    def tearDown(self):
+        self.obj = None
+
+    def test_is_dst(self):
+        nodst = datetime(2012, 12, 25, 11, 11, 11)
+        dst   = datetime(2012, 7, 21, 23, 11, 11)
+        if time.daylight: # _is_dst only true if DST exists in locale
+            self.assertTrue(self.obj._is_dst(dst))
+        else:
+            self.assertFalse(self.obj._is_dst(dst))
+        self.assertFalse(self.obj._is_dst(nodst))
+
+    def test_parse_datetime(self):
+        pdt = self.obj._parse_datetime # shorthand
+        d_abd = datetime(2012, 8, 3, 0, 0, 0) # date w/o time
+        d_mbd = datetime(2012, 7, 11, 10, 03, 06) # date w time
+        d_pbd = datetime(2012, 6, 24, 23, 59, 59) # date w time (pm)
+        t_pi  = dt_time(3, 14, 15) # just a time
+        t_pm  = dt_time(23, 11, 11) # pm
+        t_nos = dt_time(15, 42) # no seconds
+
+        # basic time tests
+        self.assertEqual(pdt(time_str='3:14:15'), t_pi)
+        self.assertEqual(pdt(time_str='23:11:11'), t_pm)
+        self.assertEqual(pdt(time_str='11:11:11 PM'), t_pm)
+        self.assertEqual(pdt(time_str='11:11:11PM'), t_pm)
+        self.assertEqual(pdt(time_str='11:11:11 pm'), t_pm)
+        self.assertEqual(pdt(time_str='11:11:11pm'), t_pm)
+        self.assertEqual(pdt(time_str='15:42'), t_nos)
+        self.assertEqual(pdt(time_str='03:42 pm'), t_nos)
+
+        # basic datetime tests
+        self.assertEqual(pdt('2012-08-03'), d_abd)
+        self.assertEqual(pdt('2012/08/03'), d_abd)
+        self.assertEqual(pdt('2012-07-11', '10:03:06'), d_mbd)
+        self.assertEqual(pdt('2012-06-24', '23:59:59'), d_pbd)
+        self.assertEqual(pdt('2012-06-24', '11:59:59 pm'), d_pbd)
+
+        # weird data that should work
+        self.assertEqual(pdt('2012  - 08  -03'), d_abd)
+        self.assertEqual(pdt('2012-8-3'), d_abd)
+        self.assertEqual(pdt('12-08-03'), d_abd)
+        self.assertEqual(pdt('99-08-03'), datetime(1999, 8, 3, 0, 0, 0))
+        self.assertEqual(pdt('2012/08/3'), d_abd)
+        self.assertEqual(pdt('2012-07-11', '10:3:6'), d_mbd)
+        self.assertEqual(pdt('2012-06-24', '23 :59:  59'), d_pbd)
+        self.assertEqual(pdt('2012-06-24', '11:59:59Pm'), d_pbd)
+        self.assertEqual(pdt('2012-06-24', '11:59:59PM'), d_pbd)
+        self.assertEqual(pdt('2012-06-24', '11:59:59 pM'), d_pbd)
+
+        # bad data
+        self.assertRaises(CommandParameterError, pdt, "I'm a penguin!")
+        self.assertRaises(CommandParameterError, pdt, '2004-11-32')
+        self.assertRaises(CommandParameterError, pdt, '2004-13-01')
+        self.assertRaises(CommandParameterError, pdt, None, "Yay penguins!")
+        self.assertRaises(CommandParameterError, pdt, None, '10:60:00')
+        self.assertRaises(CommandParameterError, pdt, None, '10:11:12 ma')
+
+        # datetime object tests: the function, passed with a single datetime
+        # object, will return that object. The function, passed with a single
+        # date and a single time object, will return a datetime object
+        # constructed with the argument objects.
+        self.assertEqual(pdt(d_pbd), d_pbd)
+        self.assertEqual(pdt(d_pbd.date(), d_pbd.time()), d_pbd)
+        self.assertEqual(pdt(None, t_pi), t_pi)
+
+        # blank input tests
+        self.assertIsNone(pdt(), '_parse_datetime(None) failed')
+        self.assertIsNone(pdt(None), '_parse_datetime(None) failed')
+        self.assertIsNone(pdt(None, None), '_parse_datetime(None, None) failed')
+        self.assertRaises(CommandParameterError, pdt, '', None)
+        self.assertRaises(CommandParameterError, pdt, None, '')
+
+    @patch('time.altzone',  new=int(edt_tzoffset))
+    @patch('time.timezone', new=int(est_tzoffset))
+    def test_utctime(self):
+        with patch(
+                    'minibot.util.DateParseMixin._is_dst',
+                    new=MagicMock(return_value=True)):
+            self.assertEqual(self.obj._utctime(self.edttime), self.utctime)
+        with patch(
+                    'minibot.util.DateParseMixin._is_dst',
+                    new=MagicMock(return_value=False)):
+            self.assertEqual(self.obj._utctime(self.esttime), self.utctime)
+
+    @patch('time.altzone',  new=int(edt_tzoffset))
+    @patch('time.timezone', new=int(est_tzoffset))
+    def test_localtime(self):
+        with patch(
+                    'minibot.util.DateParseMixin._is_dst',
+                    new=MagicMock(return_value=True)):
+            self.assertEqual(self.obj._localtime(self.utctime), self.edttime)
+        with patch(
+                    'minibot.util.DateParseMixin._is_dst',
+                    new=MagicMock(return_value=False)):
+            self.assertEqual(self.obj._localtime(self.utctime), self.esttime)
+
+
+@patch('minibot.db.Prompt.approver.c',
+        new=MagicMock(uname='test_approver_name'))
+@patch('minibot.db.Prompt.approver')
+class TestDataFormatMixin(unittest.TestCase):
+    def setUp(self):
+        self.obj = minibot.util.DataFormatMixin()
+        self.dateparser = minibot.util.DateParseMixin()
+        self.md = minibot.util.MarkdownMixin()
+        self.prompt = Prompt('test_title', 'test_text', 'test_moderator',
+                        datetime(2012, 6, 21, 11, 11, 11), 'test_user',
+                        'tstuid', 'test_source_url', 'test_source_thread',
+                        'tstthrid')
+        self.prompt.id = 271
+        self.prompt.status = Prompt.STATUS_POSTED
+        self.prompt.post_time = datetime(2012, 06, 21, 12, 11, 10)
+        self.prompt.r_post_id = 'tstpstid'
+
+    def tearDown(self):
+        self.obj = self.dateparser = self.md = self.prompt = None
+
+    def test_format_prompt(self, *dummy):
+        """ Crude format test: checks that every field is substring of output,
+        and that the short form is single-line.
+        """
+        func = self.obj._format_prompt
+        text1 = func(self.prompt)
+        text2 = func(self.prompt, True)
+        text3 = func(self.prompt, False)
+
+        self.__test_format_prompt_check_fields(text2, 'short format')
+        self.__test_format_prompt_check_fields(text3, 'long format')
+        # check single line
+
+        self.assertEqual(text1, text3,
+            '_format_prompt() default behaviour is not _format_prompt(False)')
+
+    def __test_format_prompt_check_fields(self, text, ident):
+        p = self.prompt
+
+        self.assertIn('{:d}'.format(p.id), text,
+                ''.join(['id not in ', ident]))
+
+        self.assertIn(self.obj._localtime(p.post_time).
+                strftime('%Y-%m-%d %H:%M:%S'), text,
+                ''.join(['post time not in ', ident]))
+
+        status_link = self.md.md_link(
+            p.get_status_string(), self.obj._format_url(p.r_post_id))
+        self.assertIn(status_link, text.lower(),
+            ''.join(['status and post link not in ', ident]))
+
+        self.assertIn(p.title, text,
+            ''.join(['title not in ', ident]))
+
+        self.assertIn(self.obj._localtime(p.submit_time).
+                strftime('%Y-%m-%d %H:%M:%S'), text,
+                ''.join(['submit time not in ', ident]))
+
+        source_link = self.md.md_link(p.user, p.r_source_url)
+        self.assertIn(status_link, text,
+            ''.join(['submitter and source link not in ', ident]))
+
+        self.assertIn(p.approver.c.uname, text,
+            ''.join(['approver name not in ', ident]))
+
+        self.assertIn(p.text[0:self.obj.SHORT_TEXT_LENGTH-3], text,
+            ''.join(['text not in ', ident]))
+
+    def test_header_prompt(self, *dummy):
+        func = self.obj._header_prompt
+        text1 = func()
+        text2 = func(True)
+        text3 = func(False)
+        self.assertTrue(isinstance(text1, basestring) and len(text1),
+            '_header_prompt() does not return string or empty string returned')
+        self.assertTrue(isinstance(text2, basestring) and len(text2),
+            '_header_prompt(True) does not return string '
+            'or empty string returned')
+        self.assertTrue(isinstance(text3, basestring) and len(text3),
+            '_header_prompt(False) does not return string '
+            'or empty string returned')
+        self.assertEqual(text1, text3,
+            '_header_prompt() default behaviour is not _header_prompt(False)')
+
+    def test_format_url(self, *dummy):
+        self.assertEqual(
+                         self.obj._format_url('aq64ld'),
+                         'http://reddit.com/aq64ld',
+                         "Dude, how did you mess up concatenating "
+                         "two simple strings?")
+
+class TestGlobalFunctions(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testclassname(self):
+        self.assertEqual(minibot.util.classname(self), 'TestGlobalFunctions')
+
+    def testlog_exc_info(self):
+        # somewhat frivolous as a test
+        try:
+            raise ValueError()
+        except ValueError:
+            log_info = minibot.util.log_exc_info()
+            self.assertTrue(
+                    isinstance(log_info, tuple) and
+                    len(log_info) == 3)
+            for item in log_info:
+                self.assertTrue(
+                    isinstance(item, tuple) and
+                    isinstance(item[0], basestring))
+
+    def testget_reddit_url(self):
+        reddit = MagicMock(config=MagicMock(_short_domain='!!test_shortdom',
+                                             domain='!!domain'))
+        self.assertEqual(
+                        minibot.util.get_reddit_url('!!id', reddit, True),
+                        '!!test_shortdom/!!id')
+        self.assertEqual(
+                        minibot.util.get_reddit_url('!!id', reddit),
+                        'http://!!domain/!!id')
+
+
+def suite():
+    doLoad = unittest.TestLoader().loadTestsFromTestCase
+    suites = [doLoad(TestMarkdownMixin),
+              doLoad(TestDateParseMixin),
+              doLoad(TestDataFormatMixin),
+              doLoad(TestGlobalFunctions)]
+    return unittest.TestSuite(suites)
+
+def run(ostream=sys.stdout):
+    unittest.TextTestRunner(stream=ostream, verbosity=2).run(suite())