mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
On 2026-09-29 ESPN's uefa.europa scoreboard still showed the 17 September matchday, so every event was past. Its calendar is a "list" of rounds (League Phase to 30 Jan 2027, then the knockout rounds to the final), not a match-day whitelist, and the league's season type is a soccer id rather than 2/3, so neither 3.6.1 rule applied and the check said the season had finished. When every event is past, a round in a list calendar that has not started yet now draws no conclusion. Only a round's start date counts: end dates are padded past the last game (AFL's Grand Final round still had a day to run three days after the Grand Final), and rounds in an offseason phase (college football's All-Star week) are skipped. Season end dates are still ignored, so PLL (season to 2027-01-01) stays "finished", as do the World Cup and AFL. Of 28 live ESPN scoreboards only uefa.europa's message changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
540 lines
23 KiB
Python
540 lines
23 KiB
Python
"""src.common.favorite_team_check: the messages a user sees for a bad team code.
|
|
|
|
Ported from the scoreboard plugins' own tests (hockey-scoreboard's
|
|
test_favorite_check.py and football-scoreboard's
|
|
test_schedule_note_uses_game_dates.py), which test their bundled
|
|
``<sport>_favorite_check.py`` copies. Core now owns a copy that can drift on
|
|
its own, and a plugin that deletes its copy loses those tests with it.
|
|
|
|
Everything here is offline: ESPN is replaced with a fixed roster, so the tests
|
|
pin the *messages* the user actually sees. They are the point of the feature --
|
|
the whole thing exists to turn a blank screen into a sentence that says why.
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
import unittest
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from src.common.favorite_team_check import FavoriteTeamCheck
|
|
|
|
NHL = {
|
|
"BOS": "Boston Bruins",
|
|
"TB": "Tampa Bay Lightning",
|
|
"UTAH": "Utah Mammoth",
|
|
"SEA": "Seattle Kraken",
|
|
"VGK": "Vegas Golden Knights",
|
|
}
|
|
|
|
NCAA = {
|
|
"ALA": "Alabama Crimson Tide",
|
|
"UNA": "North Alabama Lions",
|
|
"CONN": "UConn Huskies",
|
|
"SC": "South Carolina Gamecocks",
|
|
"RUTG": "Rutgers Scarlet Knights",
|
|
"GS": "Golden State Warriors",
|
|
}
|
|
|
|
|
|
class RecordingLogger(logging.Logger):
|
|
"""Captures formatted records so tests can assert on the message text."""
|
|
|
|
def __init__(self):
|
|
super().__init__("test")
|
|
self.records = []
|
|
|
|
def handle(self, record):
|
|
self.records.append((record.levelname, record.getMessage()))
|
|
|
|
def messages(self, level=None):
|
|
return [m for lvl, m in self.records if level is None or lvl == level]
|
|
|
|
|
|
class SuggestionTests(unittest.TestCase):
|
|
"""The suggestion has to name the right team, and name it first."""
|
|
|
|
def assert_first_suggestion(self, code, teams, expected):
|
|
message = FavoriteTeamCheck._suggest(code, teams)
|
|
self.assertIn(expected, message,
|
|
"{!r} did not suggest {!r}: {}".format(code, expected, message))
|
|
# Where several candidates are listed, the right one must lead, since
|
|
# users act on the first thing they read.
|
|
head = message.split("(")[0]
|
|
self.assertIn(expected, head,
|
|
"{!r} buried {!r} behind another suggestion: {}".format(
|
|
code, expected, message))
|
|
|
|
def test_retired_code_points_at_the_current_one(self):
|
|
self.assert_first_suggestion("UTA", NHL, "UTAH")
|
|
|
|
def test_common_wrong_guesses(self):
|
|
self.assert_first_suggestion("GSW", NCAA, "GS")
|
|
self.assert_first_suggestion("UCONN", NCAA, "CONN")
|
|
|
|
def test_fragment_of_a_word_is_matched(self):
|
|
# 'BAMA' abbreviates nothing in 'Alabama Crimson Tide' -- it is a chunk
|
|
# out of the middle of a word -- so the initials rule alone misses it.
|
|
self.assert_first_suggestion("BAMA", NCAA, "ALA")
|
|
|
|
def test_first_word_beats_a_mid_name_match(self):
|
|
# 'SCAR' fits Rutgers *Scar*let too, but South Carolina starts at the
|
|
# first word, which is how people actually shorten a name.
|
|
self.assert_first_suggestion("SCAR", NCAA, "SC")
|
|
|
|
def test_wrong_case_says_so_rather_than_guessing(self):
|
|
message = FavoriteTeamCheck._suggest("bos", NHL)
|
|
self.assertIn("case-sensitive", message)
|
|
self.assertIn("'BOS'", message)
|
|
|
|
def test_valid_code_draws_no_suggestion(self):
|
|
self.assertEqual(FavoriteTeamCheck._suggest("BOS", NHL), "")
|
|
|
|
def test_unmatchable_code_is_not_forced_into_a_suggestion(self):
|
|
# Nothing sensible to offer is better than something wrong.
|
|
self.assertEqual(FavoriteTeamCheck._suggest("ZZZZZZ", NHL), "")
|
|
|
|
def test_abbreviates_separates_codes_string_distance_ties(self):
|
|
# The case that motivated this: 'MUN' is equidistant from 'MAN' and
|
|
# 'SUN' by string similarity, so similarity cannot choose.
|
|
self.assertTrue(FavoriteTeamCheck._abbreviates("MUN", "Manchester United"))
|
|
self.assertFalse(FavoriteTeamCheck._abbreviates("MUN", "Sunderland"))
|
|
|
|
|
|
class CheckTests(unittest.TestCase):
|
|
"""The log output for each way a league can end up empty."""
|
|
|
|
def setUp(self):
|
|
self.logger = RecordingLogger()
|
|
self.checker = FavoriteTeamCheck(self.logger, {"nhl": ("NHL", "hockey/nhl")})
|
|
self.checker._fetch_teams = staticmethod(lambda path: dict(NHL))
|
|
self.checker._schedule_note = staticmethod(lambda path: None)
|
|
|
|
def test_bad_code_is_reported_with_a_suggestion(self):
|
|
self.checker._check("nhl", ["UTA", "BOS"])
|
|
warnings = self.logger.messages("WARNING")
|
|
self.assertEqual(len(warnings), 1)
|
|
self.assertIn("'UTA' is not a NHL team code", warnings[0])
|
|
self.assertIn("UTAH", warnings[0])
|
|
|
|
def test_all_codes_bad_says_nothing_will_show(self):
|
|
self.checker._check("nhl", ["UTA", "NOPE"])
|
|
joined = " ".join(self.logger.messages("WARNING"))
|
|
self.assertIn("no recognised favorite teams", joined)
|
|
self.assertIn("nothing will be shown", joined)
|
|
|
|
def test_good_codes_out_of_season_explain_the_empty_screen(self):
|
|
self.checker._schedule_note = staticmethod(
|
|
lambda path: "the league has nothing on until 07 October 2026")
|
|
self.checker._check("nhl", ["BOS"])
|
|
info = " ".join(self.logger.messages("INFO"))
|
|
self.assertIn("look correct", info)
|
|
self.assertIn("07 October 2026", info)
|
|
self.assertIn("not a configuration problem", info)
|
|
self.assertEqual(self.logger.messages("WARNING"), [])
|
|
|
|
def test_good_codes_in_season_stay_quiet(self):
|
|
self.checker._check("nhl", ["BOS", "TB"])
|
|
self.assertEqual(self.logger.messages("WARNING"), [])
|
|
self.assertIn("recognised: BOS, TB", " ".join(self.logger.messages("INFO")))
|
|
|
|
def test_dynamic_groups_are_not_treated_as_team_codes(self):
|
|
self.checker._check("nhl", ["AP_TOP_25", "NCAA_MENS_TOP_10"])
|
|
self.assertEqual(self.logger.messages("WARNING"), [])
|
|
|
|
def test_empty_roster_draws_no_conclusion(self):
|
|
# ESPN's college lacrosse endpoints return zero teams; a valid code
|
|
# must not be called wrong just because the roster is unavailable.
|
|
self.checker._fetch_teams = staticmethod(lambda path: {})
|
|
self.checker._check("nhl", ["ANYTHING"])
|
|
self.assertEqual(self.logger.records, [])
|
|
|
|
def test_fetch_failure_is_swallowed(self):
|
|
def boom(path):
|
|
raise RuntimeError("network down")
|
|
|
|
self.checker._fetch_teams = staticmethod(boom)
|
|
self.checker._check("nhl", ["BOS"]) # must not raise
|
|
self.assertEqual(self.logger.messages("WARNING"), [])
|
|
|
|
|
|
class ScheduleNoteTests(unittest.TestCase):
|
|
"""
|
|
Reading ESPN's scoreboard for "is there anything on?".
|
|
|
|
Both traps here are real API behaviour, confirmed against live endpoints:
|
|
an out-of-season league rolls forward to its next fixtures rather than
|
|
returning nothing, and a finished season returns its *last* game instead.
|
|
"""
|
|
|
|
def note(self, payload):
|
|
"""Run _schedule_note against a fixed payload, with no network access.
|
|
|
|
The method imports ``requests`` in its own body, so the fake has to go
|
|
into ``sys.modules`` — patching the attribute on the module has
|
|
no effect on a function-local import.
|
|
"""
|
|
import types
|
|
|
|
class Response:
|
|
@staticmethod
|
|
def json():
|
|
return payload
|
|
|
|
fake = types.ModuleType("requests")
|
|
fake.get = lambda url, timeout=None: Response()
|
|
|
|
real = sys.modules.get("requests")
|
|
sys.modules["requests"] = fake
|
|
try:
|
|
return FavoriteTeamCheck._schedule_note("hockey/nhl")
|
|
finally:
|
|
if real is None:
|
|
sys.modules.pop("requests", None)
|
|
else:
|
|
sys.modules["requests"] = real
|
|
|
|
@staticmethod
|
|
def iso(days):
|
|
from datetime import datetime, timedelta, timezone
|
|
return (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()
|
|
|
|
def test_games_today_says_nothing(self):
|
|
self.assertIsNone(self.note({"events": [{"date": self.iso(0)}]}))
|
|
|
|
def test_a_game_already_under_way_counts_as_something_on(self):
|
|
# Games that started earlier today are in the past by the clock. Reading
|
|
# them as "not upcoming" made a live slate report the season as over.
|
|
self.assertIsNone(self.note({"events": [{"date": self.iso(-0.3)}]}))
|
|
|
|
def test_an_off_day_or_two_is_not_worth_mentioning(self):
|
|
self.assertIsNone(self.note({"events": [{"date": self.iso(1)}]}))
|
|
|
|
def test_out_of_season_reports_the_next_fixture(self):
|
|
# ESPN rolls forward, so events exist but are months away.
|
|
note = self.note({"events": [{"date": self.iso(52)}, {"date": self.iso(53)}]})
|
|
self.assertIsNotNone(note)
|
|
self.assertIn("nothing on until", note)
|
|
|
|
def test_finished_season_is_reported_as_finished(self):
|
|
# A completed season returns its last game, in the past.
|
|
note = self.note({"events": [{"date": self.iso(-120)}],
|
|
"leagues": [{"calendar": [self.iso(-300)]}]})
|
|
self.assertIsNotNone(note)
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_past_dates_never_read_as_imminent(self):
|
|
# The bug this guards: taking the soonest of *all* dates makes a game
|
|
# from last March look like one happening right now, so a finished
|
|
# season silently reports itself as in progress.
|
|
note = self.note({"events": [{"date": self.iso(-120)},
|
|
{"date": self.iso(40)}]})
|
|
self.assertIn("nothing on until", note)
|
|
|
|
def test_calendar_is_used_when_there_are_no_events(self):
|
|
note = self.note({"events": [],
|
|
"leagues": [{"calendar": [{"startDate": self.iso(30)}]}]})
|
|
self.assertIn("nothing on until", note)
|
|
|
|
def test_nothing_published_draws_no_conclusion(self):
|
|
self.assertIsNone(self.note({"events": [], "leagues": [{"calendar": []}]}))
|
|
|
|
def test_unparseable_dates_are_ignored_rather_than_fatal(self):
|
|
self.assertIsNone(self.note(
|
|
{"events": [{"date": "not a date"}, {"date": None}, {}]}))
|
|
|
|
|
|
class SchedulingTests(unittest.TestCase):
|
|
"""The check must run once, off the render path, and never raise."""
|
|
|
|
def setUp(self):
|
|
self.logger = RecordingLogger()
|
|
self.checker = FavoriteTeamCheck(self.logger, {"nhl": ("NHL", "hockey/nhl")})
|
|
self.calls = []
|
|
self.checker._check = lambda key, favs: self.calls.append((key, list(favs)))
|
|
|
|
def drain(self):
|
|
import threading
|
|
for thread in threading.enumerate():
|
|
if thread.name == "favorite-team-check":
|
|
thread.join(timeout=5)
|
|
|
|
def test_runs_once_per_league(self):
|
|
for _ in range(5):
|
|
self.checker.schedule("nhl", ["BOS"])
|
|
self.drain()
|
|
self.assertEqual(len(self.calls), 1)
|
|
|
|
def test_reset_allows_a_recheck_after_a_config_edit(self):
|
|
self.checker.schedule("nhl", ["BOS"])
|
|
self.drain()
|
|
self.checker.reset()
|
|
self.checker.schedule("nhl", ["TB"])
|
|
self.drain()
|
|
self.assertEqual([favs for _, favs in self.calls], [["BOS"], ["TB"]])
|
|
|
|
def test_no_favorites_configured_does_nothing(self):
|
|
self.checker.schedule("nhl", [])
|
|
self.checker.schedule("nhl", None)
|
|
self.checker.schedule("nhl", ["", " "])
|
|
self.drain()
|
|
self.assertEqual(self.calls, [])
|
|
|
|
def test_unknown_league_key_is_ignored(self):
|
|
self.checker.schedule("not-a-league", ["BOS"])
|
|
self.drain()
|
|
self.assertEqual(self.calls, [])
|
|
|
|
def test_thread_is_a_daemon_so_it_cannot_hold_up_shutdown(self):
|
|
import threading
|
|
started = threading.Event()
|
|
seen = {}
|
|
|
|
def record(key, favs):
|
|
seen["daemon"] = threading.current_thread().daemon
|
|
started.set()
|
|
|
|
self.checker._check = record
|
|
self.checker.schedule("nhl", ["BOS"])
|
|
self.assertTrue(started.wait(timeout=5))
|
|
self.assertTrue(seen["daemon"])
|
|
|
|
|
|
class ScheduleNoteUsesGameDatesTests(unittest.TestCase):
|
|
"""The "nothing on until" note reports a game date, not a calendar boundary.
|
|
|
|
``_schedule_note`` used to pool ESPN's rolled-forward event dates with the
|
|
league calendar's week/phase startDates and take the earliest. Calendar
|
|
weeks routinely open days before their first game, so the note reported
|
|
"nothing on until 06 September" for a league whose first snap was the
|
|
10th. Events now win; the calendar only speaks when the scoreboard has no
|
|
events at all.
|
|
"""
|
|
|
|
note = ScheduleNoteTests.note
|
|
|
|
@staticmethod
|
|
def iso(base, days_out):
|
|
# Every date in a case derives from one captured *base*, so a UTC
|
|
# midnight crossing mid-test cannot make the payload and the expected
|
|
# strftime disagree about the day.
|
|
return (base + timedelta(days=days_out)).strftime("%Y-%m-%dT%H:%MZ")
|
|
|
|
def test_the_first_game_wins_over_an_earlier_calendar_boundary(self):
|
|
base = datetime.now(timezone.utc)
|
|
note = self.note({
|
|
"events": [{"date": self.iso(base, 10)}, {"date": self.iso(base, 14)}],
|
|
"leagues": [{"calendar": [{"startDate": self.iso(base, 6)}]}],
|
|
})
|
|
self.assertIn((base + timedelta(days=10)).strftime("%d %B %Y"), note)
|
|
self.assertNotIn((base + timedelta(days=6)).strftime("%d %B %Y"), note)
|
|
|
|
def test_with_no_events_the_calendar_still_gets_a_say(self):
|
|
base = datetime.now(timezone.utc)
|
|
note = self.note({"events": [], "leagues": [{"calendar": [self.iso(base, 20)]}]})
|
|
self.assertIn((base + timedelta(days=20)).strftime("%d %B %Y"), note)
|
|
|
|
def test_only_past_dates_reads_as_a_finished_season(self):
|
|
base = datetime.now(timezone.utc)
|
|
note = self.note({"events": [{"date": self.iso(base, -40)}],
|
|
"leagues": [{"calendar": []}]})
|
|
self.assertIn("finished", note)
|
|
|
|
def test_a_finished_season_is_not_dressed_up_by_an_offseason_calendar(self):
|
|
# Past events mean the season is over; a future calendar boundary
|
|
# (the draft, next season's week 1 shell) is not the next game.
|
|
base = datetime.now(timezone.utc)
|
|
note = self.note({"events": [{"date": self.iso(base, -40)}],
|
|
"leagues": [{"calendar": [{"startDate": self.iso(base, 45)}]}]})
|
|
self.assertIn("finished", note)
|
|
|
|
def test_an_imminent_slate_is_not_worth_a_note(self):
|
|
base = datetime.now(timezone.utc)
|
|
self.assertIsNone(self.note({
|
|
"events": [{"date": self.iso(base, 1)}],
|
|
"leagues": [{"calendar": [{"startDate": self.iso(base, 6)}]}],
|
|
}))
|
|
|
|
|
|
class ScheduleNotePostseasonTests(unittest.TestCase):
|
|
"""Past regular-season events are not a finished season once the postseason starts.
|
|
|
|
Captured from ESPN's MLB scoreboard on 2026-09-29, two days after the
|
|
regular season ended: the default scoreboard still returned the last
|
|
regular-season day (event ``season.type`` 2), while ``leagues[0].season``
|
|
had already moved to Postseason (type 3). The check told a Rays fan the
|
|
season was over while their upcoming manager listed TB's wild-card games.
|
|
"""
|
|
|
|
note = ScheduleNoteTests.note
|
|
|
|
@staticmethod
|
|
def payload(league_type, event_type, days=-2):
|
|
date = (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()
|
|
return {
|
|
"events": [{"date": date, "season": {"year": 2026, "type": event_type}}],
|
|
"leagues": [{"season": {"year": 2026, "type": {"type": league_type}},
|
|
"calendar": [date]}],
|
|
}
|
|
|
|
def test_postseason_after_regular_season_events_is_not_finished(self):
|
|
self.assertIsNone(self.note(self.payload(league_type=3, event_type=2)))
|
|
|
|
def test_a_finished_postseason_is_still_reported_as_finished(self):
|
|
# AFL the week after its grand final: league and events both postseason.
|
|
note = self.note(self.payload(league_type=3, event_type=3, days=-4))
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_offseason_after_the_postseason_is_still_finished(self):
|
|
note = self.note(self.payload(league_type=4, event_type=3, days=-40))
|
|
self.assertIn("season has finished", note)
|
|
|
|
|
|
class ScheduleNoteMatchdayTests(unittest.TestCase):
|
|
"""Between soccer matchdays the next fixture is only in the calendar.
|
|
|
|
Captured from ESPN's Premier League scoreboard on 2026-09-29: the
|
|
scoreboard still showed the 20 September matchday, the league was in the
|
|
same season phase as those events, and the next games (10 October) were
|
|
only in ``leagues[0].calendar`` -- a ``"day"`` calendar with
|
|
``calendarIsWhitelist`` true, i.e. the days that have games. The check
|
|
said the season had finished.
|
|
"""
|
|
|
|
note = ScheduleNoteTests.note
|
|
|
|
@staticmethod
|
|
def payload(event_days, calendar_days, whitelist=True):
|
|
from datetime import datetime, timedelta, timezone
|
|
now = datetime.now(timezone.utc)
|
|
|
|
def iso(days):
|
|
return (now + timedelta(days=days)).strftime("%Y-%m-%dT%H:%MZ")
|
|
|
|
return {
|
|
"events": [{"date": iso(d), "season": {"type": 14308}}
|
|
for d in event_days],
|
|
"leagues": [{"season": {"type": {"type": 14308}},
|
|
"calendarType": "day",
|
|
"calendarIsWhitelist": whitelist,
|
|
"calendar": [iso(d) for d in calendar_days]}],
|
|
}
|
|
|
|
def test_gap_between_matchdays_reports_the_next_matchday(self):
|
|
from datetime import datetime, timedelta, timezone
|
|
note = self.note(self.payload([-9], [-9, 11, 12]))
|
|
self.assertIn("nothing on until", note)
|
|
self.assertIn((datetime.now(timezone.utc) + timedelta(days=11))
|
|
.strftime("%d %B %Y"), note)
|
|
|
|
def test_next_matchday_within_a_couple_of_days_says_nothing(self):
|
|
self.assertIsNone(self.note(self.payload([-2], [-2, 1])))
|
|
|
|
def test_no_matchdays_left_is_still_finished(self):
|
|
# PLL on the same day: a match-day calendar whose last day is past.
|
|
note = self.note(self.payload([-9], [-30, -9]))
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_a_blacklist_day_calendar_is_not_read_as_fixtures(self):
|
|
# MLB's day calendar has calendarIsWhitelist false: it lists days
|
|
# without games, so a future entry there is not a next fixture.
|
|
note = self.note(self.payload([-9], [11], whitelist=False))
|
|
self.assertIn("season has finished", note)
|
|
|
|
|
|
class ScheduleNoteListCalendarTests(unittest.TestCase):
|
|
"""A round still to start in a "list" calendar is not a finished season.
|
|
|
|
Shapes captured from ESPN on 2026-09-29, with dates kept relative to that
|
|
day. The Europa League scoreboard still showed the 17 September matchday
|
|
and its calendar is a ``"list"`` of rounds, not match days, so the check
|
|
said the season had finished -- with the knockout rounds, and the next
|
|
league-phase matchday, still to come. PLL, the World Cup and AFL really had
|
|
finished and must still say so, although each has a season or round
|
|
``endDate`` in the future.
|
|
"""
|
|
|
|
note = ScheduleNoteTests.note
|
|
|
|
@staticmethod
|
|
def iso(days):
|
|
return (datetime.now(timezone.utc) + timedelta(days=days)).strftime(
|
|
"%Y-%m-%dT%H:%MZ")
|
|
|
|
@classmethod
|
|
def list_league(cls, event_days, rounds, league_type=14540,
|
|
event_type=14540, phase_label="UEFA Europa League",
|
|
extra_phases=()):
|
|
"""``rounds`` is ``[(label, start_day, end_day), ...]`` for one phase."""
|
|
return {
|
|
"events": [{"date": cls.iso(d), "season": {"type": event_type}}
|
|
for d in event_days],
|
|
"leagues": [{
|
|
"season": {"type": {"type": league_type}},
|
|
"calendarType": "list",
|
|
"calendarIsWhitelist": True,
|
|
"calendar": [{
|
|
"label": phase_label,
|
|
"startDate": cls.iso(-90), "endDate": cls.iso(275),
|
|
"entries": [{"label": label, "startDate": cls.iso(start),
|
|
"endDate": cls.iso(end)}
|
|
for label, start, end in rounds],
|
|
}] + list(extra_phases),
|
|
}],
|
|
}
|
|
|
|
def test_europa_between_matchdays_is_not_finished(self):
|
|
note = self.note(self.list_league([-12], [
|
|
("League Phase", -31, 123),
|
|
("Knockout Round Playoffs", 123, 151),
|
|
("Rd of 16", 151, 172),
|
|
("Quarterfinals", 172, 200),
|
|
("Semifinals", 200, 221),
|
|
("Final", 222, 275),
|
|
]))
|
|
self.assertIsNone(note)
|
|
|
|
def test_world_cup_after_the_final_is_still_finished(self):
|
|
# The competition runs to 31 December, and the last round ended 12
|
|
# days after the final; no round is still to start.
|
|
note = self.note(self.list_league([-72], [
|
|
("Group", -110, -93),
|
|
("Semifinals", -77, -72),
|
|
("Final", -72, -59),
|
|
], league_type=13803, event_type=13803, phase_label="FIFA World Cup"))
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_afl_after_the_grand_final_is_still_finished(self):
|
|
# The Grand Final round had started but had not ended yet.
|
|
note = self.note(self.list_league([-3], [
|
|
("Preliminary Finals", -13, -6),
|
|
("Grand Final", -6, 1),
|
|
], league_type=3, event_type=3, phase_label="Postseason"))
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_an_offseason_round_does_not_count(self):
|
|
# College football's "Off Season" phase holds the All-Star week.
|
|
offseason = {"label": "Off Season", "startDate": self.iso(2),
|
|
"endDate": self.iso(6),
|
|
"entries": [{"label": "All-Star", "startDate": self.iso(2),
|
|
"endDate": self.iso(6)}]}
|
|
note = self.note(self.list_league(
|
|
[-3], [("CFP", -40, 1)], league_type=3, event_type=3,
|
|
phase_label="Postseason", extra_phases=[offseason]))
|
|
self.assertIn("season has finished", note)
|
|
|
|
def test_pll_with_a_season_end_date_in_the_future_is_still_finished(self):
|
|
# A "day" whitelist whose last match day is past; the season's own
|
|
# endDate (1 January) is ignored.
|
|
note = self.note({
|
|
"events": [{"date": self.iso(-9), "season": {"type": 2}}],
|
|
"leagues": [{
|
|
"season": {"type": {"type": 2}, "startDate": self.iso(-271),
|
|
"endDate": self.iso(94)},
|
|
"calendarType": "day",
|
|
"calendarIsWhitelist": True,
|
|
"calendarEndDate": self.iso(94),
|
|
"calendar": [self.iso(-30), self.iso(-22), self.iso(-9)],
|
|
}],
|
|
})
|
|
self.assertIn("season has finished", note)
|