2024-08-09 23:14:42 +01:00
|
|
|
"""
|
|
|
|
Test functions for Matchy
|
|
|
|
"""
|
|
|
|
import discord
|
|
|
|
import pytest
|
2024-08-10 16:30:56 +01:00
|
|
|
import random
|
2024-08-10 09:44:22 +01:00
|
|
|
import matching
|
2024-08-11 12:16:23 +01:00
|
|
|
import state
|
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)
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
class Member():
|
|
|
|
def __init__(self, id: int, roles: list[Role] = []):
|
|
|
|
self._id = id
|
|
|
|
|
2024-08-10 15:12:14 +01:00
|
|
|
@property
|
|
|
|
def mention(self) -> str:
|
|
|
|
return f"<@{self._id}>"
|
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
@property
|
|
|
|
def roles(self) -> list[Role]:
|
|
|
|
return []
|
|
|
|
|
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 12:16:23 +01:00
|
|
|
def inner_validate_members_to_groups(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-11 12:16:23 +01:00
|
|
|
tmp_state = state.State()
|
|
|
|
inner_validate_members_to_groups(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-10 21:47:32 +01:00
|
|
|
], ids=['simple_history', 'fallback'])
|
2024-08-10 16:30:56 +01:00
|
|
|
def test_members_to_groups_with_history(history_data, matchees, per_group, checks):
|
|
|
|
"""Test more advanced group matching works"""
|
2024-08-11 12:16:23 +01:00
|
|
|
tmp_state = state.State()
|
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 12:16:23 +01:00
|
|
|
groups = inner_validate_members_to_groups(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-10 16:30:56 +01:00
|
|
|
def test_members_to_groups_stress_test():
|
|
|
|
"""stress test firing significant random data at the code"""
|
2024-08-10 15:12:14 +01:00
|
|
|
|
2024-08-10 16:30:56 +01:00
|
|
|
# Use a stable rand, feel free to adjust this if needed but this lets the test be stable
|
|
|
|
rand = random.Random(123)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
2024-08-10 16:30:56 +01:00
|
|
|
# Slowly ramp up the group size
|
2024-08-10 16:32:48 +01:00
|
|
|
for per_group in range(2, 6):
|
2024-08-10 15:12:14 +01:00
|
|
|
|
2024-08-10 21:47:32 +01:00
|
|
|
# Slowly ramp a randomized shuffled list of members with randomised roles
|
2024-08-10 16:30:56 +01:00
|
|
|
for num_members in range(1, 5):
|
2024-08-10 21:47:32 +01:00
|
|
|
matchees = list(Member(i, list(Role(i) for i in range(1, rand.randint(2, num_members*2 + 1))))
|
2024-08-10 16:30:56 +01:00
|
|
|
for i in range(1, rand.randint(2, num_members*10 + 1)))
|
|
|
|
rand.shuffle(matchees)
|
2024-08-10 15:12:14 +01:00
|
|
|
|
2024-08-10 16:30:56 +01:00
|
|
|
for num_history in range(8):
|
|
|
|
|
|
|
|
# Generate some super random history
|
|
|
|
# Start some time from now to the past
|
|
|
|
time = datetime.now() - timedelta(days=rand.randint(0, num_history*5))
|
|
|
|
history_data = []
|
|
|
|
for x in range(0, num_history):
|
|
|
|
run = {
|
|
|
|
"ts": time
|
|
|
|
}
|
|
|
|
groups = []
|
|
|
|
for y in range(1, num_history):
|
|
|
|
groups.append(list(Member(i)
|
|
|
|
for i in range(1, max(num_members, rand.randint(2, num_members*10 + 1)))))
|
|
|
|
run["groups"] = groups
|
|
|
|
history_data.append(run)
|
|
|
|
|
|
|
|
# Step some time backwards in time
|
|
|
|
time -= timedelta(days=rand.randint(1, num_history))
|
|
|
|
|
|
|
|
# No guarantees on history data order so make it a little harder for matchy
|
|
|
|
rand.shuffle(history_data)
|
|
|
|
|
|
|
|
# Replay the history
|
2024-08-11 12:16:23 +01:00
|
|
|
tmp_state = state.State()
|
2024-08-10 16:30:56 +01:00
|
|
|
for d in history_data:
|
2024-08-11 12:16:23 +01:00
|
|
|
tmp_state.log_groups(d["groups"], d["ts"])
|
2024-08-10 16:30:56 +01:00
|
|
|
|
2024-08-11 12:16:23 +01:00
|
|
|
inner_validate_members_to_groups(matchees, tmp_state, per_group)
|