xref: /aosp_15_r20/tools/asuite/atest/rollout_control.py (revision c2e18aaa1096c836b086f94603d04f4eb9cf37f5)
1*c2e18aaaSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*c2e18aaaSAndroid Build Coastguard Worker# Copyright 2024, The Android Open Source Project
3*c2e18aaaSAndroid Build Coastguard Worker#
4*c2e18aaaSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
5*c2e18aaaSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
6*c2e18aaaSAndroid Build Coastguard Worker# You may obtain a copy of the License at
7*c2e18aaaSAndroid Build Coastguard Worker#
8*c2e18aaaSAndroid Build Coastguard Worker#     http://www.apache.org/licenses/LICENSE-2.0
9*c2e18aaaSAndroid Build Coastguard Worker#
10*c2e18aaaSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
11*c2e18aaaSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
12*c2e18aaaSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*c2e18aaaSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
14*c2e18aaaSAndroid Build Coastguard Worker# limitations under the License.
15*c2e18aaaSAndroid Build Coastguard Worker
16*c2e18aaaSAndroid Build Coastguard Worker"""Rollout control for Atest features."""
17*c2e18aaaSAndroid Build Coastguard Worker
18*c2e18aaaSAndroid Build Coastguard Workerimport functools
19*c2e18aaaSAndroid Build Coastguard Workerimport getpass
20*c2e18aaaSAndroid Build Coastguard Workerimport hashlib
21*c2e18aaaSAndroid Build Coastguard Workerimport importlib.resources
22*c2e18aaaSAndroid Build Coastguard Workerimport logging
23*c2e18aaaSAndroid Build Coastguard Workerimport os
24*c2e18aaaSAndroid Build Coastguard Workerfrom atest import atest_enum
25*c2e18aaaSAndroid Build Coastguard Workerfrom atest import atest_utils
26*c2e18aaaSAndroid Build Coastguard Workerfrom atest.metrics import metrics
27*c2e18aaaSAndroid Build Coastguard Worker
28*c2e18aaaSAndroid Build Coastguard Worker
29*c2e18aaaSAndroid Build Coastguard Worker@functools.cache
30*c2e18aaaSAndroid Build Coastguard Workerdef _get_project_owners() -> list[str]:
31*c2e18aaaSAndroid Build Coastguard Worker  """Returns the owners of the feature."""
32*c2e18aaaSAndroid Build Coastguard Worker  owners = []
33*c2e18aaaSAndroid Build Coastguard Worker  try:
34*c2e18aaaSAndroid Build Coastguard Worker    with importlib.resources.as_file(
35*c2e18aaaSAndroid Build Coastguard Worker        importlib.resources.files('atest').joinpath('OWNERS')
36*c2e18aaaSAndroid Build Coastguard Worker    ) as version_file_path:
37*c2e18aaaSAndroid Build Coastguard Worker      owners.extend(version_file_path.read_text(encoding='utf-8').splitlines())
38*c2e18aaaSAndroid Build Coastguard Worker  except (ModuleNotFoundError, FileNotFoundError) as e:
39*c2e18aaaSAndroid Build Coastguard Worker    logging.error(e)
40*c2e18aaaSAndroid Build Coastguard Worker  try:
41*c2e18aaaSAndroid Build Coastguard Worker    with importlib.resources.as_file(
42*c2e18aaaSAndroid Build Coastguard Worker        importlib.resources.files('atest').joinpath('OWNERS_ADTE_TEAM')
43*c2e18aaaSAndroid Build Coastguard Worker    ) as version_file_path:
44*c2e18aaaSAndroid Build Coastguard Worker      owners.extend(version_file_path.read_text(encoding='utf-8').splitlines())
45*c2e18aaaSAndroid Build Coastguard Worker  except (ModuleNotFoundError, FileNotFoundError) as e:
46*c2e18aaaSAndroid Build Coastguard Worker    logging.error(e)
47*c2e18aaaSAndroid Build Coastguard Worker  return [line.split('@')[0] for line in owners if '@google.com' in line]
48*c2e18aaaSAndroid Build Coastguard Worker
49*c2e18aaaSAndroid Build Coastguard Worker
50*c2e18aaaSAndroid Build Coastguard Workerclass RolloutControlledFeature:
51*c2e18aaaSAndroid Build Coastguard Worker  """Base class for Atest features under rollout control."""
52*c2e18aaaSAndroid Build Coastguard Worker
53*c2e18aaaSAndroid Build Coastguard Worker  def __init__(
54*c2e18aaaSAndroid Build Coastguard Worker      self,
55*c2e18aaaSAndroid Build Coastguard Worker      name: str,
56*c2e18aaaSAndroid Build Coastguard Worker      rollout_percentage: float,
57*c2e18aaaSAndroid Build Coastguard Worker      env_control_flag: str,
58*c2e18aaaSAndroid Build Coastguard Worker      feature_id: int = None,
59*c2e18aaaSAndroid Build Coastguard Worker      owners: list[str] | None = None,
60*c2e18aaaSAndroid Build Coastguard Worker      print_message: str | None = None,
61*c2e18aaaSAndroid Build Coastguard Worker  ):
62*c2e18aaaSAndroid Build Coastguard Worker    """Initializes the object.
63*c2e18aaaSAndroid Build Coastguard Worker
64*c2e18aaaSAndroid Build Coastguard Worker    Args:
65*c2e18aaaSAndroid Build Coastguard Worker        name: The name of the feature.
66*c2e18aaaSAndroid Build Coastguard Worker        rollout_percentage: The percentage of users to enable the feature for.
67*c2e18aaaSAndroid Build Coastguard Worker          The value should be in [0, 100].
68*c2e18aaaSAndroid Build Coastguard Worker        env_control_flag: The environment variable name to override the feature
69*c2e18aaaSAndroid Build Coastguard Worker          enablement. When set, 'true' or '1' means enable, other values means
70*c2e18aaaSAndroid Build Coastguard Worker          disable.
71*c2e18aaaSAndroid Build Coastguard Worker        feature_id: The ID of the feature that is controlled by rollout control
72*c2e18aaaSAndroid Build Coastguard Worker          for metric collection purpose. Must be a positive integer.
73*c2e18aaaSAndroid Build Coastguard Worker        owners: The owners of the feature. If not provided, the owners of the
74*c2e18aaaSAndroid Build Coastguard Worker          feature will be read from OWNERS file.
75*c2e18aaaSAndroid Build Coastguard Worker        print_message: The message to print to the console when the feature is
76*c2e18aaaSAndroid Build Coastguard Worker          enabled for the user.
77*c2e18aaaSAndroid Build Coastguard Worker    """
78*c2e18aaaSAndroid Build Coastguard Worker    if rollout_percentage < 0 or rollout_percentage > 100:
79*c2e18aaaSAndroid Build Coastguard Worker      raise ValueError(
80*c2e18aaaSAndroid Build Coastguard Worker          'Rollout percentage must be in [0, 100]. Got %s instead.'
81*c2e18aaaSAndroid Build Coastguard Worker          % rollout_percentage
82*c2e18aaaSAndroid Build Coastguard Worker      )
83*c2e18aaaSAndroid Build Coastguard Worker    if feature_id is not None and feature_id <= 0:
84*c2e18aaaSAndroid Build Coastguard Worker      raise ValueError(
85*c2e18aaaSAndroid Build Coastguard Worker          'Feature ID must be a positive integer. Got %s instead.' % feature_id
86*c2e18aaaSAndroid Build Coastguard Worker      )
87*c2e18aaaSAndroid Build Coastguard Worker    if owners is None:
88*c2e18aaaSAndroid Build Coastguard Worker      owners = _get_project_owners()
89*c2e18aaaSAndroid Build Coastguard Worker    self._name = name
90*c2e18aaaSAndroid Build Coastguard Worker    self._rollout_percentage = rollout_percentage
91*c2e18aaaSAndroid Build Coastguard Worker    self._env_control_flag = env_control_flag
92*c2e18aaaSAndroid Build Coastguard Worker    self._feature_id = feature_id
93*c2e18aaaSAndroid Build Coastguard Worker    self._owners = owners
94*c2e18aaaSAndroid Build Coastguard Worker    self._print_message = print_message
95*c2e18aaaSAndroid Build Coastguard Worker
96*c2e18aaaSAndroid Build Coastguard Worker  def _check_env_control_flag(self) -> bool | None:
97*c2e18aaaSAndroid Build Coastguard Worker    """Checks the environment variable to override the feature enablement.
98*c2e18aaaSAndroid Build Coastguard Worker
99*c2e18aaaSAndroid Build Coastguard Worker    Returns:
100*c2e18aaaSAndroid Build Coastguard Worker        True if the feature is enabled, False if disabled, None if not set.
101*c2e18aaaSAndroid Build Coastguard Worker    """
102*c2e18aaaSAndroid Build Coastguard Worker    if self._env_control_flag not in os.environ:
103*c2e18aaaSAndroid Build Coastguard Worker      return None
104*c2e18aaaSAndroid Build Coastguard Worker    return os.environ[self._env_control_flag] in ('TRUE', 'True', 'true', '1')
105*c2e18aaaSAndroid Build Coastguard Worker
106*c2e18aaaSAndroid Build Coastguard Worker  def _is_enabled_for_user(self, username: str | None) -> bool:
107*c2e18aaaSAndroid Build Coastguard Worker    """Checks whether the feature is enabled for the user.
108*c2e18aaaSAndroid Build Coastguard Worker
109*c2e18aaaSAndroid Build Coastguard Worker    Args:
110*c2e18aaaSAndroid Build Coastguard Worker        username: The username to check the feature enablement for. If not
111*c2e18aaaSAndroid Build Coastguard Worker          provided, the current user's username will be used.
112*c2e18aaaSAndroid Build Coastguard Worker
113*c2e18aaaSAndroid Build Coastguard Worker    Returns:
114*c2e18aaaSAndroid Build Coastguard Worker        True if the feature is enabled for the user, False otherwise.
115*c2e18aaaSAndroid Build Coastguard Worker    """
116*c2e18aaaSAndroid Build Coastguard Worker    if self._rollout_percentage == 100:
117*c2e18aaaSAndroid Build Coastguard Worker      return True
118*c2e18aaaSAndroid Build Coastguard Worker
119*c2e18aaaSAndroid Build Coastguard Worker    if username is None:
120*c2e18aaaSAndroid Build Coastguard Worker      username = getpass.getuser()
121*c2e18aaaSAndroid Build Coastguard Worker
122*c2e18aaaSAndroid Build Coastguard Worker    if not username:
123*c2e18aaaSAndroid Build Coastguard Worker      logging.debug(
124*c2e18aaaSAndroid Build Coastguard Worker          'Unable to determine the username. Disabling the feature %s.',
125*c2e18aaaSAndroid Build Coastguard Worker          self._name,
126*c2e18aaaSAndroid Build Coastguard Worker      )
127*c2e18aaaSAndroid Build Coastguard Worker      return False
128*c2e18aaaSAndroid Build Coastguard Worker
129*c2e18aaaSAndroid Build Coastguard Worker    if username in self._owners:
130*c2e18aaaSAndroid Build Coastguard Worker      return True
131*c2e18aaaSAndroid Build Coastguard Worker
132*c2e18aaaSAndroid Build Coastguard Worker    hash_object = hashlib.sha256()
133*c2e18aaaSAndroid Build Coastguard Worker    hash_object.update((username + ' ' + self._name).encode('utf-8'))
134*c2e18aaaSAndroid Build Coastguard Worker    return int(hash_object.hexdigest(), 16) % 100 < self._rollout_percentage
135*c2e18aaaSAndroid Build Coastguard Worker
136*c2e18aaaSAndroid Build Coastguard Worker  @functools.cache
137*c2e18aaaSAndroid Build Coastguard Worker  def is_enabled(self, username: str | None = None) -> bool:
138*c2e18aaaSAndroid Build Coastguard Worker    """Checks whether the current feature is enabled for the user.
139*c2e18aaaSAndroid Build Coastguard Worker
140*c2e18aaaSAndroid Build Coastguard Worker    Args:
141*c2e18aaaSAndroid Build Coastguard Worker        username: The username to check the feature enablement for. If not
142*c2e18aaaSAndroid Build Coastguard Worker          provided, the current user's username will be used.
143*c2e18aaaSAndroid Build Coastguard Worker
144*c2e18aaaSAndroid Build Coastguard Worker    Returns:
145*c2e18aaaSAndroid Build Coastguard Worker        True if the feature is enabled for the user, False otherwise.
146*c2e18aaaSAndroid Build Coastguard Worker    """
147*c2e18aaaSAndroid Build Coastguard Worker    override_flag_value = self._check_env_control_flag()
148*c2e18aaaSAndroid Build Coastguard Worker    if override_flag_value is not None:
149*c2e18aaaSAndroid Build Coastguard Worker      logging.debug(
150*c2e18aaaSAndroid Build Coastguard Worker          'Feature %s is %s by env variable %s.',
151*c2e18aaaSAndroid Build Coastguard Worker          self._name,
152*c2e18aaaSAndroid Build Coastguard Worker          'enabled' if override_flag_value else 'disabled',
153*c2e18aaaSAndroid Build Coastguard Worker          self._env_control_flag,
154*c2e18aaaSAndroid Build Coastguard Worker      )
155*c2e18aaaSAndroid Build Coastguard Worker      if self._feature_id:
156*c2e18aaaSAndroid Build Coastguard Worker        metrics.LocalDetectEvent(
157*c2e18aaaSAndroid Build Coastguard Worker            detect_type=atest_enum.DetectType.ROLLOUT_CONTROLLED_FEATURE_ID_OVERRIDE,
158*c2e18aaaSAndroid Build Coastguard Worker            result=self._feature_id
159*c2e18aaaSAndroid Build Coastguard Worker            if override_flag_value
160*c2e18aaaSAndroid Build Coastguard Worker            else -self._feature_id,
161*c2e18aaaSAndroid Build Coastguard Worker        )
162*c2e18aaaSAndroid Build Coastguard Worker      return override_flag_value
163*c2e18aaaSAndroid Build Coastguard Worker
164*c2e18aaaSAndroid Build Coastguard Worker    is_enabled = self._is_enabled_for_user(username)
165*c2e18aaaSAndroid Build Coastguard Worker
166*c2e18aaaSAndroid Build Coastguard Worker    logging.debug(
167*c2e18aaaSAndroid Build Coastguard Worker        'Feature %s is %s for user %s.',
168*c2e18aaaSAndroid Build Coastguard Worker        self._name,
169*c2e18aaaSAndroid Build Coastguard Worker        'enabled' if is_enabled else 'disabled',
170*c2e18aaaSAndroid Build Coastguard Worker        username,
171*c2e18aaaSAndroid Build Coastguard Worker    )
172*c2e18aaaSAndroid Build Coastguard Worker
173*c2e18aaaSAndroid Build Coastguard Worker    if self._feature_id:
174*c2e18aaaSAndroid Build Coastguard Worker      metrics.LocalDetectEvent(
175*c2e18aaaSAndroid Build Coastguard Worker          detect_type=atest_enum.DetectType.ROLLOUT_CONTROLLED_FEATURE_ID,
176*c2e18aaaSAndroid Build Coastguard Worker          result=self._feature_id if is_enabled else -self._feature_id,
177*c2e18aaaSAndroid Build Coastguard Worker      )
178*c2e18aaaSAndroid Build Coastguard Worker
179*c2e18aaaSAndroid Build Coastguard Worker    if is_enabled and self._print_message:
180*c2e18aaaSAndroid Build Coastguard Worker      print(atest_utils.mark_magenta(self._print_message))
181*c2e18aaaSAndroid Build Coastguard Worker
182*c2e18aaaSAndroid Build Coastguard Worker    return is_enabled
183*c2e18aaaSAndroid Build Coastguard Worker
184*c2e18aaaSAndroid Build Coastguard Worker
185*c2e18aaaSAndroid Build Coastguard Workerdeprecate_bazel_mode = RolloutControlledFeature(
186*c2e18aaaSAndroid Build Coastguard Worker    name='Deprecate Bazel Mode',
187*c2e18aaaSAndroid Build Coastguard Worker    rollout_percentage=60,
188*c2e18aaaSAndroid Build Coastguard Worker    env_control_flag='DEPRECATE_BAZEL_MODE',
189*c2e18aaaSAndroid Build Coastguard Worker    feature_id=1,
190*c2e18aaaSAndroid Build Coastguard Worker)
191*c2e18aaaSAndroid Build Coastguard Worker
192*c2e18aaaSAndroid Build Coastguard Workerrolling_tf_subprocess_output = RolloutControlledFeature(
193*c2e18aaaSAndroid Build Coastguard Worker    name='Rolling TradeFed subprocess output',
194*c2e18aaaSAndroid Build Coastguard Worker    rollout_percentage=100,
195*c2e18aaaSAndroid Build Coastguard Worker    env_control_flag='ROLLING_TF_SUBPROCESS_OUTPUT',
196*c2e18aaaSAndroid Build Coastguard Worker    feature_id=2,
197*c2e18aaaSAndroid Build Coastguard Worker    print_message=(
198*c2e18aaaSAndroid Build Coastguard Worker        'You are one of the first users receiving the "Rolling subprocess'
199*c2e18aaaSAndroid Build Coastguard Worker        ' output" feature. If you are happy with it, please +1 on'
200*c2e18aaaSAndroid Build Coastguard Worker        ' http://b/380460196.'
201*c2e18aaaSAndroid Build Coastguard Worker    ),
202*c2e18aaaSAndroid Build Coastguard Worker)
203*c2e18aaaSAndroid Build Coastguard Worker
204*c2e18aaaSAndroid Build Coastguard Workertf_preparer_incremental_setup = RolloutControlledFeature(
205*c2e18aaaSAndroid Build Coastguard Worker    name='TradeFed preparer incremental setup',
206*c2e18aaaSAndroid Build Coastguard Worker    rollout_percentage=0,
207*c2e18aaaSAndroid Build Coastguard Worker    env_control_flag='TF_PREPARER_INCREMENTAL_SETUP',
208*c2e18aaaSAndroid Build Coastguard Worker    feature_id=3,
209*c2e18aaaSAndroid Build Coastguard Worker    print_message=(
210*c2e18aaaSAndroid Build Coastguard Worker        'You are one of the first users selected to receive the "Incremental'
211*c2e18aaaSAndroid Build Coastguard Worker        ' setup for TradeFed preparers" feature. If you are happy with it,'
212*c2e18aaaSAndroid Build Coastguard Worker        ' please +1 on http://b/381900378. If you experienced any issues,'
213*c2e18aaaSAndroid Build Coastguard Worker        ' please comment on the same bug.'
214*c2e18aaaSAndroid Build Coastguard Worker    ),
215*c2e18aaaSAndroid Build Coastguard Worker)
216