source: trunk/game/tests/test_dialogueprocessor.py @ 685

Revision 685, 13.7 KB checked in by technomage, 9 years ago (diff)

Patch by Technomage

  • Replaced the test_dialogue.py testsuite with the test_dialogueprocessor.py testsuite and added/updated unittest TestCases? for all public methods of the DialogueProcessor? class.
  • DialogueProcessor? was refactored to be much more liberal about raising exceptions instead of silently handling errors; the code documentation has been updated to describe this new behavior.
  • Refactored exception-handling code in the dialogueparsers.py gamemodel.py modules related to the dialogue engine to deal with the changes to the DialogueProcessor? class.
  • Added a new method to the Dialogue class, getRootSection, which returns the root DialogueSection? for the a particular dialogue; also added some error checking code for inputs to the constructor.
  • Updated the run_tests.py script to import config.py and attempt to read the FIFE Python module path from it, just like run.py.
  • Property svn:eol-style set to native
Line 
1#!/usr/bin/env python
2#
3#   This file is part of PARPG.
4#
5#   PARPG is free software: you can redistribute it and/or modify
6#   it under the terms of the GNU General Public License as published by
7#   the Free Software Foundation, either version 3 of the License, or
8#   (at your option) any later version.
9#
10#   PARPG is distributed in the hope that it will be useful,
11#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#   GNU General Public License for more details.
14#
15#   You should have received a copy of the GNU General Public License
16#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
17try:
18    # Python 2.6
19    import unittest2 as unittest
20except:
21    # Python 2.7
22    import unittest
23
24from scripts.dialogueprocessor import DialogueProcessor
25# NOTE Technomage 2010-12-08: Using the dialogue data structures might be a
26#    violation of unit test isolation, but ultimately they are just simple
27#    data structures that don't require much testing of their own so I feel
28#    that it isn't a mistake to use them.
29from scripts.dialogue import Dialogue, DialogueSection, DialogueResponse
30
31
32class NotInDict(object):
33    """
34    Object to return when a key could not be found in a dictionary and
35    None is not usable (e.g. because None can appear as a value in the dict).
36    """
37    __slots__ = []
38   
39    def __init__(self):
40        raise TypeError('NotInDict cannot be instantiated.')
41
42
43class MockPlayerCharacter(object):
44    pass
45
46
47class MockQuestEngine(object):
48    pass
49
50
51class MockDialogueAction(object):
52    keyword = 'mock_action'
53   
54    def __init__(self, *args, **kwargs):
55        self.arguments = (args, kwargs)
56        self.was_called = False
57        self.call_arguments = []
58   
59    def __call__(self, game_state):
60        self.was_called = True
61        self.call_arguments = ((game_state,), {})
62
63
64class TestDialogueProcessor(unittest.TestCase):
65    """Base class for tests of the L{DialogueProcessor} class."""
66    def assertStateEqual(self, object_, **state):
67        """
68        Assert that an object's attributes match an expected state.
69       
70        @param
71        """
72        object_dict = {}
73        for key in state.keys():
74            actual_value = getattr(object_, key, NotInDict)
75            if (actual_value is not NotInDict):
76                object_dict[key] = actual_value
77        self.assertDictContainsSubset(state, object_dict)
78   
79    def setUp(self):
80        self.npc_id = 'mr_npc'
81        self.dialogue = Dialogue(
82            npc_name='Mr. NPC',
83            avatar_path='/some/path',
84            start_section_id='main_section',
85            dialogue_sections=[
86                DialogueSection(
87                    id='main_section',
88                    text='This is the main dialogue section.',
89                    actions=[
90                        MockDialogueAction('foo'),
91                    ],
92                    responses=[
93                        DialogueResponse(
94                            text='A response.',
95                            next_section_id='another_section',
96                        ),
97                        DialogueResponse(
98                            text='A conditional response evaluated to True.',
99                            condition='True',
100                            actions=[
101                                MockDialogueAction('foo'),
102                            ],
103                            next_section_id='another_section',
104                        ),
105                        DialogueResponse(
106                            text='A conditional response evaluated to False.',
107                            condition='False',
108                            next_section_id='another_section',
109                        ),
110                        DialogueResponse(
111                            text='A response that ends the dialogue.',
112                            next_section_id='end',
113                        ),
114                    ],
115                ),
116                DialogueSection(
117                    id='another_section',
118                    text='This is another dialogue section.',
119                    responses=[
120                        DialogueResponse(
121                            text='End dialogue.',
122                            next_section_id='end',
123                        ),
124                    ],
125                ),
126            ]
127        )
128        self.game_state = {'pc': MockPlayerCharacter(),
129                           'quest': MockQuestEngine()}
130        self.dialogue_processor = DialogueProcessor(self.dialogue,
131                                                    self.game_state)
132
133
134class TestInitiateDialogue(TestDialogueProcessor):
135    """Tests of the L{DialogueProcessor.initiateDialogue} method."""
136    def testInitiateDialogue_setsState(self):
137        """Test initiateDialogue correctly sets DialogueProcessor state"""
138        dialogue_processor = self.dialogue_processor
139        dialogue_processor.initiateDialogue()
140       
141        # Root dialogue section should have been pushed onto the stack.
142        root_dialogue_section = \
143            self.dialogue.sections[self.dialogue.start_section_id]
144        self.assertStateEqual(dialogue_processor, in_dialogue=True,
145                              dialogue=self.dialogue,
146                              dialogue_section_stack=[root_dialogue_section])
147
148
149class TestEndDialogue(TestDialogueProcessor):
150    """Tests of the L{DialogueProcessor.endDialogue} method."""
151    def testEndDialogue_resetsState(self):
152        """Test endDialogue correctly resets DialogueProcessor state"""
153        dialogue_processor = self.dialogue_processor
154        # Case: No dialogue initiated.
155        assert not dialogue_processor.in_dialogue, \
156            'assumption that dialogue_processor has not initiated a dialogue '\
157            'violated'
158        self.assertStateEqual(dialogue_processor, in_dialogue=False,
159                              dialogue=self.dialogue,
160                              dialogue_section_stack=[])
161        # Case: Dialogue previously initiated.
162        dialogue_processor.initiateDialogue()
163        assert dialogue_processor.in_dialogue, \
164            'assumption that dialogue_processor initiated a dialogue violated'
165        dialogue_processor.endDialogue()
166        self.assertStateEqual(dialogue_processor, in_dialogue=False,
167                              dialogue=self.dialogue,
168                              dialogue_section_stack=[])
169
170
171class TestContinueDialogue(TestDialogueProcessor):
172    """Tests of the L{DialogueProcessor.continueDialogue} method."""
173    def setUp(self):
174        TestDialogueProcessor.setUp(self)
175        self.dialogue_processor.initiateDialogue()
176        self.dialogue_action = self.dialogue.getRootSection().actions[0]
177   
178    def testRunsDialogueActions(self):
179        """Test continueDialogue executes all DialogueActions"""
180        dialogue_processor = self.dialogue_processor
181        dialogue_processor.continueDialogue()
182        self.assertTrue(self.dialogue_action.was_called)
183        expected_tuple = ((self.game_state,), {})
184        self.assertTupleEqual(expected_tuple,
185                              self.dialogue_action.call_arguments)
186   
187    def testReturnsValidResponses(self):
188        """Test continueDialogue returns list of valid DialogueResponses"""
189        dialogue_processor = self.dialogue_processor
190        valid_responses = \
191            dialogue_processor.dialogue_section_stack[0].responses
192        valid_responses.pop(2)
193        # Sanity check, all "valid" responses should have a condition that
194        # evaluates to True.
195        for response in valid_responses:
196            if (response.condition is not None):
197                result = eval(response.condition, self.game_state, {})
198                self.assertTrue(result)
199        responses = dialogue_processor.continueDialogue()
200        self.assertItemsEqual(responses, valid_responses)
201
202class TestGetCurrentDialogueSection(TestDialogueProcessor):
203    """Tests of the L{DialogueProcessor.getCurrentDialogueSection} method."""
204    def setUp(self):
205        TestDialogueProcessor.setUp(self)
206        self.dialogue_processor.initiateDialogue()
207   
208    def testReturnsCorrectDialogueSection(self):
209        """Test getCurrentDialogueSection returns section at top of stack"""
210        dialogue_processor = self.dialogue_processor
211        expected_dialogue_section = \
212            self.dialogue.sections[self.dialogue.start_section_id]
213        actual_dialogue_section = \
214            dialogue_processor.getCurrentDialogueSection()
215        self.assertEqual(expected_dialogue_section, actual_dialogue_section)
216
217
218class TestRunDialogueActions(TestDialogueProcessor):
219    """Tests of the L{DialogueProcessor.runDialogueActions} method."""
220    def setUp(self):
221        TestDialogueProcessor.setUp(self)
222        self.dialogue_processor.initiateDialogue()
223        self.dialogue_section = DialogueSection(
224            id='some_section',
225            text='Test dialogue section.',
226            actions=[
227                MockDialogueAction('foo'),
228            ],
229        )
230        self.dialogue_response = DialogueResponse(
231            text='A response.',
232            actions=[
233                MockDialogueAction('foo'),
234            ],
235            next_section_id='end',
236        )
237   
238    def testExecutesDialogueActions(self):
239        """Test runDialogueActions correctly executes DialogueActions"""
240        dialogue_processor = self.dialogue_processor
241        # Case: DialogueSection
242        dialogue_processor.runDialogueActions(self.dialogue_section)
243        dialogue_section_action = self.dialogue_section.actions[0]
244        self.assertTrue(dialogue_section_action.was_called)
245        expected_call_args = ((self.game_state,), {})
246        self.assertTupleEqual(expected_call_args,
247                              dialogue_section_action.call_arguments)
248        # Case: DialogueResponse
249        dialogue_processor.runDialogueActions(self.dialogue_response)
250        dialogue_response_action = self.dialogue_response.actions[0]
251        self.assertTrue(dialogue_response_action.was_called)
252        self.assertTupleEqual(expected_call_args,
253                              dialogue_response_action.call_arguments)
254
255
256class TestGetValidResponses(TestDialogueProcessor):
257    """Tests of the L{DialogueProcessor.getValidResponses} method."""
258    def setUp(self):
259        TestDialogueProcessor.setUp(self)
260        self.dialogue_processor.initiateDialogue()
261   
262    def testReturnsValidResponses(self):
263        """Test getValidResponses returns list of valid DialogueResponses"""
264        dialogue_processor = self.dialogue_processor
265        valid_responses = \
266            dialogue_processor.dialogue_section_stack[0].responses
267        valid_responses.pop(2)
268        # Sanity check, all "valid" responses should have a condition that
269        # evaluates to True.
270        for response in valid_responses:
271            if (response.condition is not None):
272                result = eval(response.condition, self.game_state, {})
273                self.assertTrue(result)
274        responses = dialogue_processor.continueDialogue()
275        self.assertItemsEqual(responses, valid_responses)
276
277
278class TestReply(TestDialogueProcessor):
279    """Tests of the L{DialogueProcessor.reply} method."""
280    def setUp(self):
281        TestDialogueProcessor.setUp(self)
282        self.response = self.dialogue.getRootSection().responses[1]
283        self.ending_response = self.dialogue.getRootSection().responses[3]
284   
285    def testRaisesExceptionWhenNotInitiated(self):
286        """Test reply raises exception when called before initiateDialogue"""
287        dialogue_processor = self.dialogue_processor
288        # Sanity check: A dialogue must not have been initiated beforehand.
289        self.assertFalse(dialogue_processor.in_dialogue)
290        with self.assertRaisesRegexp(RuntimeError, r'initiateDialogue'):
291            dialogue_processor.reply(self.response)
292   
293    def testExecutesDialogueActions(self):
294        """Test reply correctly executes DialogueActions in DialogueResponse"""
295        dialogue_processor = self.dialogue_processor
296        dialogue_processor.initiateDialogue()
297        dialogue_processor.reply(self.response)
298        dialogue_action = self.response.actions[0]
299        self.assertTrue(dialogue_action.was_called)
300        expected_call_args = ((self.game_state,), {})
301        self.assertTupleEqual(expected_call_args,
302                              dialogue_action.call_arguments)
303   
304    def testJumpsToCorrectSection(self):
305        """Test reply pushes section specified by response onto stack"""
306        dialogue_processor = self.dialogue_processor
307        dialogue_processor.initiateDialogue()
308        # Sanity check: Test response's next_section_id attribute must be refer
309        # to a valid DialogueSection in the test Dialogue.
310        self.assertIn(self.response.next_section_id,
311                      self.dialogue.sections.keys())
312        dialogue_processor.reply(self.response)
313        root_section = self.dialogue.getRootSection()
314        next_section = self.dialogue.sections[self.response.next_section_id]
315        self.assertStateEqual(
316            dialogue_processor,
317            in_dialogue=True,
318            dialogue=self.dialogue,
319            dialogue_section_stack=[root_section, next_section],
320        )
321   
322    def testCorrectlyEndsDialogue(self):
323        """Test reply ends dialogue when DialogueResponse specifies 'end'"""
324        dialogue_processor = self.dialogue_processor
325        dialogue_processor.initiateDialogue()
326        # Sanity check: Test response must have a next_section_id of 'end'.
327        self.assertEqual(self.ending_response.next_section_id, 'end')
328        dialogue_processor.reply(self.ending_response)
329        self.assertStateEqual(dialogue_processor, in_dialogue=False,
330                              dialogue=self.dialogue,
331                              dialogue_section_stack=[])
332
333
334if __name__ == "__main__":
335    unittest.main()
Note: See TracBrowser for help on using the repository browser.