1

Before this question is immediately marked as a duplicate let me say that I have already tried every solution which were the two questions most relevant to my situation. I would appreciate it if someone could at least look at my particular issue before closing this question if necessary.

I have a finite state machine object called e which is a MCFiniteSM object. At the core of e is a dictionary called state_dict which stores "process" ids ('1', '2', etc) and an associated dictionary which stores more information about each "process". I am running unittests to add processes, changes their states based on given parameters, etc. However, in between function calls in the unittest file the finite state machine seems to be cleared. I have looked at the two questions listed above in order to avoid this and persist changes but no matter what I try the changes to the finite state machine are not persisted. Here is the uniitest file.

from finite_state_machine import MCFiniteSM
from unittest import TestLoader, TestCase, main as unimain
from datetime import datetime
import time, calendar

class MyUnitTest(TestCase):

    @classmethod
    def setUpClass(cls):
        cls.e = MCFiniteSM()
        cls.timestamp = datetime.strftime(datetime.fromtimestamp(calendar.timegm(time.gmtime())), '%Y/%m/%d %H:%M:%S')

class TestFSM(MyUnitTest):

    @classmethod
    def setUpClass(cls):
        super(TestFSM, cls).setUpClass()
        #e = MCFiniteSM()
        #timestamp = datetime.strftime(datetime.fromtimestamp(calendar.timegm(time.gmtime())), '%Y/%m/%d %H:%M:%S')

    def test_add_convert_processes(self):

        self.e.add_process('1', 'S', self.timestamp, 100, 'start message for process 1')
        self.e.add_process('2', 'S', self.timestamp, 200, 'start message for process 2')
        self.e.add_process('3', 'S', self.timestamp, 300, 'start message for process 3')
        self.e.add_process('4', 'S', self.timestamp, 400, 'start message for process 4')
        self.e.add_process('5', 'S', self.timestamp, 500, 'start message for process 5')
        self.e.add_process('6', 'S', self.timestamp, 600, 'start message for process 6')

        self.assertEqual(self.e.state_dict['1'], {'id':'1', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':100, 'message': 'start message for process 1'})
        self.assertEqual(self.e.state_dict['2'], {'id':'2', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':200, 'message': 'start message for process 2'})
        self.assertEqual(self.e.state_dict['3'], {'id':'3', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':300, 'message': 'start message for process 3'})
        self.assertEqual(self.e.state_dict['4'], {'id':'4', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':400, 'message': 'start message for process 4'})
        self.assertEqual(self.e.state_dict['5'], {'id':'5', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':500, 'message': 'start message for process 5'})
        self.assertEqual(self.e.state_dict['6'], {'id':'6', 'message_type': 'S', 'timestamp':self.timestamp, 'state': 'i', 'threshold':600, 'message': 'start message for process 6'})

        self.e.convert_state('1', 'S')
        self.e.convert_state('2', 'S')
        self.e.convert_state('3', 'S')
        self.e.convert_state('4', 'S')
        self.e.convert_state('5', 'S')
        self.e.convert_state('6', 'S')

        self.assertEqual(self.e.state_dict['2']['state'], 'a')
        self.assertEqual(self.e.state_dict['3']['state'], 'a')
        self.assertEqual(self.e.state_dict['4']['state'], 'a')

        self.e.add_process('2', 'E', self.timestamp, None, 'end message for process 2')
        self.e.add_process('3', 'E', self.timestamp, None, 'end message for process 3')
        self.e.add_process('4', 'E', self.timestamp, None, 'end message for process 4')

        self.assertEqual(self.e.state_dict['2']['state'], 'i')
        self.assertEqual(self.e.state_dict['3']['state'], 'i')
        self.assertEqual(self.e.state_dict['4']['state'], 'i')

    def test_active_start_conversion(self):
        print self.e
        print 'trying...'
        import sys
        from StringIO import StringIO

        orig = sys.stdout
        try:
            output = StringIO()
            sys.stdout = output
            self.e.convert_state('1', 'S')
            out = output.getvalue().strip()
            test = "Process ID:", '1', "\n" \
            "Process Message Type:", 'S', "\n" \
            "Process timestamp:", self.timestamp, "\n" \
            "Process Threshold:", 100, "\n" \
            "Process Message:", 'start message for process 1', "\n" \
            "Log Message:", "PROCESS WITH ID 1 SENT MULTIPLE START MESSAGES", "\n"
            self.assertEqual(out, test)
        finally:
            sys.stdout = orig

if __name__ == '__main__':
        unimain()

'e' is the variable I want to keep modified between function calls. When I get down to TestFSM.test_active_start_conversion the size of e prints out 0, when it should be 6. The TestFSM.test_add_convert_processes method runs successfully.

The actual error is a key error. Here is the stack trace:

Error
Traceback (most recent call last):
  File "/home/Desktop/fsm_unit_tests.py", line 68, in test_active_start_conversion
    self.e.convert_state('1', 'S')
  File "/home/Desktop/finite_state_machine.py", line 26, in convert_state
    cb_id = self.state_dict[id]['id']
KeyError: '1'

The line where the error is thrown is the self.e.convert_state('1', 'S') where I am telling the fsm to change the state of the process with ID '1'. It believes that there is no process with ID 1 and upon printing the size of the fsm it believes the finite state machine is empty. This must be due to the fact that e is not continually maintained but I do not see why.

I have tried converting self.e to self.__class__.e (same with timestamp) as well as just keeping e and timestamp global and calling TestFSM.e and TestFSM.timestamp to persist changes. Both these solutions were listed in the other questions, both of them still produced a Key Error. I tried setting up setUpClass(), but that still produced a key error.

How can I persist e and timestamp?

Community
  • 1
  • 1
Jeremy Fisher
  • 2,510
  • 7
  • 30
  • 59
  • 1
    It seems to me that you're relying on the *order* under which the unit-tests execute. In this case, I'm guessing `test_active_start_conversion` runs before `test_add_convert_processes`. Since the methods are stored on the class dictionary and dicts are unordered, the order of execution is not defined (and can even vary from one run to the next with certain python versions/implementations). – mgilson Jun 22 '15 at 20:24
  • Thanks. It seems ordering was the problem. I changed the test names to "test_1" and "test_2" and at least it didn't throw the KeyError. I was unaware of ordering as I assumed it would interpret sequentially – Jeremy Fisher Jun 22 '15 at 20:27
  • You could use an `sqlite3` or `shelve` database to persist the data instead of storing it into a dictionary in a class instance. You could also persist data into a dictionary in a loop (a la `tornado` or `asyncio`) running in a separate process. -- I suggest this because it helps with thread safety, locking, scope and will persist for the duration of your tests. `shelve` is probably the easiest to work with given your current setup. – NuclearPeon Jun 22 '15 at 20:28

1 Answers1

5

Test methods are executed in alphabetical order. So test_active_start_conversion is executed before test_add_convert_processes.

However, really your tests should be independent - you should be doing the set up in the actual setUp method, not in a test method.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • I agree that the tests should be independent. I tried setting up e and timestamp in the setUpClass method but that always produces a KeyError. Is there a way to keep the tests independent while persisting changes? – Jeremy Fisher Jun 22 '15 at 20:31
  • +1 -- I never realized that unittest actually provides a consistent order, but apparently it does: "Note that the order in which the various test cases will be run is determined by sorting the test function names with respect to the built-in ordering for strings" – mgilson Jun 22 '15 at 20:31
  • It also strongly helps for clarity and decomposition to shunt all setup code into `setUp` method. Sometimes you can use a decorator. – smci Jun 22 '15 at 23:02