Skip to content

Commit f0fa489

Browse files
authored
Merge pull request CloudBotIRC#137 from linuxdaemon/gonzobot+pytest-leaks
Refactor pytest tests
2 parents c348317 + 38ad13d commit f0fa489

12 files changed

Lines changed: 93 additions & 57 deletions

File tree

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ install:
1111
- "pip install -r ./travis/requirements.txt"
1212

1313
script:
14-
- "git diff --diff-filter=d --name-only ${TRAVIS_COMMIT_RANGE} | grep -i '\\.py$' | xargs -r pylint --rcfile=travis/pylintrc"
15-
- "py.test . -v --cov . --cov-report term-missing"
14+
- "py.test . -R : -v --cov . --cov-report term-missing --pylint --pylint-rcfile=travis/pylintrc"
1615

1716
after_success:
1817
- "coveralls"

plugins/admin_bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def me(text, conn, chan, nick, admin_log):
372372
@asyncio.coroutine
373373
@hook.command(autohelp=False, permissions=["botcontrol"])
374374
def listchans(conn, chan, message, notice):
375-
"""-- Lists the current channels the bot is in"""
375+
"""- Lists the current channels the bot is in"""
376376
chans = ', '.join(sorted(conn.channels, key=lambda x: x.strip('#').lower()))
377377
lines = formatting.chunk_str("I am currently in: {}".format(chans))
378378
for line in lines:

plugins/cryptocurrency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def init_aliases():
6767
# main command
6868
@hook.command("crypto", "cryptocurrency")
6969
def crypto_command(text, reply):
70-
""" <ticker> [currency] -- Returns current value of a cryptocurrency """
70+
"""<ticker> [currency] - Returns current value of a cryptocurrency"""
7171
args = text.split()
7272
ticker = args.pop(0)
7373

plugins/drinks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def load_drinks(bot):
1616

1717
@hook.command()
1818
def drink(text, chan, action):
19-
"""<nick>, makes the user a random cocktail."""
19+
"""<nick> - makes the user a random cocktail."""
2020
index = random.randint(0, len(drinks) - 1)
2121
drink = drinks[index]['title']
2222
url = web.try_shorten(drinks[index]['url'])

plugins/geoip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def load_geoip(loop):
7777
@asyncio.coroutine
7878
@hook.command
7979
def geoip(text, reply, loop):
80-
""" geoip <host|ip> -- Looks up the physical location of <host|ip> using Maxmind GeoLite """
80+
"""<host|ip> - Looks up the physical location of <host|ip> using Maxmind GeoLite """
8181
global geoip_reader
8282

8383
if not geoip_reader:

plugins/piglatin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def load_nltk():
6868

6969
@hook.command("pig", "piglatin")
7070
def piglatin(text):
71-
""" pig <text> -- Converts <text> to pig latin. """
71+
"""<text> - Converts <text> to pig latin."""
7272
global pronunciations
7373
if not pronunciations:
7474
return "Please wait, getting NLTK ready!"

plugins/profile.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def moreprofile(text, chan, nick, notice):
8484

8585
@hook.command()
8686
def profile(text, chan, notice, nick):
87-
"""<nick> [category] Returns a user's saved profile data from \"<category>\", or lists all available profile categories for the user if no category specified"""
87+
"""<nick> [category] - Returns a user's saved profile data from \"<category>\", or lists all available profile categories for the user if no category specified"""
8888
chan_cf = chan.casefold()
8989
nick_cf = nick.casefold()
9090

@@ -130,7 +130,7 @@ def profile(text, chan, notice, nick):
130130

131131
@hook.command()
132132
def profileadd(text, chan, nick, notice, db):
133-
"""<category> <content> Adds data to your profile in the current channel under \"<category>\""""
133+
"""<category> <content> - Adds data to your profile in the current channel under \"<category>\""""
134134
if nick.casefold() == chan.casefold():
135135
return "Profile data can not be set outside of channels"
136136

@@ -160,7 +160,7 @@ def profileadd(text, chan, nick, notice, db):
160160

161161
@hook.command()
162162
def profiledel(nick, chan, text, notice, db):
163-
"""<category> Deletes \"<category>\" from a user's profile"""
163+
"""<category> - Deletes \"<category>\" from a user's profile"""
164164
if nick.casefold() == chan.casefold():
165165
return "Profile data can not be set outside of channels"
166166

plugins/time_plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def load_key(bot):
4545

4646
@hook.command("time")
4747
def time_command(text, reply):
48-
"""<location> -- Gets the current time in <location>."""
48+
"""<location> - Gets the current time in <location>."""
4949
if not dev_key:
5050
return "This command requires a Google Developers Console API key."
5151

@@ -111,7 +111,7 @@ def time_command(text, reply):
111111

112112
@hook.command(autohelp=False)
113113
def beats(text):
114-
""" -- Gets the current time in .beats (Swatch Internet Time). """
114+
"""- Gets the current time in .beats (Swatch Internet Time)."""
115115

116116
if text.lower() == "wut":
117117
return "Instead of hours and minutes, the mean solar day is divided " \

plugins/wyr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def get_wyr(headers):
4646

4747
@hook.command("wyr", "wouldyourather", autohelp=False)
4848
def wyr(bot):
49-
""" -- What would you rather do? """
49+
"""- What would you rather do?"""
5050
headers = {"User-Agent": bot.user_agent}
5151

5252
# keep trying to get entries until we find one that is not filtered

tests/core_tests/test_plugin_hooks.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,35 @@
22
Validates all hook registrations in all plugins
33
"""
44
import importlib
5-
import string
5+
import re
66
from numbers import Number
77
from pathlib import Path
88

99
from sqlalchemy import MetaData
1010

11+
from cloudbot.event import Event, CommandEvent, RegexEvent, CapEvent, PostHookEvent, IrcOutEvent
1112
from cloudbot.hook import Action
1213
from cloudbot.plugin import Plugin, Hook
14+
from cloudbot.util import database
15+
16+
database.metadata = MetaData()
17+
Hook.original_init = Hook.__init__
18+
19+
DOC_RE = re.compile(r"^(?:(?:<.+?>|{.+?}|\[.+?\]).+?)*?-\s.+$")
20+
PLUGINS = []
21+
22+
23+
class MockBot:
24+
def __init__(self):
25+
self.loop = None
1326

1427

1528
def patch_hook_init(self, _type, plugin, func_hook):
1629
self.original_init(_type, plugin, func_hook)
30+
self.func_hook = func_hook
31+
1732

18-
assert not func_hook.kwargs, \
19-
"Unknown arguments '{}' passed during registration of hook '{}'".format(func_hook.kwargs, self.function_name)
33+
Hook.__init__ = patch_hook_init
2034

2135

2236
def gather_plugins():
@@ -25,12 +39,7 @@ def gather_plugins():
2539
return path_list
2640

2741

28-
def load_plugin(plugin_path, monkeypatch):
29-
monkeypatch.setattr('cloudbot.plugin.Hook.original_init', Hook.__init__, raising=False)
30-
monkeypatch.setattr('cloudbot.plugin.Hook.__init__', patch_hook_init)
31-
32-
monkeypatch.setattr('cloudbot.util.database.metadata', MetaData())
33-
42+
def load_plugin(plugin_path):
3443
path = Path(plugin_path)
3544
file_path = path.resolve()
3645
file_name = file_path.name
@@ -45,10 +54,23 @@ def load_plugin(plugin_path, monkeypatch):
4554
return Plugin(str(file_path), file_name, title, plugin_module)
4655

4756

57+
def get_plugins():
58+
if not PLUGINS:
59+
PLUGINS.extend(map(load_plugin, gather_plugins()))
60+
61+
return PLUGINS
62+
63+
4864
def pytest_generate_tests(metafunc):
49-
if 'plugin_path' in metafunc.fixturenames:
50-
paths = list(gather_plugins())
51-
metafunc.parametrize('plugin_path', paths, ids=list(map(str, paths)))
65+
if 'plugin' in metafunc.fixturenames:
66+
plugins = get_plugins()
67+
metafunc.parametrize('plugin', plugins, ids=[plugin.title for plugin in plugins])
68+
elif 'hook' in metafunc.fixturenames:
69+
plugins = get_plugins()
70+
hooks = [hook for plugin in plugins for hook_list in plugin.hooks.values() for hook in hook_list]
71+
metafunc.parametrize(
72+
'hook', hooks, ids=["{}.{}".format(hook.plugin.title, hook.function_name) for hook in hooks]
73+
)
5274

5375

5476
HOOK_ATTR_TYPES = {
@@ -67,15 +89,12 @@ def pytest_generate_tests(metafunc):
6789
}
6890

6991

70-
def test_plugin(plugin_path, monkeypatch):
71-
plugin = load_plugin(plugin_path, monkeypatch)
72-
for hooks in plugin.hooks.values():
73-
for hook in hooks:
74-
_test_hook(hook)
75-
92+
def test_hook_kwargs(hook):
93+
assert not hook.func_hook.kwargs, \
94+
"Unknown arguments '{}' passed during registration of hook '{}'".format(
95+
hook.func_hook.kwargs, hook.function_name
96+
)
7697

77-
def _test_hook(hook):
78-
assert 'async' not in hook.required_args, "Use of deprecated function Event.async"
7998
for name, types in HOOK_ATTR_TYPES.items():
8099
try:
81100
attr = getattr(hook, name)
@@ -85,6 +104,33 @@ def _test_hook(hook):
85104
assert isinstance(attr, types), \
86105
"Unexpected type '{}' for hook attribute '{}'".format(type(attr).__name__, name)
87106

107+
108+
def test_hook_doc(hook):
88109
if hook.type == "command" and hook.doc:
89-
assert hook.doc[:1] not in "." + string.ascii_letters, \
110+
assert DOC_RE.match(hook.doc), \
90111
"Invalid docstring '{}' format for command hook".format(hook.doc)
112+
113+
114+
def test_hook_args(hook):
115+
assert 'async' not in hook.required_args, "Use of deprecated function Event.async"
116+
117+
bot = MockBot()
118+
if hook.type in ("irc_raw", "perm_check", "periodic", "on_start", "on_stop", "event", "on_connect"):
119+
event = Event(bot=bot)
120+
elif hook.type == "command":
121+
event = CommandEvent(bot=bot, hook=hook, text="", triggered_command="")
122+
elif hook.type == "regex":
123+
event = RegexEvent(bot=bot, hook=hook, match=None)
124+
elif hook.type.startswith("on_cap"):
125+
event = CapEvent(bot=bot, cap="")
126+
elif hook.type == "post_hook":
127+
event = PostHookEvent(bot=bot)
128+
elif hook.type == "irc_out":
129+
event = IrcOutEvent(bot=bot)
130+
elif hook.type == "sieve":
131+
return
132+
else:
133+
assert False, "Unhandled hook type '{}' in tests".format(hook.type)
134+
135+
for arg in hook.required_args:
136+
assert hasattr(event, arg), "Undefined parameter '{}' for hook function".format(arg)

0 commit comments

Comments
 (0)