bulid 63
This commit is contained in:
357
tests/test_match_details.py
Normal file
357
tests/test_match_details.py
Normal file
@@ -0,0 +1,357 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from hockey_data.config import HockeySettingsStore
|
||||
from hockey_data.match_details_parser import (
|
||||
parse_match_details_json,
|
||||
parse_players_xml,
|
||||
parse_referees_xml,
|
||||
)
|
||||
from hockey_data.match_parser import ParsedGame, parse_date_value
|
||||
from hockey_data.models import Game, Tournament
|
||||
from hockey_data.service import HockeyDataService
|
||||
from support import LocalTestDatabase
|
||||
|
||||
|
||||
PLAYERS_XML = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PlayerStatsList tournamentId="1369">
|
||||
<PlayerStats id="101" pos="\xd0\xbd" name="\xd0\x98\xd0\xb2\xd0\xb0\xd0\xbd\xd0\xbe\xd0\xb2 \xd0\x98\xd0\xb2\xd0\xb0\xd0\xbd" nameen="Ivanov Ivan"
|
||||
lastname="\xd0\x98\xd0\xb2\xd0\xb0\xd0\xbd\xd0\xbe\xd0\xb2" firstname="\xd0\x98\xd0\xb2\xd0\xb0\xd0\xbd" lastnameen="Ivanov" firstnameen="Ivan" birthdate="2000-01-02"
|
||||
gp="63" g="11" a="14" pts="25" pm="5" pim="24" sog="111" sog_pct="9.9" hits="22" bls="50" fo="100" fow="55" fo_pct="55.0" toi_avg="14:54" />
|
||||
<PlayerStats id="202" pos="\xd0\xb2" name="\xd0\x9f\xd0\xb5\xd1\x82\xd1\x80\xd0\xbe\xd0\xb2 \xd0\x9f\xd1\x91\xd1\x82\xd1\x80" nameen="Petrov Pyotr"
|
||||
lastname="\xd0\x9f\xd0\xb5\xd1\x82\xd1\x80\xd0\xbe\xd0\xb2" firstname="\xd0\x9f\xd1\x91\xd1\x82\xd1\x80" lastnameen="Petrov" firstnameen="Pyotr"
|
||||
gp="40" w="25" l="10" n="0" sop="5" sog="1000" ga="80" sv="920" sv_pct="92.0" gaa="2.00" so="5" toi="2400:00" />
|
||||
</PlayerStatsList>"""
|
||||
|
||||
REFEREES_XML = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Referees tournamentId="1369">
|
||||
<Referee id="301" name="\xd0\xa1\xd0\xb8\xd0\xb4\xd0\xbe\xd1\x80\xd0\xbe\xd0\xb2 \xd0\x90\xd0\xbb\xd0\xb5\xd0\xba\xd1\x81\xd0\xb5\xd0\xb9" nameen="Sidorov Alexei"
|
||||
lastname="\xd0\xa1\xd0\xb8\xd0\xb4\xd0\xbe\xd1\x80\xd0\xbe\xd0\xb2" firstname="\xd0\x90\xd0\xbb\xd0\xb5\xd0\xba\xd1\x81\xd0\xb5\xd0\xb9" lastnameen="Sidorov" firstnameen="Alexei"
|
||||
pos="m" jn="18" isactive="Y" />
|
||||
<Referee id="302" name="\xd0\xa1\xd0\xbc\xd0\xb8\xd1\x80\xd0\xbd\xd0\xbe\xd0\xb2 \xd0\x9e\xd0\xbb\xd0\xb5\xd0\xb3" nameen="Smirnov Oleg" pos="l" jn="22" isactive="Y" />
|
||||
</Referees>"""
|
||||
|
||||
MATCH_RU = """{
|
||||
"game": {
|
||||
"idschedule": 900001,
|
||||
"teamA": "Команда А",
|
||||
"teamB": "Команда Б",
|
||||
"idclubA": 1,
|
||||
"idteamA": 11,
|
||||
"idclubB": 2,
|
||||
"idteamB": 22,
|
||||
"mref1": "Сидоров Алексей",
|
||||
"mref1_id": 301,
|
||||
"mref1_num": "18",
|
||||
"lref1": "Смирнов Олег",
|
||||
"lref1_id": 302,
|
||||
"lref1_num": "22",
|
||||
"status": "матч не начался"
|
||||
},
|
||||
"teams": {
|
||||
"A": {"coach_fullname": "Тренер Первый", "shots": 31, "goals": 3, "fo": 50, "fow": 28, "fow_pct": "56.00", "hits": 14, "bls": 9, "pim": 6, "toa": "14:20"},
|
||||
"B": {"coach_fullname": "Тренер Второй", "shots": 27, "goals": 2, "fo": 50, "fow": 22, "fow_pct": "44.00", "hits": 18, "bls": 12, "pim": 8, "toa": "11:05"}
|
||||
},
|
||||
"teams_periods": {
|
||||
"A": {"1": {"shots": 10, "goals": 1, "fo": 18, "fow": 11, "pim": 2}, "2": {"shots": 12, "goals": 2, "fo": 17, "fow": 9, "pim": 4}},
|
||||
"B": {"1": {"shots": 8, "goals": 0, "fo": 18, "fow": 7, "pim": 4}, "2": {"shots": 11, "goals": 1, "fo": 17, "fow": 8, "pim": 2}}
|
||||
},
|
||||
"players": {
|
||||
"A": {"17": {"id": "101", "name": "Иванов Иван", "ps": "н", "pig": 1, "line": "1", "ca": "к", "goals": 1, "assists": 2, "pts": 3, "shots": 4, "hits": 2, "bls": 1, "fo": 10, "fow": 6, "toi": "17:20", "tie": "13:00", "tipp": "03:10", "tish": "01:10", "si": 21}},
|
||||
"B": {"30": {"id": "202", "name": "Петров Пётр", "ps": "в", "pig": 1, "line": "", "sog": 31, "sv": 29, "ga": 2, "sv_pct": 93.55, "toi": "59:40"}}
|
||||
}
|
||||
}"""
|
||||
|
||||
MATCH_EN = """{
|
||||
"game": {
|
||||
"idschedule": 900001,
|
||||
"teamA": "Team A",
|
||||
"teamB": "Team B",
|
||||
"idclubA": 1,
|
||||
"idteamA": 11,
|
||||
"idclubB": 2,
|
||||
"idteamB": 22,
|
||||
"mref1": "Sidorov Alexei",
|
||||
"mref1_id": 301,
|
||||
"mref1_num": "18",
|
||||
"lref1": "Smirnov Oleg",
|
||||
"lref1_id": 302,
|
||||
"lref1_num": "22",
|
||||
"status": "scheduled"
|
||||
},
|
||||
"teams": {
|
||||
"A": {"coach_fullname": "Coach One", "shots": 31, "goals": 3, "fo": 50, "fow": 28, "fow_pct": "56.00", "hits": 14, "bls": 9, "pim": 6, "toa": "14:20"},
|
||||
"B": {"coach_fullname": "Coach Two", "shots": 27, "goals": 2, "fo": 50, "fow": 22, "fow_pct": "44.00", "hits": 18, "bls": 12, "pim": 8, "toa": "11:05"}
|
||||
},
|
||||
"teams_periods": {
|
||||
"A": {"1": {"shots": 10, "goals": 1, "fo": 18, "fow": 11, "pim": 2}, "2": {"shots": 12, "goals": 2, "fo": 17, "fow": 9, "pim": 4}},
|
||||
"B": {"1": {"shots": 8, "goals": 0, "fo": 18, "fow": 7, "pim": 4}, "2": {"shots": 11, "goals": 1, "fo": 17, "fow": 8, "pim": 2}}
|
||||
},
|
||||
"players": {
|
||||
"A": {"17": {"id": "101", "name": "Ivanov Ivan", "ps": "f", "pig": 1, "line": "1", "ca": "c", "goals": 1, "assists": 2, "pts": 3, "shots": 4, "hits": 2, "bls": 1, "fo": 10, "fow": 6, "toi": "17:20", "tie": "13:00", "tipp": "03:10", "tish": "01:10", "si": 21}},
|
||||
"B": {"30": {"id": "202", "name": "Petrov Pyotr", "ps": "g", "pig": 1, "line": "", "sog": 31, "sv": 29, "ga": 2, "sv_pct": 93.55, "toi": "59:40"}}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class MatchDetailsParserTests(unittest.TestCase):
|
||||
|
||||
def test_parses_localized_match_datetime_into_real_game_date(self) -> None:
|
||||
self.assertEqual(
|
||||
parse_date_value("16 августа 2026, Вс 13:00:00"),
|
||||
date(2026, 8, 16),
|
||||
)
|
||||
self.assertEqual(
|
||||
parse_date_value("August 16, 2026, Sun 13:00:00"),
|
||||
date(2026, 8, 16),
|
||||
)
|
||||
|
||||
def test_details_date_is_used_only_as_schedule_fallback(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_dir = Path(temp_dir)
|
||||
database = LocalTestDatabase(base_dir / "test.sqlite3")
|
||||
database.create_all()
|
||||
settings = HockeySettingsStore(base_dir / "settings")
|
||||
service = HockeyDataService(
|
||||
base_dir=base_dir,
|
||||
database=database,
|
||||
settings=settings,
|
||||
)
|
||||
with database.session() as session:
|
||||
session.add(Tournament(external_id="1369"))
|
||||
session.add(
|
||||
Game(
|
||||
external_id="900001",
|
||||
tournament_external_id="1369",
|
||||
)
|
||||
)
|
||||
|
||||
match_ru = MATCH_RU.replace(
|
||||
'"status": "матч не начался"',
|
||||
'"status": "матч не начался", "date": "16 августа 2026, Вс 13:00:00"',
|
||||
)
|
||||
details = parse_match_details_json(match_ru, MATCH_EN)
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="900001").one()
|
||||
service._apply_details_game_fields(game, details, now=datetime.utcnow())
|
||||
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="900001").one()
|
||||
self.assertEqual(game.game_date, date(2026, 8, 16))
|
||||
self.assertEqual(game.start_time, "13:00")
|
||||
self.assertEqual(game.start_datetime_raw, "16 августа 2026, Вс 13:00:00")
|
||||
|
||||
# A later schedule refresh with an empty date must not erase
|
||||
# the date recovered from the selected game's detail card.
|
||||
service._upsert_game(
|
||||
session,
|
||||
ParsedGame(
|
||||
external_id="900001",
|
||||
tournament_external_id="1369",
|
||||
game_date=None,
|
||||
start_time="",
|
||||
start_datetime_raw="",
|
||||
),
|
||||
endpoint="schedule-test",
|
||||
now=datetime.utcnow(),
|
||||
)
|
||||
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="900001").one()
|
||||
self.assertEqual(game.game_date, date(2026, 8, 16))
|
||||
self.assertEqual(game.start_time, "13:00")
|
||||
|
||||
# If the schedule does have a real date, it stays primary.
|
||||
game.game_date = date(2026, 8, 17)
|
||||
session.flush()
|
||||
service._apply_details_game_fields(game, details, now=datetime.utcnow())
|
||||
self.assertEqual(game.game_date, date(2026, 8, 17))
|
||||
database.engine.dispose()
|
||||
|
||||
|
||||
def test_exact_khl_game_date_and_starttime_are_saved(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_dir = Path(temp_dir)
|
||||
database = LocalTestDatabase(base_dir / "test.sqlite3")
|
||||
database.create_all()
|
||||
settings = HockeySettingsStore(base_dir / "settings")
|
||||
service = HockeyDataService(
|
||||
base_dir=base_dir,
|
||||
database=database,
|
||||
settings=settings,
|
||||
)
|
||||
payload = json.dumps({
|
||||
"game": {
|
||||
"idschedule": 902733,
|
||||
"number": 5,
|
||||
"date": "16 августа 2026, Вс",
|
||||
"starttime": "13:00:00",
|
||||
"teamA": "Спартак",
|
||||
"teamB": "Северсталь",
|
||||
"idclubA": 7,
|
||||
"idteamA": 15655,
|
||||
"idclubB": 56,
|
||||
"idteamB": 15652,
|
||||
"score": "6:4",
|
||||
"status": "матч завершен",
|
||||
"statusvalue": 90,
|
||||
"place": "ЛД «Легенды хоккея - Сокольники»"
|
||||
}
|
||||
}, ensure_ascii=False)
|
||||
details = parse_match_details_json(payload, expected_game_id="902733")
|
||||
with database.session() as session:
|
||||
game = Game(external_id="902733", tournament_external_id="1369")
|
||||
session.add(game)
|
||||
service._apply_details_game_fields(game, details, now=datetime.utcnow())
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="902733").one()
|
||||
self.assertEqual(game.game_date, date(2026, 8, 16))
|
||||
self.assertEqual(game.start_time, "13:00")
|
||||
self.assertEqual(game.start_datetime_raw, "16 августа 2026, Вс 13:00:00")
|
||||
|
||||
def test_schedule_raw_localized_datetime_recovers_game_date(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_dir = Path(temp_dir)
|
||||
database = LocalTestDatabase(base_dir / "test.sqlite3")
|
||||
database.create_all()
|
||||
settings = HockeySettingsStore(base_dir / "settings")
|
||||
service = HockeyDataService(
|
||||
base_dir=base_dir,
|
||||
database=database,
|
||||
settings=settings,
|
||||
)
|
||||
with database.session() as session:
|
||||
service._upsert_game(
|
||||
session,
|
||||
ParsedGame(
|
||||
external_id="902733",
|
||||
tournament_external_id="1369",
|
||||
game_date=None,
|
||||
start_time="",
|
||||
start_datetime_raw="16 августа 2026, Вс 13:00:00",
|
||||
),
|
||||
endpoint="raw-date-test",
|
||||
now=datetime.utcnow(),
|
||||
)
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="902733").one()
|
||||
self.assertEqual(game.game_date, date(2026, 8, 16))
|
||||
self.assertEqual(game.start_time, "13:00")
|
||||
|
||||
def test_backfills_existing_null_game_date_from_raw_datetime(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_dir = Path(temp_dir)
|
||||
database = LocalTestDatabase(base_dir / "test.sqlite3")
|
||||
database.create_all()
|
||||
settings = HockeySettingsStore(base_dir / "settings")
|
||||
service = HockeyDataService(
|
||||
base_dir=base_dir,
|
||||
database=database,
|
||||
settings=settings,
|
||||
)
|
||||
with database.session() as session:
|
||||
session.add(
|
||||
Game(
|
||||
external_id="902733",
|
||||
tournament_external_id="1369",
|
||||
game_date=None,
|
||||
start_time="13:00",
|
||||
start_datetime_raw="16 августа 2026, Вс 13:00:00",
|
||||
)
|
||||
)
|
||||
with database.session() as session:
|
||||
repaired = service._backfill_missing_game_dates(session)
|
||||
self.assertEqual(repaired, 1)
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="902733").one()
|
||||
self.assertEqual(game.game_date, date(2026, 8, 16))
|
||||
self.assertEqual(game.start_time, "13:00")
|
||||
|
||||
def test_parses_bilingual_rosters_and_officials(self) -> None:
|
||||
details = parse_match_details_json(
|
||||
MATCH_RU,
|
||||
MATCH_EN,
|
||||
expected_game_id="900001",
|
||||
)
|
||||
self.assertEqual(details.game_external_id, "900001")
|
||||
self.assertEqual(len(details.rosters), 2)
|
||||
self.assertEqual(details.rosters[0].role, "forward")
|
||||
self.assertEqual(details.rosters[0].captain_role, "captain")
|
||||
self.assertEqual(details.rosters[1].role, "goalkeeper")
|
||||
self.assertEqual(len(details.officials), 2)
|
||||
self.assertEqual(details.officials[0].role, "head")
|
||||
|
||||
def test_stores_and_serializes_match_details(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_dir = Path(temp_dir)
|
||||
database = LocalTestDatabase(base_dir / "test.sqlite3")
|
||||
database.create_all()
|
||||
settings = HockeySettingsStore(base_dir / "settings")
|
||||
service = HockeyDataService(
|
||||
base_dir=base_dir,
|
||||
database=database,
|
||||
settings=settings,
|
||||
)
|
||||
with database.session() as session:
|
||||
session.add(Tournament(external_id="1369"))
|
||||
with database.session() as session:
|
||||
session.add(
|
||||
Game(
|
||||
external_id="900001",
|
||||
tournament_external_id="1369",
|
||||
home_team_external_id="1",
|
||||
away_team_external_id="2",
|
||||
home_team_name_ru="Команда А",
|
||||
home_team_name_en="Team A",
|
||||
away_team_name_ru="Команда Б",
|
||||
away_team_name_en="Team B",
|
||||
)
|
||||
)
|
||||
|
||||
details = parse_match_details_json(MATCH_RU, MATCH_EN)
|
||||
players = parse_players_xml(PLAYERS_XML)
|
||||
referees = parse_referees_xml(REFEREES_XML)
|
||||
with database.session() as session:
|
||||
game = session.query(Game).filter_by(external_id="900001").one()
|
||||
service._store_match_details(
|
||||
session,
|
||||
game=game,
|
||||
details=details,
|
||||
player_profiles=players,
|
||||
referee_profiles=referees,
|
||||
endpoint_ru="1369/json/900001.json",
|
||||
raw_ru=MATCH_RU,
|
||||
raw_en=MATCH_EN,
|
||||
now=datetime.utcnow(),
|
||||
)
|
||||
|
||||
payload_ru = service.game_details("900001", language="ru")
|
||||
payload_en = service.game_details("900001", language="en")
|
||||
self.assertIsNotNone(payload_ru)
|
||||
self.assertEqual(payload_ru["game"]["home"]["players"][0]["name"], "Иванов Иван")
|
||||
self.assertEqual(payload_en["game"]["away"]["players"][0]["name"], "Petrov Pyotr")
|
||||
self.assertEqual(len(payload_ru["referees"]), 2)
|
||||
self.assertEqual(payload_ru["details"]["home_players"], 1)
|
||||
self.assertEqual(payload_ru["team_statistics"]["segments"], ["total", "1", "2"])
|
||||
self.assertEqual(payload_ru["team_statistics"]["total"]["home"]["shots"], 31)
|
||||
self.assertEqual(payload_ru["team_statistics"]["periods"]["1"]["away"]["goals"], 0)
|
||||
self.assertEqual(payload_ru["game"]["home"]["players"][0]["statistics"]["points"], 3)
|
||||
self.assertEqual(payload_ru["game"]["home"]["players"][0]["statistics"]["faceoff_pct"], 60.0)
|
||||
self.assertEqual(payload_ru["game"]["away"]["players"][0]["statistics"]["saves"], 29)
|
||||
self.assertEqual(payload_ru["game"]["away"]["players"][0]["statistics"]["save_pct"], 93.55)
|
||||
self.assertTrue(payload_ru["game"]["player_statistics"]["available"])
|
||||
self.assertEqual(payload_ru["game"]["home"]["players"][0]["season_statistics"]["games"], 63)
|
||||
self.assertEqual(payload_ru["game"]["home"]["players"][0]["season_statistics"]["points"], 25)
|
||||
self.assertEqual(payload_ru["game"]["away"]["players"][0]["season_statistics"]["wins"], 25)
|
||||
self.assertEqual(payload_ru["game"]["away"]["players"][0]["season_statistics"]["save_pct"], 92)
|
||||
self.assertTrue(payload_ru["game"]["season_player_statistics"]["available"])
|
||||
database.engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user