diff --git a/criclive/main.py b/criclive/main.py index c5b37c4..a0522e1 100644 --- a/criclive/main.py +++ b/criclive/main.py @@ -45,7 +45,11 @@ def _parse_matches_from_script(text): start = m.start() - 1 # include the opening { match_data = _extract_json_object(unescaped, start) if match_data: - parsed = _parse_match(match_data) + try: + parsed = _parse_match(match_data) + except (AttributeError, TypeError, ValueError): + # Skip one malformed match instead of losing every match + continue if parsed: matches.append(parsed) @@ -96,11 +100,7 @@ def _parse_match(data): status = info.get("stateTitle", "") or info.get("state", "") - start_ts = info.get("startDate") - date_str = "" - if start_ts: - dt = datetime.fromtimestamp(start_ts / 1000, tz=timezone.utc) - date_str = dt.strftime("%b %d, %H:%M GMT") + date_str = _format_match_date(info.get("startDate")) venue = info.get("venueInfo", {}) venue_str = "" @@ -129,6 +129,20 @@ def _parse_match(data): } +def _format_match_date(start_ts): + """Format a Cricbuzz timestamp, or return "" when it is missing or invalid.""" + if isinstance(start_ts, bool): + return "" + try: + start_ms = float(start_ts) + if not start_ms: + return "" + dt = datetime.fromtimestamp(start_ms / 1000, tz=timezone.utc) + except (OverflowError, OSError, TypeError, ValueError): + return "" + return dt.strftime("%b %d, %H:%M GMT") + + def _format_score(team_score): """Format innings scores into a readable string.""" parts = [] diff --git a/tests.py b/tests.py index 7e7b56c..abbe4cc 100644 --- a/tests.py +++ b/tests.py @@ -1,8 +1,9 @@ +import copy import json import unittest from unittest.mock import patch, MagicMock -from criclive.main import get_scores, _format_score, _parse_match +from criclive.main import get_scores, _format_score, _format_match_date, _parse_match SAMPLE_MATCH = { @@ -72,21 +73,63 @@ def test_parse_match_no_teams(self): data = {"match": {"matchInfo": {}, "matchScore": {}}} self.assertIsNone(_parse_match(data)) + def test_parse_match_ignores_malformed_start_date(self): + data = { + "match": { + "matchInfo": { + "matchId": 12345, + "team1": {"teamName": "India", "teamSName": "IND"}, + "team2": {"teamName": "Australia", "teamSName": "AUS"}, + "startDate": "not-a-timestamp", + }, + "matchScore": {}, + } + } + result = _parse_match(data) + self.assertIsNotNone(result) + self.assertEqual(result["date"], "") + + +class TestFormatMatchDate(unittest.TestCase): + def test_formats_numeric_timestamp(self): + self.assertEqual(_format_match_date(1700000000000), "Nov 14, 22:13 GMT") + + def test_formats_numeric_string_timestamp(self): + self.assertEqual(_format_match_date("1700000000000"), "Nov 14, 22:13 GMT") + + def test_zero_timestamp_returns_empty(self): + self.assertEqual(_format_match_date(0), "") + self.assertEqual(_format_match_date("0"), "") + + def test_out_of_range_timestamp_returns_empty(self): + self.assertEqual(_format_match_date(1e20), "") + + def test_boolean_timestamp_returns_empty(self): + self.assertEqual(_format_match_date(True), "") + + def test_invalid_timestamp_returns_empty(self): + self.assertEqual(_format_match_date("invalid"), "") + + +def _live_scores_page(*matches): + """Build a minimal HTML page with embedded match data (compact JSON, no spaces).""" + matches_json = ",".join( + json.dumps(m, separators=(",", ":")).replace('"', '\\"') for m in matches + ) + return f""" + + + + """ + class TestGetScores(unittest.TestCase): @patch("criclive.main.requests.get") def test_get_scores_parses_embedded_json(self, mock_get): - # Build a minimal HTML page with embedded match data (compact JSON, no spaces) - match_json = json.dumps(SAMPLE_MATCH, separators=(",", ":")).replace('"', '\\"') - html = f""" - - - - """ mock_response = MagicMock() - mock_response.text = html + mock_response.text = _live_scores_page(SAMPLE_MATCH) mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response @@ -94,6 +137,19 @@ def test_get_scores_parses_embedded_json(self, mock_get): self.assertEqual(len(scores), 1) self.assertEqual(scores[0]["first_team"]["name"], "IND") + @patch("criclive.main.requests.get") + def test_get_scores_skips_malformed_match(self, mock_get): + bad_match = copy.deepcopy(SAMPLE_MATCH) + bad_match["match"]["matchInfo"]["matchId"] = 999 + bad_match["match"]["matchScore"]["team1Score"] = None + mock_response = MagicMock() + mock_response.text = _live_scores_page(bad_match, SAMPLE_MATCH) + mock_response.raise_for_status = MagicMock() + mock_get.return_value = mock_response + + scores = get_scores() + self.assertEqual([s["match_id"] for s in scores], [12345]) + @patch("criclive.main.requests.get") def test_get_scores_empty_page(self, mock_get): mock_response = MagicMock()