mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
With scroll_card.date_format "weekday", a Friday 8 PM ET game read "Sat Oct 2" on the scroll and Vegas cards. Cause: the extractor prints the "M/D" in the plugin's resolved zone (its own setting, then the global one, then the system zone), but the card is handed only the plugin's config. Its timezone ships as "", so card_tzinfo fell back to UTC and the weekday belonged to the UTC date: the next day for evening games in the Americas, the previous day for morning games east of UTC (Auckland, Kiritimati). Fix: every zone is within a day of UTC, so the printed date is the start's UTC date or a neighbour of it. _format_date_as now takes the game and names the weekday of whichever of those days has the printed month and day, falling back to the zone-based weekday only when the start cannot place the date (no offset, unparseable, or more than a day away). The switch-mode scorebug shares the formatter and passes the game too, so the twins stay identical; it already used the resolved zone and draws what it drew before. Public signatures are unchanged. Tests cover US DST end, New Year's Eve, both sides of the date line, NZ DST start and UTC+14. The twins test's weekday pin is updated: the drawn date now agrees, and only the bare weekday helpers still differ. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
440 lines
21 KiB
Python
440 lines
21 KiB
Python
"""The card helpers the eight scoreboards now share.
|
|
|
|
These bodies lived in eight byte-identical copies. Moving them here means one
|
|
fix reaches every scoreboard — and that a mistake does too, which is what this
|
|
file guards. Each case below is one the plugins' own code already handled; the
|
|
point is that it keeps handling it.
|
|
|
|
The functions take ``config``/``logger``/``fonts`` as arguments rather than
|
|
reading them off an instance, so a plugin keeps its method and delegates the
|
|
body. That is what let all eight adopt this with byte-identical renders.
|
|
"""
|
|
|
|
import logging
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
from src.common import sports_card as C
|
|
|
|
|
|
@pytest.fixture
|
|
def log():
|
|
return logging.getLogger("test_sports_card")
|
|
|
|
|
|
class TestSettingsLookup:
|
|
def test_reads_the_scroll_card_block(self):
|
|
cfg = {"scroll_card": {"vs_text": "@"}}
|
|
assert C.scroll_card_option(cfg, "vs_text", "VS") == "@"
|
|
|
|
@pytest.mark.parametrize("cfg", [None, {}, {"scroll_card": None},
|
|
{"scroll_card": {"vs_text": None}}])
|
|
def test_missing_or_null_falls_back(self, cfg):
|
|
"""A null in config means "unset", not "empty string"."""
|
|
assert C.scroll_card_option(cfg, "vs_text", "VS") == "VS"
|
|
|
|
def test_upcoming_center_rejects_unknown_modes(self):
|
|
for bad in ("sideways", "", None, 7):
|
|
assert C.upcoming_center_mode({"scroll_card": {"upcoming_center": bad}}) == "vs"
|
|
assert C.upcoming_center_mode({"scroll_card": {"upcoming_center": "DATE_TIME"}}) == "date_time"
|
|
|
|
|
|
class TestColour:
|
|
def test_rgb_list_and_hex_both_work(self):
|
|
assert C.element_color({"customization": {"score_text": {"text_color": [1, 2, 3]}}},
|
|
"score_text") == (1, 2, 3)
|
|
assert C.element_color({"customization": {"score_text": {"text_color": "#ff8000"}}},
|
|
"score_text") == (255, 128, 0)
|
|
|
|
@pytest.mark.parametrize("value", ["nope", "#fff", [1, 2], None, ["a", "b", "c"]])
|
|
def test_unusable_colour_falls_back(self, value):
|
|
cfg = {"customization": {"score_text": {"text_color": value}}}
|
|
assert C.element_color(cfg, "score_text", (9, 9, 9)) == (9, 9, 9)
|
|
|
|
def test_out_of_range_components_are_clamped_not_rejected(self):
|
|
"""The readers this replaced clamped, and the eight scoreboards' own
|
|
tests pin it. Rejecting instead turned a typo'd [999, -5, 20] -- a
|
|
bright red -- into the default white, which is a colour the user never
|
|
asked for rather than the one they nearly asked for."""
|
|
cfg = {"customization": {"score_text": {"text_color": [999, -5, 20]}}}
|
|
assert C.element_color(cfg, "score_text", (9, 9, 9)) == (255, 0, 20)
|
|
|
|
def test_coerce_rgb_clamps_rather_than_rejecting(self):
|
|
assert C.coerce_rgb([300, -5, 20], (0, 0, 0)) == (255, 0, 20)
|
|
|
|
def test_coerce_rgb_refuses_a_three_character_string(self):
|
|
""""123" would otherwise iterate into three digits and yield a colour."""
|
|
assert C.coerce_rgb("123", (7, 7, 7)) == (7, 7, 7)
|
|
|
|
def test_font_colour_is_resolved_by_identity(self):
|
|
a, b = object(), object()
|
|
cfg = {"customization": {"score_text": {"text_color": [4, 5, 6]}}}
|
|
assert C.font_color(cfg, {"score": a, "team": b}, a) == (4, 5, 6)
|
|
|
|
def test_a_shared_face_takes_the_one_colour_that_was_configured(self):
|
|
"""A face shared by two elements used to go out white even when only
|
|
one of them had a colour set -- and a bitmap face is *always* shared,
|
|
because a freetype.Face cannot be re-instantiated to un-share it. That
|
|
is how an element in any of the 32 shipped BDF fonts silently lost the
|
|
colour its picker had offered all along. One configured colour among
|
|
the sharers is the only thing the user can have meant."""
|
|
shared = object()
|
|
cfg = {"customization": {"score_text": {"text_color": [4, 5, 6]}}}
|
|
assert C.font_color(cfg, {"score": shared, "team": shared},
|
|
shared) == (4, 5, 6)
|
|
|
|
def test_a_shared_face_still_gives_up_when_the_colours_disagree(self):
|
|
"""Two different answers is the case with no right answer."""
|
|
shared = object()
|
|
cfg = {"customization": {"score_text": {"text_color": [4, 5, 6]},
|
|
"team_name": {"text_color": [7, 8, 9]}}}
|
|
assert C.font_color(cfg, {"score": shared, "team": shared},
|
|
shared) == (255, 255, 255)
|
|
|
|
def test_sharers_that_agree_resolve_to_that_colour(self):
|
|
shared = object()
|
|
cfg = {"customization": {"score_text": {"text_color": [4, 5, 6]},
|
|
"team_name": {"text_color": [4, 5, 6]}}}
|
|
assert C.font_color(cfg, {"score": shared, "team": shared},
|
|
shared) == (4, 5, 6)
|
|
|
|
def test_an_unconfigured_shared_face_keeps_the_default(self):
|
|
shared = object()
|
|
assert C.font_color({}, {"score": shared, "team": shared}, shared,
|
|
(9, 9, 9)) == (9, 9, 9)
|
|
|
|
|
|
class TestFavourites:
|
|
GAME = {"home_abbr": "TB", "away_abbr": "NO", "home_score": "21", "away_score": "17"}
|
|
|
|
def test_win_loss_and_tie(self):
|
|
cfg = {"favorite_teams": ["TB"]}
|
|
assert C.favorite_result(cfg, self.GAME) == "win"
|
|
assert C.favorite_result({"favorite_teams": ["NO"]}, self.GAME) == "loss"
|
|
tied = dict(self.GAME, home_score="3", away_score="3")
|
|
assert C.favorite_result(cfg, tied) == "tie"
|
|
|
|
def test_no_verdict_without_exactly_one_favourite_side(self):
|
|
assert C.favorite_result({}, self.GAME) is None
|
|
assert C.favorite_result({"favorite_teams": ["TB", "NO"]}, self.GAME) is None
|
|
assert C.favorite_result({"favorite_teams": ["SEA"]}, self.GAME) is None
|
|
|
|
def test_unusable_scores_give_no_verdict(self):
|
|
bad = dict(self.GAME, home_score="x")
|
|
assert C.favorite_result({"favorite_teams": ["TB"]}, bad) is None
|
|
|
|
def test_nested_payload_shape_is_read_too(self):
|
|
game = {"home_team": {"abbrev": "TB", "score": 9},
|
|
"away_team": {"abbrev": "NO", "score": 2}}
|
|
assert C.side_score(game, "home") == 9
|
|
assert C.side_is_favorite(game, "home", {"TB"}) is True
|
|
|
|
def test_matches_on_id_where_abbreviations_collide(self):
|
|
"""NRL keys favourites by ESPN id; abbreviations are not unique there."""
|
|
game = {"home_abbr": "SYD", "home_id": "4321"}
|
|
assert C.side_is_favorite(game, "home", {"4321"}) is True
|
|
|
|
def test_game_and_config_favourites_are_both_used(self):
|
|
"""Games carry resolved dynamic groups; config catches later edits."""
|
|
game = dict(self.GAME, favorite_teams=["NO"], league="nfl")
|
|
assert set(C.favorite_teams_for({"nfl": {"favorite_teams": ["TB"]}}, game)) == {"NO", "TB"}
|
|
|
|
|
|
class TestDateAndTime:
|
|
def test_date_formats(self, log):
|
|
for fmt, want in [("abbrev", "Sep 5"), ("numeric", "9/5"),
|
|
("day_first", "5 Sep"), ("numeric_day_first", "5/9")]:
|
|
cfg = {"scroll_card": {"date_format": fmt}}
|
|
assert C.format_game_date(cfg, log, "9/5") == want
|
|
|
|
@pytest.mark.parametrize("raw", ["", "garbage", "13/40", "no/slash/here"])
|
|
def test_unparseable_dates_pass_through(self, log, raw):
|
|
assert C.format_game_date({}, log, raw) == raw.strip()
|
|
|
|
def test_24h_conversion(self):
|
|
cfg = {"scroll_card": {"time_format": "24h"}}
|
|
assert C.format_game_time(cfg, "7:30 PM") == "19:30"
|
|
assert C.format_game_time(cfg, "12:00 AM") == "00:00"
|
|
assert C.format_game_time(cfg, "12:15 PM") == "12:15"
|
|
|
|
def test_12h_is_left_alone_and_junk_survives(self):
|
|
assert C.format_game_time({}, "7:30 PM") == "7:30 PM"
|
|
assert C.format_game_time({"scroll_card": {"time_format": "24h"}}, "soon") == "soon"
|
|
|
|
def test_a_bad_timezone_falls_back_to_utc(self, log):
|
|
"""A typo in config should not blank the card."""
|
|
from datetime import timezone
|
|
assert C.card_tzinfo({"timezone": "Not/AZone"}, log) is timezone.utc
|
|
|
|
|
|
class TestWeekdayMatchesThePrintedDate:
|
|
"""The weekday is the printed date's, whichever zone printed it.
|
|
|
|
The extractor prints "M/D" in the plugin's resolved zone (its own
|
|
setting, else the global one, else the system zone). The card is handed
|
|
only the plugin's config, whose ``timezone`` ships as "" -- so a weekday
|
|
taken in card_tzinfo's zone was UTC's, and an evening kickoff in the
|
|
Americas read "Sat Oct 2" for a Friday game.
|
|
"""
|
|
|
|
WEEKDAY = {"timezone": "", "scroll_card": {"date_format": "weekday"}}
|
|
|
|
@staticmethod
|
|
def _as_printed(start_utc, zone):
|
|
"""The game dict and the date text, as the extractor builds them."""
|
|
local = datetime.fromisoformat(start_utc).astimezone(ZoneInfo(zone))
|
|
game = {"start_time_utc": datetime.fromisoformat(start_utc),
|
|
"game_date": f"{local.month}/{local.day}"}
|
|
want = f"{C.WEEKDAY_ABBR[local.weekday()]} {C.MONTH_ABBR[local.month - 1]} {local.day}"
|
|
return game, want
|
|
|
|
@pytest.mark.parametrize("start_utc, zone, want", [
|
|
# Friday 8 PM EDT is Saturday in UTC.
|
|
("2026-10-03T00:00:00+00:00", "America/New_York", "Fri Oct 2"),
|
|
# The night US clocks go back: 8:30 PM EDT Saturday, then 11 PM EST
|
|
# Sunday, each the next day in UTC.
|
|
("2026-11-01T00:30:00+00:00", "America/New_York", "Sat Oct 31"),
|
|
("2026-11-02T04:00:00+00:00", "America/New_York", "Sun Nov 1"),
|
|
# New Year's Eve on the west coast is New Year's Day in UTC.
|
|
("2027-01-01T04:00:00+00:00", "America/Los_Angeles", "Thu Dec 31"),
|
|
# Just east of the date line: Pago Pago's Friday evening.
|
|
("2026-10-03T05:00:00+00:00", "Pacific/Pago_Pago", "Fri Oct 2"),
|
|
# Just west of it, the other way: Saturday morning in Auckland is
|
|
# Friday in UTC -- and the 10 AM game on the day NZ clocks go forward.
|
|
("2026-10-02T20:00:00+00:00", "Pacific/Auckland", "Sat Oct 3"),
|
|
("2026-09-26T21:00:00+00:00", "Pacific/Auckland", "Sun Sep 27"),
|
|
# UTC+14, the furthest any zone sits from UTC.
|
|
("2026-10-02T11:00:00+00:00", "Pacific/Kiritimati", "Sat Oct 3"),
|
|
# A zone on UTC's own date needs nothing.
|
|
("2026-10-02T19:00:00+00:00", "Europe/London", "Fri Oct 2"),
|
|
])
|
|
def test_the_shipped_blank_timezone(self, log, start_utc, zone, want):
|
|
game, printed = self._as_printed(start_utc, zone)
|
|
assert printed == want # the case says what the extractor prints
|
|
assert C.format_game_date(self.WEEKDAY, log, game["game_date"], game) == want
|
|
|
|
def test_an_iso_string_start_reads_the_same(self, log):
|
|
game = {"start_time_utc": "2026-10-03T00:00:00Z"}
|
|
assert C.format_game_date(self.WEEKDAY, log, "10/2", game) == "Fri Oct 2"
|
|
assert C.format_game_date(self.WEEKDAY, log, "10/02", game) == "Fri Oct 2"
|
|
|
|
def test_a_plugin_level_zone_still_agrees(self, log):
|
|
game, want = self._as_printed("2026-10-03T00:00:00+00:00", "America/Chicago")
|
|
cfg = dict(self.WEEKDAY, timezone="America/Chicago")
|
|
assert C.format_game_date(cfg, log, game["game_date"], game) == want == "Fri Oct 2"
|
|
|
|
def test_a_date_no_zone_could_print_keeps_the_zone_weekday(self, log):
|
|
"""More than a day from the start: nothing to anchor to, so the
|
|
weekday is card_tzinfo's, as it always was."""
|
|
game = {"start_time_utc": datetime(2026, 10, 3, 0, 0, tzinfo=timezone.utc)}
|
|
assert C.format_game_date(self.WEEKDAY, log, "10/9", game) == "Sat Oct 9"
|
|
|
|
def test_a_start_without_an_offset_keeps_the_zone_weekday(self, log):
|
|
"""A naive time names no instant, so it cannot place the date."""
|
|
game = {"start_time_utc": datetime(2026, 10, 2, 20, 0)}
|
|
assert C.format_game_date(self.WEEKDAY, log, "10/3", game) == \
|
|
f"{C.weekday_for(self.WEEKDAY, log, game)} Oct 3"
|
|
|
|
@pytest.mark.parametrize("game", [None, {}, {"start_time_utc": "garbage"}])
|
|
def test_no_usable_start_draws_no_weekday(self, log, game):
|
|
assert C.format_game_date(self.WEEKDAY, log, "10/2", game) == "Oct 2"
|
|
|
|
def test_the_scorebug_twin_formats_the_same(self, log):
|
|
"""Switch mode (SportsCoreSharedMixin) shares the formatter body."""
|
|
from src.common.sports_shared import SportsCoreSharedMixin
|
|
|
|
class Host(SportsCoreSharedMixin):
|
|
config = {"scroll_card": {"date_format": "weekday",
|
|
"switch_date_format": "inherit"}}
|
|
logger = log
|
|
|
|
def _get_timezone(self):
|
|
return ZoneInfo("America/New_York")
|
|
|
|
game, want = self._as_printed("2026-11-01T00:30:00+00:00", "America/New_York")
|
|
assert Host()._format_game_date(game["game_date"], game) == want
|
|
assert C.format_game_date(Host.config, log, game["game_date"], game) == want
|
|
|
|
|
|
class TestFontSizing:
|
|
def test_snaps_to_the_faces_pixel_grid(self):
|
|
assert C.crisp_size("4x6-font.ttf", 6) == 7 # 7px grid
|
|
assert C.crisp_size("PressStart2P-Regular.ttf", 10) == 8
|
|
assert C.crisp_size("PressStart2P-Regular.ttf", 13) == 16
|
|
|
|
def test_an_unknown_face_is_never_second_guessed(self):
|
|
assert C.crisp_size("SomeUserFont.ttf", 11) == 11
|
|
|
|
def test_aliases_resolve_before_the_grid_lookup(self):
|
|
assert C.crisp_size("four_by_six", 6) == C.crisp_size("4x6-font.ttf", 6)
|
|
|
|
@pytest.mark.parametrize("desired", [0, -3, None])
|
|
def test_unusable_sizes_pass_through_without_raising(self, desired):
|
|
"""None reached this in the field; football's variant raised TypeError."""
|
|
assert C.crisp_size("4x6-font.ttf", desired) == desired
|
|
|
|
def test_schema_cache_is_keyed_per_schema_not_globally(self, tmp_path):
|
|
"""Two plugins declaring different defaults must not share an answer."""
|
|
a, b = tmp_path / "a.json", tmp_path / "b.json"
|
|
for path, size in ((a, 11), (b, 22)):
|
|
path.write_text(json.dumps({"properties": {"customization": {"properties": {
|
|
"score_text": {"properties": {"font_size": {"default": size}}}}}}}))
|
|
assert C.schema_font_size(str(a), "score_text") == 11
|
|
assert C.schema_font_size(str(b), "score_text") == 22
|
|
|
|
def test_a_missing_schema_is_not_an_error(self, tmp_path):
|
|
assert C.schema_font_size(str(tmp_path / "nope.json"), "score_text") is None
|
|
|
|
def test_a_configured_size_matching_the_schema_default_is_not_a_choice(self, tmp_path):
|
|
"""The web UI writes the whole default block on every save, so
|
|
font_size == schema default carries no intent and must not pin the
|
|
install to an off-grid size forever."""
|
|
schema = tmp_path / "s.json"
|
|
schema.write_text(json.dumps({"properties": {"customization": {"properties": {
|
|
"score_text": {"properties": {"font_size": {"default": 10}}}}}}}))
|
|
got = C.resolve_font_size(str(schema), {"font_size": 10}, "score_text", 10,
|
|
"PressStart2P-Regular.ttf")
|
|
assert got == 8, "a default-valued size should snap to the grid"
|
|
|
|
def test_a_real_choice_wins(self, tmp_path):
|
|
schema = tmp_path / "s.json"
|
|
schema.write_text(json.dumps({"properties": {"customization": {"properties": {
|
|
"score_text": {"properties": {"font_size": {"default": 10}}}}}}}))
|
|
got = C.resolve_font_size(str(schema), {"font_size": 13}, "score_text", 10,
|
|
"PressStart2P-Regular.ttf")
|
|
assert got == 13, "an explicit size the user chose is not second-guessed"
|
|
|
|
|
|
class TestTables:
|
|
def test_every_font_key_maps_to_an_element(self):
|
|
assert set(C.ELEMENT_FOR_FONT) == {"score", "time", "team", "status", "detail", "rank"}
|
|
|
|
def test_result_colours_cover_every_verdict(self):
|
|
assert set(C.FAVORITE_RESULT_COLOR_DEFAULTS) == {"win", "loss", "tie"}
|
|
|
|
def test_month_and_weekday_tables_are_complete(self):
|
|
assert len(C.MONTH_ABBR) == 12 and len(C.WEEKDAY_ABBR) == 7
|
|
|
|
|
|
class TestUnshareElementFonts:
|
|
"""Re-instantiated faces must match the ones they replace.
|
|
|
|
``unshare_element_fonts`` rebuilds a duplicate face purely so two elements
|
|
can be told apart by ``id()``. That is only safe while the rebuilt face
|
|
lays text out identically -- and PIL's two layout engines disagree on
|
|
fractional advances, which is why ``src.common.font_layout`` exists and
|
|
pins one. Rebuilding through bare ``ImageFont.truetype`` took the default
|
|
engine instead, so a re-instantiated face could measure differently from
|
|
the shared face it replaced.
|
|
"""
|
|
|
|
def _face(self, size=10):
|
|
from src.common.font_layout import load_truetype
|
|
path = os.path.join("assets", "fonts", "PressStart2P-Regular.ttf")
|
|
return load_truetype(path, size)
|
|
|
|
def test_rebuilt_face_goes_through_the_pinned_loader(self, log, monkeypatch):
|
|
"""Asserted on the loader, not on the resulting engine value.
|
|
|
|
PIL only selects Raqm when it is installed; where it is not, a bare
|
|
``ImageFont.truetype`` returns BASIC too, so comparing engine values
|
|
passes on those machines whether or not the pin is honoured -- this
|
|
test did exactly that before it was rewritten. Spying on the pinned
|
|
loader fails on every machine when the pin is bypassed, which is the
|
|
point: the cross-machine mismatch font_layout exists to prevent
|
|
cannot be reproduced on a Raqm-less runner.
|
|
"""
|
|
import src.common.font_layout as fl
|
|
|
|
# Build the face BEFORE patching: _face() loads through the same
|
|
# pinned loader, so patching first would let the fixture's own call
|
|
# satisfy the assertion and the test would pass either way.
|
|
shared = self._face()
|
|
|
|
calls = []
|
|
real = fl.load_truetype
|
|
|
|
def spy(font, size, **kwargs):
|
|
calls.append((font, size))
|
|
return real(font, size, **kwargs)
|
|
|
|
monkeypatch.setattr(fl, "load_truetype", spy)
|
|
fonts = {"score": shared, "time": shared}
|
|
C.unshare_element_fonts(log, fonts)
|
|
assert fonts["time"] is not shared, "the duplicate should have been rebuilt"
|
|
assert calls, "the rebuild must go through the pinned loader"
|
|
|
|
def test_rebuilt_face_keeps_the_shared_faces_layout_engine(self, log):
|
|
shared = self._face()
|
|
fonts = {"score": shared, "time": shared}
|
|
C.unshare_element_fonts(log, fonts)
|
|
assert fonts["time"].layout_engine == shared.layout_engine
|
|
|
|
def test_rebuilt_face_measures_identically(self, log):
|
|
shared = self._face()
|
|
fonts = {"score": shared, "time": shared}
|
|
C.unshare_element_fonts(log, fonts)
|
|
text = "88-88"
|
|
assert (fonts["time"].getlength(text)
|
|
== shared.getlength(text)), "metrics must not shift"
|
|
|
|
def test_distinct_faces_are_left_alone(self, log):
|
|
a, b = self._face(10), self._face(8)
|
|
fonts = {"score": a, "time": b}
|
|
C.unshare_element_fonts(log, fonts)
|
|
assert fonts["score"] is a and fonts["time"] is b
|
|
|
|
class TestSharedElementReaders:
|
|
"""The scoreboards' colour and offset reads now go through one
|
|
implementation.
|
|
|
|
There were two copies of the colour read and three of the offset read.
|
|
They had already drifted: the scroll-card renderer carries a comment
|
|
noting it used to ignore offsets its own schema advertised. Sharing one
|
|
reader is what lets a fix (or an alias, or a per-mode override) reach
|
|
the full-screen scorebug and the scroll card together.
|
|
"""
|
|
|
|
CONFIG = {
|
|
"customization": {
|
|
"score_text": {"text_color": [255, 200, 0]},
|
|
"status": {"text_color": "#00ff00"},
|
|
"layout": {"score": {"y_offset": -3}},
|
|
"modes": {
|
|
"live": {"score_text": {"text_color": [255, 0, 0]},
|
|
"layout": {"score": {"y_offset": 7}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
def test_colour_reads_the_configured_value(self, log):
|
|
assert C.element_color(self.CONFIG, "score_text") == (255, 200, 0)
|
|
|
|
def test_hex_colours_are_still_accepted(self, log):
|
|
"""These readers have always taken '#RRGGBB' as well as [r, g, b],
|
|
so the shared one had to learn it rather than the callers losing it."""
|
|
assert C.element_color(self.CONFIG, "status") == (0, 255, 0)
|
|
|
|
def test_colour_resolves_through_an_alias(self, log):
|
|
"""The style block says status_text where this config says status."""
|
|
assert C.element_color(self.CONFIG, "status_text") == (0, 255, 0)
|
|
|
|
def test_a_mode_overrides_the_colour(self, log):
|
|
assert C.element_color(self.CONFIG, "score_text",
|
|
mode="live") == (255, 0, 0)
|
|
|
|
def test_an_unset_element_gets_the_default(self, log):
|
|
assert C.element_color(self.CONFIG, "nothing", (1, 2, 3)) == (1, 2, 3)
|
|
|
|
@pytest.mark.parametrize("config", [
|
|
None, {}, "nonsense", {"customization": "nonsense"},
|
|
{"customization": {"score_text": "nonsense"}},
|
|
{"customization": {"score_text": {"text_color": "not a colour"}}},
|
|
])
|
|
def test_a_hostile_config_gives_the_default(self, config, log):
|
|
assert C.element_color(config, "score_text", (9, 9, 9)) == (9, 9, 9)
|