blob: bb8b3356e8b182c27c91f445c747caf985da1425 [file]
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
import json
import os
import re
import sys
REPOSITORY_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
sys.path.insert(0, REPOSITORY_ROOT)
import components.cronet.tools.breakages_constants as breakages_constants # pylint: disable=wrong-import-position
import components.cronet.tools.utils as cronet_utils # pylint: disable=wrong-import-position
FILE_PATH = os.path.join(REPOSITORY_ROOT, 'components', 'cronet', 'android',
'breakages.json')
class TestBreakagesJson(unittest.TestCase):
parsed_breakages = []
@classmethod
def createFromBreakagesJson(cls):
"""Loads the JSON file once before tests run."""
if not os.path.exists(FILE_PATH):
raise FileNotFoundError(
f"CRITICAL: The file '{FILE_PATH}' could not be found.")
content = json.loads(cronet_utils.read_file(FILE_PATH))
if "breakages" not in content:
raise ValueError("CRITICAL: Root of JSON must contain a 'breakages' key.")
if not isinstance(content["breakages"], list):
raise ValueError("CRITICAL: 'breakages' key must contain a list.")
cls.parsed_breakages = content["breakages"]
def _is_valid_change_id(self, change_id):
if not isinstance(change_id, str):
return False
pattern = r'^I[0-9a-fA-F]{40}$'
zeros_pattern = r'^I00*$'
return bool(
re.fullmatch(pattern, change_id)
and not re.fullmatch(zeros_pattern, change_id))
def _format_error(self, entry, message):
entry_str = json.dumps(entry, indent=2)
return f"\n{message}\n\nFailing Entry:\n{entry_str}\n"
def test_id_validation_logic(self):
self.assertTrue(self._is_valid_change_id("I" + "a" * 40))
self.assertFalse(self._is_valid_change_id("I" + "a" * 39)) # Too short
self.assertFalse(self._is_valid_change_id("a" * 40)) # Missing 'I'
self.assertFalse(self._is_valid_change_id("I" + "z" * 40)) # Non-hex
self.assertFalse(self._is_valid_change_id("I" + "0" * 40)) # All zeros
def test_missing_mandatory_keys(self):
required_keys = [
breakages_constants.BUG_TXT, breakages_constants.BAD_CHANGE_ID_TXT,
breakages_constants.GOOD_CHANGE_IDS_TXT, breakages_constants.COMMENT_TXT
]
for entry in self.parsed_breakages:
for key in required_keys:
self.assertIn(
key, entry,
self._format_error(entry, f"Missing mandatory key: '{key}'"))
def test_unknown_keys(self):
allowed_keys = {
breakages_constants.GOOD_CHANGE_IDS_TXT,
breakages_constants.BAD_CHANGE_ID_TXT, breakages_constants.BUG_TXT,
breakages_constants.COMMENT_TXT
}
for entry in self.parsed_breakages:
for key in entry:
if key.startswith('_'):
continue # comments are allowed
self.assertIn(
key, allowed_keys,
self._format_error(
entry, f"Unknown key found: '{key}'. Allowed: {allowed_keys}"))
def test_bad_change_id_format_in_file(self):
"""Test that 'bad_change_id' entries in the file follow the regex."""
for entry in self.parsed_breakages:
val = entry[breakages_constants.BAD_CHANGE_ID_TXT]
self.assertTrue(
self._is_valid_change_id(val),
self._format_error(
entry,
f"Invalid '{breakages_constants.BAD_CHANGE_ID_TXT}' format: '{val}'"
))
def test_good_change_ids_is_list(self):
for entry in self.parsed_breakages:
val = entry[breakages_constants.GOOD_CHANGE_IDS_TXT]
self.assertIsInstance(
val, list,
self._format_error(
entry,
f"'{breakages_constants.GOOD_CHANGE_IDS_TXT}' must be a list. Found {type(val)}"
))
def test_good_change_ids_format_in_file(self):
for entry in self.parsed_breakages:
for val in entry[breakages_constants.GOOD_CHANGE_IDS_TXT]:
self.assertTrue(
self._is_valid_change_id(val),
self._format_error(
entry,
f"Invalid ID in '{breakages_constants.GOOD_CHANGE_IDS_TXT}': '{val}'"
))
if __name__ == '__main__':
unittest.main()