Refactor all test classes into evennia.utils.test_resources. Update docs.

This commit is contained in:
Griatch 2022-01-21 00:17:24 +01:00
parent 7912351e01
commit bbf45af2dd
28 changed files with 528 additions and 588 deletions

View file

@ -1,6 +1,5 @@
# Unit Testing
*Unit testing* means testing components of a program in isolation from each other to make sure every
part works on its own before using it with others. Extensive testing helps avoid new updates causing
unexpected side effects as well as alleviates general code rot (a more comprehensive wikipedia
@ -31,9 +30,9 @@ how many tests were run and how long it took. If something went wrong you will g
If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an
unexpected bug.
## Running tests with custom settings file
## Running tests for your game dir
If you have implemented your own tests for your game (see below) you can run them from your game dir
If you have implemented your own tests for your game you can run them from your game dir
with
evennia test .
@ -41,8 +40,8 @@ with
The period (`.`) means to run all tests found in the current directory and all subdirectories. You
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
Those tests will all be run using the default settings. To run the tests with your own settings file
you must use the `--settings` option:
An important thing to note is that those tests will all be run using the _default Evennia settings_.
To run the tests with your own settings file you must use the `--settings` option:
evennia test --settings settings.py .
@ -50,108 +49,184 @@ The `--settings` option of Evennia takes a file name in the `mygame/server/conf`
normally used to swap settings files for testing and development. In combination with `test`, it
forces Evennia to use this settings file over the default one.
You can also test specific things by giving their path
evennia test --settings settings.py .world.tests.YourTest
## Writing new tests
Evennia's test suite makes use of Django unit test system, which in turn relies on Python's
*unittest* module.
> If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
test coverage and which does not.
To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`,
`tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good
idea to look at some of Evennia's `tests.py` modules to see how they look.
Inside a testing file, a `unittest.TestCase` class is used to test a single aspect or component in
various ways. Each test case contains one or more *test methods* - these define the actual tests to
run. You can name the test methods anything you want as long as the name starts with "`test_`".
Your `TestCase` class can also have a method `setUp()`. This is run before each test, setting up and
storing whatever preparations the test methods need. Conversely, a `tearDown()` method can
optionally do cleanup after each test.
Inside the module you need to put a class inheriting (at any distance) from `unittest.TestCase`. Each
method on that class that starts with `test_` will be run separately as a unit test. There
are two special, optional methods `setUp` and `tearDown` that will (if you define them) run before
_every_ test. This can be useful for setting up and deleting things.
To test the results, you use special methods of the `TestCase` class. Many of those start with
"`assert`", such as `assertEqual` or `assertTrue`.
To actually test things, you use special `assert...` methods on the class. Most common on is
`assertEqual`, which makes sure a result is what you expect it to be.
Example of a `TestCase` class:
Here's an example of the principle. Let's assume you put this in `mygame/world/tests.py`
and want to test a function in `mygame/world/myfunctions.py`
```python
# in a module tests.py somewhere i your game dir
import unittest
from evennia import create_object
# the function we want to test
from mypath import myfunc
from .myfunctions import myfunc
class TestObj(unittest.TestCase):
"This tests a function myfunc."
def setUp(self):
"""done before every of the test_ * methods below"""
self.obj = create_object("mytestobject")
def tearDown(self):
"""done after every test_* method below """
self.obj.delete()
def test_return_value(self):
"test method. Makes sure return value is as expected."
expected_return = "This is me being nice."
actual_return = myfunc()
"""test method. Makes sure return value is as expected."""
actual_return = myfunc(self.obj)
expected_return = "This is the good object 'mytestobject'."
# test
self.assertEqual(expected_return, actual_return)
def test_alternative_call(self):
"test method. Calls with a keyword argument."
expected_return = "This is me being baaaad."
actual_return = myfunc(bad=True)
"""test method. Calls with a keyword argument."""
actual_return = myfunc(self.obj, bad=True)
expected_return = "This is the baaad object 'mytestobject'."
# test
self.assertEqual(expected_return, actual_return)
```
You might also want to read the [documentation for the unittest
module](https://docs.python.org/library/unittest.html).
To test this, run
### Using the EvenniaTest class
evennia test --settings settings.py .
Evennia offers a custom TestCase, the `evennia.utils.test_resources.EvenniaTest` class. This class
initiates a range of useful properties on themselves for testing Evennia systems. Examples are
`.account` and `.session` representing a mock connected Account and its Session and `.char1` and
`char2` representing Characters complete with a location in the test database. These are all useful
when testing Evennia system requiring any of the default Evennia typeclasses as inputs. See the full
definition of the `EvenniaTest` class in
[evennia/utils/test_resources.py](https://github.com/evennia/evennia/blob/master/evennia/utils/test_resources.py).
to run the entire test module
evennia test --settings setings.py .world.tests
or a specific class:
evennia test --settings settings.py .world.tests.TestObj
You can also run a specific test:
evennia test --settings settings.py .world.tests.TestObj.test_alternative_call
You might also want to read the [Python documentation for the unittest module](https://docs.python.org/library/unittest.html).
## Using the Evennia testing classes
Evennia offers many custom testing classes that helps with testing Evennia features.
They are all found in [evennia.utils.test_resources](evennia.utils.test_resources). Note that
these classes implement the `setUp` and `tearDown` already, so if you want to add stuff in them
yourself you should remember to use e.g. `super().setUp()` in your code.
### Classes for testing your game dir
These all use whatever setting you pass to them and works well for testing code in your game dir.
- `EvenniaTest` - this sets up a full object environment for your test. All the created entities
can be accesses as properties on the class:
- `.account` - A fake [Account](evennia.accounts.accounts.DefaultAccount) named "TestAccount".
- `.account2` - Another account named "TestAccount2"
- `char1` - A [Character](evennia.objects.objects.DefaultCharacter) linked to `.account`, named `Char`.
This has 'Developer' permissions but is not a superuser.
- `.char2` - Another character linked to `account`, named `Char2`. This has base permissions (player).
- `.obj1` - A regular [Object](evennia.objects.objects.DefaultObject) named "Obj".
- `.obj2` - Another object named "Obj2".
- `.room1` - A [Room](evennia.objects.objects.DefaultRoom) named "Room". Both characters and both
objects are located inside this room. It has a description of "room_desc".
- `.room2` - Another room named "Room2". It is empty and has no set description.
- `.exit` - An exit named "out" that leads from `.room1` to `.room2`.
- `.script` - A [Script](evennia.scripts.scripts.DefaultScript) named "Script". It's an inert script
without a timing component.
- `.session` - A fake [Session](evennia.server.serversession.ServerSession) that mimics a player
connecting to the game. It is used by `.account1` and has a sessid of 1.
- `EvenniaCommandTest` - has the same environment like `EvenniaTest` but also adds a special
[.call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call) method specifically for
testing Evennia [Commands](Commands.md). It allows you to compare what the command _actually_
returns to the player with what you expect. Read the `call` api doc for more info.
- `EvenniaTestCase` - This is identical to the regular Python `TestCase` class, it's
just there for naming symmetry with `BaseEvenniaTestCase` below.
Here's an example of using `EvenniaTest`
```python
# in a test module
from evennia.utils.test_resources import BaseEvenniaTest
from evennia.utils.test_resources import EvenniaTest
class TestObject(BaseEvenniaTest):
def test_object_search(self):
# char1 and char2 are both created in room1
class TestObject(EvenniaTest):
"""Remember that the testing class creates char1 and char2 inside room1 ..."""
def test_object_search_character(self):
"""Check that char1 can search for char2 by name"""
self.assertEqual(self.char1.search(self.char2.key), self.char2)
def test_location_search(self):
"""Check so that char1 can find the current location by name"""
self.assertEqual(self.char1.search(self.char1.location.key), self.char1.location)
# ...
```
### Testing in-game Commands
In-game Commands are a special case. Tests for the default commands are put in
`evennia/commands/default/tests.py`. This uses a custom `CommandTest` class that inherits from
`evennia.utils.test_resources.EvenniaTest` described above. `CommandTest` supplies extra convenience
functions for executing commands and check that their return values (calls of `msg()` returns
expected values. It uses Characters and Sessions generated on the `EvenniaTest` class to call each
class).
Each command tested should have its own `TestCase` class. Inherit this class from the `CommandTest`
class in the same module to get access to the command-specific utilities mentioned.
This example tests a custom command.
```python
from evennia.commands.default.tests import CommandTest
from evennia.commands.default import general
class TestSet(CommandTest):
"tests the look command by simple call, using Char2 as a target"
def test_mycmd_char(self):
self.call(general.CmdLook(), "Char2", "Char2(#7)")
from evennia.commands.default.tests import EvenniaCommandTest
from commands import command as mycommand
class TestSet(EvenniaCommandTest):
"tests the look command by simple call, using Char2 as a target"
def test_mycmd_char(self):
self.call(mycommand.CmdMyLook(), "Char2", "Char2(#7)")
def test_mycmd_room(self):
"tests the look command by simple call, with target as room"
def test_mycmd_room(self):
self.call(general.CmdLook(), "Room",
"Room(#1)\nroom_desc\nExits: out(#3)\n"
"You see: Obj(#4), Obj2(#5), Char2(#7)")
self.call(mycommand.CmdMyLook(), "Room",
"Room(#1)\nroom_desc\nExits: out(#3)\n"
"You see: Obj(#4), Obj2(#5), Char2(#7)")
```
### Unit testing contribs with custom models
When using `.call`, you don't need to specify the entire string; you can just give the beginning
of it and if it matches, that's enough. Use `\n` to denote line breaks and (this is a special for
the `.call` helper), `||` to indicate multiple uses of `.msg()` in the Command. The `.call` helper
has a lot of arguments for mimicing different ways of calling a Command, so make sure to
[read the API docs for .call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call).
### Classes for testing Evennia core
These are used for testing Evennia itself. They provide the same resources as the classes
above but enforce Evennias default settings found in `evennia/settings_default.py`, ignoring
any settings changes in your game dir.
- `BaseEvenniaTest` - all the default objects above but with enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, but with enforced default settings
- `BaseEvenniaTestCase` - no default objects, only enforced default settings
There are also two special 'mixin' classes. These are uses in the classes above, but may also
be useful if you want to mix your own testing classes:
- `EvenniaTestMixin` - A class mixin that creates all test environment objects.
- `EvenniaCommandMixin` - A class mixin that adds the `.call()` Command-tester helper.
If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
test coverage and which does not. All help is appreciated!
## Unit testing contribs with custom models
A special case is if you were to create a contribution to go to the `evennia/contrib` folder that
uses its [own database models](../Concepts/New-Models.md). The problem with this is that Evennia (and Django) will
@ -216,14 +291,8 @@ class TestMyModel(BaseEvenniaTest):
# test case here
```
### A note on adding new tests
Having an extensive tests suite is very important for avoiding code degradation as Evennia is
developed. Only a small fraction of the Evennia codebase is covered by test suites at this point.
Writing new tests is not hard, it's more a matter of finding the time to do so. So adding new tests
is really an area where everyone can contribute, also with only limited Python skills.
### A note on making the test runner faster
## A note on making the test runner faster
If you have custom models with a large number of migrations, creating the test database can take a
very long time. If you don't require migrations to run for your tests, you can disable them with the
@ -246,156 +315,4 @@ After doing so, you can then run tests without migrations by adding the `--nomig
```
evennia test --settings settings.py --nomigrations .
```
## Testing for Game development (mini-tutorial)
Unit testing can be of paramount importance to game developers. When starting with a new game, it is
recommended to look into unit testing as soon as possible; an already huge game is much harder to
write tests for. The benefits of testing a game aren't different from the ones regarding library
testing. For example it is easy to introduce bugs that affect previously working code. Testing is
there to ensure your project behaves the way it should and continue to do so.
If you have never used unit testing (with Python or another language), you might want to check the
[official Python documentation about unit testing](https://docs.python.org/2/library/unittest.html),
particularly the first section dedicated to a basic example.
### Basic testing using Evennia
Evennia's test runner can be used to launch tests in your game directory (let's call it 'mygame').
Evennia's test runner does a few useful things beyond the normal Python unittest module:
* It creates and sets up an empty database, with some useful objects (accounts, characters and
rooms, among others).
* It provides simple ways to test commands, which can be somewhat tricky at times, if not tested
properly.
Therefore, you should use the command-line to execute the test runner, while specifying your own
game directories (not the one containing evennia). Go to your game directory (referred as 'mygame'
in this section) and execute the test runner:
evennia --settings settings.py test commands
This command will execute Evennia's test runner using your own settings file. It will set up a dummy
database of your choice and look into the 'commands' package defined in your game directory
(`mygame/commands` in this example) to find tests. The test module's name should begin with 'test'
and contain one or more `TestCase`. A full example can be found below.
### A simple example
In your game directory, go to `commands` and create a new file `tests.py` inside (it could be named
anything starting with `test`). We will start by making a test that has nothing to do with Commands,
just to show how unit testing works:
```python
# mygame/commands/tests.py
import unittest
class TestString(unittest.TestCase):
"""Unittest for strings (just a basic example)."""
def test_upper(self):
"""Test the upper() str method."""
self.assertEqual('foo'.upper(), 'FOO')
```
This example, inspired from the Python documentation, is used to test the 'upper()' method of the
'str' class. Not very useful, but it should give you a basic idea of how tests are used.
Let's execute that test to see if it works.
> evennia --settings settings.py test commands
TESTING: Using specified settings file 'server.conf.settings'.
(Obs: Evennia's full test suite may not pass if the settings are very
different from the default. Use 'test .' as arguments to run only tests
on the game dir.)
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
We specified the `commands` package to the evennia test command since that's where we put our test
file. In this case we could just as well just said `.` to search all of `mygame` for testing files.
If we have a lot of tests it may be useful to test only a single set at a time though. We get an
information text telling us we are using our custom settings file (instead of Evennia's default
file) and then the test runs. The test passes! Change the "FOO" string to something else in the test
to see how it looks when it fails.
### Testing commands
```{warning} This is not correct anymore.
```
This section will test the proper execution of the 'abilities' command, as described in the DELETED
tutorial to create the 'abilities' command, we will need it to test it.
Testing commands in Evennia is a bit more complex than the simple testing example we have seen.
Luckily, Evennia supplies a special test class to do just that ... we just need to inherit from it
and use it properly. This class is called 'CommandTest' and is defined in the
'evennia.commands.default.tests' package. To create a test for our 'abilities' command, we just
need to create a class that inherits from 'CommandTest' and add methods.
We could create a new test file for this but for now we just append to the `tests.py` file we
already have in `commands` from before.
```python
# bottom of mygame/commands/tests.py
from evennia.commands.default.tests import CommandTest
from commands.command import CmdAbilities
from typeclasses.characters import Character
class TestAbilities(CommandTest):
character_typeclass = Character
def test_simple(self):
self.call(CmdAbilities(), "", "STR: 5, AGI: 4, MAG: 2")
```
* Line 1-4: we do some importing. 'CommandTest' is going to be our base class for our test, so we
need it. We also import our command ('CmdAbilities' in this case). Finally we import the
'Character' typeclass. We need it, since 'CommandTest' doesn't use 'Character', but
'DefaultCharacter', which means the character calling the command won't have the abilities we have
written in the 'Character' typeclass.
* Line 6-8: that's the body of our test. Here, a single command is tested in an entire class.
Default commands are usually grouped by category in a single class. There is no rule, as long as
you know where you put your tests. Note that we set the 'character_typeclass' class attribute to
Character. As explained above, if you didn't do that, the system would create a 'DefaultCharacter'
object, not a 'Character'. You can try to remove line 4 and 8 to see what happens when running the
test.
* Line 10-11: our unique testing method. Note its name: it should begin by 'test_'. Apart from
that, the method is quite simple: it's an instance method (so it takes the 'self' argument) but no
other arguments are needed. Line 11 uses the 'call' method, which is defined in 'CommandTest'.
It's a useful method that compares a command against an expected result. It would be like comparing
two strings with 'assertEqual', but the 'call' method does more things, including testing the
command in a realistic way (calling its hooks in the right order, so you don't have to worry about
that).
Line 11 can be understood as: test the 'abilities' command (first parameter), with no argument
(second parameter), and check that the character using it receives his/her abilities (third
parameter).
Let's run our new test:
> evennia --settings settings.py test commands
[...]
Creating test database for alias 'default'...
..
----------------------------------------------------------------------
Ran 2 tests in 0.156s
OK
Destroying test database for alias 'default'...
Two tests were executed, since we have kept 'TestString' from last time. In case of failure, you
will get much more information to help you fix the bug.
```

View file

@ -11,8 +11,6 @@ main test suite started with
> python game/manage.py test.
"""
import re
import types
import datetime
from anything import Anything
@ -23,7 +21,7 @@ from unittest.mock import patch, Mock, MagicMock
from evennia import DefaultRoom, DefaultExit, ObjectDB
from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest
from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest, EvenniaCommandTest # noqa
from evennia.commands.default import (
help as help_module,
general,
@ -40,305 +38,18 @@ from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi, utils, gametime, create
from evennia.utils import utils, gametime, create
from evennia.server.sessionhandler import SESSIONS
from evennia import search_object
from evennia import DefaultObject, DefaultCharacter
from evennia.prototypes import prototypes as protlib
# set up signal here since we are not starting the server
_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# ------------------------------------------------------------
# Command testing
# ------------------------------------------------------------
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class CommandTestMixin:
"""
Mixin to add to a test in order to provide the `.call` helper for
testing the execution and returns of a command.
Tests a Command by running it and comparing what messages it sends with
expected values. This tests without actually spinning up the cmdhandler
for every test, which is more controlled.
Example:
::
from commands.echo import CmdEcho
class MyCommandTest(EvenniaTest, CommandTestMixin):
def test_echo(self):
'''
Test that the echo command really returns
what you pass into it.
'''
self.call(MyCommand(), "hello world!",
"You hear your echo: 'Hello world!'")
"""
# formatting for .call's error message
_ERROR_FORMAT = """
=========================== Wanted message ===================================
{expected_msg}
=========================== Returned message =================================
{returned_msg}
==============================================================================
""".rstrip()
def call(
self,
cmdobj,
input_args,
msg=None,
cmdset=None,
noansi=True,
caller=None,
receiver=None,
cmdstring=None,
obj=None,
inputs=None,
raw_string=None,
):
"""
Test a command by assigning all the needed properties to a cmdobj and
running the sequence. The resulting `.msg` calls will be mocked and
the text= calls to them compared to a expected output.
Args:
cmdobj (Command): The command object to use.
input_args (str): This should be the full input the Command should
see, such as 'look here'. This will become `.args` for the Command
instance to parse.
msg (str or dict, optional): This is the expected return value(s)
returned through `caller.msg(text=...)` calls in the command. If a string, the
receiver is controlled with the `receiver` kwarg (defaults to `caller`).
If this is a `dict`, it is a mapping
`{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
ignored. The message(s) are compared with the actual messages returned
to the receiver(s) as the Command runs. Each check uses `.startswith`,
so you can choose to only include the first part of the
returned message if that's enough to verify a correct result. EvMenu
decorations (like borders) are stripped and should not be included. This
should also not include color tags unless `noansi=False`.
If the command returns texts in multiple separate `.msg`-
calls to a receiver, separate these with `|` if `noansi=True`
(default) and `||` if `noansi=False`. If no `msg` is given (`None`),
then no automatic comparison will be done.
cmdset (str, optional): If given, make `.cmdset` available on the Command
instance as it runs. While `.cmdset` is normally available on the
Command instance by default, this is usually only used by
commands that explicitly operates/displays cmdsets, like
`examine`.
noansi (str, optional): By default the color tags of the `msg` is
ignored, this makes them significant. If unset, `msg` must contain
the same color tags as the actual return message.
caller (Object or Account, optional): By default `self.char1` is used as the
command-caller (the `.caller` property on the Command). This allows to
execute with another caller, most commonly an Account.
receiver (Object or Account, optional): This is the object to receive the
return messages we want to test. By default this is the same as `caller`
(which in turn defaults to is `self.char1`). Note that if `msg` is
a `dict`, this is ignored since the receiver is already specified there.
cmdstring (str, optional): Normally this is the Command's `key`.
This allows for tweaking the `.cmdname` property of the
Command`. This isb used for commands with multiple aliases,
where the command explicitly checs which alias was used to
determine its functionality.
obj (str, optional): This sets the `.obj` property of the Command - the
object on which the Command 'sits'. By default this is the same as `caller`.
This can be used for testing on-object Command interactions.
inputs (list, optional): A list of strings to pass to functions that pause to
take input from the user (normally using `@interactive` and
`ret = yield(question)` or `evmenu.get_input`). Each element of the
list will be passed into the command as if the user wrote that at the prompt.
raw_string (str, optional): Normally the `.raw_string` property is set as
a combination of your `key/cmdname` and `input_args`. This allows
direct control of what this is, for example for testing edge cases
or malformed inputs.
Returns:
str or dict: The message sent to `receiver`, or a dict of
`{receiver: "msg", ...}` if multiple are given. This is usually
only used with `msg=None` to do the validation externally.
Raises:
AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
match `expected_input`.
Notes:
As part of the tests, all methods of the Command will be called in
the proper order:
- cmdobj.at_pre_cmd()
- cmdobj.parse()
- cmdobj.func()
- cmdobj.at_post_cmd()
"""
# The `self.char1` is created in the `EvenniaTest` base along with
# other helper objects like self.room and self.obj
caller = caller if caller else self.char1
cmdobj.caller = caller
cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
cmdobj.raw_cmdname = cmdobj.cmdname
cmdobj.cmdstring = cmdobj.cmdname # deprecated
cmdobj.args = input_args
cmdobj.cmdset = cmdset
cmdobj.session = SESSIONS.session_from_sessid(1)
cmdobj.account = self.account
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + input_args
cmdobj.obj = obj or (caller if caller else self.char1)
inputs = inputs or []
# set up receivers
receiver_mapping = {}
if isinstance(msg, dict):
# a mapping {receiver: msg, ...}
receiver_mapping = {recv: str(msg).strip() if msg else None
for recv, msg in msg.items()}
else:
# a single expected string and thus a single receiver (defaults to caller)
receiver = receiver if receiver else caller
receiver_mapping[receiver] = str(msg).strip() if msg is not None else None
unmocked_msg_methods = {}
for receiver in receiver_mapping:
# save the old .msg method so we can get it back
# cleanly after the test
unmocked_msg_methods[receiver] = receiver.msg
# replace normal `.msg` with a mock
receiver.msg = Mock()
# Run the methods of the Command. This mimics what happens in the
# cmdhandler. This will have the mocked .msg be called as part of the
# execution. Mocks remembers what was sent to them so we will be able
# to retrieve what was sent later.
try:
if cmdobj.at_pre_cmd():
return
cmdobj.parse()
ret = cmdobj.func()
# handle func's with yield in them (making them generators)
if isinstance(ret, types.GeneratorType):
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
# this mimics a user's reply to a prompt
ret.send(inp)
except TypeError:
next(ret)
ret = ret.send(inp)
else:
# non-input yield, like yield(10). We don't pause
# but fire it immediately.
next(ret)
except StopIteration:
break
cmdobj.at_post_cmd()
except StopIteration:
pass
except InterruptCommand:
pass
for inp in inputs:
# if there are any inputs left, we may have a non-generator
# input to handle (get_input/ask_yes_no that uses a separate
# cmdset rather than a yield
caller.execute_cmd(inp)
# At this point the mocked .msg methods on each receiver will have
# stored all calls made to them (that's a basic function of the Mock
# class). We will not extract them and compare to what we expected to
# go to each receiver.
returned_msgs = {}
for receiver, expected_msg in receiver_mapping.items():
# get the stored messages from the Mock with Mock.mock_calls.
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in receiver.msg.mock_calls
]
# we can return this now, we are done using the mock
receiver.msg = unmocked_msg_methods[receiver]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [str(smsg[0])
if isinstance(smsg, tuple) else str(smsg) for smsg in stored_msg]
if expected_msg is None:
# no expected_msg; just build the returned_msgs dict
returned_msg = "\n".join(str(msg) for msg in stored_msg)
returned_msgs[receiver] = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
else:
# compare messages to expected
# set our separator for returned messages based on parsing ansi or not
msg_sep = "|" if noansi else "||"
# We remove Evmenu decorations since that just makes it harder
# to write the comparison string. We also strip ansi before this
# comparison since otherwise it would mess with the regex.
returned_msg = msg_sep.join(
_RE_STRIP_EVMENU.sub(
"", ansi.parse_ansi(mess, strip_ansi=noansi))
for mess in stored_msg).strip()
# this is the actual test
if expected_msg == "" and returned_msg or not returned_msg.startswith(expected_msg):
# failed the test
raise AssertionError(
self._ERROR_FORMAT.format(
expected_msg=expected_msg, returned_msg=returned_msg)
)
# passed!
returned_msgs[receiver] = returned_msg
if len(returned_msgs) == 1:
return list(returned_msgs.values())[0]
return returned_msgs
@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS", MuxCommand)
class EvenniaCommandTest(BaseEvenniaTest, CommandTestMixin):
"""
Commands only using the default settings.
"""
class CommandTest(EvenniaTest, CommandTestMixin):
"""
Parent class to inherit from - makes tests use your own
classes and settings in mygame.
"""
# ------------------------------------------------------------
# Individual module Tests
# ------------------------------------------------------------
class TestGeneral(EvenniaCommandTest):
class TestGeneral(BaseEvenniaCommandTest):
def test_look(self):
rid = self.room1.id
self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid))
@ -434,7 +145,7 @@ class TestGeneral(EvenniaCommandTest):
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
class TestHelp(EvenniaCommandTest):
class TestHelp(BaseEvenniaCommandTest):
maxDiff = None
@ -584,7 +295,7 @@ class TestHelp(EvenniaCommandTest):
cmdset=TestCmdSet())
class TestSystem(EvenniaCommandTest):
class TestSystem(BaseEvenniaCommandTest):
def test_py(self):
# we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
# since the server is not running during these tests.
@ -608,7 +319,7 @@ _TASK_HANDLER = None
def func_test_cmd_tasks():
return 'success'
class TestCmdTasks(EvenniaCommandTest):
class TestCmdTasks(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
@ -768,7 +479,7 @@ class TestCmdTasks(EvenniaCommandTest):
self.call(system.CmdTasks(), f'/cancel', wanted_msg)
class TestAdmin(EvenniaCommandTest):
class TestAdmin(BaseEvenniaCommandTest):
def test_emit(self):
self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest")
@ -799,7 +510,7 @@ class TestAdmin(EvenniaCommandTest):
)
class TestAccount(EvenniaCommandTest):
class TestAccount(BaseEvenniaCommandTest):
def test_ooc_look(self):
if settings.MULTISESSION_MODE < 2:
self.call(
@ -923,7 +634,7 @@ class TestAccount(EvenniaCommandTest):
)
class TestBuilding(EvenniaCommandTest):
class TestBuilding(BaseEvenniaCommandTest):
def test_create(self):
name = settings.BASE_OBJECT_TYPECLASS.rsplit(".", 1)[1]
self.call(
@ -1991,7 +1702,7 @@ from evennia.comms.comms import DefaultChannel # noqa
@patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel)
class TestCommsChannel(EvenniaCommandTest):
class TestCommsChannel(BaseEvenniaCommandTest):
"""
Test the central `channel` command.
@ -2214,7 +1925,7 @@ class TestCommsChannel(EvenniaCommandTest):
from evennia.commands.default import comms # noqa
class TestComms(EvenniaCommandTest):
class TestComms(BaseEvenniaCommandTest):
def test_page(self):
self.call(
@ -2226,7 +1937,7 @@ class TestComms(EvenniaCommandTest):
)
class TestBatchProcess(EvenniaCommandTest):
class TestBatchProcess(BaseEvenniaCommandTest):
"""
Test the batch processor.
@ -2262,13 +1973,13 @@ class CmdInterrupt(Command):
self.msg("in func")
class TestInterruptCommand(EvenniaCommandTest):
class TestInterruptCommand(BaseEvenniaCommandTest):
def test_interrupt_command(self):
ret = self.call(CmdInterrupt(), "")
self.assertEqual(ret, "")
class TestUnconnectedCommand(EvenniaCommandTest):
class TestUnconnectedCommand(BaseEvenniaCommandTest):
def test_info_command(self):
# instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
gametime.SERVER_START_TIME = 86400
@ -2288,7 +1999,7 @@ class TestUnconnectedCommand(EvenniaCommandTest):
# Test syscommands
class TestSystemCommands(EvenniaCommandTest):
class TestSystemCommands(BaseEvenniaCommandTest):
def test_simple_defaults(self):
self.call(syscommands.SystemNoInput(), "")
self.call(syscommands.SystemNoMatch(), "Huh?")

View file

@ -3,7 +3,7 @@ Building menu tests.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . building_menu import BuildingMenu, CmdNoMatch
@ -12,7 +12,7 @@ class Submenu(BuildingMenu):
self.add_choice("title", key="t", attr="key")
class TestBuildingMenu(EvenniaCommandTest):
class TestBuildingMenu(BaseEvenniaCommandTest):
def setUp(self):
super(TestBuildingMenu, self).setUp()
self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test")

View file

@ -3,11 +3,11 @@ Test email login.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import email_login
class TestEmailLogin(EvenniaCommandTest):
class TestEmailLogin(BaseEvenniaCommandTest):
def test_connect(self):
self.call(
email_login.CmdUnconnectedConnect(),

View file

@ -7,7 +7,7 @@ from textwrap import dedent
from django.conf import settings
from evennia import ScriptDB
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.objects.objects import ExitCommand
from evennia.utils import ansi, utils
from evennia.utils.create import create_object, create_script
@ -246,7 +246,7 @@ class TestEventHandler(BaseEvenniaTest):
self.assertEqual(self.room1.callbacks.all(), {})
class TestCmdCallback(EvenniaCommandTest):
class TestCmdCallback(BaseEvenniaCommandTest):
"""Test the @callback command."""
@ -425,7 +425,7 @@ class TestCmdCallback(EvenniaCommandTest):
self.assertEqual(callback.valid, True)
class TestDefaultCallbacks(EvenniaCommandTest):
class TestDefaultCallbacks(BaseEvenniaCommandTest):
"""Test the default callbacks."""

View file

@ -3,10 +3,10 @@ Test menu_login
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import menu_login
class TestMenuLogin(EvenniaCommandTest):
class TestMenuLogin(BaseEvenniaCommandTest):
def test_cmdunloggedlook(self):
self.call(menu_login.CmdUnloggedinLook(), "", "======")

View file

@ -3,11 +3,11 @@ Legacy Mux comms tests (extracted from 0.9.5)
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mux_comms_cmds as comms
class TestLegacyMuxComms(EvenniaCommandTest):
class TestLegacyMuxComms(BaseEvenniaCommandTest):
"""
Test the legacy comms contrib.
"""

View file

@ -3,7 +3,7 @@ Test of the Unixcommand.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from .unixcommand import UnixCommand
@ -30,7 +30,7 @@ class CmdDummy(UnixCommand):
self.msg("{} * {} = {}".format(nb1, nb2, result))
class TestUnixCommand(EvenniaCommandTest):
class TestUnixCommand(BaseEvenniaCommandTest):
def test_success(self):
"""See the command parsing succeed."""
self.call(CmdDummy(), "5 10", "5 * 10 = 50")

View file

@ -5,7 +5,7 @@ Unit tests for the Evscaperoom
import inspect
import pkgutil
from os import path
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia import InterruptCommand
from evennia.utils.test_resources import BaseEvenniaTest
from evennia.utils import mod_import
@ -15,7 +15,7 @@ from . import objects
from . import utils
class TestEvscaperoomCommands(EvenniaCommandTest):
class TestEvscaperoomCommands(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
self.room1 = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")

View file

@ -3,12 +3,12 @@ Test the contrib barter system
"""
from mock import Mock
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from . import barter
class TestBarter(EvenniaCommandTest):
class TestBarter(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
self.tradeitem1 = create_object(key="TradeItem1", location=self.char1)

View file

@ -3,14 +3,14 @@ Testing clothing contrib
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from evennia.objects.objects import DefaultRoom
from evennia.utils.test_resources import BaseEvenniaTest
from . import clothing
class TestClothingCmd(EvenniaCommandTest):
class TestClothingCmd(BaseEvenniaCommandTest):
def test_clothingcommands(self):
wearer = create_object(clothing.ClothedCharacter, key="Wearer")
friend = create_object(clothing.ClothedCharacter, key="Friend")

View file

@ -6,7 +6,7 @@ Unit tests for the crafting system contrib.
from unittest import mock
from django.test import override_settings
from django.core.exceptions import ObjectDoesNotExist
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTestCase
from evennia.utils.create import create_object
from . import crafting, example_recipes
@ -655,7 +655,7 @@ class TestCraftSword(BaseEvenniaTestCase):
@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(EvenniaCommandTest):
class TestCraftCommand(BaseEvenniaCommandTest):
"""Test the crafting command"""
def setUp(self):

View file

@ -4,13 +4,13 @@ Test gendersub contrib.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from mock import patch
from . import gendersub
class TestGenderSub(EvenniaCommandTest):
class TestGenderSub(BaseEvenniaCommandTest):
def test_setgender(self):
self.call(gendersub.SetGender(), "male", "Your gender was set to male.")
self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.")

View file

@ -3,11 +3,11 @@ Test mail contrib
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mail
class TestMail(EvenniaCommandTest):
class TestMail(BaseEvenniaCommandTest):
def test_mail(self):
self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account)
self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account)

View file

@ -3,11 +3,11 @@ Test multidescer contrib.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import multidescer
class TestMultidescer(EvenniaCommandTest):
class TestMultidescer(BaseEvenniaCommandTest):
def test_cmdmultidesc(self):
self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:")
self.call(

View file

@ -9,12 +9,12 @@ import re
import itertools
from mock import Mock
from evennia.utils import search
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from . import puzzles
class TestPuzzles(EvenniaCommandTest):
class TestPuzzles(BaseEvenniaCommandTest):
def setUp(self):
super(TestPuzzles, self).setUp()
self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location)

View file

@ -4,14 +4,14 @@ Turnbattle tests.
"""
from mock import patch, MagicMock
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTest
from evennia.objects.objects import DefaultRoom
from . import tb_basic, tb_equip, tb_range, tb_items, tb_magic
class TestTurnBattleBasicCmd(EvenniaCommandTest):
class TestTurnBattleBasicCmd(BaseEvenniaCommandTest):
# Test basic combat commands
def test_turnbattlecmd(self):
@ -22,7 +22,7 @@ class TestTurnBattleBasicCmd(EvenniaCommandTest):
self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleEquipCmd(EvenniaCommandTest):
class TestTurnBattleEquipCmd(BaseEvenniaCommandTest):
def setUp(self):
super(TestTurnBattleEquipCmd, self).setUp()
self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
@ -45,7 +45,7 @@ class TestTurnBattleEquipCmd(EvenniaCommandTest):
self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleRangeCmd(EvenniaCommandTest):
class TestTurnBattleRangeCmd(BaseEvenniaCommandTest):
# Test range commands
def test_turnbattlerangecmd(self):
# Start with range module specific commands.
@ -61,7 +61,7 @@ class TestTurnBattleRangeCmd(EvenniaCommandTest):
self.call(tb_range.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleItemsCmd(EvenniaCommandTest):
class TestTurnBattleItemsCmd(BaseEvenniaCommandTest):
def setUp(self):
super(TestTurnBattleItemsCmd, self).setUp()
self.testitem = create_object(key="test item")
@ -78,7 +78,7 @@ class TestTurnBattleItemsCmd(EvenniaCommandTest):
self.call(tb_items.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleMagicCmd(EvenniaCommandTest):
class TestTurnBattleMagicCmd(BaseEvenniaCommandTest):
# Test magic commands
def test_turnbattlemagiccmd(self):

View file

@ -6,7 +6,7 @@ Testing of ExtendedRoom contrib
import datetime
from mock import patch, Mock
from django.conf import settings
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.objects.objects import DefaultRoom
from . import extended_room
@ -24,7 +24,7 @@ class ForceUTCDatetime(datetime.datetime):
@patch("evennia.contrib.grid.extended_room.extended_room.datetime.datetime", ForceUTCDatetime)
# mock gametime to return April 9, 2064, at 21:06 (spring evening)
@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766))
class TestExtendedRoom(EvenniaCommandTest):
class TestExtendedRoom(BaseEvenniaCommandTest):
room_typeclass = extended_room.ExtendedRoom
DETAIL_DESC = "A test detail."
SPRING_DESC = "A spring description."

View file

@ -3,7 +3,7 @@ Test map builder.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mapbuilder
# -*- coding: utf-8 -*-
@ -187,7 +187,7 @@ EXAMPLE2_LEGEND = {
}
class TestMapBuilder(EvenniaCommandTest):
class TestMapBuilder(BaseEvenniaCommandTest):
def test_cmdmapbuilder(self):
self.call(
mapbuilder.CmdMapBuilder(),

View file

@ -4,11 +4,11 @@ Tests of simpledoor.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import simpledoor
class TestSimpleDoor(EvenniaCommandTest):
class TestSimpleDoor(BaseEvenniaCommandTest):
def test_cmdopen(self):
self.call(
simpledoor.CmdOpen(),

View file

@ -4,7 +4,7 @@ Slow exit tests.
"""
from mock import Mock, patch
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from . import slow_exit
@ -16,7 +16,7 @@ def _cancellable_mockdelay(time, callback, *args, **kwargs):
return Mock()
class TestSlowExit(EvenniaCommandTest):
class TestSlowExit(BaseEvenniaCommandTest):
@patch("evennia.utils.delay", _cancellable_mockdelay)
def test_exit(self):
exi = create_object(

View file

@ -3,13 +3,13 @@ Testing of TestDice.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from mock import patch
from . import dice
@patch("evennia.contrib.rpg.dice.dice.randint", return_value=5)
class TestDice(EvenniaCommandTest):
class TestDice(BaseEvenniaCommandTest):
def test_roll_dice(self, mocked_randint):
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)

View file

@ -4,7 +4,7 @@ Tests for RP system
"""
import time
from anything import Anything
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest
from evennia import create_object
@ -278,7 +278,7 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
class TestRPSystemCommands(EvenniaCommandTest):
class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
self.char1.swap_typeclass(rpsystem.ContribRPCharacter)

View file

@ -2,12 +2,12 @@
Tutorial - talking NPC tests.
"""
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from . import talking_npc
class TestTalkingNPC(EvenniaCommandTest):
class TestTalkingNPC(BaseEvenniaCommandTest):
def test_talkingnpc(self):
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)")

View file

@ -6,7 +6,7 @@ Test tutorial_world/mob
from mock import patch
from twisted.trial.unittest import TestCase as TwistedTestCase
from twisted.internet.base import DelayedCall
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTest, mockdelay, mockdeferLater
from . import mob, objects as tutobjects, rooms as tutrooms
@ -30,7 +30,7 @@ class TestTutorialWorldMob(BaseEvenniaTest):
DelayedCall.debug = True
class TestTutorialWorldObjects(TwistedTestCase, EvenniaCommandTest):
class TestTutorialWorldObjects(TwistedTestCase, BaseEvenniaCommandTest):
def test_tutorialobj(self):
obj1 = create_object(tutobjects.TutorialObject, key="tutobj")
obj1.reset()
@ -129,7 +129,7 @@ class TestTutorialWorldObjects(TwistedTestCase, EvenniaCommandTest):
self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack)
class TestTutorialWorldRooms(EvenniaCommandTest):
class TestTutorialWorldRooms(BaseEvenniaCommandTest):
def test_cmdtutorial(self):
room = create_object(tutrooms.TutorialRoom, key="tutroom")
self.char1.location = room

View file

@ -4,9 +4,8 @@ Test the main server component
"""
from unittest import TestCase
from mock import MagicMock, patch, DEFAULT, call
from django.test import override_settings
from evennia.utils.test_resources import unload_module
from mock import MagicMock, patch, DEFAULT, call
@patch("evennia.server.server.LoopingCall", new=MagicMock())
@ -191,7 +190,7 @@ class TestServer(TestCase):
evennia.run_initial_setup()
acct.delete()
@override_settings(DEFAULT_HOME="#1")
@override_settings(_TEST_ENVIRONMENT=True)
def test_run_init_hooks(self):
from evennia.utils import create

View file

@ -1,12 +1,34 @@
"""
Various helper resources for writing unittests.
Classes for testing Evennia core:
- `BaseEvenniaTestCase` - no default objects, only enforced default settings
- `BaseEvenniaTest` - all default objects, enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, enforced default settings
Classes for testing game folder content:
- `EvenniaTestCase` - no default objects, using gamedir settings (identical to
standard Python TestCase)
- `EvenniaTest` - all default objects, using gamedir settings
- `EvenniaCommandTest` - for testing game folder commands, using gamedir settings
Other:
- `EvenniaTestMixin` - A class mixin for creating the test environment objects, for
making custom tests.
- `EvenniaCommandMixin` - A class mixin that adds support for command testing with the .call()
helper. Used by the command-test classes, but can be used for making a customt test class.
"""
import sys
import re
import types
from twisted.internet.defer import Deferred
from django.conf import settings
from django.test import TestCase, override_settings
from mock import Mock, patch
from mock import Mock, patch, MagicMock
from evennia.objects.objects import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.scripts import DefaultScript
@ -14,8 +36,14 @@ from evennia.server.serversession import ServerSession
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import create
from evennia.utils.idmapper.models import flush_cache
from evennia.utils.utils import all_from_module
from evennia.utils.utils import all_from_module, to_str
from evennia.utils import ansi
from evennia import settings_default
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import InterruptCommand
_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# set up a 'pristine' setting, unaffected by any changes in mygame
@ -242,14 +270,275 @@ class EvenniaTestMixin:
super().tearDown()
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class EvenniaCommandTestMixin:
"""
Mixin to add to a test in order to provide the `.call` helper for
testing the execution and returns of a command.
Tests a Command by running it and comparing what messages it sends with
expected values. This tests without actually spinning up the cmdhandler
for every test, which is more controlled.
Example:
::
from commands.echo import CmdEcho
class MyCommandTest(EvenniaTest, CommandTestMixin):
def test_echo(self):
'''
Test that the echo command really returns
what you pass into it.
'''
self.call(MyCommand(), "hello world!",
"You hear your echo: 'Hello world!'")
"""
# formatting for .call's error message
_ERROR_FORMAT = """
=========================== Wanted message ===================================
{expected_msg}
=========================== Returned message =================================
{returned_msg}
==============================================================================
""".rstrip()
def call(
self,
cmdobj,
input_args,
msg=None,
cmdset=None,
noansi=True,
caller=None,
receiver=None,
cmdstring=None,
obj=None,
inputs=None,
raw_string=None,
):
"""
Test a command by assigning all the needed properties to a cmdobj and
running the sequence. The resulting `.msg` calls will be mocked and
the text= calls to them compared to a expected output.
Args:
cmdobj (Command): The command object to use.
input_args (str): This should be the full input the Command should
see, such as 'look here'. This will become `.args` for the Command
instance to parse.
msg (str or dict, optional): This is the expected return value(s)
returned through `caller.msg(text=...)` calls in the command. If a string, the
receiver is controlled with the `receiver` kwarg (defaults to `caller`).
If this is a `dict`, it is a mapping
`{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
ignored. The message(s) are compared with the actual messages returned
to the receiver(s) as the Command runs. Each check uses `.startswith`,
so you can choose to only include the first part of the
returned message if that's enough to verify a correct result. EvMenu
decorations (like borders) are stripped and should not be included. This
should also not include color tags unless `noansi=False`.
If the command returns texts in multiple separate `.msg`-
calls to a receiver, separate these with `|` if `noansi=True`
(default) and `||` if `noansi=False`. If no `msg` is given (`None`),
then no automatic comparison will be done.
cmdset (str, optional): If given, make `.cmdset` available on the Command
instance as it runs. While `.cmdset` is normally available on the
Command instance by default, this is usually only used by
commands that explicitly operates/displays cmdsets, like
`examine`.
noansi (str, optional): By default the color tags of the `msg` is
ignored, this makes them significant. If unset, `msg` must contain
the same color tags as the actual return message.
caller (Object or Account, optional): By default `self.char1` is used as the
command-caller (the `.caller` property on the Command). This allows to
execute with another caller, most commonly an Account.
receiver (Object or Account, optional): This is the object to receive the
return messages we want to test. By default this is the same as `caller`
(which in turn defaults to is `self.char1`). Note that if `msg` is
a `dict`, this is ignored since the receiver is already specified there.
cmdstring (str, optional): Normally this is the Command's `key`.
This allows for tweaking the `.cmdname` property of the
Command`. This isb used for commands with multiple aliases,
where the command explicitly checs which alias was used to
determine its functionality.
obj (str, optional): This sets the `.obj` property of the Command - the
object on which the Command 'sits'. By default this is the same as `caller`.
This can be used for testing on-object Command interactions.
inputs (list, optional): A list of strings to pass to functions that pause to
take input from the user (normally using `@interactive` and
`ret = yield(question)` or `evmenu.get_input`). Each element of the
list will be passed into the command as if the user wrote that at the prompt.
raw_string (str, optional): Normally the `.raw_string` property is set as
a combination of your `key/cmdname` and `input_args`. This allows
direct control of what this is, for example for testing edge cases
or malformed inputs.
Returns:
str or dict: The message sent to `receiver`, or a dict of
`{receiver: "msg", ...}` if multiple are given. This is usually
only used with `msg=None` to do the validation externally.
Raises:
AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
match `expected_input`.
Notes:
As part of the tests, all methods of the Command will be called in
the proper order:
- cmdobj.at_pre_cmd()
- cmdobj.parse()
- cmdobj.func()
- cmdobj.at_post_cmd()
"""
# The `self.char1` is created in the `EvenniaTest` base along with
# other helper objects like self.room and self.obj
caller = caller if caller else self.char1
cmdobj.caller = caller
cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
cmdobj.raw_cmdname = cmdobj.cmdname
cmdobj.cmdstring = cmdobj.cmdname # deprecated
cmdobj.args = input_args
cmdobj.cmdset = cmdset
cmdobj.session = SESSIONS.session_from_sessid(1)
cmdobj.account = self.account
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + input_args
cmdobj.obj = obj or (caller if caller else self.char1)
inputs = inputs or []
# set up receivers
receiver_mapping = {}
if isinstance(msg, dict):
# a mapping {receiver: msg, ...}
receiver_mapping = {recv: str(msg).strip() if msg else None
for recv, msg in msg.items()}
else:
# a single expected string and thus a single receiver (defaults to caller)
receiver = receiver if receiver else caller
receiver_mapping[receiver] = str(msg).strip() if msg is not None else None
unmocked_msg_methods = {}
for receiver in receiver_mapping:
# save the old .msg method so we can get it back
# cleanly after the test
unmocked_msg_methods[receiver] = receiver.msg
# replace normal `.msg` with a mock
receiver.msg = Mock()
# Run the methods of the Command. This mimics what happens in the
# cmdhandler. This will have the mocked .msg be called as part of the
# execution. Mocks remembers what was sent to them so we will be able
# to retrieve what was sent later.
try:
if cmdobj.at_pre_cmd():
return
cmdobj.parse()
ret = cmdobj.func()
# handle func's with yield in them (making them generators)
if isinstance(ret, types.GeneratorType):
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
# this mimics a user's reply to a prompt
ret.send(inp)
except TypeError:
next(ret)
ret = ret.send(inp)
else:
# non-input yield, like yield(10). We don't pause
# but fire it immediately.
next(ret)
except StopIteration:
break
cmdobj.at_post_cmd()
except StopIteration:
pass
except InterruptCommand:
pass
for inp in inputs:
# if there are any inputs left, we may have a non-generator
# input to handle (get_input/ask_yes_no that uses a separate
# cmdset rather than a yield
caller.execute_cmd(inp)
# At this point the mocked .msg methods on each receiver will have
# stored all calls made to them (that's a basic function of the Mock
# class). We will not extract them and compare to what we expected to
# go to each receiver.
returned_msgs = {}
for receiver, expected_msg in receiver_mapping.items():
# get the stored messages from the Mock with Mock.mock_calls.
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", to_str(kwargs))
for name, args, kwargs in receiver.msg.mock_calls
]
# we can return this now, we are done using the mock
receiver.msg = unmocked_msg_methods[receiver]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [str(smsg[0])
if isinstance(smsg, tuple) else str(smsg) for smsg in stored_msg]
if expected_msg is None:
# no expected_msg; just build the returned_msgs dict
returned_msg = "\n".join(str(msg) for msg in stored_msg)
returned_msgs[receiver] = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
else:
# compare messages to expected
# set our separator for returned messages based on parsing ansi or not
msg_sep = "|" if noansi else "||"
# We remove Evmenu decorations since that just makes it harder
# to write the comparison string. We also strip ansi before this
# comparison since otherwise it would mess with the regex.
returned_msg = msg_sep.join(
_RE_STRIP_EVMENU.sub(
"", ansi.parse_ansi(mess, strip_ansi=noansi))
for mess in stored_msg).strip()
# this is the actual test
if expected_msg == "" and returned_msg or not returned_msg.startswith(expected_msg):
# failed the test
raise AssertionError(
self._ERROR_FORMAT.format(
expected_msg=expected_msg, returned_msg=returned_msg)
)
# passed!
returned_msgs[receiver] = returned_msg
if len(returned_msgs) == 1:
return list(returned_msgs.values())[0]
return returned_msgs
# Base testing classes
@override_settings(**DEFAULT_SETTINGS)
class BaseEvenniaTestCase(TestCase):
"""
Base test (with no default objects) but with
enforced default settings.
Base test (with no default objects) but with enforced default settings.
"""
class EvenniaTestCase(TestCase):
"""
For use with gamedir settings; Just like the normal test case, only for naming consistency.
"""
pass
@override_settings(**DEFAULT_SETTINGS)
class BaseEvenniaTest(EvenniaTestMixin, TestCase):
@ -258,7 +547,6 @@ class BaseEvenniaTest(EvenniaTestMixin, TestCase):
"""
class EvenniaTest(EvenniaTestMixin, TestCase):
"""
This test class is intended for inheriting in mygame tests.
@ -273,3 +561,28 @@ class EvenniaTest(EvenniaTestMixin, TestCase):
exit_typeclass = settings.BASE_EXIT_TYPECLASS
room_typeclass = settings.BASE_ROOM_TYPECLASS
script_typeclass = settings.BASE_SCRIPT_TYPECLASS
@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS", MuxCommand)
class BaseEvenniaCommandTest(BaseEvenniaTest, EvenniaCommandTestMixin):
"""
Commands only using the default settings.
"""
class EvenniaCommandTest(EvenniaTest, EvenniaCommandTestMixin):
"""
Parent class to inherit from - makes tests use your own
classes and settings in mygame.
"""

View file

@ -4,10 +4,10 @@ Test eveditor
"""
from evennia.utils import eveditor
from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default.tests import BaseEvenniaCommandTest
class TestEvEditor(EvenniaCommandTest):
class TestEvEditor(BaseEvenniaCommandTest):
def test_eveditor_view_cmd(self):
eveditor.EvEditor(self.char1)
self.call(