From f1a127c98627b3d1f6063ef0d3c3ea33d92bbf6b Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Fri, 19 Apr 2019 12:31:37 -0500 Subject: [PATCH 1/7] Refactor duckhunt .killers/.friends commands Move all common code to a central function Fix issues in certain error responses --- plugins/duckhunt.py | 163 ++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 82 deletions(-) diff --git a/plugins/duckhunt.py b/plugins/duckhunt.py index 38418c1a0..a78e41686 100644 --- a/plugins/duckhunt.py +++ b/plugins/duckhunt.py @@ -4,7 +4,7 @@ from threading import Lock from time import time, sleep -from sqlalchemy import Table, Column, String, Integer, PrimaryKeyConstraint, desc, Boolean, and_ +from sqlalchemy import Boolean, Column, Integer, PrimaryKeyConstraint, String, Table, and_, desc from sqlalchemy.sql import select from cloudbot import hook @@ -584,105 +584,104 @@ def top_list(prefix, data, join_char=' • '): ) +def get_scores(db, score_type, network, chan=None): + clause = table.c.network == network + if chan is not None: + clause = and_(clause, table.c.chan == chan.lower()) + + query = select([table.c.name, table.c[score_type]], clause) \ + .order_by(desc(table.c[score_type])) + + scores = db.execute(query).fetchall() + return scores + + +class ScoreType: + def __init__(self, name, column_name, noun, verb): + self.name = name + self.column_name = column_name + self.noun = noun + self.verb = verb + + +SCORE_TYPES = { + 'friend': ScoreType('befriend', 'befriend', 'friend', 'friended'), + 'killer': ScoreType('killer', 'shot', 'killer', 'killed'), +} + + +def display_scores(score_type: ScoreType, text, chan, conn, db): + if is_opt_out(conn.name, chan): + return + + global_pfx = "Duck {noun} scores across the network: ".format( + noun=score_type.noun + ) + chan_pfx = "Duck {noun} scores in {chan}: ".format( + noun=score_type.noun, chan=chan + ) + no_ducks = "It appears no one has {verb} any ducks yet." + + scores_dict = defaultdict(int) + chancount = defaultdict(int) + text_lower = text.lower() + if text_lower in ('global', 'average'): + out = global_pfx + scores = get_scores(db, score_type.column_name, conn.name) + if not scores: + return no_ducks + + for row in scores: + if row[1] == 0: + continue + + chancount[row[0]] += 1 + scores_dict[row[0]] += row[1] + + if text_lower == 'average': + for k, v in scores_dict.items(): + scores_dict[k] = int(v / chancount[k]) + else: + out = chan_pfx + scores = get_scores(db, score_type.column_name, conn.name, chan) + if not scores: + return no_ducks + + for row in scores: + if row[1] == 0: + continue + + scores_dict[row[0]] += row[1] + + return top_list(out, scores_dict.items()) + + @hook.command("friends", autohelp=False) def friends(text, chan, conn, db): - """[{global|average}] - Prints a list of the top duck friends in the channel, if 'global' is specified all channels - in the database are included. + """[{global|average}] - Prints a list of the top duck friends in the + channel, if 'global' is specified all channels in the database are + included. :type text: str :type chan: str :type conn: cloudbot.client.Client :type db: sqlalchemy.orm.Session """ - if is_opt_out(conn.name, chan): - return - - friends_dict = defaultdict(int) - chancount = defaultdict(int) - if text.lower() in ('global', 'average'): - out = "Duck friend scores across the network: " - scores = db.execute(select([table.c.name, table.c.befriend]) \ - .where(table.c.network == conn.name) \ - .order_by(desc(table.c.befriend))) - if scores: - for row in scores: - if row[1] == 0: - continue - - chancount[row[0]] += 1 - friends_dict[row[0]] += row[1] - - if text.lower() == 'average': - for k, v in friends_dict.items(): - friends_dict[k] = int(v / chancount[k]) - else: - return "it appears no on has friended any ducks yet." - else: - out = "Duck friend scores in {}: ".format(chan) - scores = db.execute(select([table.c.name, table.c.befriend]) \ - .where(table.c.network == conn.name) \ - .where(table.c.chan == chan.lower()) \ - .order_by(desc(table.c.befriend))) - if scores: - for row in scores: - if row[1] == 0: - continue - friends_dict[row[0]] += row[1] - else: - return "it appears no on has friended any ducks yet." - - return top_list(out, friends_dict.items()) + return display_scores(SCORE_TYPES['friend'], text, chan, conn, db) @hook.command("killers", autohelp=False) def killers(text, chan, conn, db): - """[{global|average}] - Prints a list of the top duck killers in the channel, if 'global' is specified all channels - in the database are included. + """[{global|average}] - Prints a list of the top duck killers in the + channel, if 'global' is specified all channels in the database are + included. :type text: str :type chan: str :type conn: cloudbot.client.Client :type db: sqlalchemy.orm.Session """ - if is_opt_out(conn.name, chan): - return - - killers_dict = defaultdict(int) - chancount = defaultdict(int) - if text.lower() in ('global', 'average'): - out = "Duck killer scores across the network: " - scores = db.execute(select([table.c.name, table.c.shot]) \ - .where(table.c.network == conn.name) \ - .order_by(desc(table.c.shot))) - if scores: - for row in scores: - if row[1] == 0: - continue - - chancount[row[0]] += 1 - killers_dict[row[0]] += row[1] - - if text.lower() == 'average': - for k, v in killers_dict.items(): - killers_dict[k] = int(v / chancount[k]) - else: - return "it appears no on has killed any ducks yet." - else: - out = "Duck killer scores in {}: ".format(chan) - scores = db.execute(select([table.c.name, table.c.shot]) \ - .where(table.c.network == conn.name) \ - .where(table.c.chan == chan.lower()) \ - .order_by(desc(table.c.shot))) - if scores: - for row in scores: - if row[1] == 0: - continue - - killers_dict[row[0]] += row[1] - else: - return "it appears no on has killed any ducks yet." - - return top_list(out, killers_dict.items()) + return display_scores(SCORE_TYPES['killer'], text, chan, conn, db) @hook.command("duckforgive", permissions=["op", "ignore"]) From 55c8f6f1f0f01b71c8d84648891ea9276ad78096 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Fri, 19 Apr 2019 12:58:18 -0500 Subject: [PATCH 2/7] Split score calculations to distinct functions --- plugins/duckhunt.py | 79 +++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/plugins/duckhunt.py b/plugins/duckhunt.py index a78e41686..721574050 100644 --- a/plugins/duckhunt.py +++ b/plugins/duckhunt.py @@ -610,6 +610,49 @@ def __init__(self, name, column_name, noun, verb): } +def get_channel_scores(db, score_type: ScoreType, conn, chan): + scores_dict = defaultdict(int) + scores = get_scores(db, score_type.column_name, conn.name, chan) + if not scores: + return None + + for row in scores: + if row[1] == 0: + continue + + scores_dict[row[0]] += row[1] + + return scores_dict + + +def get_global_scores(db, score_type: ScoreType, conn): + scores_dict = defaultdict(int) + chancount = defaultdict(int) + scores = get_scores(db, score_type.column_name, conn.name) + if not scores: + return None, None + + for row in scores: + if row[1] == 0: + continue + + chancount[row[0]] += 1 + scores_dict[row[0]] += row[1] + + return scores_dict, chancount + + +def get_average_scores(db, score_type: ScoreType, conn): + scores_dict, chancount = get_global_scores(db, score_type, conn) + if not scores_dict: + return None + + for k, v in scores_dict.items(): + scores_dict[k] = int(v / chancount[k]) + + return scores_dict + + def display_scores(score_type: ScoreType, text, chan, conn, db): if is_opt_out(conn.name, chan): return @@ -622,38 +665,24 @@ def display_scores(score_type: ScoreType, text, chan, conn, db): ) no_ducks = "It appears no one has {verb} any ducks yet." - scores_dict = defaultdict(int) - chancount = defaultdict(int) text_lower = text.lower() - if text_lower in ('global', 'average'): + if text_lower == 'global': out = global_pfx - scores = get_scores(db, score_type.column_name, conn.name) - if not scores: - return no_ducks - - for row in scores: - if row[1] == 0: - continue - - chancount[row[0]] += 1 - scores_dict[row[0]] += row[1] - - if text_lower == 'average': - for k, v in scores_dict.items(): - scores_dict[k] = int(v / chancount[k]) + scores_dict = get_global_scores(db, score_type, conn) + elif text_lower == 'average': + out = global_pfx + scores_dict = get_average_scores(db, score_type, conn) else: out = chan_pfx - scores = get_scores(db, score_type.column_name, conn.name, chan) - if not scores: - return no_ducks + scores_dict = get_channel_scores(db, score_type, conn, chan) - for row in scores: - if row[1] == 0: - continue + if not scores_dict: + return no_ducks - scores_dict[row[0]] += row[1] + if scores_dict: + return top_list(out, scores_dict.items()) - return top_list(out, scores_dict.items()) + return None @hook.command("friends", autohelp=False) From ca9ec6609d81f58dc5f53b01fa4e097220699b2c Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Fri, 19 Apr 2019 13:10:24 -0500 Subject: [PATCH 3/7] Use dict instead of if-elif block for calc lookup --- plugins/duckhunt.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/plugins/duckhunt.py b/plugins/duckhunt.py index 721574050..1134783e7 100644 --- a/plugins/duckhunt.py +++ b/plugins/duckhunt.py @@ -11,6 +11,7 @@ from cloudbot.event import EventType from cloudbot.util import database from cloudbot.util.formatting import pluralize_auto, truncate +from cloudbot.util.func_utils import call_with_args duck_tail = "・゜゜・。。・゜゜" duck = ["\\_o< ", "\\_O< ", "\\_0< ", "\\_\u00f6< ", "\\_\u00f8< ", "\\_\u00f3< "] @@ -604,12 +605,6 @@ def __init__(self, name, column_name, noun, verb): self.verb = verb -SCORE_TYPES = { - 'friend': ScoreType('befriend', 'befriend', 'friend', 'friended'), - 'killer': ScoreType('killer', 'shot', 'killer', 'killed'), -} - - def get_channel_scores(db, score_type: ScoreType, conn, chan): scores_dict = defaultdict(int) scores = get_scores(db, score_type.column_name, conn.name, chan) @@ -653,6 +648,18 @@ def get_average_scores(db, score_type: ScoreType, conn): return scores_dict +SCORE_TYPES = { + 'friend': ScoreType('befriend', 'befriend', 'friend', 'friended'), + 'killer': ScoreType('killer', 'shot', 'killer', 'killed'), +} + +DISPLAY_FUNCS = { + 'average': get_average_scores, + 'global': get_global_scores, + None: get_channel_scores, +} + + def display_scores(score_type: ScoreType, text, chan, conn, db): if is_opt_out(conn.name, chan): return @@ -665,24 +672,20 @@ def display_scores(score_type: ScoreType, text, chan, conn, db): ) no_ducks = "It appears no one has {verb} any ducks yet." - text_lower = text.lower() - if text_lower == 'global': - out = global_pfx - scores_dict = get_global_scores(db, score_type, conn) - elif text_lower == 'average': - out = global_pfx - scores_dict = get_average_scores(db, score_type, conn) - else: - out = chan_pfx - scores_dict = get_channel_scores(db, score_type, conn, chan) + out = global_pfx if text else chan_pfx + + func = DISPLAY_FUNCS[text.lower() or None] + scores_dict = call_with_args(func, { + 'db': db, + 'score_type': score_type, + 'conn': conn, + 'chan': chan, + }) if not scores_dict: return no_ducks - if scores_dict: - return top_list(out, scores_dict.items()) - - return None + return top_list(out, scores_dict.items()) @hook.command("friends", autohelp=False) From fe72cd7433e59d4874a00050f473910b6d652066 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Sat, 20 Apr 2019 14:18:11 -0500 Subject: [PATCH 4/7] Add tests for new duck score display --- tests/plugin_tests/test_duckhunt.py | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/plugin_tests/test_duckhunt.py diff --git a/tests/plugin_tests/test_duckhunt.py b/tests/plugin_tests/test_duckhunt.py new file mode 100644 index 000000000..48883f00f --- /dev/null +++ b/tests/plugin_tests/test_duckhunt.py @@ -0,0 +1,95 @@ +import importlib + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + +from plugins.duckhunt import top_list + + +class MockDB: + def __init__(self): + self.engine = create_engine('sqlite:///:memory:') + self.session = scoped_session(sessionmaker(self.engine)) + + +@pytest.fixture() +def mock_db(): + return MockDB() + + +@pytest.mark.parametrize('prefix,items,result', [ + ( + 'Duck friend scores in #TestChannel: ', + { + 'testuser': 5, + 'testuser1': 1, + }, + 'Duck friend scores in #TestChannel: \x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' + ), +]) +def test_top_list(prefix, items, result): + assert top_list(prefix, items.items()) == result + + +def test_display_scores(mock_db): + from cloudbot.util.database import metadata + metadata.bind = mock_db.engine + from plugins import duckhunt + + importlib.reload(duckhunt) + + metadata.create_all(checkfirst=True) + + session = mock_db.session() + + class Conn: + name = 'TestConn' + + conn = Conn() + + duckhunt.update_score('TestUser', '#TestChannel', session, conn, 5, 4) + duckhunt.update_score('TestUser1', '#TestChannel', session, conn, 1, 7) + duckhunt.update_score('OtherUser', '#TestChannel1', session, conn, 9, 2) + + assert duckhunt.get_channel_scores( + session, duckhunt.SCORE_TYPES['friend'], conn, '#TestChannel' + ) == {'testuser': 4, 'testuser1': 7} + + assert repr(duckhunt.friends('', '#TestChannel', conn, session)) == repr( + 'Duck friend scores in #TestChannel: ' + '\x02t\u200bestuser1\x02: 7 • \x02t\u200bestuser\x02: 4' + ) + + assert repr(duckhunt.killers('', '#TestChannel', conn, session)) == repr( + 'Duck killer scores in #TestChannel: ' + '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' + ) + + assert repr(duckhunt.friends('global', '#TestChannel', conn, session)) == repr( + 'Duck friend scores across the network: ' + '\x02t\u200bestuser1\x02: 7' + ' • \x02t\u200bestuser\x02: 4' + ' • \x02o\u200btheruser\x02: 2' + ) + + assert repr(duckhunt.killers('global', '#TestChannel', conn, session)) == repr( + 'Duck killer scores across the network: ' + '\x02o\u200btheruser\x02: 9' + ' • \x02t\u200bestuser\x02: 5' + ' • \x02t\u200bestuser1\x02: 1' + ) + + assert repr(duckhunt.friends('average', '#TestChannel', conn, session)) == repr( + 'Duck friend scores across the network: ' + '\x02t\u200bestuser1\x02: 7' + ' • \x02t\u200bestuser\x02: 4' + ' • \x02o\u200btheruser\x02: 2' + ) + + assert repr(duckhunt.killers('average', '#TestChannel', conn, session)) == repr( + 'Duck killer scores across the network: ' + '\x02o\u200btheruser\x02: 9' + ' • \x02t\u200bestuser\x02: 5' + ' • \x02t\u200bestuser1\x02: 1' + ) From 7fbe5e33195cc44ab59077a6ff485431ae27ac80 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Sat, 20 Apr 2019 14:18:25 -0500 Subject: [PATCH 5/7] Wrap internal get_global_scores This is due to a bug where get_global_scores returned a tuple for use by get_average_scores, but display_scores expected only one value --- plugins/duckhunt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/duckhunt.py b/plugins/duckhunt.py index 1134783e7..281dacc3f 100644 --- a/plugins/duckhunt.py +++ b/plugins/duckhunt.py @@ -620,7 +620,7 @@ def get_channel_scores(db, score_type: ScoreType, conn, chan): return scores_dict -def get_global_scores(db, score_type: ScoreType, conn): +def _get_global_scores(db, score_type: ScoreType, conn): scores_dict = defaultdict(int) chancount = defaultdict(int) scores = get_scores(db, score_type.column_name, conn.name) @@ -637,8 +637,12 @@ def get_global_scores(db, score_type: ScoreType, conn): return scores_dict, chancount +def get_global_scores(db, score_type: ScoreType, conn): + return _get_global_scores(db, score_type, conn)[0] + + def get_average_scores(db, score_type: ScoreType, conn): - scores_dict, chancount = get_global_scores(db, score_type, conn) + scores_dict, chancount = _get_global_scores(db, score_type, conn) if not scores_dict: return None From c6993757086742f43e064cb01472cd568f57b424 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Sat, 20 Apr 2019 14:20:47 -0500 Subject: [PATCH 6/7] Fix line lengths in duckhunt tests --- tests/plugin_tests/test_duckhunt.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/plugin_tests/test_duckhunt.py b/tests/plugin_tests/test_duckhunt.py index 48883f00f..25fee53a7 100644 --- a/tests/plugin_tests/test_duckhunt.py +++ b/tests/plugin_tests/test_duckhunt.py @@ -25,7 +25,8 @@ def mock_db(): 'testuser': 5, 'testuser1': 1, }, - 'Duck friend scores in #TestChannel: \x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' + 'Duck friend scores in #TestChannel: ' + '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' ), ]) def test_top_list(prefix, items, result): @@ -56,38 +57,50 @@ class Conn: session, duckhunt.SCORE_TYPES['friend'], conn, '#TestChannel' ) == {'testuser': 4, 'testuser1': 7} - assert repr(duckhunt.friends('', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.friends('', '#TestChannel', conn, session) + ) == repr( 'Duck friend scores in #TestChannel: ' '\x02t\u200bestuser1\x02: 7 • \x02t\u200bestuser\x02: 4' ) - assert repr(duckhunt.killers('', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.killers('', '#TestChannel', conn, session) + ) == repr( 'Duck killer scores in #TestChannel: ' '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' ) - assert repr(duckhunt.friends('global', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.friends('global', '#TestChannel', conn, session) + ) == repr( 'Duck friend scores across the network: ' '\x02t\u200bestuser1\x02: 7' ' • \x02t\u200bestuser\x02: 4' ' • \x02o\u200btheruser\x02: 2' ) - assert repr(duckhunt.killers('global', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.killers('global', '#TestChannel', conn, session) + ) == repr( 'Duck killer scores across the network: ' '\x02o\u200btheruser\x02: 9' ' • \x02t\u200bestuser\x02: 5' ' • \x02t\u200bestuser1\x02: 1' ) - assert repr(duckhunt.friends('average', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.friends('average', '#TestChannel', conn, session) + ) == repr( 'Duck friend scores across the network: ' '\x02t\u200bestuser1\x02: 7' ' • \x02t\u200bestuser\x02: 4' ' • \x02o\u200btheruser\x02: 2' ) - assert repr(duckhunt.killers('average', '#TestChannel', conn, session)) == repr( + assert repr( + duckhunt.killers('average', '#TestChannel', conn, session) + ) == repr( 'Duck killer scores across the network: ' '\x02o\u200btheruser\x02: 9' ' • \x02t\u200bestuser\x02: 5' From a6db6522e52c1fe9cc597f224ad3d026bf8a0bdb Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Fri, 26 Apr 2019 10:50:33 -0500 Subject: [PATCH 7/7] Clean up duckhunt tests --- tests/plugin_tests/test_duckhunt.py | 74 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/tests/plugin_tests/test_duckhunt.py b/tests/plugin_tests/test_duckhunt.py index 25fee53a7..64ff4188a 100644 --- a/tests/plugin_tests/test_duckhunt.py +++ b/tests/plugin_tests/test_duckhunt.py @@ -19,15 +19,15 @@ def mock_db(): @pytest.mark.parametrize('prefix,items,result', [ - ( - 'Duck friend scores in #TestChannel: ', - { - 'testuser': 5, - 'testuser1': 1, - }, - 'Duck friend scores in #TestChannel: ' - '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' - ), + [ + 'Duck friend scores in #TestChannel: ', + { + 'testuser': 5, + 'testuser1': 1, + }, + 'Duck friend scores in #TestChannel: ' + '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' + ], ]) def test_top_list(prefix, items, result): assert top_list(prefix, items.items()) == result @@ -49,60 +49,68 @@ class Conn: conn = Conn() - duckhunt.update_score('TestUser', '#TestChannel', session, conn, 5, 4) - duckhunt.update_score('TestUser1', '#TestChannel', session, conn, 1, 7) - duckhunt.update_score('OtherUser', '#TestChannel1', session, conn, 9, 2) + chan = '#TestChannel' + chan1 = '#TestChannel1' + + duckhunt.update_score('TestUser', chan, session, conn, 5, 4) + duckhunt.update_score('TestUser1', chan, session, conn, 1, 7) + duckhunt.update_score('OtherUser', chan1, session, conn, 9, 2) + + expected_testchan_friend_scores = {'testuser': 4, 'testuser1': 7} + + actual_testchan_friend_scores = duckhunt.get_channel_scores( + session, duckhunt.SCORE_TYPES['friend'], + conn, chan + ) - assert duckhunt.get_channel_scores( - session, duckhunt.SCORE_TYPES['friend'], conn, '#TestChannel' - ) == {'testuser': 4, 'testuser1': 7} + assert actual_testchan_friend_scores == expected_testchan_friend_scores - assert repr( - duckhunt.friends('', '#TestChannel', conn, session) - ) == repr( + chan_friends = ( 'Duck friend scores in #TestChannel: ' '\x02t\u200bestuser1\x02: 7 • \x02t\u200bestuser\x02: 4' ) - assert repr( - duckhunt.killers('', '#TestChannel', conn, session) - ) == repr( + chan_kills = ( 'Duck killer scores in #TestChannel: ' '\x02t\u200bestuser\x02: 5 • \x02t\u200bestuser1\x02: 1' ) - assert repr( - duckhunt.friends('global', '#TestChannel', conn, session) - ) == repr( + global_friends = ( 'Duck friend scores across the network: ' '\x02t\u200bestuser1\x02: 7' ' • \x02t\u200bestuser\x02: 4' ' • \x02o\u200btheruser\x02: 2' ) - assert repr( - duckhunt.killers('global', '#TestChannel', conn, session) - ) == repr( + global_kills = ( 'Duck killer scores across the network: ' '\x02o\u200btheruser\x02: 9' ' • \x02t\u200bestuser\x02: 5' ' • \x02t\u200bestuser1\x02: 1' ) - assert repr( - duckhunt.friends('average', '#TestChannel', conn, session) - ) == repr( + average_friends = ( 'Duck friend scores across the network: ' '\x02t\u200bestuser1\x02: 7' ' • \x02t\u200bestuser\x02: 4' ' • \x02o\u200btheruser\x02: 2' ) - assert repr( - duckhunt.killers('average', '#TestChannel', conn, session) - ) == repr( + average_kills = ( 'Duck killer scores across the network: ' '\x02o\u200btheruser\x02: 9' ' • \x02t\u200bestuser\x02: 5' ' • \x02t\u200bestuser1\x02: 1' ) + + assert duckhunt.friends('', chan, conn, session) == chan_friends + + assert duckhunt.killers('', chan, conn, session) == chan_kills + + assert duckhunt.friends('global', chan, conn, session) == global_friends + + assert duckhunt.killers('global', chan, conn, session) == global_kills + + assert duckhunt.friends('average', chan, conn, session) == average_friends + + assert duckhunt.killers('average', chan, conn, session) == average_kills