2024-08-09 23:14:42 +01:00
|
|
|
"""
|
2024-08-11 17:53:37 +01:00
|
|
|
Test functions for the matching module
|
2024-08-09 23:14:42 +01:00
|
|
|
"""
|
|
|
|
import discord
|
|
|
|
import pytest
|
2024-08-10 16:30:56 +01:00
|
|
|
import random
|
2024-08-14 22:42:53 +01:00
|
|
|
import matchy.matching as matching
|
|
|
|
import matchy.files.state as state
|
2024-08-11 22:07:43 +01:00
|
|
|
import copy
|
|
|
|
import itertools
|
2024-08-10 15:12:14 +01:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
def test_protocols():
|
|
|
|
"""Verify the protocols we're using match the discord ones"""
|
|
|
|
assert isinstance(discord.Member, matching.Member)
|
|
|
|
assert isinstance(discord.Guild, matching.Guild)
|
|
|
|
assert isinstance(discord.Role, matching.Role)
|
2024-08-10 21:47:32 +01:00
|
|
|
assert isinstance(Member, matching.Member)
|
2024-08-12 19:31:41 +01:00
|
|
|
assert isinstance(Role, matching.Role)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
class Role():
|
2024-08-10 15:12:14 +01:00
|
|
|
def __init__(self, id: int):
|
|
|
|
self._id = id
|
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
@property
|
|
|
|
def id(self) -> int:
|
|
|
|
return self._id
|
|
|
|
|
2024-08-12 19:31:41 +01:00
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
pass
|
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
|
|
|
|
class Member():
|
|
|
|
def __init__(self, id: int, roles: list[Role] = []):
|
|
|
|
self._id = id
|
2024-08-11 17:53:37 +01:00
|
|
|
self._roles = roles
|
2024-08-10 21:47:32 +01:00
|
|
|
|
2024-08-10 15:12:14 +01:00
|
|
|
@property
|
|
|
|
def mention(self) -> str:
|
|
|
|
return f"<@{self._id}>"
|
|
|
|
|
2024-08-12 19:31:41 +01:00
|
|
|
@property
|
|
|
|
def display_name(self) -> str:
|
|
|
|
return f"{self._id}"
|
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
@property
|
|
|
|
def roles(self) -> list[Role]:
|
2024-08-11 17:53:37 +01:00
|
|
|
return self._roles
|
2024-08-10 21:47:32 +01:00
|
|
|
|
2024-08-11 22:07:43 +01:00
|
|
|
@roles.setter
|
|
|
|
def roles(self, roles: list[Role]):
|
|
|
|
self._roles = roles
|
|
|
|
|
2024-08-10 15:12:14 +01:00
|
|
|
@property
|
|
|
|
def id(self) -> int:
|
|
|
|
return self._id
|
|
|
|
|
2024-08-09 23:14:42 +01:00
|
|
|
|
2024-08-11 22:07:43 +01:00
|
|
|
def members_to_groups_validate(matchees: list[Member], tmp_state: state.State, per_group: int):
|
2024-08-10 16:30:56 +01:00
|
|
|
"""Inner function to validate the main output of the groups function"""
|
2024-08-11 12:16:23 +01:00
|
|
|
groups = matching.members_to_groups(matchees, tmp_state, per_group)
|
2024-08-10 16:30:56 +01:00
|
|
|
|
|
|
|
# We should always have one group
|
|
|
|
assert len(groups)
|
|
|
|
|
|
|
|
# Log the groups to history
|
|
|
|
# This will validate the internals
|
2024-08-11 12:16:23 +01:00
|
|
|
tmp_state.log_groups(groups)
|
2024-08-10 16:30:56 +01:00
|
|
|
|
|
|
|
# Ensure each group contains within the bounds of expected members
|
|
|
|
for group in groups:
|
|
|
|
if len(matchees) >= per_group:
|
|
|
|
assert len(group) >= per_group
|
|
|
|
else:
|
|
|
|
assert len(group) == len(matchees)
|
|
|
|
assert len(group) < per_group*2 # TODO: We could be more strict here
|
|
|
|
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
2024-08-09 23:14:42 +01:00
|
|
|
@pytest.mark.parametrize("matchees, per_group", [
|
2024-08-10 15:12:14 +01:00
|
|
|
# Simplest test possible
|
2024-08-10 16:30:56 +01:00
|
|
|
([Member(1)], 1),
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
# More requested than we have
|
2024-08-10 16:30:56 +01:00
|
|
|
([Member(1)], 2),
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
# A selection of hyper-simple checks to validate core functionality
|
2024-08-10 16:30:56 +01:00
|
|
|
([Member(1)] * 100, 3),
|
|
|
|
([Member(1)] * 12, 5),
|
|
|
|
([Member(1)] * 11, 2),
|
|
|
|
([Member(1)] * 356, 8),
|
2024-08-10 21:47:32 +01:00
|
|
|
], ids=['single', "larger_groups", "100_members", "5_group", "pairs", "356_big_groups"])
|
2024-08-10 16:30:56 +01:00
|
|
|
def test_members_to_groups_no_history(matchees, per_group):
|
2024-08-10 15:12:14 +01:00
|
|
|
"""Test simple group matching works"""
|
2024-08-13 23:32:28 +01:00
|
|
|
tmp_state = state.State(state._EMPTY_DICT)
|
2024-08-11 22:07:43 +01:00
|
|
|
members_to_groups_validate(matchees, tmp_state, per_group)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
def items_found_in_lists(list_of_lists, items):
|
|
|
|
"""validates if any sets of items are found in individual lists"""
|
|
|
|
for sublist in list_of_lists:
|
|
|
|
if all(item in sublist for item in items):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("history_data, matchees, per_group, checks", [
|
|
|
|
# Slightly more difficult test
|
|
|
|
(
|
2024-08-11 12:16:23 +01:00
|
|
|
# Describe a history where we previously matched up some people and ensure they don't get rematched
|
2024-08-10 15:12:14 +01:00
|
|
|
[
|
|
|
|
{
|
|
|
|
"ts": datetime.now() - timedelta(days=1),
|
|
|
|
"groups": [
|
2024-08-10 16:30:56 +01:00
|
|
|
[Member(1), Member(2)],
|
|
|
|
[Member(3), Member(4)],
|
2024-08-10 15:12:14 +01:00
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
[
|
2024-08-10 16:30:56 +01:00
|
|
|
Member(1),
|
|
|
|
Member(2),
|
|
|
|
Member(3),
|
|
|
|
Member(4),
|
2024-08-10 15:12:14 +01:00
|
|
|
],
|
|
|
|
2,
|
|
|
|
[
|
|
|
|
lambda groups: not items_found_in_lists(
|
2024-08-10 16:30:56 +01:00
|
|
|
groups, [Member(1), Member(2)]),
|
2024-08-10 15:12:14 +01:00
|
|
|
lambda groups: not items_found_in_lists(
|
2024-08-10 16:30:56 +01:00
|
|
|
groups, [Member(3), Member(4)])
|
2024-08-10 15:12:14 +01:00
|
|
|
]
|
|
|
|
),
|
|
|
|
# Feed the system an "impossible" test
|
|
|
|
# The function should fall back to ignoring history and still give us something
|
|
|
|
(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"ts": datetime.now() - timedelta(days=1),
|
|
|
|
"groups": [
|
2024-08-10 21:47:32 +01:00
|
|
|
[
|
|
|
|
Member(1),
|
|
|
|
Member(2),
|
|
|
|
Member(3)
|
|
|
|
],
|
|
|
|
[
|
|
|
|
Member(4),
|
|
|
|
Member(5),
|
|
|
|
Member(6)
|
|
|
|
],
|
2024-08-10 15:12:14 +01:00
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
[
|
2024-08-10 21:47:32 +01:00
|
|
|
Member(1, [Role(1), Role(2), Role(3), Role(4)]),
|
|
|
|
Member(2, [Role(1), Role(2), Role(3), Role(4)]),
|
|
|
|
Member(3, [Role(1), Role(2), Role(3), Role(4)]),
|
|
|
|
Member(4, [Role(1), Role(2), Role(3), Role(4)]),
|
|
|
|
Member(5, [Role(1), Role(2), Role(3), Role(4)]),
|
|
|
|
Member(6, [Role(1), Role(2), Role(3), Role(4)]),
|
2024-08-10 15:12:14 +01:00
|
|
|
],
|
|
|
|
3,
|
|
|
|
[
|
|
|
|
# Nothing specific to validate
|
|
|
|
]
|
|
|
|
),
|
2024-08-11 17:53:37 +01:00
|
|
|
# Specific test pulled out of the stress test
|
|
|
|
(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"ts": datetime.now() - timedelta(days=4),
|
|
|
|
"groups": [
|
|
|
|
[Member(i) for i in [1, 2, 3, 4, 5, 6,
|
|
|
|
7, 8, 9, 10, 11, 12, 13, 14, 15]]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"ts": datetime.now() - timedelta(days=5),
|
|
|
|
"groups": [
|
|
|
|
[Member(i) for i in [1, 2, 3, 4, 5, 6, 7, 8]]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
[Member(i) for i in [1, 2, 11, 4, 12, 3, 7, 5, 8, 10, 9, 6]],
|
|
|
|
3,
|
|
|
|
[
|
|
|
|
# Nothing specific to validate
|
|
|
|
]
|
|
|
|
),
|
|
|
|
# Silly example that failued due to bad role logic
|
|
|
|
(
|
|
|
|
[
|
|
|
|
# No history
|
|
|
|
],
|
|
|
|
[
|
|
|
|
# print([(m.id, [r.id for r in m.roles]) for m in matchees]) to get the below
|
|
|
|
Member(i, [Role(r) for r in roles]) for (i, roles) in
|
|
|
|
[
|
|
|
|
(4, [1, 2, 3, 4, 5, 6, 7, 8]),
|
|
|
|
(8, [1]),
|
|
|
|
(9, [1, 2, 3, 4, 5]),
|
|
|
|
(6, [1, 2, 3]),
|
|
|
|
(11, [1, 2, 3]),
|
|
|
|
(7, [1, 2, 3, 4, 5, 6, 7]),
|
|
|
|
(1, [1, 2, 3, 4]),
|
|
|
|
(5, [1, 2, 3, 4, 5]),
|
|
|
|
(12, [1, 2, 3, 4]),
|
|
|
|
(10, [1]),
|
|
|
|
(13, [1, 2, 3, 4, 5, 6]),
|
|
|
|
(2, [1, 2, 3, 4, 5, 6]),
|
|
|
|
(3, [1, 2, 3, 4, 5, 6, 7])
|
|
|
|
]
|
|
|
|
],
|
|
|
|
2,
|
|
|
|
[
|
|
|
|
# Nothing else
|
|
|
|
]
|
2024-08-11 22:07:43 +01:00
|
|
|
),
|
|
|
|
# Another weird one pulled out of the stress test
|
|
|
|
(
|
|
|
|
[
|
|
|
|
# print([(str(h["ts"]), [[f"Member({gm.id})" for gm in g] for g in h["groups"]]) for h in history_data])
|
|
|
|
{"ts": datetime.strptime(ts, r"%Y-%m-%d %H:%M:%S.%f"), "groups": [
|
|
|
|
[Member(m) for m in group] for group in groups]}
|
|
|
|
for (ts, groups) in
|
|
|
|
[
|
|
|
|
(
|
|
|
|
'2024-07-07 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2, 3, 4, 5],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|
|
|
[1],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8]
|
|
|
|
]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
'2024-07-13 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
|
|
|
12, 13, 14, 15, 16, 17, 18],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
|
|
[1]
|
|
|
|
]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
'2024-06-29 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2, 3, 4, 5],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
|
|
|
|
13, 14, 15, 16, 17, 18, 19, 20],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
|
|
|
11, 12, 13, 14, 15, 16, 17],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
|
|
|
12, 13, 14, 15, 16, 17, 18, 19, 20]
|
|
|
|
]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
'2024-06-25 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
|
|
|
12, 13, 14, 15, 16, 17, 18],
|
|
|
|
[1, 2],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
|
|
|
12, 13, 14, 15, 16, 17, 18, 19],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
|
|
[1, 2]
|
|
|
|
]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
'2024-07-04 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2, 3, 4, 5],
|
|
|
|
[1, 2, 3],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7]
|
|
|
|
]
|
|
|
|
),
|
|
|
|
(
|
|
|
|
'2024-07-16 20:25:56.313993',
|
|
|
|
[
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
|
|
|
|
13, 14, 15, 16, 17, 18, 19, 20],
|
|
|
|
[1, 2, 3, 4, 5, 6],
|
|
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
|
|
|
11, 12, 13, 14, 15, 16, 17, 18]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
],
|
|
|
|
[
|
|
|
|
# print([(m.id, [r.id for r in m.roles]) for m in matchees]) to get the below
|
|
|
|
Member(i, [Role(r) for r in roles]) for (i, roles) in
|
|
|
|
[
|
|
|
|
(10, [1, 2, 3]),
|
|
|
|
(4, [1, 2, 3]),
|
|
|
|
(5, [1, 2]),
|
|
|
|
(13, [1, 2]),
|
|
|
|
(3, [1, 2, 3, 4]),
|
|
|
|
(14, [1]),
|
|
|
|
(6, [1, 2, 3, 4]),
|
|
|
|
(11, [1]),
|
|
|
|
(9, [1]),
|
|
|
|
(1, [1, 2, 3]),
|
|
|
|
(16, [1, 2]),
|
|
|
|
(15, [1, 2]),
|
|
|
|
(2, [1, 2, 3]),
|
|
|
|
(7, [1, 2, 3]),
|
|
|
|
(12, [1, 2]),
|
|
|
|
(8, [1, 2, 3, 4])
|
|
|
|
]
|
|
|
|
],
|
|
|
|
5,
|
|
|
|
[
|
|
|
|
# Nothing
|
|
|
|
]
|
2024-08-11 17:53:37 +01:00
|
|
|
)
|
2024-08-11 22:07:43 +01:00
|
|
|
], ids=['simple_history', 'fallback', 'example_1', 'example_2', 'example_3'])
|
2024-08-11 23:03:39 +01:00
|
|
|
def test_unique_regressions(history_data, matchees, per_group, checks):
|
|
|
|
"""Test a bunch of unqiue failures that happened in the past"""
|
2024-08-13 23:32:28 +01:00
|
|
|
tmp_state = state.State(state._EMPTY_DICT)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
# Replay the history
|
|
|
|
for d in history_data:
|
2024-08-11 12:16:23 +01:00
|
|
|
tmp_state.log_groups(d["groups"], d["ts"])
|
2024-08-10 15:12:14 +01:00
|
|
|
|
2024-08-11 22:07:43 +01:00
|
|
|
groups = members_to_groups_validate(matchees, tmp_state, per_group)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
|
|
|
# Run the custom validate functions
|
|
|
|
for check in checks:
|
|
|
|
assert check(groups)
|
|
|
|
|
|
|
|
|
2024-08-11 22:07:43 +01:00
|
|
|
def random_chunk(li, min_chunk, max_chunk, rand):
|
|
|
|
"""
|
|
|
|
"Borrowed" from https://stackoverflow.com/questions/21439011/best-way-to-split-a-list-into-randomly-sized-chunks
|
|
|
|
"""
|
|
|
|
it = iter(li)
|
|
|
|
while True:
|
|
|
|
nxt = list(itertools.islice(it, rand.randint(min_chunk, max_chunk)))
|
|
|
|
if nxt:
|
|
|
|
yield nxt
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Generate a large set of "interesting" tests that replay a fake history onto random people
|
|
|
|
# Increase these numbers for some extreme programming
|
|
|
|
@pytest.mark.parametrize("per_group, num_members, num_history", (
|
|
|
|
(per_group, num_members, num_history)
|
2024-08-11 23:01:47 +01:00
|
|
|
# Most of the time groups are gonna be from 2 to 5
|
|
|
|
for per_group in range(2, 5)
|
|
|
|
# Going lower than 8 members doesn't give the bot much of a chance
|
|
|
|
# And it will fail to not fall back sometimes
|
|
|
|
# That's probably OK frankly
|
|
|
|
for num_members in range(8, 32, 5)
|
|
|
|
# Throw up to 7 histories at the algorithmn
|
|
|
|
for num_history in range(0, 8)))
|
2024-08-11 22:07:43 +01:00
|
|
|
def test_stess_random_groups(per_group, num_members, num_history):
|
|
|
|
"""Run a randomised test based on the input"""
|
|
|
|
|
|
|
|
# Seed the random based on the inputs paird with primes
|
|
|
|
# Ensures the test has interesting fake data, but is stable
|
|
|
|
rand = random.Random(per_group*3 + num_members*5 + num_history*7)
|
|
|
|
|
|
|
|
# Start with a list of all possible members
|
|
|
|
possible_members = [Member(i) for i in range(num_members*2)]
|
|
|
|
for member in possible_members:
|
|
|
|
# Give each member 3 random roles from 1-7
|
|
|
|
member.roles = [Role(i) for i in rand.sample(range(1, 8), 3)]
|
|
|
|
|
2024-08-11 23:01:47 +01:00
|
|
|
# For each history item match up groups and log those
|
2024-08-13 23:32:28 +01:00
|
|
|
cumulative_state = state.State(state._EMPTY_DICT)
|
2024-08-11 23:01:47 +01:00
|
|
|
for i in range(num_history+1):
|
2024-08-11 22:07:43 +01:00
|
|
|
|
2024-08-11 23:01:47 +01:00
|
|
|
# Grab the num of members and replay
|
2024-08-11 22:07:43 +01:00
|
|
|
rand.shuffle(possible_members)
|
2024-08-11 23:01:47 +01:00
|
|
|
members = copy.deepcopy(possible_members[:num_members])
|
2024-08-10 16:30:56 +01:00
|
|
|
|
2024-08-11 23:01:47 +01:00
|
|
|
groups = members_to_groups_validate(
|
|
|
|
members, cumulative_state, per_group)
|
|
|
|
cumulative_state.log_groups(
|
|
|
|
groups, datetime.now() - timedelta(days=num_history-i))
|
2024-08-11 17:53:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_auth_scopes():
|
2024-08-13 23:32:28 +01:00
|
|
|
tmp_state = state.State(state._EMPTY_DICT)
|
2024-08-11 17:53:37 +01:00
|
|
|
|
|
|
|
id = "1"
|
2024-08-13 20:29:26 +01:00
|
|
|
assert not tmp_state.get_user_has_scope(id, state.AuthScope.MATCHER)
|
2024-08-11 17:53:37 +01:00
|
|
|
|
|
|
|
id = "2"
|
|
|
|
tmp_state.set_user_scope(id, state.AuthScope.MATCHER)
|
|
|
|
assert tmp_state.get_user_has_scope(id, state.AuthScope.MATCHER)
|
|
|
|
|
|
|
|
tmp_state.validate()
|
|
|
|
|
|
|
|
|
|
|
|
def test_iterate_all_shifts():
|
|
|
|
original = [1, 2, 3, 4]
|
|
|
|
lists = [val for val in matching.iterate_all_shifts(original)]
|
|
|
|
assert lists == [
|
|
|
|
[1, 2, 3, 4],
|
|
|
|
[2, 3, 4, 1],
|
|
|
|
[3, 4, 1, 2],
|
|
|
|
[4, 1, 2, 3],
|
|
|
|
]
|