Mercurial > parpg-core
diff tests/test_dialogueprocessor.py @ 0:1fd2201f5c36
Initial commit of parpg-core.
author | M. George Hansen <technopolitica@gmail.com> |
---|---|
date | Sat, 14 May 2011 01:12:35 -0700 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_dialogueprocessor.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# This file is part of PARPG. +# +# PARPG 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. +# +# PARPG 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 PARPG. If not, see <http://www.gnu.org/licenses/>. +try: + # Python 2.6 + import unittest2 as unittest +except: + # Python 2.7 + import unittest + +from parpg.common.utils import dedent_chomp +from parpg.dialogueprocessor import DialogueProcessor +# NOTE Technomage 2010-12-08: Using the dialogue data structures might be a +# violation of unit test isolation, but ultimately they are just simple +# data structures that don't require much testing of their own so I feel +# that it isn't a mistake to use them. +from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse, + DialogueGreeting) + +class MockDialogueAction(object): + keyword = 'mock_action' + + def __init__(self, *args, **kwargs): + self.arguments = (args, kwargs) + self.was_called = False + self.call_arguments = [] + + def __call__(self, game_state): + self.was_called = True + self.call_arguments = ((game_state,), {}) + + +class TestDialogueProcessor(unittest.TestCase): + """Base class for tests of the L{DialogueProcessor} class.""" + def assertStateEqual(self, object_, **state): + """ + Assert that an object's attributes match an expected state. + + @param + """ + object_dict = {} + for key in state.keys(): + if (hasattr(object_, key)): + actual_value = getattr(object_, key) + object_dict[key] = actual_value + self.assertDictContainsSubset(state, object_dict) + + def setUp(self): + self.npc_id = 'mr_npc' + self.dialogue = Dialogue( + npc_name='Mr. NPC', + avatar_path='/some/path', + default_greeting=DialogueSection( + id_='greeting', + text='This is the root dialogue section.', + actions=[ + MockDialogueAction('foo'), + ], + responses=[ + DialogueResponse( + text='A response.', + next_section_id='another_section', + ), + DialogueResponse( + text='A conditional response evaluated to True.', + condition='True', + actions=[ + MockDialogueAction('foo'), + ], + next_section_id='another_section', + ), + DialogueResponse( + text='A conditional response evaluated to False.', + condition='False', + next_section_id='another_section', + ), + DialogueResponse( + text='A response that ends the dialogue.', + next_section_id='end', + ), + ], + ), + greetings=[ + DialogueGreeting( + id_='alternative_greeting', + condition='use_alternative_root is True', + text='This is an alternate root section.', + responses=[ + DialogueResponse( + text='End dialogue.', + next_section_id='end', + ), + ], + ), + ], + sections=[ + DialogueSection( + id_='another_section', + text='This is another dialogue section.', + responses=[ + DialogueResponse( + text='End dialogue.', + next_section_id='end', + ), + ], + ), + ] + ) + self.game_state = {'use_alternative_root': False} + + +class TestInitiateDialogue(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.initiateDialogue} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue = Dialogue( + npc_name='Mr. NPC', + avatar_path='/some/path', + default_greeting=DialogueSection( + id_='greeting', + text='This is the one (and only) dialogue section.', + responses=[ + DialogueResponse( + text=dedent_chomp(''' + A response that moves the dialogue to + another_section. + '''), + next_section_id='another_section' + ), + DialogueResponse( + text='A response that ends the dialogue.', + next_section_id='end', + ), + ], + ), + sections=[ + DialogueSection( + id_='another_section', + text='This is another section.', + responses=[ + DialogueResponse( + text='A response that ends the dialogue', + next_section_id='end', + ) + ], + ), + ] + ) + self.dialogue_processor = DialogueProcessor(self.dialogue, {}) + + def testSetsState(self): + """initiateDialogue correctly sets DialogueProcessor state""" + dialogue_processor = self.dialogue_processor + dialogue_processor.initiateDialogue() + + # Default root dialogue section should have been pushed onto the stack. + default_greeting = self.dialogue.default_greeting + self.assertStateEqual(dialogue_processor, in_dialogue=True, + dialogue=self.dialogue, + dialogue_section_stack=[default_greeting]) + + def testEndsExistingDialogue(self): + """initiateDialogue ends a previously initiated dialogue""" + dialogue_processor = self.dialogue_processor + dialogue_processor.initiateDialogue() + valid_responses = dialogue_processor.continueDialogue() + dialogue_processor.reply(valid_responses[0]) + + # Sanity check. + assert dialogue_processor.in_dialogue + dialogue_processor.initiateDialogue() + default_greeting = self.dialogue.default_greeting + self.assertStateEqual(dialogue_processor, in_dialogue=True, + dialogue=self.dialogue, + dialogue_section_stack=[default_greeting]) + +class TestEndDialogue(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.endDialogue} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + + def testResetsState(self): + """endDialogue correctly resets DialogueProcessor state""" + dialogue_processor = self.dialogue_processor + # Case: No dialogue initiated. + assert not dialogue_processor.in_dialogue, \ + 'assumption that dialogue_processor has not initiated a dialogue '\ + 'violated' + self.assertStateEqual(dialogue_processor, in_dialogue=False, + dialogue=self.dialogue, + dialogue_section_stack=[]) + # Case: Dialogue previously initiated. + dialogue_processor.initiateDialogue() + assert dialogue_processor.in_dialogue, \ + 'assumption that dialogue_processor initiated a dialogue violated' + dialogue_processor.endDialogue() + self.assertStateEqual(dialogue_processor, in_dialogue=False, + dialogue=self.dialogue, + dialogue_section_stack=[]) + + +class TestContinueDialogue(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.continueDialogue} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + self.dialogue_processor.initiateDialogue() + self.dialogue_action = \ + self.dialogue.default_greeting.actions[0] + + def testRunsDialogueActions(self): + """continueDialogue executes all DialogueActions""" + dialogue_processor = self.dialogue_processor + dialogue_processor.continueDialogue() + self.assertTrue(self.dialogue_action.was_called) + expected_tuple = ((self.game_state,), {}) + self.assertTupleEqual(expected_tuple, + self.dialogue_action.call_arguments) + + def testReturnsValidResponses(self): + """continueDialogue returns list of valid DialogueResponses""" + dialogue_processor = self.dialogue_processor + valid_responses = \ + dialogue_processor.dialogue_section_stack[0].responses + valid_responses.pop(2) + # Sanity check, all "valid" responses should have a condition that + # evaluates to True. + for response in valid_responses: + if (response.condition is not None): + result = eval(response.condition, self.game_state, {}) + self.assertTrue(result) + responses = dialogue_processor.continueDialogue() + self.assertItemsEqual(responses, valid_responses) + + +class TestGetRootDialogueSection(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.getDialogueGreeting} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor( + self.dialogue, + {'use_alternative_root': True} + ) + self.dialogue_processor.initiateDialogue() + + def testReturnsCorrectDialogueSection(self): + """getDialogueGreeting returns first section with true condition""" + dialogue_processor = self.dialogue_processor + dialogue = self.dialogue + root_dialogue_section = dialogue_processor.getDialogueGreeting() + expected_dialogue_section = dialogue.greetings[0] + self.assertEqual(root_dialogue_section, expected_dialogue_section) + + +class TestGetCurrentDialogueSection(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.getCurrentDialogueSection} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + self.dialogue_processor.initiateDialogue() + + def testReturnsCorrectDialogueSection(self): + """getCurrentDialogueSection returns section at top of stack""" + dialogue_processor = self.dialogue_processor + expected_dialogue_section = self.dialogue.default_greeting + actual_dialogue_section = \ + dialogue_processor.getCurrentDialogueSection() + self.assertEqual(expected_dialogue_section, actual_dialogue_section) + + +class TestRunDialogueActions(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.runDialogueActions} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + self.dialogue_processor.initiateDialogue() + self.dialogue_section = DialogueSection( + id_='some_section', + text='Test dialogue section.', + actions=[ + MockDialogueAction('foo'), + ], + ) + self.dialogue_response = DialogueResponse( + text='A response.', + actions=[ + MockDialogueAction('foo'), + ], + next_section_id='end', + ) + + def testExecutesDialogueActions(self): + """runDialogueActions correctly executes DialogueActions""" + dialogue_processor = self.dialogue_processor + # Case: DialogueSection + dialogue_processor.runDialogueActions(self.dialogue_section) + dialogue_section_action = self.dialogue_section.actions[0] + self.assertTrue(dialogue_section_action.was_called) + expected_call_args = ((self.game_state,), {}) + self.assertTupleEqual(expected_call_args, + dialogue_section_action.call_arguments) + # Case: DialogueResponse + dialogue_processor.runDialogueActions(self.dialogue_response) + dialogue_response_action = self.dialogue_response.actions[0] + self.assertTrue(dialogue_response_action.was_called) + self.assertTupleEqual(expected_call_args, + dialogue_response_action.call_arguments) + + +class TestGetValidResponses(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.getValidResponses} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + self.dialogue_processor.initiateDialogue() + + def testReturnsValidResponses(self): + """getValidResponses returns list of valid DialogueResponses""" + dialogue_processor = self.dialogue_processor + valid_responses = \ + dialogue_processor.dialogue_section_stack[0].responses + valid_responses.pop(2) + # Sanity check, all "valid" responses should have a condition that + # evaluates to True. + for response in valid_responses: + if (response.condition is not None): + result = eval(response.condition, {}, {}) + self.assertTrue(result) + responses = dialogue_processor.continueDialogue() + self.assertItemsEqual(responses, valid_responses) + + +class TestReply(TestDialogueProcessor): + """Tests of the L{DialogueProcessor.reply} method.""" + def setUp(self): + TestDialogueProcessor.setUp(self) + self.dialogue_processor = DialogueProcessor(self.dialogue, + self.game_state) + self.response = self.dialogue.default_greeting.responses[1] + self.ending_response = \ + self.dialogue.default_greeting.responses[3] + + def testRaisesExceptionWhenNotInitiated(self): + """reply raises exception when called before initiateDialogue""" + dialogue_processor = self.dialogue_processor + # Sanity check: A dialogue must not have been initiated beforehand. + self.assertFalse(dialogue_processor.in_dialogue) + with self.assertRaisesRegexp(RuntimeError, r'initiateDialogue'): + dialogue_processor.reply(self.response) + + def testExecutesDialogueActions(self): + """reply correctly executes DialogueActions in a DialogueResponse""" + dialogue_processor = self.dialogue_processor + dialogue_processor.initiateDialogue() + dialogue_processor.reply(self.response) + dialogue_action = self.response.actions[0] + self.assertTrue(dialogue_action.was_called) + expected_call_args = ((self.game_state,), {}) + self.assertTupleEqual(expected_call_args, + dialogue_action.call_arguments) + + def testJumpsToCorrectSection(self): + """reply pushes section specified by response onto stack""" + dialogue_processor = self.dialogue_processor + dialogue_processor.initiateDialogue() + # Sanity check: Test response's next_section_id attribute must be refer + # to a valid DialogueSection in the test Dialogue. + self.assertIn(self.response.next_section_id, + self.dialogue.sections.keys()) + dialogue_processor.reply(self.response) + greeting = self.dialogue.default_greeting + next_section = self.dialogue.sections[self.response.next_section_id] + self.assertStateEqual( + dialogue_processor, + in_dialogue=True, + dialogue=self.dialogue, + dialogue_section_stack=[greeting, next_section], + ) + + def testCorrectlyEndsDialogue(self): + """reply ends dialogue when DialogueResponse specifies 'end'""" + dialogue_processor = self.dialogue_processor + dialogue_processor.initiateDialogue() + # Sanity check: Test response must have a next_section_id of 'end'. + self.assertEqual(self.ending_response.next_section_id, 'end') + dialogue_processor.reply(self.ending_response) + self.assertStateEqual(dialogue_processor, in_dialogue=False, + dialogue=self.dialogue, + dialogue_section_stack=[]) + + +if __name__ == "__main__": + unittest.main()