Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/sugar/tutorius/tests/coretests.py
blob: c27846d1e2c5cec3283be23a35f383b0f3ba97bf (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
# 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

import logging
from sugar.tutorius.actions import Action, OnceWrapper, ClickAction, TypeTextAction
from sugar.tutorius.core import *
from sugar.tutorius.filters import *


from actiontests import CountAction

# 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 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):
        self.active = False
        
    def do(self):
        self.active = True
        
    def undo(self):
        self.active = False
        
class ClickableWidget():
    """
    This class fakes a widget with a clicked() method
    """
    def __init__(self):
        self.click_count = 0
        
    def clicked(self):
        self.click_count += 1
      
class FakeTextEntry():
    """
    This class fakes a widget with an insert_text() method
    """
    def __init__(self):
        self.text_lines = []
        self.last_entered_line = ""
        self.displayed_text = ""
        
    def insert_text(self, text, index):
        self.last_entered_line = text
        self.text_lines.append(text)
        self.displayed_text = self.displayed_text[0:index] + text + self.displayed_text[index+1:]

class FakeParentWidget():
    """
    This class fakes a widet container, it implements the get_children() method
    """
    def __init__(self):
        self._children = []
        
    def add_child(self, child):
        self._children.append(child)
        
    def get_children(self):
        return self._children




class TriggerEventFilter(EventFilter):
    """
    This event filter can be triggered by simply calling its do_callback function.
    
    Used to fake events and see the effect on the FSM.
    """
    def __init__(self, next_state):
        EventFilter.__init__(self, next_state)
        self.toggle_on_callback = False
    
    def install_handlers(self, callback, **kwargs):
        """
        Forsakes the incoming callback function and just set the inner one.
        """
        self._callback = self._inner_cb
    
    def _inner_cb(self, event_filter):
        self.toggle_on_callback = not self.toggle_on_callback

class FakeEventFilter(TriggerEventFilter):
    """
    This is a fake event that is connected to the tutorial.
    
    The difference between this one and the TriggerEventFilter is that the
    tutorial's set_state will be called on the callback.
    
    Do not forget to add the do_callback() after creating the object.
    """
    def set_tutorial(self, tutorial):
        self.tutorial = tutorial
    
    def _inner_cb(self, event_filter):
        self.toggle_on_callback = not self.toggle_on_callback
        self.tutorial.set_state(event_filter.get_next_state())
    

class ClickActionTests(unittest.TestCase):
    """
    Test class for click action
    """
    def test_do_action(self):
        activity = FakeParentWidget()
        widget = ClickableWidget()
        activity.add_child(widget)
        ObjectStore().activity = activity
        
        action = ClickAction("0.0")
        
        assert widget == ObjectStore().activity.get_children()[0],\
            "The clickable widget isn't reachable from the object store \
            the test cannot pass"
        
        action.do()
        
        assert widget.click_count == 1, "clicked() should have been called by do()"
        
        action.do()
        
        assert widget.click_count == 2, "clicked() should have been called by do()"

    def test_undo(self):
        activity = FakeParentWidget()
        widget = ClickableWidget()
        activity.add_child(widget)
        ObjectStore().activity = activity
        
        action = ClickAction("0.0")
        
        assert widget == ObjectStore().activity.get_children()[0],\
            "The clickable widget isn't reachable from the object store \
            the test cannot pass"
        
        action.undo()
        
        #There is no undo for this action so the test should not fail
        assert True
        
        

class TypeTextActionTests(unittest.TestCase):
    """
    Test class for type text action
    """
    def test_do_action(self):
        activity = FakeParentWidget()
        widget = FakeTextEntry()
        activity.add_child(widget)
        ObjectStore().activity = activity

        test_text = "This is text"
       
        
        action = TypeTextAction("0.0", test_text)
        
        assert widget == ObjectStore().activity.get_children()[0],\
            "The clickable widget isn't reachable from the object store \
            the test cannot pass"
        
        action.do()
        
        assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
        
        action.do()
        
        assert widget.last_entered_line == test_text, "insert_text() should have been called by do()"
        assert len(widget.text_lines) == 2, "insert_text() should have been called twice"

    def test_undo(self):
        activity = FakeParentWidget()
        widget = FakeTextEntry()
        activity.add_child(widget)
        ObjectStore().activity = activity

        test_text = "This is text"
       
        
        action = TypeTextAction("0.0", test_text)
        
        assert widget == ObjectStore().activity.get_children()[0],\
            "The clickable widget isn't reachable from the object store \
            the test cannot pass"
        
        action.undo()
        
        #There is no undo for this action so the test should not fail
        assert True

# 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 = TriggerEventFilter("second_state")
        
        state = State("event_test", event_filter_list=[event_filter])
        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"
        
        # Try to add a second time an action that was already inserted
        assert state.add_action(act1) == False, "Not supposed to insert an action twice"
        
        # 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 = TriggerEventFilter("s")
        event2 = TriggerEventFilter("t")
        event3 = TriggerEventFilter("r")
        
        # Insert the event filters
        assert state.add_event_filter(event1), "Could not add event filter 1"
        assert state.add_event_filter(event2), "Could not add event filter 2"
        assert state.add_event_filter(event3), "Could not add event filter 3"
        
        # Make sure we cannot insert an event twice
        assert state.add_event_filter(event1) == False, "Could add twice the event filter"
        
        # Get the list of event filters
        event_filters = state.get_event_filter_list()
        
        assert event1 in event_filters and event2 in event_filters and event3 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"
    
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("SECOND")
        
        content = {
            "INIT": State("INIT", action_list=[act_init],event_filter_list=[event_init]),
            "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=[TriggerEventFilter("second")])
        st2 = State("second", event_filter_list=[TriggerEventFilter("third")])
        st3 = State("third", event_filter_list=[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
    
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(TriggerEventFilter("Second"))
        st1.add_event_filter(TriggerEventFilter("Third"))
        
        st2 = State("Second")
        st2.add_action(TrueWhileActiveAction())
        st2.add_event_filter(TriggerEventFilter("Third"))
        st2.add_event_filter(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()