def test_get_matches(mocker): # Given these possible connections where every pair is scored 1, there is only one matching that scores # as high as 2, we better get that! # # A---B # \ / # C # | # D match_maker = MatchMaker(match_request_since_date='does not matter') match_maker._pair_score_and_topic = mocker.Mock() match_maker._pair_score_and_topic.return_value = (1, 'history') match_maker._possible_matches = { 'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B', 'D'], 'D': ['C'], } matches = match_maker._get_matches() matches = set( key(*m) for m in matches ) # convert to set of sorted tuples so that ordering doesn't matter assert matches == {key('A', 'B'), key('C', 'D')} for forbidden_call in [ call('D', 'A'), call('A', 'D'), call('D', 'B'), call('B', 'D') ]: assert forbidden_call not in match_maker._pair_score_and_topic.call_args_list, \ "we should not make score calls for pairs that aren't in _possible_matches (very inefficient)"
def test_match_unmatched(mocker): # scenario: assume that the matches are A+B in the topic of 'math' and C+D in the topic of 'history', and E and F are # unmatched. E is a _possible_match with A or C in the topic of 'math' and F is a _possible_match with A or C in the # topic of 'history' therefore the final matching should be A+B+E in 'math' and C+D+F in 'history' match_maker = MatchMaker(match_request_since_date='does not matter') matches = [['A', 'B'], ['C', 'D']] def mock_pair_score_and_topic(p1, p2): return { key('A', 'B'): (1, 'math'), key('C', 'D'): (1, 'history'), }[key(p1, p2)] match_maker._pair_score_and_topic = mocker.Mock() match_maker._pair_score_and_topic.side_effect = mock_pair_score_and_topic match_maker._match_requests_profile_to_topic = { 'A': ['math'], 'B': ['math'], 'C': ['history'], 'D': ['history'], 'E': ['math'], 'F': ['history'], } match_maker._possible_matches = { 'E': ['A', 'C'], 'F': ['A', 'C'], # really the other keys would be here, but the method wouldn't currently call them } matches = match_maker._match_unmatched(matches) matches = set( key(*m) for m in matches ) # convert to set of sorted tuples so that ordering doesn't matter assert matches == {key('A', 'B', 'E'), key('C', 'D', 'F')}
def test_not_memoized_pair_score_and_topic(): match_maker = MatchMaker(match_request_since_date='does not matter') now = datetime.now().astimezone(timezone.utc) match_maker._recent_match_by_profile_pair = { key('A', 'B'): { 'date': now - timedelta(weeks=20) }, } match_maker._most_recent_match_by_profile = { key('A'): { 'date': now - timedelta(weeks=20) }, key('B'): { 'date': now - timedelta(weeks=20) }, } match_maker._recent_match_by_profile_pair_and_topic = { key('A', 'B', 'science'): { 'date': now - timedelta(weeks=10) }, key('A', 'B', 'art'): { 'date': now - timedelta(weeks=20) }, } match_maker._match_requests_profile_to_topic = { 'A': {'science', 'art', 'history'}, 'B': {'science', 'art', 'religion'}, } score, topic = match_maker._not_memoized_pair_score_and_topic('A', 'B') assert score > 0 assert score != int( score ), "score is a whole number, that's unlikely if we're really exercising the functionality" assert topic == 'art'
def test_select_topic(): match_maker = MatchMaker(match_request_since_date='does not matter') now = datetime.now().astimezone(timezone.utc) match_maker._recent_match_by_profile_pair_and_topic = { key('A', 'B', 'science'): { 'date': now - timedelta(weeks=10) }, key('A', 'B', 'art'): { 'date': now - timedelta(weeks=20) }, } met_recently = True topics = {'science', 'art', 'history'} selected_topic = match_maker._select_topic('A', 'B', met_recently, topics) assert selected_topic == 'history', 'choose the topic they have not yet met in' topics = {'science', 'art'} selected_topic = match_maker._select_topic('A', 'B', met_recently, topics) assert selected_topic == 'art', 'if they have already met in all topics, choose the least recent topic'
def test_penalty_based_on_recent_pairing(): match_maker = MatchMaker(match_request_since_date='does not matter') now = datetime.now().astimezone(timezone.utc) match_maker._recent_match_by_profile_pair = { key('A', 'B'): { 'date': now - timedelta(days=1) }, key('A', 'C'): { 'date': now - timedelta(weeks=10) }, key('A', 'D'): { 'date': now - timedelta(weeks=20) }, # key('A', 'E'): {'date': now - infinity}, <- no entry is treated as never having met } scoreAB = match_maker._penalty_based_on_recent_pairing('A', 'B') scoreAC = match_maker._penalty_based_on_recent_pairing('A', 'C') scoreAD = match_maker._penalty_based_on_recent_pairing('A', 'D') scoreAE = match_maker._penalty_based_on_recent_pairing('A', 'E') assert scoreAB > scoreAC > scoreAD > scoreAE, 'the penalty is greater if they have met more recently' assert scoreAB == float( 'infinity'), 'having just met implies "infinite" penalty'
def test_boost_based_upon_recency_of_match(): match_maker = MatchMaker(match_request_since_date='does not matter') now = datetime.now().astimezone(timezone.utc) match_maker._most_recent_match_by_profile = { key('A'): { 'date': now }, key('B'): { 'date': now - timedelta(weeks=1) }, key('C'): { 'date': now - timedelta(weeks=2) }, # key('D'): {'date': now - infinity}, <- no entry is treated as never having met # key('E'): {'date': now - infinity}, <- no entry is treated as never having met } scoreAB = match_maker._boost_based_upon_recency_of_match('A', 'B') scoreAC = match_maker._boost_based_upon_recency_of_match('A', 'C') scoreAD = match_maker._boost_based_upon_recency_of_match('A', 'D') scoreDE = match_maker._boost_based_upon_recency_of_match('D', 'E') assert scoreAB < scoreAC < scoreAD < scoreDE, 'there is a larger boost for those who have not met in a while' assert scoreDE == 2, \ 'boosts of the individual profiles are additive, and if a player has never been in a match their boost is 1'
def mock_pair_score_and_topic(p1, p2): return { key('A', 'B'): (1, 'math'), key('C', 'D'): (2, 'history'), }[key(p1, p2)]
def test_gather_data(): now = datetime.now().astimezone(timezone.utc) prof1, prof2, prof3, prof4, prof5, prof6, prof_too_old = [ SocialProfileFactory(email=f'prof{i+1}@email.com') for i in range(7) ] prof_too_old.email = '*****@*****.**' prof_too_old.save() topic_channel_science = TopicChannelFactory(name='science') topic_channel_art = TopicChannelFactory(name='art') with freeze_time(now - timedelta(weeks=52), tz_offset=0): MatchRequestFactory(profile=prof_too_old) with freeze_time(now - timedelta(weeks=1), tz_offset=0): MatchRequestFactory( profile=prof1, topic_channel=topic_channel_art, ) MatchRequestFactory( profile=prof1, topic_channel=topic_channel_science, ) MatchRequestFactory(profile=prof2, topic_channel=topic_channel_art) MatchRequestFactory(profile=prof3, topic_channel=topic_channel_science) MatchRequestFactory(profile=prof4, topic_channel=topic_channel_science) with freeze_time(now - timedelta(weeks=3), tz_offset=0): MatchFactory( topic_channel=topic_channel_art, profiles=(prof1, prof2, prof3), ) MatchFactory( topic_channel=topic_channel_science, profiles=(prof4, prof5), ) MatchFactory( topic_channel=topic_channel_science, profiles=(prof6, prof_too_old), ) match_maker = MatchMaker(match_request_since_date=now - timedelta(weeks=2)) match_maker._gather_data() # _recent_match_by_profile_pair, _recent_match_by_profile_pair, and _recent_match_by_profile_pair_and_topic # are used to look up information useful in creating a score assert 'prof_too_old' not in set( chain(*set(match_maker._recent_match_by_profile_pair.keys()))) assert set(match_maker._recent_match_by_profile_pair.keys()) == { key('*****@*****.**', '*****@*****.**'), # note that the meeting with 3 people got turned into 3 pairs here key('*****@*****.**', '*****@*****.**'), key('*****@*****.**', '*****@*****.**'), key('*****@*****.**', '*****@*****.**'), } assert match_maker._recent_match_by_profile_pair[key( '*****@*****.**', '*****@*****.**')]['num_attending'] == 3 assert set(match_maker._recent_match_by_profile_topic) == { key('*****@*****.**', 'art'), key('*****@*****.**', 'art'), key('*****@*****.**', 'art'), key('*****@*****.**', 'science'), key('*****@*****.**', 'science'), } assert set(match_maker._recent_match_by_profile_pair_and_topic.keys()) == { key('*****@*****.**', '*****@*****.**', 'art'), key('*****@*****.**', '*****@*****.**', 'art'), key('*****@*****.**', '*****@*****.**', 'art'), key('*****@*****.**', '*****@*****.**', 'science'), } # _match_requests_profile_to_topic, _match_requests_topic_to_profile, and _possible_matches # are used largely as filters to quickly find topics for people, people for topics, and people that share a topic # respectively assert match_maker._match_requests_profile_to_topic == { '*****@*****.**': {'science', 'art'}, '*****@*****.**': {'art'}, '*****@*****.**': {'science'}, '*****@*****.**': {'science'}, } assert match_maker._match_requests_topic_to_profile == { 'art': {'*****@*****.**', '*****@*****.**'}, 'science': {'*****@*****.**', '*****@*****.**', '*****@*****.**'}, } assert match_maker._possible_matches == { '*****@*****.**': {'*****@*****.**', '*****@*****.**', '*****@*****.**'}, '*****@*****.**': {'*****@*****.**'}, '*****@*****.**': {'*****@*****.**', '*****@*****.**'}, '*****@*****.**': {'*****@*****.**', '*****@*****.**'}, }