diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 85e1cb2386..48a3d55f74 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -6,6 +6,7 @@ Test the funcparser module. import time from ast import literal_eval +import unittest from unittest.mock import MagicMock, patch from django.test import TestCase, override_settings @@ -77,6 +78,10 @@ def _lsum_callable(*args, **kwargs): return "" +def _raises_callable(*args, **kwargs): + raise funcparser.ParsingError("Test exception raised by test callable") + + _test_callables = { "foo": _test_callable, "bar": _test_callable, @@ -89,6 +94,7 @@ _test_callables = { "add": _add_callable, "lit": _lit_callable, "sum": _lsum_callable, + "raise": _raises_callable, } @@ -102,6 +108,22 @@ class TestFuncParser(TestCase): self.parser = funcparser.FuncParser(_test_callables) + def test_constructor_wrong_args(self): + # Given list argument doesn't contain modules or paths. + with self.assertRaises(AttributeError): + parser = funcparser.FuncParser(["foo", _test_callable]) + + def test_constructor_ignore_non_callables(self): + # Ignores callables that aren't actual functions. + parser = funcparser.FuncParser({"foo": 1, "bar": "baz"}) + + @patch("evennia.utils.funcparser.variable_from_module") + def test_constructor_raises(self, patched_variable_from_module): + # Patched variable from module returns FUNCPARSER_CALLABLES that isn't dict. + patched_variable_from_module.return_value = ["foo"] + with self.assertRaises(funcparser.ParsingError): + parser = funcparser.FuncParser("foo.module") + @parameterized.expand( [ ("Test normal string", "Test normal string"), @@ -216,12 +238,48 @@ class TestFuncParser(TestCase): # print(f"time: {(t1-t0)*1000} ms") self.assertEqual(expected, ret) - def test_parse_raise(self): + @parameterized.expand( + ( + "Test malformed This is $dummy(a, b) and $bar(", + "Test $funcNotFound()", + ) + ) + def test_parse_raise_unparseable(self, unparseable): """ Make sure error is raised if told to do so. """ - string = "Test malformed This is $dummy(a, b) and $bar(" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(unparseable, raise_errors=True) + + @patch("evennia.utils.funcparser._MAX_NESTING", 2) + def test_parse_max_nesting(self): + """ + Make sure it is an error if the max nesting value is reached. + + TODO: Does this make sense? When it sees the first function, len(callstack) + is 0. It doesn't raise until the stack length is greater than the + _MAX_NESTING value, which means you can nest 4 values with a value of + 2, as demonstrated by this test. + """ + string = "$add(1, $add(1, $add(1, $add(1, $toint(42)))))" + ret = self.parser.parse(string) + + # TODO: Does this return value actually make sense? + # It removed the spaces from the calls. + self.assertEqual("$add(1,$add(1,$add(1,$add(1,$toint(42)))))", ret) + + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, raise_errors=True) + + def test_parse_underlying_exception(self): + string = "test $add(1, 1) $raise()" + ret = self.parser.parse(string) + + # TODO: Does this return value actually make sense? + # It completed the first function call. + self.assertEqual("test 2 $raise()", ret) + with self.assertRaises(funcparser.ParsingError): self.parser.parse(string, raise_errors=True) @@ -368,8 +426,7 @@ class TestDefaultCallables(TestCase): ) def test_conjugate(self, string, expected_you, expected_them): """ - Test callables with various input strings - + Test the $conj(), $you() and $pron callables with various input strings. """ mapping = {"char1": self.obj1, "char2": self.obj2} ret = self.parser.parse( @@ -381,6 +438,46 @@ class TestDefaultCallables(TestCase): ) self.assertEqual(expected_them, ret) + def test_conjugate_missing_args(self): + string = "You $conj(smile)" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, raise_errors=True) + + @parameterized.expand( + [ + ("male", "Char1 smiles at himself"), + ("female", "Char1 smiles at herself"), + ("neutral", "Char1 smiles at itself"), + ("plural", "Char1 smiles at itself"), + ] + ) + def test_pronoun_gender(self, gender, expected): + string = "Char1 smiles at $pron(yourself)" + + self.obj1.gender = gender + ret = self.parser.parse(string, caller=self.obj1, raise_errors=True) + self.assertEqual(expected, ret) + + self.obj1.gender = lambda: gender + ret = self.parser.parse(string, caller=self.obj1, raise_errors=True) + self.assertEqual(expected, ret) + + def test_pronoun_viewpoint(self): + string = "Char1 smiles at $pron(I)" + + ret = self.parser.parse(string, caller=self.obj1, viewpoint="op", raise_errors=True) + self.assertEqual("Char1 smiles at it", ret) + + def test_pronoun_capitalize(self): + string = "Char1 smiles at $pron(I)" + + ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True) + self.assertEqual("Char1 smiles at It", ret) + + string = "Char1 smiles at $Pron(I)" + ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True) + self.assertEqual("Char1 smiles at It", ret) + @parameterized.expand( [ ("Test $pad(Hello, 20, c, -) there", "Test -------Hello-------- there"), @@ -396,6 +493,7 @@ class TestDefaultCallables(TestCase): ("Some $mult(3, 2) things", "Some 6 things"), ("Some $div(6, 2) things", "Some 3.0 things"), ("Some $toint(6) things", "Some 6 things"), + ("Some $toint(3 + 3) things", "Some 6 things"), ("Some $ljust(Hello, 30)", "Some Hello "), ("Some $rjust(Hello, 30)", "Some Hello"), ("Some $rjust(Hello, width=30)", "Some Hello"), @@ -415,6 +513,33 @@ class TestDefaultCallables(TestCase): ("There is $an(thing) here", "There is a thing here"), ("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"), ('$crop("spider\'s silk", 5)', "spide"), + ("$an(apple)", "an apple"), + # These two are broken because of https://github.com/evennia/evennia/issues/2912 + # ("$round(2.9) apples", "3.0 apples"), + # ("$round(2.967, 1) apples", "3.0 apples"), + # Degenerate cases + ("$int2str() apples", " apples"), + ("$int2str(x) apples", "x apples"), + ("$int2str(1 + 1) apples", "1 + 1 apples"), + ("$int2str(13) apples", "13 apples"), + ("$toint([1, 2, 3]) apples", "[1, 2, 3] apples"), + ("$an() foo bar", " foo bar"), + ("$add(1) apple", " apple"), + ("$add(1, [1, 2]) apples", " apples"), + ("$round() apples", " apples"), + ("$choice() apple", " apple"), + ("A $pad() apple", "A apple"), + ("A $pad(tasty, 13, x, -) apple", "A ----tasty---- apple"), + ("A $crop() apple", "A apple"), + ("A $space() apple", "A apple"), + ("A $justify() apple", "A apple"), + ("A $clr() apple", "A apple"), + ("A $clr(red) apple", "A red apple"), + ("10 $pluralize()", "10 "), + ("10 $pluralize(apple, 10)", "10 apples"), + ("1 $pluralize(apple)", "1 apple"), + ("You $conj()", "You "), + ("$pron() smiles", " smiles"), ] ) def test_other_callables(self, string, expected): @@ -426,6 +551,9 @@ class TestDefaultCallables(TestCase): self.assertEqual(expected, ret) def test_random(self): + """ + Test random callable, with ranges of expected values. + """ string = "$random(1,10)" for i in range(100): ret = self.parser.parse_to_any(string, raise_errors=True) @@ -436,12 +564,36 @@ class TestDefaultCallables(TestCase): ret = self.parser.parse_to_any(string, raise_errors=True) self.assertTrue(0 <= ret <= 1) + string = "$random(2)" + for i in range(100): + ret = self.parser.parse_to_any(string, raise_errors=True) + self.assertTrue(0 <= ret <= 2) + string = "$random(1.0, 3.0)" for i in range(100): ret = self.parser.parse_to_any(string, raise_errors=True) self.assertTrue(isinstance(ret, float)) self.assertTrue(1.0 <= ret <= 3.0) + string = "$random([1,2]) apples" + ret = self.parser.parse_to_any(string) + self.assertEqual(" apples", ret) + with self.assertRaises(TypeError): + ret = self.parser.parse_to_any(string, raise_errors=True) + + @unittest.skip("underlying function seems broken") + def test_choice(self): + """ + Test choice callable, where output could be either choice. + """ + string = "$choice(red, green) apple" + ret = self.parser.parse(string, raise_errors=True) + self.assertIn(ret, ("red apple", "green apple")) + + string = "$choice([red, green]) apple" + ret = self.parser.parse(string, raise_errors=True) + self.assertIn(ret, ("red apple", "green apple")) + def test_randint(self): string = "$randint(1.0, 3.0)" ret = self.parser.parse_to_any(string, raise_errors=True) @@ -553,3 +705,55 @@ class TestCallableSearch(test_resources.BaseEvenniaTest): ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) self.assertEqual(expected, ret) + + def test_search_tag(self): + """ + Test searching for a tag + """ + self.char1.tags.add("foo") + + string = "This is $search(foo, type=tag)" + expected = "This is %s" % str(self.char1) + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + self.assertEqual(expected, ret) + + @unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916") + def test_search_not_found(self): + string = "$search(foo)" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=False) + self.assertEqual("$search(foo)", ret) + + ret = self.parser.parse(string, caller=self.char1, return_list=True, raise_errors=False) + self.assertEqual([], ret) + + @unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916") + def test_search_multiple_results_no_list(self): + """ + Test exception when search returns multiple results but list is not requested + """ + string = "$search(BaseObject)" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + + @unittest.skip("broken, see https://github.com/evennia/evennia/issues/2917") + def test_search_no_access(self): + string = "Go to $search(Room)" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, caller=self.char2, return_list=True, raise_errors=True) + + def test_search_no_caller(self): + string = "$search(Char)" + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, caller=None, raise_errors=True) + + def test_search_no_args(self): + string = "$search()" + ret = self.parser.parse(string, caller=self.char1, return_list=False, raise_errors=True) + self.assertEqual("None", ret) + + ret = self.parser.parse(string, caller=self.char1, return_list=True, raise_errors=True) + self.assertEqual("[]", ret)