You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/metadata/fields/custom/update_mechanism.py

178 lines
6.9 KiB
Python

#!/usr/bin/env python3
# Copyright 2025 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
from typing import Optional, Tuple
import metadata.fields.field_types as field_types
import metadata.fields.util as util
import metadata.validation_result as vr
# The regex for validating the structure of the Update Mechanism field.
UPDATE_MECHANISM_REGEX = re.compile(
r"""
^ # Start of the string.
# Group 1: Primary Mechanism (e.g., "Autoroll", "Manual", "Static").
# It matches one or more characters that are not a dot, whitespace,
([^.\s(]+) # or an opening parenthesis.
# Group 2 (optional) Secondary Mechanism: preceded by a dot (e.g., ".HardFork").
(?: # Start of a non-capturing group for the optional part.
\. # .
([^\s(]+) # Capture the secondary mechanism.
)? # Indicates 'secondary' is optional.
# Group 3 (optional): A bug link in parentheses (e.g., "(https://crbug.com/12345)").
(?: # Use a non-capturing group to catch the parentheses and whitespace.
\s* # Optional whitespace.
\( # (
([^)]+) # Capture the content inside the parentheses.
\) # )
)? # Indicates 'bug_link' is optional.
$ # End of the string.
""", re.VERBOSE)
# Regex for validating the format of the bug link and capturing the bug ID.
BUG_LINK_REGEX = re.compile(r"^https://crbug\.com/(\d+)$")
# A set of the fully-qualified, allowed mechanism values.
ALLOWED_MECHANISMS = {
"Autoroll",
"Manual",
"Static",
"Static.HardFork",
}
def parse_update_mechanism(
value: str) -> Tuple[str, Optional[str], Optional[str]]:
"""
Parses the Update Mechanism field value using a regular expression.
Values are expected to be in the form Primary.Secondary (bug link)
Args:
value: The string value of the Update Mechanism field.
Returns:
A tuple (primary, secondary, bug_link).
If the structure is invalid, all elements of the tuple are None.
"""
match = UPDATE_MECHANISM_REGEX.match(value.strip())
if not match:
return None, None, None
return match.groups()
class UpdateMechanismField(field_types.SingleLineTextField):
"""
Field for 'Update Mechanism: <Value>'.
The format is Primary[.SubsetSpecifier] [(crbug.com/BUG_ID)].
"""
def __init__(self):
super().__init__(name="Update Mechanism")
def validate(self, value: str) -> Optional[vr.ValidationResult]:
"""
Checks if the value is a valid Update Mechanism entry, including the
logic for when a bug link is required or disallowed.
"""
if util.is_empty(value):
return vr.ValidationError(
reason=f"{self._name} field cannot be empty.",
additional=[
f"Must be one of {util.quoted(sorted(ALLOWED_MECHANISMS))}.",
"Example: 'Autoroll' or 'Manual (https://crbug.com/12345)'"
])
primary, secondary, bug_link = parse_update_mechanism(value)
# First, check if the value matches the general format.
if primary is None:
return vr.ValidationError(
reason=f"Invalid format for {self._name} field.",
additional=[
"Expected format: Mechanism[.SubMechanism] [(bug)]",
f"Allowed mechanisms: {util.quoted(sorted(ALLOWED_MECHANISMS))}.",
"Example: 'Static.HardFork (https://crbug.com/12345)'",
])
mechanism = primary
if secondary:
mechanism += f".{secondary}"
# Second, check if the mechanism is a known, allowed value.
if mechanism not in ALLOWED_MECHANISMS:
return vr.ValidationError(
reason=f"{self._name} has invalid mechanism '{mechanism}'.",
additional=[
f"Must be one of {util.quoted(sorted(ALLOWED_MECHANISMS))}.",
])
# If it's not Autorolled, it SHOULD have a bug link.
# Only warn for Static, for now.
elif primary == "Static" and bug_link is None:
return vr.ValidationWarning(
reason="{self._name} has no link to autoroll exception.",
additional=[
"Please add a link if an exception bug has been filed.",
f"Example: '{mechanism} (https://crbug.com/12345)'"
])
# Autoroll must not have a bug link.
if primary == "Autoroll" and bug_link:
return vr.ValidationError(
reason="Autoroll does not permit an autoroll exception.",
additional=[
f"Please remove the unnecessary bug link {bug_link}.",
"If this bug is still relevant then maybe Autoroll isn't the right choice",
"You could move it to the description.",
])
# Validate the bug link format if present.
if bug_link:
bug_num = bug_link.split("/")[-1]
canonical_bug_link = f"https://crbug.com/{bug_num}"
if not BUG_LINK_REGEX.match(bug_link):
# If it ends in a '/number', then provide a copy pastable correct value.
if bug_num.isdigit():
return vr.ValidationError(
reason=f"{self._name} bug link should be `({canonical_bug_link})`.",
additional=[
f"{bug_link} is not a valid crbug link."
])
# Does not match the expected crbug link format at all.
return vr.ValidationError(
reason=f"{self._name} has invalid bug link format '{bug_link}'.",
additional=[
"Bug links must be of the form (https://crbug.com/12345).",
f"Example: '{mechanism} (https://crbug.com/12345)'",
])
return None
def narrow_type(self,
value: str) -> Tuple[str, Optional[str], Optional[str]]:
"""
Parses the field value into its components if it is valid.
Returns:
A tuple of (primary, secondary, bug_link) if valid, otherwise None, None, None.
"""
validation = self.validate(value)
if validation and validation.is_fatal():
# It cannot be narrowed if there is a fatal error.
return None, None, None
primary, secondary, bug_link = parse_update_mechanism(value)
# If a bug link is present, parse it into canonical format.
if bug_link:
bug_num = bug_link.split("/")[-1]
bug_link = f"https://crbug.com/{bug_num}"
return primary, secondary, bug_link