diff --git a/experiments/field_trials.py b/experiments/field_trials.py index eb7d7bebd3..d69bd02ca0 100755 --- a/experiments/field_trials.py +++ b/experiments/field_trials.py @@ -8,28 +8,33 @@ # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. +import datetime +from datetime import date import sys -from typing import FrozenSet +from typing import FrozenSet, Set import argparse import dataclasses -# TODO(bugs.webrtc.org/14154): End date and bug should also be stored. @dataclasses.dataclass(frozen=True) class FieldTrial: """Representation of all attributes associated with a field trial. Attributes: key: Field trial key. + bug: Associated open bug containing more context. + end_date: Date when the field trial expires and must be deleted. """ key: str + bug: str + end_date: date # As per the policy in `g3doc/field-trials.md`, all field trials should be # registered in the container below. Please keep the keys sorted. REGISTERED_FIELD_TRIALS: FrozenSet[FieldTrial] = frozenset([ - FieldTrial(''), # TODO(bugs.webrtc.org/14154): Populate + FieldTrial('', '', date(1, 1, 1)), # TODO(bugs.webrtc.org/14154): Populate ]) @@ -44,7 +49,11 @@ def registry_header( String representation of a C++ header file containing all field trial keys. - >>> trials = {FieldTrial('B'), FieldTrial('A'), FieldTrial('B')} + >>> trials = { + ... FieldTrial('B', '', date(1, 1, 1)), + ... FieldTrial('A', '', date(1, 1, 1)), + ... FieldTrial('B', '', date(2, 2, 2)), + ... } >>> print(registry_header(trials)) // This file was automatically generated. Do not edit. @@ -65,7 +74,7 @@ def registry_header( #endif // GEN_REGISTERED_FIELD_TRIALS_H_ """ - registered_keys = [f.key for f in field_trials] + registered_keys = {f.key for f in field_trials} keys = '\n'.join(f' "{k}",' for k in sorted(registered_keys)) return ('// This file was automatically generated. Do not edit.\n' '\n' @@ -85,13 +94,54 @@ def registry_header( '#endif // GEN_REGISTERED_FIELD_TRIALS_H_\n') +def expired_field_trials( + threshold: date, + field_trials: FrozenSet[FieldTrial] = REGISTERED_FIELD_TRIALS +) -> Set[FieldTrial]: + """Obtains expired field trials. + + Args: + threshold: Date from which to check end date. + field_trials: Field trials to validate. + + Returns: + All expired field trials. + + >>> trials = { + ... FieldTrial('Expired', '', date(1, 1, 1)), + ... FieldTrial('Not-Expired', '', date(1, 1, 2)), + ... } + >>> expired_field_trials(date(1, 1, 1), trials) + {FieldTrial(key='Expired', bug='', end_date=datetime.date(1, 1, 1))} + """ + return {f for f in field_trials if f.end_date <= threshold} + + def cmd_header(args: argparse.Namespace) -> None: args.output.write(registry_header()) +def cmd_expired(args: argparse.Namespace) -> None: + now = datetime.datetime.now(datetime.timezone.utc) + today = date(now.year, now.month, now.day) + diff = datetime.timedelta(days=args.in_days) + expired = expired_field_trials(today + diff) + + if len(expired) <= 0: + return + + expired_by_date = sorted([(f.end_date, f.key) for f in expired]) + print('\n'.join( + f'{key} {"expired" if date <= today else "expires"} on {date}' + for date, key in expired_by_date)) + if any(date <= today for date, _ in expired_by_date): + sys.exit(1) + + def main() -> None: parser = argparse.ArgumentParser() subcommand = parser.add_subparsers(dest='cmd') + parser_header = subcommand.add_parser( 'header', help='generate C++ header file containing registered field trial keys') @@ -101,6 +151,22 @@ def main() -> None: required=False, help='output file') parser_header.set_defaults(cmd=cmd_header) + + parser_expired = subcommand.add_parser( + 'expired', + help='lists all expired field trials', + description=''' + Lists all expired field trials. Exits with a non-zero exit status if + any field trials has expired, ignoring the --in-days argument. + ''') + parser_expired.add_argument( + '--in-days', + default=0, + type=int, + required=False, + help='number of days relative to today to check') + parser_expired.set_defaults(cmd=cmd_expired) + args = parser.parse_args() if not args.cmd: