Source code for evennia.contrib.game_systems.crafting.tests

"""
Unit tests for the crafting system contrib.

"""

from unittest import mock

from django.core.exceptions import ObjectDoesNotExist
from django.test import override_settings

from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTestCase

from . import crafting, example_recipes


[docs]class TestCraftUtils(BaseEvenniaTestCase): """ Test helper utils for crafting. """ maxDiff = None
[docs] @override_settings(CRAFT_RECIPE_MODULES=[]) def test_load_recipes(self): """This should only load the example module now""" crafting._load_recipes() self.assertEqual( crafting._RECIPE_CLASSES, { "crucible steel": example_recipes.CrucibleSteelRecipe, "leather": example_recipes.LeatherRecipe, "fireball": example_recipes.FireballRecipe, "heal": example_recipes.HealingRecipe, "oak bark": example_recipes.OakBarkRecipe, "pig iron": example_recipes.PigIronRecipe, "rawhide": example_recipes.RawhideRecipe, "sword": example_recipes.SwordRecipe, "sword blade": example_recipes.SwordBladeRecipe, "sword guard": example_recipes.SwordGuardRecipe, "sword handle": example_recipes.SwordHandleRecipe, "sword pommel": example_recipes.SwordPommelRecipe, }, )
class _TestMaterial: def __init__(self, name): self.name = name def __repr__(self): return self.name
[docs]class TestCraftingRecipeBase(BaseEvenniaTestCase): """ Test the parent recipe class. """
[docs] def setUp(self): self.crafter = mock.MagicMock() self.crafter.msg = mock.MagicMock() self.inp1 = _TestMaterial("test1") self.inp2 = _TestMaterial("test2") self.inp3 = _TestMaterial("test3") self.kwargs = {"kw1": 1, "kw2": 2} self.recipe = crafting.CraftingRecipeBase( self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs )
[docs] def test_msg(self): """Test messaging to crafter""" self.recipe.msg("message") self.crafter.msg.assert_called_with("message", {"type": "crafting"})
[docs] def test_pre_craft(self): """Test validating hook""" self.recipe.pre_craft() self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))
[docs] def test_pre_craft_fail(self): """Should rase error if validation fails""" self.recipe.allow_craft = False with self.assertRaises(crafting.CraftingValidationError): self.recipe.pre_craft()
[docs] def test_craft_hook__succeed(self): """Test craft hook, the main access method.""" expected_result = _TestMaterial("test_result") self.recipe.do_craft = mock.MagicMock(return_value=expected_result) self.assertTrue(self.recipe.allow_craft) result = self.recipe.craft() # check result self.assertEqual(result, expected_result) self.recipe.do_craft.assert_called_with(kw1=1, kw2=2) # since allow_reuse is False, this usage should now be turned off self.assertFalse(self.recipe.allow_craft) # trying to re-run again should fail since rerun is False with self.assertRaises(crafting.CraftingError): self.recipe.craft()
[docs] def test_craft_hook__fail(self): """Test failing the call""" self.recipe.do_craft = mock.MagicMock(return_value=None) # trigger exception with self.assertRaises(crafting.CraftingError): self.recipe.craft(raise_exception=True) # reset and try again without exception self.recipe.allow_craft = True result = self.recipe.craft() self.assertEqual(result, None)
class _MockRecipe(crafting.CraftingRecipe): name = "testrecipe" tool_tags = ["tool1", "tool2"] consumable_tags = ["cons1", "cons2", "cons3"] output_prototypes = [ { "key": "Result1", "prototype_key": "resultprot", "tags": [("result1", "crafting_material")], } ]
[docs]@override_settings(CRAFT_RECIPE_MODULES=[]) class TestCraftingRecipe(BaseEvenniaTestCase): """ Test the CraftingRecipe class with one recipe """ maxDiff = None
[docs] def setUp(self): self.crafter = mock.MagicMock() self.crafter.msg = mock.MagicMock() self.tool1 = create_object(key="tool1", tags=[("tool1", "crafting_tool")], nohome=True) self.tool2 = create_object(key="tool2", tags=[("tool2", "crafting_tool")], nohome=True) self.cons1 = create_object(key="cons1", tags=[("cons1", "crafting_material")], nohome=True) self.cons2 = create_object(key="cons2", tags=[("cons2", "crafting_material")], nohome=True) self.cons3 = create_object(key="cons3", tags=[("cons3", "crafting_material")], nohome=True)
[docs] def tearDown(self): try: self.tool1.delete() self.tool2.delete() self.cons1.delete() self.cons2.delete() self.cons3.delete() except ObjectDoesNotExist: pass
[docs] def test_error_format(self): """Test the automatic error formatter""" recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 ) msg = "{missing},{tools},{consumables},{inputs},{outputs}" "{i0},{i1},{o0}" kwargs = { "missing": "foo", "tools": ["bar", "bar2", "bar3"], "consumables": ["cons1", "cons2"], } expected = { "missing": "foo", "i0": "cons1", "i1": "cons2", "i2": "cons3", "o0": "Result1", "tools": "bar, bar2, and bar3", "consumables": "cons1 and cons2", "inputs": "cons1, cons2, and cons3", "outputs": "Result1", } result = recipe._format_message(msg, **kwargs) self.assertEqual(result, msg.format_map(expected))
[docs] def test_craft__success(self): """Test to create a result from the recipe""" recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 ) result = recipe.craft() self.assertEqual(result[0].key, "Result1") self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( recipe.success_message.format(outputs="Result1"), {"type": "crafting"} ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) self.assertIsNone(self.cons2.pk) self.assertIsNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_seed__success(self): """Test seed helper classmethod""" # needed for other dbs to pass seed homeroom = create_object(key="HomeRoom", nohome=True) # call classmethod directly with override_settings(DEFAULT_HOME=f"#{homeroom.id}"): tools, consumables = _MockRecipe.seed() # this should be a normal successful crafting recipe = _MockRecipe(self.crafter, *(tools + consumables)) result = recipe.craft() self.assertEqual(result[0].key, "Result1") self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( recipe.success_message.format(outputs="Result1"), {"type": "crafting"} ) # make sure consumables are gone for cons in consumables: self.assertIsNone(cons.pk) # make sure tools remain for tool in tools: self.assertIsNotNone(tool.pk)
[docs] def test_craft_missing_tool__fail(self): """Fail craft by missing tool2""" recipe = _MockRecipe(self.crafter, self.tool1, self.cons1, self.cons2, self.cons3) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_missing_cons__fail(self): """Fail craft by missing cons3""" recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_missing_cons__always_consume__fail(self): """Fail craft by missing cons3, with always-consume flag""" cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True) recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, cons4) recipe.consume_on_fail = True result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), {"type": "crafting"}, ) # make sure consumables are deleted even though we failed self.assertIsNone(self.cons1.pk) self.assertIsNone(self.cons2.pk) # the extra should also be gone self.assertIsNone(cons4.pk) # but cons3 should be fine since it was not included self.assertIsNotNone(self.cons3.pk) # make sure tools remain as normal self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_wrong_tool__fail(self): """Fail craft by including a wrong tool""" wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True) recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, wrong) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_tool_excess_message.format( outputs="Result1", excess=wrong.get_display_name(looker=self.crafter) ), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_tool_excess__fail(self): """Fail by too many consumables""" # note that this is a valid tag! tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True) recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 ) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_tool_excess_message.format( outputs="Result1", excess=tool3.get_display_name(looker=self.crafter) ), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk) self.assertIsNotNone(tool3.pk)
[docs] def test_craft_cons_excess__fail(self): """Fail by too many consumables""" # note that this is a valid tag! cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True) recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 ) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_consumable_excess_message.format( outputs="Result1", excess=cons4.get_display_name(looker=self.crafter) ), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) self.assertIsNotNone(cons4.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_tool_excess__sucess(self): """Allow too many consumables""" tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True) recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 ) recipe.exact_tools = False result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( recipe.success_message.format(outputs="Result1"), {"type": "crafting"} ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) self.assertIsNone(self.cons2.pk) self.assertIsNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_cons_excess__sucess(self): """Allow too many consumables""" cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True) recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 ) recipe.exact_consumables = False result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( recipe.success_message.format(outputs="Result1"), {"type": "crafting"} ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) self.assertIsNone(self.cons2.pk) self.assertIsNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_tool_order__fail(self): """Strict tool-order recipe fail""" recipe = _MockRecipe( self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3 ) recipe.exact_tool_order = True result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_tool_order_message.format( outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter) ), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs] def test_craft_cons_order__fail(self): """Strict tool-order recipe fail""" recipe = _MockRecipe( self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1 ) recipe.exact_consumable_order = True result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( recipe.error_consumable_order_message.format( outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter) ), {"type": "crafting"}, ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) self.assertIsNotNone(self.cons3.pk) # make sure tools remain self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool2.pk)
[docs]class TestCraftSword(BaseEvenniaTestCase): """ Test the `craft` function by crafting the example sword. """
[docs] def setUp(self): self.crafter = mock.MagicMock() self.crafter.msg = mock.MagicMock()
[docs] @override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999") @mock.patch("evennia.contrib.game_systems.crafting.example_recipes.random") def test_craft_sword(self, mockrandom): """ Craft example sword. For the test, every crafting works. """ # make sure every craft succeeds mockrandom.random = mock.MagicMock(return_value=0.2) def _co(key, tagkey, is_tool=False): tagcat = "crafting_tool" if is_tool else "crafting_material" return create_object(key=key, tags=[(tagkey, tagcat)], nohome=True) def _craft(recipe_name, *inputs): """shortcut to shorten and return only one element""" result = crafting.craft(self.crafter, recipe_name, *inputs, raise_exception=True) return result[0] if len(result) == 1 else result # generate base materials iron_ore1 = _co("Iron ore ingot", "iron ore") iron_ore2 = _co("Iron ore ingot", "iron ore") iron_ore3 = _co("Iron ore ingot", "iron ore") ash1 = _co("Pile of Ash", "ash") ash2 = _co("Pile of Ash", "ash") ash3 = _co("Pile of Ash", "ash") sand1 = _co("Pile of sand", "sand") sand2 = _co("Pile of sand", "sand") sand3 = _co("Pile of sand", "sand") coal01 = _co("Pile of coal", "coal") coal02 = _co("Pile of coal", "coal") coal03 = _co("Pile of coal", "coal") coal04 = _co("Pile of coal", "coal") coal05 = _co("Pile of coal", "coal") coal06 = _co("Pile of coal", "coal") coal07 = _co("Pile of coal", "coal") coal08 = _co("Pile of coal", "coal") coal09 = _co("Pile of coal", "coal") coal10 = _co("Pile of coal", "coal") coal11 = _co("Pile of coal", "coal") coal12 = _co("Pile of coal", "coal") oak_wood = _co("Pile of oak wood", "oak wood") water = _co("Bucket of water", "water") fur = _co("Bundle of Animal fur", "fur") # tools blast_furnace = _co("Blast furnace", "blast furnace", is_tool=True) furnace = _co("Smithing furnace", "furnace", is_tool=True) crucible = _co("Smelting crucible", "crucible", is_tool=True) anvil = _co("Smithing anvil", "anvil", is_tool=True) hammer = _co("Smithing hammer", "hammer", is_tool=True) knife = _co("Working knife", "knife", is_tool=True) cauldron = _co("Cauldron", "cauldron", is_tool=True) # making pig iron inputs = [iron_ore1, coal01, coal02, blast_furnace] pig_iron1 = _craft("pig iron", *inputs) inputs = [iron_ore2, coal03, coal04, blast_furnace] pig_iron2 = _craft("pig iron", *inputs) inputs = [iron_ore3, coal05, coal06, blast_furnace] pig_iron3 = _craft("pig iron", *inputs) # making crucible steel inputs = [pig_iron1, ash1, sand1, coal07, coal08, crucible] crucible_steel1 = _craft("crucible steel", *inputs) inputs = [pig_iron2, ash2, sand2, coal09, coal10, crucible] crucible_steel2 = _craft("crucible steel", *inputs) inputs = [pig_iron3, ash3, sand3, coal11, coal12, crucible] crucible_steel3 = _craft("crucible steel", *inputs) # smithing inputs = [crucible_steel1, hammer, anvil, furnace] sword_blade = _craft("sword blade", *inputs) inputs = [crucible_steel2, hammer, anvil, furnace] sword_pommel = _craft("sword pommel", *inputs) inputs = [crucible_steel3, hammer, anvil, furnace] sword_guard = _craft("sword guard", *inputs) # stripping fur inputs = [fur, knife] rawhide = _craft("rawhide", *inputs) # making bark (tannin) and cleaned wood inputs = [oak_wood, knife] oak_bark, cleaned_oak_wood = _craft("oak bark", *inputs) # leathermaking inputs = [rawhide, oak_bark, water, cauldron] leather = _craft("leather", *inputs) # sword handle inputs = [cleaned_oak_wood, knife] sword_handle = _craft("sword handle", *inputs) # sword (order matters) inputs = [ sword_blade, sword_guard, sword_pommel, sword_handle, leather, knife, hammer, furnace, ] sword = _craft("sword", *inputs) self.assertEqual(sword.key, "Sword") # make sure all materials and intermediaries are deleted self.assertIsNone(iron_ore1.pk) self.assertIsNone(iron_ore2.pk) self.assertIsNone(iron_ore3.pk) self.assertIsNone(ash1.pk) self.assertIsNone(ash2.pk) self.assertIsNone(ash3.pk) self.assertIsNone(sand1.pk) self.assertIsNone(sand2.pk) self.assertIsNone(sand3.pk) self.assertIsNone(coal01.pk) self.assertIsNone(coal02.pk) self.assertIsNone(coal03.pk) self.assertIsNone(coal04.pk) self.assertIsNone(coal05.pk) self.assertIsNone(coal06.pk) self.assertIsNone(coal07.pk) self.assertIsNone(coal08.pk) self.assertIsNone(coal09.pk) self.assertIsNone(coal10.pk) self.assertIsNone(coal11.pk) self.assertIsNone(coal12.pk) self.assertIsNone(oak_wood.pk) self.assertIsNone(water.pk) self.assertIsNone(fur.pk) self.assertIsNone(pig_iron1.pk) self.assertIsNone(pig_iron2.pk) self.assertIsNone(pig_iron3.pk) self.assertIsNone(crucible_steel1.pk) self.assertIsNone(crucible_steel2.pk) self.assertIsNone(crucible_steel3.pk) self.assertIsNone(sword_blade.pk) self.assertIsNone(sword_pommel.pk) self.assertIsNone(sword_guard.pk) self.assertIsNone(rawhide.pk) self.assertIsNone(oak_bark.pk) self.assertIsNone(leather.pk) self.assertIsNone(sword_handle.pk) # make sure all tools remain self.assertIsNotNone(blast_furnace) self.assertIsNotNone(furnace) self.assertIsNotNone(crucible) self.assertIsNotNone(anvil) self.assertIsNotNone(hammer) self.assertIsNotNone(knife) self.assertIsNotNone(cauldron)
[docs]@mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock()) @mock.patch( "evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe}, ) @override_settings(CRAFT_RECIPE_MODULES=[]) class TestCraftCommand(BaseEvenniaCommandTest): """Test the crafting command"""
[docs] def setUp(self): super().setUp() tools, consumables = _MockRecipe.seed( tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1} )
[docs] def test_craft__success(self): "Successfully craft using command" self.call( crafting.CmdCraft(), "testrecipe from cons1, cons2, cons3 using tool1, tool2", _MockRecipe.success_message.format(outputs="Result1"), )
[docs] def test_craft__notools__failure(self): "Craft fail no tools" self.call( crafting.CmdCraft(), "testrecipe from cons1, cons2, cons3", _MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1"), )
[docs] def test_craft__nocons__failure(self): self.call( crafting.CmdCraft(), "testrecipe using tool1, tool2", _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"), )
[docs] def test_craft__unknown_recipe__failure(self): self.call( crafting.CmdCraft(), "nonexistent from cons1, cons2, cons3 using tool1, tool2", "Unknown recipe 'nonexistent'", )