Add sub-command for listing expired field trials

Bug: webrtc:14154
Change-Id: I9c5c8c4a177fb863af7e2c0ed7fa99454019cfbe
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/321183
Commit-Queue: Emil Lundmark <lndmrk@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40817}
diff --git a/experiments/field_trials.py b/experiments/field_trials.py
index eb7d7be..d69bd02 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 @@
       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.
     <BLANKLINE>
@@ -65,7 +74,7 @@
     #endif  // GEN_REGISTERED_FIELD_TRIALS_H_
     <BLANKLINE>
     """
-    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 @@
             '#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 @@
                                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: