Skip to content

Commit

Permalink
Merge pull request #137 from linuxdaemon/gonzobot+pytest-leaks
Browse files Browse the repository at this point in the history
Refactor pytest tests
  • Loading branch information
edwardslabs authored Jan 3, 2018
2 parents c348317 + 38ad13d commit f0fa489
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 57 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ install:
- "pip install -r ./travis/requirements.txt"

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

after_success:
- "coveralls"
Expand Down
2 changes: 1 addition & 1 deletion plugins/admin_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def me(text, conn, chan, nick, admin_log):
@asyncio.coroutine
@hook.command(autohelp=False, permissions=["botcontrol"])
def listchans(conn, chan, message, notice):
"""-- Lists the current channels the bot is in"""
"""- Lists the current channels the bot is in"""
chans = ', '.join(sorted(conn.channels, key=lambda x: x.strip('#').lower()))
lines = formatting.chunk_str("I am currently in: {}".format(chans))
for line in lines:
Expand Down
2 changes: 1 addition & 1 deletion plugins/cryptocurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def init_aliases():
# main command
@hook.command("crypto", "cryptocurrency")
def crypto_command(text, reply):
""" <ticker> [currency] -- Returns current value of a cryptocurrency """
"""<ticker> [currency] - Returns current value of a cryptocurrency"""
args = text.split()
ticker = args.pop(0)

Expand Down
2 changes: 1 addition & 1 deletion plugins/drinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def load_drinks(bot):

@hook.command()
def drink(text, chan, action):
"""<nick>, makes the user a random cocktail."""
"""<nick> - makes the user a random cocktail."""
index = random.randint(0, len(drinks) - 1)
drink = drinks[index]['title']
url = web.try_shorten(drinks[index]['url'])
Expand Down
2 changes: 1 addition & 1 deletion plugins/geoip.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def load_geoip(loop):
@asyncio.coroutine
@hook.command
def geoip(text, reply, loop):
""" geoip <host|ip> -- Looks up the physical location of <host|ip> using Maxmind GeoLite """
"""<host|ip> - Looks up the physical location of <host|ip> using Maxmind GeoLite """
global geoip_reader

if not geoip_reader:
Expand Down
2 changes: 1 addition & 1 deletion plugins/piglatin.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def load_nltk():

@hook.command("pig", "piglatin")
def piglatin(text):
""" pig <text> -- Converts <text> to pig latin. """
"""<text> - Converts <text> to pig latin."""
global pronunciations
if not pronunciations:
return "Please wait, getting NLTK ready!"
Expand Down
6 changes: 3 additions & 3 deletions plugins/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def moreprofile(text, chan, nick, notice):

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

Expand Down Expand Up @@ -130,7 +130,7 @@ def profile(text, chan, notice, nick):

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions plugins/time_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def load_key(bot):

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

Expand Down Expand Up @@ -111,7 +111,7 @@ def time_command(text, reply):

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

if text.lower() == "wut":
return "Instead of hours and minutes, the mean solar day is divided " \
Expand Down
2 changes: 1 addition & 1 deletion plugins/wyr.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_wyr(headers):

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

# keep trying to get entries until we find one that is not filtered
Expand Down
88 changes: 67 additions & 21 deletions tests/core_tests/test_plugin_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@
Validates all hook registrations in all plugins
"""
import importlib
import string
import re
from numbers import Number
from pathlib import Path

from sqlalchemy import MetaData

from cloudbot.event import Event, CommandEvent, RegexEvent, CapEvent, PostHookEvent, IrcOutEvent
from cloudbot.hook import Action
from cloudbot.plugin import Plugin, Hook
from cloudbot.util import database

database.metadata = MetaData()
Hook.original_init = Hook.__init__

DOC_RE = re.compile(r"^(?:(?:<.+?>|{.+?}|\[.+?\]).+?)*?-\s.+$")
PLUGINS = []


class MockBot:
def __init__(self):
self.loop = None


def patch_hook_init(self, _type, plugin, func_hook):
self.original_init(_type, plugin, func_hook)
self.func_hook = func_hook


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


def gather_plugins():
Expand All @@ -25,12 +39,7 @@ def gather_plugins():
return path_list


def load_plugin(plugin_path, monkeypatch):
monkeypatch.setattr('cloudbot.plugin.Hook.original_init', Hook.__init__, raising=False)
monkeypatch.setattr('cloudbot.plugin.Hook.__init__', patch_hook_init)

monkeypatch.setattr('cloudbot.util.database.metadata', MetaData())

def load_plugin(plugin_path):
path = Path(plugin_path)
file_path = path.resolve()
file_name = file_path.name
Expand All @@ -45,10 +54,23 @@ def load_plugin(plugin_path, monkeypatch):
return Plugin(str(file_path), file_name, title, plugin_module)


def get_plugins():
if not PLUGINS:
PLUGINS.extend(map(load_plugin, gather_plugins()))

return PLUGINS


def pytest_generate_tests(metafunc):
if 'plugin_path' in metafunc.fixturenames:
paths = list(gather_plugins())
metafunc.parametrize('plugin_path', paths, ids=list(map(str, paths)))
if 'plugin' in metafunc.fixturenames:
plugins = get_plugins()
metafunc.parametrize('plugin', plugins, ids=[plugin.title for plugin in plugins])
elif 'hook' in metafunc.fixturenames:
plugins = get_plugins()
hooks = [hook for plugin in plugins for hook_list in plugin.hooks.values() for hook in hook_list]
metafunc.parametrize(
'hook', hooks, ids=["{}.{}".format(hook.plugin.title, hook.function_name) for hook in hooks]
)


HOOK_ATTR_TYPES = {
Expand All @@ -67,15 +89,12 @@ def pytest_generate_tests(metafunc):
}


def test_plugin(plugin_path, monkeypatch):
plugin = load_plugin(plugin_path, monkeypatch)
for hooks in plugin.hooks.values():
for hook in hooks:
_test_hook(hook)

def test_hook_kwargs(hook):
assert not hook.func_hook.kwargs, \
"Unknown arguments '{}' passed during registration of hook '{}'".format(
hook.func_hook.kwargs, hook.function_name
)

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


def test_hook_doc(hook):
if hook.type == "command" and hook.doc:
assert hook.doc[:1] not in "." + string.ascii_letters, \
assert DOC_RE.match(hook.doc), \
"Invalid docstring '{}' format for command hook".format(hook.doc)


def test_hook_args(hook):
assert 'async' not in hook.required_args, "Use of deprecated function Event.async"

bot = MockBot()
if hook.type in ("irc_raw", "perm_check", "periodic", "on_start", "on_stop", "event", "on_connect"):
event = Event(bot=bot)
elif hook.type == "command":
event = CommandEvent(bot=bot, hook=hook, text="", triggered_command="")
elif hook.type == "regex":
event = RegexEvent(bot=bot, hook=hook, match=None)
elif hook.type.startswith("on_cap"):
event = CapEvent(bot=bot, cap="")
elif hook.type == "post_hook":
event = PostHookEvent(bot=bot)
elif hook.type == "irc_out":
event = IrcOutEvent(bot=bot)
elif hook.type == "sieve":
return
else:
assert False, "Unhandled hook type '{}' in tests".format(hook.type)

for arg in hook.required_args:
assert hasattr(event, arg), "Undefined parameter '{}' for hook function".format(arg)
3 changes: 3 additions & 0 deletions travis/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ pytest
responses
pytest-cov
pytest-pep8
pytest-leaks
pytest-travis-fold
pytest-pylint
flake8
python-coveralls
pylint
Expand Down
34 changes: 11 additions & 23 deletions travis/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,21 @@
Test JSON files for errors.
"""

import codecs
import fnmatch
import json
import os
import sys
from collections import OrderedDict
from pathlib import Path

exit_code = 0
print("Travis: Testing all JSON files in source")

for root, dirs, files in os.walk('.'):
for filename in fnmatch.filter(files, '*.json'):
file = os.path.join(root, filename)
with codecs.open(file, encoding="utf-8") as f:
text = f.read()
def pytest_generate_tests(metafunc):
if 'json_file' in metafunc.fixturenames:
paths = list(Path().rglob("*.json"))
metafunc.parametrize('json_file', paths, ids=list(map(str, paths)))

try:
data = json.loads(text, object_pairs_hook=OrderedDict)
except Exception as e:
exit_code |= 1
print("Travis: {} is not a valid JSON file, json.load threw exception:\n{}".format(file, e))
else:
formatted_text = json.dumps(data, indent=4) + '\n'

if text != formatted_text:
exit_code |= 2
print("Travis: {} is not a properly formatted JSON file".format(file))
def test_json(json_file):
with json_file.open(encoding="utf-8") as f:
text = f.read()

if exit_code != 0:
sys.exit(exit_code)
data = json.loads(text, object_pairs_hook=OrderedDict)
formatted_text = json.dumps(data, indent=4) + '\n'
assert formatted_text == text, "Improperly formatted JSON file"

0 comments on commit f0fa489

Please sign in to comment.