Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tests/coretests.py
blob: 1cc5431177b761be9c3ed5f8aebc615245d77f69 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# Copyright (C) 2009, Tutorius.org
# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
"""
Core Tests

This module contains all the tests that pertain to the usage of the Tutorius
Core. This means that the Event Filters, the Finite State Machine and all the 
related elements and interfaces are tested here.

Usage of actions and event filters is tested, but not the concrete actions
and event filters. Those are in their separate test module

"""

import unittest

from copy import deepcopy
import logging
from sugar.tutorius.actions import *
from sugar.tutorius.addon import *
from sugar.tutorius.core import *
from sugar.tutorius.filters import *

from actiontests import CountAction, FakeEventFilter

# Helper classes to help testing
class SimpleTutorial(Tutorial):
    """
    Fake tutorial
    """
    def __init__(self, start_name="INIT"):
        #Tutorial.__init__(self, "Simple Tutorial", None)
        self.current_state_name = start_name
        self.activity = "TODO : This should be an activity"
    
    def set_state(self, name):
        self.current_state_name = name

class TutorialTest(unittest.TestCase):
    """Tests the tutorial functions that are not covered elsewhere."""
    def test_detach(self):
        class Activity(object):
            name = "this"

        activity1 = Activity()
        activity2 = Activity()

        fsm = FiniteStateMachine("Sample example")
        
        tutorial = Tutorial("Test tutorial", fsm)

        assert tutorial.activity == None, "There is a default activity in the tutorial"

        tutorial.attach(activity1)

        assert tutorial.activity == activity1, "Activity should have been associated to this tutorial"

        tutorial.attach(activity2)
        assert tutorial.activity == activity2, "Activity should have been changed to activity2"

class TutorialWithFSM(Tutorial):
    """
    Fake tutorial, but associated with a FSM.
    """
    def __init__(self, start_name="INIT", fsm=None):
        Tutorial.__init__(self, start_name, fsm)
        self.activity = activity.Activity()
    
class TrueWhileActiveAction(Action):
    """
    This action's active member is set to True after a do and to False after
    an undo.
    
    Used to verify that a State correctly triggers the do and undo actions.
    """
    def __init__(self):
        Action.__init__(self)
        self.active = False
        
    def do(self):
        self.active = True
        
    def undo(self):
        self.active = False

# State testing class
class StateTest(unittest.TestCase):
    """
    This class has to test the State interface as well as the expected 
    functionality.
    """
    
    def test_action_toggle(self):
        """
        Validate that the actions are properly done on setup and undone on
        teardown.
        
        Pretty awesome.
        """
        act = TrueWhileActiveAction()
        
        state = State("action_test", action_list=[act])
        
        assert act.active == False, "Action is not initialized properly"
        
        state.setup()
        
        assert act.active == True, "Action was not triggered properly"
        
        state.teardown()
        
        assert act.active == False, "Action was not undone properly"
    
    def test_event_filter(self):
        """
        Tests the fact that the event filters are correctly installed on setup
        and uninstalled on teardown.
        """
        event_filter = addon.create('TriggerEventFilter')
        
        state = State("event_test", event_filter_list=[(event_filter, "second_state")])
        state.set_tutorial(SimpleTutorial())
        
        assert event_filter.toggle_on_callback == False, "Wrong init of event_filter"
        assert event_filter._callback == None, "Event filter has a registered callback before installing handlers"
        
        state.setup()
        
        assert event_filter._callback != None, "Event filter did not register callback!"
        
        # 'Trigger' the event - This is more like a EventFilter test.
        event_filter.do_callback()
        
        assert event_filter.toggle_on_callback == True, "Event filter did not execute callback"
        
        state.teardown()
        
        assert event_filter._callback == None, "Event filter did not remove callback properly"
    
    def test_warning_set_tutorial_twice(self):
        """
        Calls set_tutorial twice and expects a warning on the second.
        """
        state = State("start_state")
        tut = SimpleTutorial("First")
        tut2 = SimpleTutorial("Second")
        
        state.set_tutorial(tut)
        
        try:
            state.set_tutorial(tut2)
            assert False, "No RuntimeWarning was raised on second set_tutorial"
        except :
            pass
        
    def test_add_action(self):
        """
        Tests on manipulating the actions inside a state.
        """
        state = State("INIT")
        
        act1 = CountAction()
        act2 = CountAction()
        act3 = CountAction()
        
        # Try to add the actions
        assert state.add_action(act1), "Could not add the first action"
        assert state.add_action(act2), "Could not add the second action"
        assert state.add_action(act3), "Could not add the third action"
        
        # Fetch the associated actions
        actions = state.get_action_list()
        
        # Make sure all the actions are present in the state
        assert act1 in actions and act2 in actions and act3 in actions, \
            "The actions were not properly inserted in the state"
        
        # Clear the list
        state.clear_actions()
        
        # Make sure the list of actions is empty now
        assert len(state.get_action_list()) == 0, "Clearing of actions failed"
        
    def test_add_event_filter(self):
        state = State("INIT")
        
        event1 = addon.create('TriggerEventFilter')
        event2 = addon.create('TriggerEventFilter')
        
        # Insert the event filters
        assert state.add_event_filter(event1, "s"), "Could not add event filter 1"
        
        # Make sure we cannot insert an event twice
        assert state.add_event_filter(event1, "s") == False, "Could add twice the event filter"
        assert state.add_event_filter(event2, "t") == False, "Could add event filter 2"
        
        # Get the list of event filters
        event_filters = map(lambda x: x[0],state.get_event_filter_list())
        
        #even if we added only the event 1, they are equivalent
        assert event1 in event_filters and event2 in event_filters, \
            "The event filters were not all added inside the state"
        
        # Clear the list
        state.clear_event_filters()
        
        assert len(state.get_event_filter_list()) == 0, \
            "Could not clear the event filter list properly"

    def test_eq_simple(self):
        """
        Two empty states with the same name must be identical
        """
        st1 = State("Identical")
        st2 = State("Identical")

        assert st1 == st2, "Empty states with the same name should be identical"

    def test_eq(self):
        """
        Test whether two states share the same set of actions and event filters.
        """
        st1 = State("Identical")
        st2 = State("Identical")

        non_state = object()

        act1 = addon.create("BubbleMessage", message="Hi", position=[132,450])
        act2 = addon.create("BubbleMessage", message="Hi", position=[132,450])

        event1 = addon.create("GtkWidgetEventFilter", object_id="0.0.0.1.1.2.3.1", event_name="clicked")

        act3 = addon.create("DialogMessage", message="Hello again.", position=[200, 400])

        # Build the first state
        st1.add_action(act1)
        st1.add_action(act3)
        st1.add_event_filter(event1, "nextState")

        # Build the second state
        st2.add_action(act2)
        st2.add_action(act3)
        st2.add_event_filter(event1, "nextState")

        # Make sure that they are identical for now
        assert st1 == st2, "States should be considered as identical"
        assert st2 == st1, "States should be considered as identical"
    
        # Modify the second bubble message action
        act2.message = "New message"

        # Since one action changed in the second state, this should indicate that the states
        # are not identical anymore
        assert not (st1 == st2), "Action was changed and states should be different"
        assert not (st2 == st1), "Action was changed and states should be different"

        # Make sure that trying to find identity with something else than a State object fails properly
        assert not (st1 == non_state), "Passing a non-State object should fail for identity"
        
        st2.name = "Not identical anymore"
        assert not(st1 == st2), "Different state names should give different states"
        st2.name = "Identical"

        st3 = deepcopy(st1)
        st3.add_action(addon.create("BubbleMessage", message="Hi!", position=[128,264]))

        assert not (st1 == st3), "States having a different number of actions should be different"

        st4 = deepcopy(st1)
        st4.add_event_filter(addon.create("GtkWidgetEventFilter", object_id="0.0.1.1.2.2.3", event_name="clicked"), "next_state")

        assert not (st1 == st4), "States having a different number of events should be different"

        st5 = deepcopy(st1)
        st5._event_filters = []
    
        st5.add_event_filter(addon.create("GtkWidgetEventFilter", object_id="0.1.2.3.4.1.2", event_name="pressed"), "other_state")

        assert not (st1 == st5), "States having the same number of event filters" \
             + " but those being different should be different"

class FSMTest(unittest.TestCase):
    """
    This class needs to text the interface and functionality of the Finite 
    State Machine.
    """

    def test_sample_usage(self):
        act_init = TrueWhileActiveAction()
        act_second = TrueWhileActiveAction()
        
        event_init = FakeEventFilter()
        
        content = {
            "INIT": State("INIT", action_list=[act_init],event_filter_list=[(event_init,"SECOND")]),
            "SECOND": State("SECOND", action_list=[act_second])
        }
        
        fsm = FiniteStateMachine("SampleUsage", state_dict=content)
        
        assert fsm is not None, "Unable to create FSM"
        
        tut = Tutorial("SampleUsageTutorial", fsm)
        
        tut.attach(None)
        event_init.set_tutorial(tut)
        
        assert fsm.current_state.name == "INIT", "Unable to set state to initial state"
        
        assert act_init.active, "FSM did not call the state's action DO properly"
        
        # Trigger the event of the INIT state
        event_init.do_callback()
        
        assert act_init.active == False, "FSM did not teardown INIT properly"
        
        assert fsm.current_state.name == "SECOND", "FSM did not switch to SECOND state"
        
        assert act_second.active == True, "FSM did not setup SECOND properly"
        
        tut.detach()
        
        assert act_second.active == False, "FSM did not teardown SECOND properly"
        
    
    
    def test_state_insert(self):
        """
        This is a simple test to insert, then find a state.
        """
        st1 = State("FakeState")
        
        fsm = FiniteStateMachine("StateInsertTest")
        
        fsm.add_state(st1)
        
        inserted_state = fsm.get_state_by_name(st1.name)
        
        assert inserted_state is st1, "Inserting, then fetching a state did not work"
        
        # Make sure we cannot insert it twice
        try :
            fsm.add_state(st1)
            assert False, "No error raised on addition of an already present state"
        except KeyError:
            pass
    
    def test_state_find_by_name(self):
        """
        Tests the interface for fetching a state by name.
        - Basic functionnality
        - Non-existent state
        """
        
        st1 = State("INIT")
        
        st2 = State("second")
        
        fsm = FiniteStateMachine("StateFindTest")
        
        fsm.add_state(st1)
        fsm.add_state(st2)
        
        # Test the fetch by name
        fetched_st1 = fsm.get_state_by_name(st1.name)
        
        assert fetched_st1 is st1, "Fetched state is not the same as the inserted one"
        
        fetched_st2 = fsm.get_state_by_name(st2.name)
        
        assert fetched_st2 is st2, "Fetched state is not the same as the inserted one"
        
        try:
            fsm.get_state_by_name("no such state")
            assert False, "Did not get a KeyError on non-existing key search"
        except KeyError:
            pass
        except Exception:
            assert False, "Did not get the right error on non-existing key search"
    
    def test_state_removal(self):
        """
        This test removes a state from the FSM. It also verifies that the links
        from other states going into the removed state are gone.
        """
        st1 = State("INIT", event_filter_list=[(addon.create('TriggerEventFilter'), "second")])
        st2 = State("second", event_filter_list=[(addon.create('TriggerEventFilter'), "third")])
        st3 = State("third", event_filter_list=[(addon.create('TriggerEventFilter'), "second")])
        
        fsm = FiniteStateMachine("StateRemovalTest")
        
        fsm.add_state(st1)
        fsm.add_state(st2)
        fsm.add_state(st3)
        
        # First tests - Removing a non-existing state and make sure we get a
        # KeyError
        try:
            fsm.remove_state("Non-existing")
            assert False, "Removing a non-existing state did not throw a KeyError"
        except KeyError:
            pass
        except Exception:
            assert False, "Removing a non-existing state dit not throw the right kind of exception"
        
        # Now try removing the second state
        fsm.remove_state("second")
        
        # Make sure it cannot be fetched
        try :
            fetched_state = fsm.get_state_by_name("second")
            assert False, "The supposedly removed state is still present in the FSM"
        except KeyError:
            pass
        
        # Make sure that there is no link to the removed state in the rest
        # of the FSM
        assert "second" not in fsm.get_following_states("INIT"), \
            "The link to second from INIT still exists after removal"
        
        assert "second" not in fsm.get_following_states("third"), \
            "The link to second from third still exists after removal"
    
    def test_set_same_state(self):
        fsm = FiniteStateMachine("Set same state")
        
        st1 = State("INIT")
        st1.add_action(CountAction())
        
        fsm.add_state(st1)
        
        tut = SimpleTutorial()
        
        fsm.set_tutorial(tut)
        
        fsm.set_state("INIT")
        
        assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \
            "The action was not triggered on 'INIT'"
        
        fsm.set_state("INIT")
        
        do_count = fsm.get_state_by_name("INIT").get_action_list()[0].do_count
        assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \
            "The action was triggered a second time, do_count = %d"%do_count
        
        undo_count = fsm.get_state_by_name("INIT").get_action_list()[0].undo_count
        assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0, \
            "The action has been undone unappropriately, undo_count = %d"%undo_count

    def test_setup(self):
        fsm = FiniteStateMachine("New state machine")
        
        try:
            fsm.setup()
            assert False, "fsm should throw an exception when trying to setup and not bound to a tutorial"
        except UnboundLocalError:
            pass    

    def test_setup_actions(self):
        tut = SimpleTutorial()

        states_dict = {"INIT": State("INIT")}
        fsm = FiniteStateMachine("New FSM", state_dict=states_dict)

        act = CountAction()
        fsm.add_action(act)

        fsm.set_tutorial(tut)
        
        fsm.setup()

        # Let's also test the current state name
        assert fsm.get_current_state_name() == "INIT", "Initial state should be INIT"

        assert act.do_count == 1, "Action should have been called during setup"

        fsm._fsm_has_finished = True

        fsm.teardown()

        assert act.undo_count == 1, "Action should have been undone"

    def test_string_rep(self):
        fsm = FiniteStateMachine("Testing machine")

        st1 = State("INIT")
        st2 = State("Other State")
        st3 = State("Final State")

        st1.add_action(addon.create("BubbleMessage", message="Hi!", position=[132,312]))

        fsm.add_state(st1)
        fsm.add_state(st2)
        fsm.add_state(st3)

        assert str(fsm) == "INIT, Final State, Other State, "
        
    def test_eq_(self):
        fsm = FiniteStateMachine("Identity test")

        non_fsm_object = object()
        
        assert not (fsm == non_fsm_object), "Testing with non FSM object should not give identity"
        
        # Compare FSMs
        act1 = CountAction()

        fsm.add_action(act1)

        fsm2 = deepcopy(fsm)

        assert fsm == fsm2

        act2 = CountAction()
        fsm2.add_action(act2)

        assert not(fsm == fsm2), \
            "FSMs having a different number of actions should be different"

        fsm3 = FiniteStateMachine("Identity test")

        act3 = addon.create("BubbleMessage", message="Hi!", position=[123,312])
        fsm3.add_action(act3)
        
        assert not(fsm3 == fsm), \
            "Actions having the same number of actions but different ones should be different"

        st1 = State("INIT")
        
        st2 = State("OtherState")

        fsm.add_state(st1)
        fsm.add_state(st2)

        fsm4 = deepcopy(fsm)

        assert fsm == fsm4

        st3 = State("Last State")

        fsm4.add_state(st3)

        assert not (fsm == fsm4), "FSMs having a different number of states should not be identical"

        fsm4.remove_state("OtherState")

        assert not (fsm == fsm4), "FSMs having different states should be different"
        
        fsm4.remove_state("Last State")
        
        st5 = State("OtherState")
        st5.add_action(CountAction())

        fsm4.add_state(st5)

        assert not(fsm == fsm4), "FSMs having states with same name but different content should be different"
    
class FSMExplorationTests(unittest.TestCase):
    def setUp(self):
        self.buildFSM()
    
    def buildFSM(self):
        """
        Create a sample FSM to play with in the rest of the tests.
        """
        st1 = State("INIT")
        st1.add_action(CountAction())
        st1.add_event_filter(addon.create('TriggerEventFilter'), "Second")
        st1.add_event_filter(addon.create('TriggerEventFilter'), "Third")
        
        st2 = State("Second")
        st2.add_action(TrueWhileActiveAction())
        st2.add_event_filter(addon.create('TriggerEventFilter'), "Third")
        st2.add_event_filter(addon.create('TriggerEventFilter'), "Fourth")
        
        st3 = State("Third")
        st3.add_action(CountAction())
        st3.add_action(TrueWhileActiveAction())

        self.fsm = FiniteStateMachine("ExplorationTestingMachine")
        self.fsm.add_state(st1)
        self.fsm.add_state(st2)
        self.fsm.add_state(st3)
    
    def validate_following_states(self, in_name, out_name_list):
        nextStates = self.fsm.get_following_states(in_name)
        assert list(nextStates).sort() == list(out_name_list).sort(), \
            "The following states for %s are wrong : got %s"%\
            (in_name, str(nextStates))
            
    def validate_previous_states(self, in_name, out_name_list):
        prevStates = self.fsm.get_previous_states(in_name)
        assert list(prevStates).sort() == list(out_name_list).sort(), \
            "The following states for %s are wrong : got %s"%\
            (in_name, str(prevStates))
    
    def test_get_following_states(self):
        self.validate_following_states("INIT", ('Second', 'Third'))
        
        self.validate_following_states("Second", ("Third", "Fourth"))
        
        self.validate_following_states("Third", ())
    
    def test_get_previous_states(self):
        self.validate_previous_states("INIT", ())
        
        self.validate_previous_states("Second", ("INIT"))
        
        self.validate_previous_states("Third", ("INIT", "Second"))
        
        self.validate_previous_states("Fourth", ("Second"))
        
if __name__ == "__main__":
    unittest.main()