Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# 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

    class TestCraftUtils(BaseEvenniaTestCase):
        """
        Test helper utils for crafting.

        """

        maxDiff = None

        @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

    class TestCraftingRecipeBase(BaseEvenniaTestCase):
        """
        Test the parent recipe class.
        """

        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
            )

        def test_msg(self):
            """Test messaging to crafter"""

            self.recipe.msg("message")
            self.crafter.msg.assert_called_with("message", {"type": "crafting"})

        def test_pre_craft(self):
            """Test validating hook"""
            self.recipe.pre_craft()
            self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))

        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()

        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()

        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")],
            }
        ]

    @override_settings(CRAFT_RECIPE_MODULES=[])
    class TestCraftingRecipe(BaseEvenniaTestCase):
        """
        Test the CraftingRecipe class with one recipe
        """

        maxDiff = None

        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)

        def tearDown(self):
            try:
                self.tool1.delete()
                self.tool2.delete()
                self.cons1.delete()
                self.cons2.delete()
                self.cons3.delete()
            except ObjectDoesNotExist:
                pass

        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))

        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)

        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)

        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)

        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)

        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)

        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)

        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)

        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)

        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)

        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)

        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)

        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)

    class TestCraftSword(BaseEvenniaTestCase):
        """
        Test the `craft` function by crafting the example sword.

        """

        def setUp(self):
            self.crafter = mock.MagicMock()
            self.crafter.msg = mock.MagicMock()

        @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)

    @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"""

        def setUp(self):
            super().setUp()

            tools, consumables = _MockRecipe.seed(
                tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1}
            )

        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"),
            )

        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"),
            )

        def test_craft__nocons__failure(self):
            self.call(
                crafting.CmdCraft(),
                "testrecipe using tool1, tool2",
                _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"),
            )

        def test_craft__unknown_recipe__failure(self):
            self.call(
                crafting.CmdCraft(),
                "nonexistent from cons1, cons2, cons3 using tool1, tool2",
                "Unknown recipe 'nonexistent'",
            )

Note

You are reading an old version of the Evennia documentation. The latest version is here

.