mirror of
https://github.com/mongodb/mongo.git
synced 2024-11-21 12:39:08 +01:00
SERVER-93515 Apply new VP and Overall quotas for the code lockdown (#26106)
GitOrigin-RevId: 56309603a6e1d87c9caa8b78b3208104d9e76866
This commit is contained in:
parent
db80281e66
commit
cfa71f3776
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Optional, Set
|
||||
|
||||
@ -14,9 +15,9 @@ class BfCategory(str, Enum):
|
||||
|
||||
|
||||
class CategorizedBFs(NamedTuple):
|
||||
hot_bfs: Set[str]
|
||||
cold_bfs: Set[str]
|
||||
perf_bfs: Set[str]
|
||||
hot_bfs: Set[BfIssue]
|
||||
cold_bfs: Set[BfIssue]
|
||||
perf_bfs: Set[BfIssue]
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> CategorizedBFs:
|
||||
@ -36,13 +37,13 @@ class CategorizedBFs(NamedTuple):
|
||||
test_type = TestType.from_evg_project_name(evg_project)
|
||||
|
||||
if test_type == TestType.PERFORMANCE:
|
||||
self.perf_bfs.add(bf.key)
|
||||
self.perf_bfs.add(bf)
|
||||
|
||||
if test_type == TestType.CORRECTNESS:
|
||||
if bf.temperature == BfTemperature.HOT:
|
||||
self.hot_bfs.add(bf.key)
|
||||
self.hot_bfs.add(bf)
|
||||
if bf.temperature in [BfTemperature.COLD, BfTemperature.NONE]:
|
||||
self.cold_bfs.add(bf.key)
|
||||
self.cold_bfs.add(bf)
|
||||
|
||||
def add(self, more_bfs: CategorizedBFs) -> None:
|
||||
"""
|
||||
@ -76,43 +77,38 @@ class BFsReport(NamedTuple):
|
||||
def get_bf_count(
|
||||
self,
|
||||
bf_category: BfCategory,
|
||||
assigned_team: Optional[str] = None,
|
||||
include_bfs_older_than_time: Optional[datetime] = None,
|
||||
assigned_teams: Optional[List[str]] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Calculate BFs count for a given criteria.
|
||||
|
||||
:param bf_category: BF category (hot, cold, perf).
|
||||
:param assigned_team: Assigned team criterion, all teams if None.
|
||||
:param include_bfs_older_than_time: Count BFs that have created date older than provided time.
|
||||
:param assigned_teams: List of Assigned teams criterion, all teams if None.
|
||||
:return: BFs count.
|
||||
"""
|
||||
total_bf_count = 0
|
||||
|
||||
if include_bfs_older_than_time is None:
|
||||
include_bfs_older_than_time = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
|
||||
team_reports = self.team_reports.values()
|
||||
if assigned_team is not None:
|
||||
team_reports = [self.team_reports[assigned_team]]
|
||||
if assigned_teams is not None:
|
||||
team_reports = [
|
||||
self.team_reports.get(team, CategorizedBFs.empty()) for team in assigned_teams
|
||||
]
|
||||
|
||||
for team_report in team_reports:
|
||||
bfs = set()
|
||||
if bf_category == BfCategory.HOT:
|
||||
total_bf_count += len(team_report.hot_bfs)
|
||||
bfs = team_report.hot_bfs
|
||||
if bf_category == BfCategory.COLD:
|
||||
total_bf_count += len(team_report.cold_bfs)
|
||||
bfs = team_report.cold_bfs
|
||||
if bf_category == BfCategory.PERF:
|
||||
total_bf_count += len(team_report.perf_bfs)
|
||||
bfs = team_report.perf_bfs
|
||||
total_bf_count += len(
|
||||
[bf for bf in bfs if bf.created_time < include_bfs_older_than_time]
|
||||
)
|
||||
|
||||
return total_bf_count
|
||||
|
||||
def combine_teams(self, team_names: List[str], group_name: str) -> None:
|
||||
"""
|
||||
Mutate `self.team_reports` by removing `team_names` entries and combining their BFs under `group_name`.
|
||||
|
||||
:param team_names: Names of teams to combine BFs.
|
||||
:param group_name: Group name to combine BFs under.
|
||||
"""
|
||||
group_bfs = CategorizedBFs.empty()
|
||||
|
||||
for team in team_names:
|
||||
team_bfs = self.team_reports.pop(team, None)
|
||||
if team_bfs is not None:
|
||||
group_bfs.add(team_bfs)
|
||||
|
||||
self.team_reports[group_name] = group_bfs
|
||||
|
@ -43,12 +43,8 @@ JIRA_SERVER = "https://jira.mongodb.org"
|
||||
DEFAULT_REPO = "10gen/mongo"
|
||||
DEFAULT_BRANCH = "master"
|
||||
SLACK_CHANNEL = "#10gen-mongo-code-lockdown"
|
||||
FRIDAY_INDEX = 4 # datetime.weekday() returns Monday as 0 and Sunday as 6
|
||||
EVERGREEN_LOOKBACK_DAYS = 14
|
||||
|
||||
GREEN_THRESHOLD_PERCENTS = 100
|
||||
RED_THRESHOLD_PERCENTS = 200
|
||||
|
||||
|
||||
def iterable_to_jql(entries: Iterable[str]) -> str:
|
||||
return ", ".join(f'"{entry}"' for entry in entries)
|
||||
@ -66,57 +62,25 @@ ACTIVE_BFS_QUERY = (
|
||||
)
|
||||
|
||||
|
||||
class ExitCode(Enum):
|
||||
SUCCESS = 0
|
||||
PREVIOUS_BUILD_STATUS_UNKNOWN = 1
|
||||
|
||||
|
||||
class BuildStatus(Enum):
|
||||
class CodeMergeStatus(Enum):
|
||||
RED = "RED"
|
||||
YELLOW = "YELLOW"
|
||||
GREEN = "GREEN"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, status: Optional[str]) -> BuildStatus:
|
||||
try:
|
||||
return cls[status]
|
||||
except KeyError:
|
||||
return cls.UNKNOWN
|
||||
|
||||
@classmethod
|
||||
def from_threshold_percentages(cls, threshold_percentages: List[float]) -> BuildStatus:
|
||||
if any(percentage > RED_THRESHOLD_PERCENTS for percentage in threshold_percentages):
|
||||
def from_threshold_percentages(cls, threshold_percentages: List[float]) -> CodeMergeStatus:
|
||||
if any(percentage > 100 for percentage in threshold_percentages):
|
||||
return cls.RED
|
||||
if all(percentage < GREEN_THRESHOLD_PERCENTS for percentage in threshold_percentages):
|
||||
return cls.GREEN
|
||||
return cls.YELLOW
|
||||
return cls.GREEN
|
||||
|
||||
|
||||
class SummaryMsg(Enum):
|
||||
TITLE = "`[SUMMARY]`"
|
||||
PREFIX = "`[SUMMARY]`"
|
||||
|
||||
BELOW_THRESHOLDS = f"All metrics are within {GREEN_THRESHOLD_PERCENTS}% of their thresholds."
|
||||
BELOW_THRESHOLDS = "All metrics are within 100% of their thresholds.\nAll merges are allowed."
|
||||
THRESHOLD_EXCEEDED = (
|
||||
f"At least one metric exceeds {GREEN_THRESHOLD_PERCENTS}% of its threshold."
|
||||
"At least one metric exceeds 100% of its threshold.\n"
|
||||
"Approve only changes that fix BFs, Bugs, and Performance Regressions in the following scopes:"
|
||||
)
|
||||
THRESHOLD_EXCEEDED_X2 = (
|
||||
f"At least one metric exceeds {RED_THRESHOLD_PERCENTS}% of its threshold."
|
||||
)
|
||||
|
||||
STATUS_CHANGED = "<!here> Build status has changed to `{status}`."
|
||||
STATUS_IS = "Build status is `{status}`."
|
||||
|
||||
ENTER_RED = "We are entering the code block state."
|
||||
STILL_RED = "We are still in the code block state."
|
||||
EXIT_RED = "We are exiting the code block state."
|
||||
|
||||
ACTION_ON_RED = "Approve only changes that fix BFs, Bugs, and Performance Regressions."
|
||||
ACTION_ON_YELLOW = (
|
||||
"Warning: all merges are still allowed, but now is a good time to get BFs, Bugs, and"
|
||||
" Performance Regressions under control to avoid a blocking state."
|
||||
)
|
||||
ACTION_ON_GREEN = "All merges are allowed."
|
||||
|
||||
PLAYBOOK_REFERENCE = f"Refer to our playbook at <{BLOCK_ON_RED_PLAYBOOK_URL}> for details."
|
||||
|
||||
@ -132,13 +96,9 @@ class MonitorBuildStatusOrchestrator:
|
||||
self.evg_service = evg_service
|
||||
self.code_lockdown_config = code_lockdown_config
|
||||
|
||||
def evaluate_build_redness(
|
||||
self, repo: str, branch: str, notify: bool, input_status_file: str, output_status_file: str
|
||||
) -> ExitCode:
|
||||
exit_code = ExitCode.SUCCESS
|
||||
|
||||
def evaluate_build_redness(self, repo: str, branch: str, notify: bool) -> None:
|
||||
status_message = f"\n`[STATUS]` '{repo}' repo '{branch}' branch"
|
||||
threshold_percentages = []
|
||||
scope_percentages: Dict[str, List[float]] = {}
|
||||
|
||||
LOGGER.info("Getting Evergreen projects data")
|
||||
evg_projects_info = self.evg_service.get_evg_project_info(repo, branch)
|
||||
@ -150,7 +110,7 @@ class MonitorBuildStatusOrchestrator:
|
||||
bfs_report, self.code_lockdown_config
|
||||
)
|
||||
status_message = f"{status_message}\n{bf_count_status_msg}\n"
|
||||
threshold_percentages.extend(bf_count_percentages)
|
||||
scope_percentages.update(bf_count_percentages)
|
||||
|
||||
# We are looking for Evergreen versions that started before the beginning of yesterday
|
||||
# to give them time to complete
|
||||
@ -167,48 +127,7 @@ class MonitorBuildStatusOrchestrator:
|
||||
)
|
||||
status_message = f"{status_message}\n{waterfall_failure_rate_status_msg}\n"
|
||||
|
||||
previous_build_status = BuildStatus.UNKNOWN
|
||||
if os.path.exists(input_status_file):
|
||||
with open(input_status_file, "r") as input_file:
|
||||
input_file_content = input_file.read().strip()
|
||||
LOGGER.info(
|
||||
"Input status file content",
|
||||
file=input_status_file,
|
||||
content=input_file_content,
|
||||
)
|
||||
previous_build_status = BuildStatus.from_str(input_file_content)
|
||||
if previous_build_status == BuildStatus.UNKNOWN:
|
||||
LOGGER.error(
|
||||
"Could not parse previous build status from the input build status file,"
|
||||
" the file was corrupted or contained unexpected string"
|
||||
)
|
||||
else:
|
||||
LOGGER.warning("Input status file is absent", file=input_status_file)
|
||||
LOGGER.warning(
|
||||
"The absence of input build status file is expected if the task is running for"
|
||||
" the first time or for the first time after the file storage location is changed"
|
||||
)
|
||||
|
||||
if previous_build_status == BuildStatus.UNKNOWN:
|
||||
LOGGER.warning(
|
||||
"We were not able to get the previous build status, which could lead to build status"
|
||||
" being changed from RED to YELLOW, which should not happen, and/or summary messages"
|
||||
" could be somewhat off"
|
||||
)
|
||||
LOGGER.warning(
|
||||
"If we are in a YELLOW condition, there's different behavior to communicate if that"
|
||||
" was previously GREEN (things are worse, but not RED yet), YELLOW (no change), or"
|
||||
" RED (still RED but improving)"
|
||||
)
|
||||
LOGGER.warning(
|
||||
"The state will still be reported, but may lose accuracy on specific actions to take"
|
||||
)
|
||||
exit_code = ExitCode.PREVIOUS_BUILD_STATUS_UNKNOWN
|
||||
|
||||
current_build_status = BuildStatus.from_threshold_percentages(threshold_percentages)
|
||||
resulting_build_status, summary = self._summarize(
|
||||
previous_build_status, current_build_status, self._today_is_friday()
|
||||
)
|
||||
summary = self._summarize(scope_percentages)
|
||||
status_message = f"{status_message}\n{summary}"
|
||||
|
||||
for line in status_message.split("\n"):
|
||||
@ -221,11 +140,6 @@ class MonitorBuildStatusOrchestrator:
|
||||
msg=status_message.strip(),
|
||||
)
|
||||
|
||||
with open(output_status_file, "w") as output_file:
|
||||
output_file.write(resulting_build_status.value)
|
||||
|
||||
return exit_code
|
||||
|
||||
def _make_bfs_report(self, evg_projects_info: EvgProjectsInfo) -> BFsReport:
|
||||
query = (
|
||||
f'{ACTIVE_BFS_QUERY} AND "{JiraCustomFieldNames.EVERGREEN_PROJECT}" in'
|
||||
@ -245,17 +159,16 @@ class MonitorBuildStatusOrchestrator:
|
||||
@staticmethod
|
||||
def _get_bf_counts_status(
|
||||
bfs_report: BFsReport, code_lockdown_config: CodeLockdownConfig
|
||||
) -> Tuple[str, List[float]]:
|
||||
for group in code_lockdown_config.team_groups or []:
|
||||
bfs_report.combine_teams(group.teams, group.name)
|
||||
) -> Tuple[str, Dict[str, List[float]]]:
|
||||
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
scope_percentages: Dict[str, List[float]] = {}
|
||||
|
||||
percentages = []
|
||||
status_message = "`[STATUS]` The current BF count"
|
||||
headers = ["Assigned Team/Group", "Hot BFs", "Cold BFs", "Perf BFs"]
|
||||
headers = ["Scope", "Hot BFs", "Cold BFs", "Perf BFs"]
|
||||
table_data = []
|
||||
|
||||
def _process_thresholds(
|
||||
label: str,
|
||||
scope: str,
|
||||
hot_bf_count: int,
|
||||
cold_bf_count: int,
|
||||
perf_bf_count: int,
|
||||
@ -268,40 +181,60 @@ class MonitorBuildStatusOrchestrator:
|
||||
cold_bf_percentage = cold_bf_count / thresholds.cold_bf_count * 100
|
||||
perf_bf_percentage = perf_bf_count / thresholds.perf_bf_count * 100
|
||||
|
||||
percentages.extend([hot_bf_percentage, cold_bf_percentage, perf_bf_percentage])
|
||||
scope_percentages[scope] = [hot_bf_percentage, cold_bf_percentage, perf_bf_percentage]
|
||||
|
||||
table_data.append(
|
||||
[
|
||||
label,
|
||||
scope,
|
||||
f"{hot_bf_percentage:.0f}% ({hot_bf_count} / {thresholds.hot_bf_count})",
|
||||
f"{cold_bf_percentage:.0f}% ({cold_bf_count} / {thresholds.cold_bf_count})",
|
||||
f"{perf_bf_percentage:.0f}% ({perf_bf_count} / {thresholds.perf_bf_count})",
|
||||
]
|
||||
)
|
||||
|
||||
for assigned_team in sorted(list(bfs_report.team_reports.keys())):
|
||||
_process_thresholds(
|
||||
assigned_team,
|
||||
bfs_report.get_bf_count(BfCategory.HOT, assigned_team),
|
||||
bfs_report.get_bf_count(BfCategory.COLD, assigned_team),
|
||||
bfs_report.get_bf_count(BfCategory.PERF, assigned_team),
|
||||
code_lockdown_config.get_thresholds(assigned_team),
|
||||
)
|
||||
|
||||
overall_older_than_time = now - timedelta(
|
||||
hours=code_lockdown_config.overall_thresholds.include_bfs_older_than_hours
|
||||
)
|
||||
_process_thresholds(
|
||||
"Overall",
|
||||
bfs_report.get_bf_count(BfCategory.HOT),
|
||||
bfs_report.get_bf_count(BfCategory.COLD),
|
||||
bfs_report.get_bf_count(BfCategory.PERF),
|
||||
"[Org] Overall",
|
||||
bfs_report.get_bf_count(BfCategory.HOT, overall_older_than_time),
|
||||
bfs_report.get_bf_count(BfCategory.COLD, overall_older_than_time),
|
||||
bfs_report.get_bf_count(BfCategory.PERF, overall_older_than_time),
|
||||
code_lockdown_config.overall_thresholds,
|
||||
)
|
||||
|
||||
for group in code_lockdown_config.team_groups or []:
|
||||
group_thresholds = code_lockdown_config.get_thresholds(group.name)
|
||||
group_older_than_time = now - timedelta(
|
||||
hours=group_thresholds.include_bfs_older_than_hours
|
||||
)
|
||||
_process_thresholds(
|
||||
f"[Group] {group.name}",
|
||||
bfs_report.get_bf_count(BfCategory.HOT, group_older_than_time, group.teams),
|
||||
bfs_report.get_bf_count(BfCategory.COLD, group_older_than_time, group.teams),
|
||||
bfs_report.get_bf_count(BfCategory.PERF, group_older_than_time, group.teams),
|
||||
group_thresholds,
|
||||
)
|
||||
|
||||
for assigned_team in sorted(list(bfs_report.team_reports.keys())):
|
||||
team_thresholds = code_lockdown_config.get_thresholds(assigned_team)
|
||||
team_older_than_time = now - timedelta(
|
||||
hours=team_thresholds.include_bfs_older_than_hours
|
||||
)
|
||||
_process_thresholds(
|
||||
f"[Team] {assigned_team}",
|
||||
bfs_report.get_bf_count(BfCategory.HOT, team_older_than_time, [assigned_team]),
|
||||
bfs_report.get_bf_count(BfCategory.COLD, team_older_than_time, [assigned_team]),
|
||||
bfs_report.get_bf_count(BfCategory.PERF, team_older_than_time, [assigned_team]),
|
||||
team_thresholds,
|
||||
)
|
||||
|
||||
table_str = tabulate(
|
||||
table_data, headers, tablefmt="outline", colalign=("left", "right", "right", "right")
|
||||
)
|
||||
status_message = f"{status_message}\n```\n{table_str}\n```"
|
||||
|
||||
return status_message, percentages
|
||||
return status_message, scope_percentages
|
||||
|
||||
def _make_waterfall_report(
|
||||
self, evg_project_names: List[str], window_end: datetime
|
||||
@ -376,64 +309,25 @@ class MonitorBuildStatusOrchestrator:
|
||||
return status_message
|
||||
|
||||
@staticmethod
|
||||
def _summarize(
|
||||
previous_status: BuildStatus, current_status: BuildStatus, today_is_friday: bool
|
||||
) -> Tuple[BuildStatus, str]:
|
||||
resulting_status = previous_status
|
||||
if current_status == BuildStatus.RED and previous_status != BuildStatus.RED:
|
||||
if today_is_friday:
|
||||
resulting_status = BuildStatus.RED
|
||||
else:
|
||||
resulting_status = BuildStatus.YELLOW
|
||||
if current_status == BuildStatus.YELLOW and previous_status != BuildStatus.RED:
|
||||
resulting_status = BuildStatus.YELLOW
|
||||
if current_status == BuildStatus.GREEN:
|
||||
resulting_status = BuildStatus.GREEN
|
||||
def _summarize(scope_percentages: Dict[str, List[float]]) -> str:
|
||||
summary = SummaryMsg.PREFIX.value
|
||||
|
||||
summary = SummaryMsg.TITLE.value
|
||||
red_scopes = []
|
||||
for scope, percentages in scope_percentages.items():
|
||||
status = CodeMergeStatus.from_threshold_percentages(percentages)
|
||||
if status == CodeMergeStatus.RED:
|
||||
red_scopes.append(scope)
|
||||
|
||||
if resulting_status != previous_status:
|
||||
status_msg = SummaryMsg.STATUS_CHANGED.value.format(status=resulting_status.value)
|
||||
if len(red_scopes) == 0:
|
||||
summary = f"{summary} {SummaryMsg.BELOW_THRESHOLDS.value}"
|
||||
else:
|
||||
status_msg = SummaryMsg.STATUS_IS.value.format(status=resulting_status.value)
|
||||
summary = f"{summary}\n{status_msg}"
|
||||
|
||||
if current_status == BuildStatus.RED:
|
||||
summary = f"{summary}\n{SummaryMsg.THRESHOLD_EXCEEDED_X2.value}"
|
||||
if current_status == BuildStatus.YELLOW:
|
||||
summary = f"{summary}\n{SummaryMsg.THRESHOLD_EXCEEDED.value}"
|
||||
if current_status == BuildStatus.GREEN:
|
||||
summary = f"{summary}\n{SummaryMsg.BELOW_THRESHOLDS.value}"
|
||||
|
||||
if previous_status != BuildStatus.RED and resulting_status == BuildStatus.RED:
|
||||
summary = f"{summary}\n{SummaryMsg.ENTER_RED.value}"
|
||||
if previous_status == BuildStatus.RED and resulting_status == BuildStatus.RED:
|
||||
summary = f"{summary}\n{SummaryMsg.STILL_RED.value}"
|
||||
if previous_status == BuildStatus.RED and resulting_status != BuildStatus.RED:
|
||||
summary = f"{summary}\n{SummaryMsg.EXIT_RED.value}"
|
||||
|
||||
if resulting_status == BuildStatus.RED:
|
||||
summary = f"{summary}\n{SummaryMsg.ACTION_ON_RED.value}"
|
||||
if resulting_status == BuildStatus.YELLOW:
|
||||
summary = f"{summary}\n{SummaryMsg.ACTION_ON_YELLOW.value}"
|
||||
if resulting_status == BuildStatus.GREEN:
|
||||
summary = f"{summary}\n{SummaryMsg.ACTION_ON_GREEN.value}"
|
||||
summary = f"{summary} {SummaryMsg.THRESHOLD_EXCEEDED.value}"
|
||||
for scope in red_scopes:
|
||||
summary = f"{summary}\n\t- {scope}"
|
||||
|
||||
summary = f"{summary}\n\n{SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
|
||||
LOGGER.info(
|
||||
"Got build statuses",
|
||||
previous_build_status=previous_status.value,
|
||||
current_build_status=current_status.value,
|
||||
resulting_build_status=resulting_status.value,
|
||||
today_is_friday=today_is_friday,
|
||||
)
|
||||
|
||||
return resulting_status, summary
|
||||
|
||||
@staticmethod
|
||||
def _today_is_friday() -> bool:
|
||||
return datetime.utcnow().weekday() == FRIDAY_INDEX
|
||||
return summary
|
||||
|
||||
|
||||
def main(
|
||||
@ -446,12 +340,6 @@ def main(
|
||||
notify: Annotated[
|
||||
bool, typer.Option(help="Whether to send slack notification with the results")
|
||||
] = False, # default to the more "quiet" setting
|
||||
input_status_file: Annotated[
|
||||
str, typer.Option(help="The file that contains a previous build status")
|
||||
] = "input_build_status_file.txt",
|
||||
output_status_file: Annotated[
|
||||
str, typer.Option(help="The file that contains the current build status")
|
||||
] = "output_build_status_file.txt",
|
||||
) -> None:
|
||||
"""
|
||||
Analyze Jira BFs count and Evergreen redness data.
|
||||
@ -484,10 +372,7 @@ def main(
|
||||
code_lockdown_config=code_lockdown_config,
|
||||
)
|
||||
|
||||
exit_code = orchestrator.evaluate_build_redness(
|
||||
github_repo, branch, notify, input_status_file, output_status_file
|
||||
)
|
||||
sys.exit(exit_code.value)
|
||||
orchestrator.evaluate_build_redness(github_repo, branch, notify)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -10,6 +10,7 @@ class BfCountThresholds(BaseModel):
|
||||
hot_bf_count: int
|
||||
cold_bf_count: int
|
||||
perf_bf_count: int
|
||||
include_bfs_older_than_hours: int
|
||||
|
||||
|
||||
class TeamGroupConfig(BaseModel):
|
||||
@ -38,8 +39,8 @@ class CodeLockdownConfig(BaseModel):
|
||||
"""
|
||||
Get group or default team thresholds.
|
||||
|
||||
:param group_name: The name of the group or team.
|
||||
:return: Group or default team thresholds.
|
||||
:param group_name: The name of the group.
|
||||
:return: Group thresholds or default team thresholds.
|
||||
"""
|
||||
for group in self.team_groups or []:
|
||||
if group.name == group_name:
|
||||
|
@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List, NamedTuple, Optional
|
||||
from typing import Any, List, NamedTuple, Optional
|
||||
|
||||
import dateutil.parser
|
||||
from jira import Issue
|
||||
|
||||
from buildscripts.client.jiraclient import JiraClient
|
||||
@ -58,6 +60,7 @@ class BfIssue(NamedTuple):
|
||||
assigned_team: str
|
||||
evergreen_projects: List[str]
|
||||
temperature: BfTemperature
|
||||
created_time: datetime
|
||||
|
||||
@classmethod
|
||||
def from_jira_issue(cls, issue: Issue) -> BfIssue:
|
||||
@ -88,8 +91,15 @@ class BfIssue(NamedTuple):
|
||||
assigned_team=assigned_team,
|
||||
evergreen_projects=evergreen_projects,
|
||||
temperature=temperature,
|
||||
created_time=dateutil.parser.parse(issue.fields.created),
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, self.__class__) and self.key == other.key
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.key)
|
||||
|
||||
|
||||
class JiraService:
|
||||
def __init__(self, jira_client: JiraClient) -> None:
|
||||
|
@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
import buildscripts.monitor_build_status.bfs_report as under_test
|
||||
|
||||
@ -24,12 +25,13 @@ class TestBFsReport(unittest.TestCase):
|
||||
assigned_team="team-1",
|
||||
evergreen_projects=["mongodb-mongo-master", "mongodb-mongo-master-nightly"],
|
||||
temperature=under_test.BfTemperature.HOT,
|
||||
created_time=datetime.now(),
|
||||
)
|
||||
bfs_report = under_test.BFsReport.empty()
|
||||
|
||||
bfs_report.add_bf_data(bf_1, evg_projects_info)
|
||||
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].hot_bfs, {"BF-1"})
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].hot_bfs, {bf_1})
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].cold_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].perf_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
@ -40,12 +42,13 @@ class TestBFsReport(unittest.TestCase):
|
||||
assigned_team="team-1",
|
||||
evergreen_projects=["mongodb-mongo-master"],
|
||||
temperature=under_test.BfTemperature.HOT,
|
||||
created_time=datetime.now(),
|
||||
)
|
||||
bfs_report = under_test.BFsReport.empty()
|
||||
|
||||
bfs_report.add_bf_data(bf_1, evg_projects_info)
|
||||
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].hot_bfs, {"BF-1"})
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].hot_bfs, {bf_1})
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].cold_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].perf_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
@ -56,13 +59,14 @@ class TestBFsReport(unittest.TestCase):
|
||||
assigned_team="team-1",
|
||||
evergreen_projects=["mongodb-mongo-master"],
|
||||
temperature=under_test.BfTemperature.COLD,
|
||||
created_time=datetime.now(),
|
||||
)
|
||||
bfs_report = under_test.BFsReport.empty()
|
||||
|
||||
bfs_report.add_bf_data(bf_1, evg_projects_info)
|
||||
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].hot_bfs), 0)
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].cold_bfs, {"BF-1"})
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].cold_bfs, {bf_1})
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].perf_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
|
||||
@ -72,6 +76,7 @@ class TestBFsReport(unittest.TestCase):
|
||||
assigned_team="team-1",
|
||||
evergreen_projects=["sys-perf"],
|
||||
temperature=under_test.BfTemperature.HOT,
|
||||
created_time=datetime.now(),
|
||||
)
|
||||
bfs_report = under_test.BFsReport.empty()
|
||||
|
||||
@ -79,7 +84,7 @@ class TestBFsReport(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].hot_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].cold_bfs), 0)
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].perf_bfs, {"BF-1"})
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].perf_bfs, {bf_1})
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
|
||||
def test_add_cold_perf_bf(self):
|
||||
@ -88,6 +93,7 @@ class TestBFsReport(unittest.TestCase):
|
||||
assigned_team="team-1",
|
||||
evergreen_projects=["sys-perf"],
|
||||
temperature=under_test.BfTemperature.COLD,
|
||||
created_time=datetime.now(),
|
||||
)
|
||||
bfs_report = under_test.BFsReport.empty()
|
||||
|
||||
@ -95,37 +101,5 @@ class TestBFsReport(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].hot_bfs), 0)
|
||||
self.assertEqual(len(bfs_report.team_reports["team-1"].cold_bfs), 0)
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].perf_bfs, {"BF-1"})
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
|
||||
def test_combine_teams(self):
|
||||
bfs_report = under_test.BFsReport(
|
||||
team_reports={
|
||||
"Team 1": under_test.CategorizedBFs(
|
||||
hot_bfs={"HOT-BF-1", "HOT-BF-2"},
|
||||
cold_bfs={"COLD-BF-1", "COLD-BF-2"},
|
||||
perf_bfs={"PERF-BF-1", "PERF-BF-2"},
|
||||
),
|
||||
"Team 2": under_test.CategorizedBFs(
|
||||
hot_bfs={"HOT-BF-3", "HOT-BF-4"},
|
||||
cold_bfs={"COLD-BF-3", "COLD-BF-4"},
|
||||
perf_bfs={"PERF-BF-3", "PERF-BF-4"},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
bfs_report.combine_teams(["Team 1", "Team 2"], "Group 1")
|
||||
|
||||
self.assertEqual(
|
||||
bfs_report.team_reports["Group 1"].hot_bfs,
|
||||
{"HOT-BF-1", "HOT-BF-2", "HOT-BF-3", "HOT-BF-4"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bfs_report.team_reports["Group 1"].cold_bfs,
|
||||
{"COLD-BF-1", "COLD-BF-2", "COLD-BF-3", "COLD-BF-4"},
|
||||
)
|
||||
self.assertEqual(
|
||||
bfs_report.team_reports["Group 1"].perf_bfs,
|
||||
{"PERF-BF-1", "PERF-BF-2", "PERF-BF-3", "PERF-BF-4"},
|
||||
)
|
||||
self.assertEqual(bfs_report.team_reports["team-1"].perf_bfs, {bf_1})
|
||||
self.assertEqual(len(bfs_report.team_reports.keys()), 1)
|
||||
|
@ -4,345 +4,57 @@ import buildscripts.monitor_build_status.cli as under_test
|
||||
|
||||
|
||||
class TestSummarize(unittest.TestCase):
|
||||
def test_previous_unknown_current_red_not_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.UNKNOWN
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = False
|
||||
def test_all_thresholds_below_100(self):
|
||||
scope_percentages = {
|
||||
"Scope 1": [0.0, 0.0, 0.0],
|
||||
"Scope 2": [0.0, 0.0, 0.0],
|
||||
"Scope 3": [0.0, 0.0, 0.0],
|
||||
"Scope 4": [0.0, 0.0, 0.0],
|
||||
}
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
summary = under_test.MonitorBuildStatusOrchestrator._summarize(scope_percentages)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PREFIX.value} "
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_green_current_red_not_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.GREEN
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = False
|
||||
def test_all_thresholds_are_100(self):
|
||||
scope_percentages = {
|
||||
"Scope 1": [100.0, 100.0, 100.0],
|
||||
"Scope 2": [100.0, 100.0, 100.0],
|
||||
"Scope 3": [100.0, 100.0, 100.0],
|
||||
"Scope 4": [100.0, 100.0, 100.0],
|
||||
}
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
summary = under_test.MonitorBuildStatusOrchestrator._summarize(scope_percentages)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PREFIX.value} "
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_yellow_current_red_not_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.YELLOW
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = False
|
||||
def test_some_threshold_exceeded_100(self):
|
||||
scope_percentages = {
|
||||
"Scope 1": [101.0, 0.0, 0.0],
|
||||
"Scope 2": [0.0, 101.0, 0.0],
|
||||
"Scope 3": [0.0, 0.0, 101.0],
|
||||
"Scope 4": [0.0, 0.0, 0.0],
|
||||
}
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
summary = under_test.MonitorBuildStatusOrchestrator._summarize(scope_percentages)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_red_current_red_not_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.RED
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.STILL_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_unknown_current_red_on_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.UNKNOWN
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = True
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ENTER_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_green_current_red_on_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.GREEN
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = True
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ENTER_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_yellow_current_red_on_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.YELLOW
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = True
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.ENTER_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_red_current_red_on_friday(self):
|
||||
previous_build_status = under_test.BuildStatus.RED
|
||||
current_build_status = under_test.BuildStatus.RED
|
||||
today_is_friday = True
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED_X2.value}\n"
|
||||
f"{under_test.SummaryMsg.STILL_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_unknown_current_yellow(self):
|
||||
previous_build_status = under_test.BuildStatus.UNKNOWN
|
||||
current_build_status = under_test.BuildStatus.YELLOW
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.PREFIX.value} "
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"\t- Scope 1\n\t- Scope 2\n\t- Scope 3\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_green_current_yellow(self):
|
||||
previous_build_status = under_test.BuildStatus.GREEN
|
||||
current_build_status = under_test.BuildStatus.YELLOW
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_yellow_current_yellow(self):
|
||||
previous_build_status = under_test.BuildStatus.YELLOW
|
||||
current_build_status = under_test.BuildStatus.YELLOW
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.YELLOW
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_YELLOW.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_red_current_yellow(self):
|
||||
previous_build_status = under_test.BuildStatus.RED
|
||||
current_build_status = under_test.BuildStatus.YELLOW
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.RED
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.THRESHOLD_EXCEEDED.value}\n"
|
||||
f"{under_test.SummaryMsg.STILL_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_RED.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_unknown_current_green(self):
|
||||
previous_build_status = under_test.BuildStatus.UNKNOWN
|
||||
current_build_status = under_test.BuildStatus.GREEN
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.GREEN
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_GREEN.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_green_current_green(self):
|
||||
previous_build_status = under_test.BuildStatus.GREEN
|
||||
current_build_status = under_test.BuildStatus.GREEN
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.GREEN
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_IS.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_GREEN.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_yellow_current_green(self):
|
||||
previous_build_status = under_test.BuildStatus.YELLOW
|
||||
current_build_status = under_test.BuildStatus.GREEN
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.GREEN
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_GREEN.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
||||
def test_previous_red_current_green(self):
|
||||
previous_build_status = under_test.BuildStatus.RED
|
||||
current_build_status = under_test.BuildStatus.GREEN
|
||||
today_is_friday = False
|
||||
|
||||
resulting_build_status, summary = under_test.MonitorBuildStatusOrchestrator._summarize(
|
||||
previous_build_status, current_build_status, today_is_friday
|
||||
)
|
||||
|
||||
expected_status = under_test.BuildStatus.GREEN
|
||||
expected_summary = (
|
||||
f"{under_test.SummaryMsg.TITLE.value}\n"
|
||||
f"{under_test.SummaryMsg.STATUS_CHANGED.value.format(status=expected_status.value)}\n"
|
||||
f"{under_test.SummaryMsg.BELOW_THRESHOLDS.value}\n"
|
||||
f"{under_test.SummaryMsg.EXIT_RED.value}\n"
|
||||
f"{under_test.SummaryMsg.ACTION_ON_GREEN.value}\n\n"
|
||||
f"{under_test.SummaryMsg.PLAYBOOK_REFERENCE.value}\n"
|
||||
)
|
||||
|
||||
self.assertEqual(resulting_build_status, expected_status)
|
||||
self.assertEqual(summary, expected_summary)
|
||||
|
@ -5,45 +5,56 @@ import buildscripts.monitor_build_status.jira_service as under_test
|
||||
|
||||
|
||||
class TestBfIssue(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.created = "Mon, 5 Aug 2024 15:05:10 +0000"
|
||||
|
||||
def test_parse_assigned_team_from_jira_issue(self):
|
||||
team_name = "Team Name"
|
||||
jira_issue_1 = MagicMock(fields=MagicMock(customfield_12751=[MagicMock(value=team_name)]))
|
||||
jira_issue_1 = MagicMock(
|
||||
fields=MagicMock(customfield_12751=[MagicMock(value=team_name)], created=self.created)
|
||||
)
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_1)
|
||||
self.assertEqual(bf_issue.assigned_team, team_name)
|
||||
|
||||
jira_issue_2 = MagicMock(fields=MagicMock(customfield_12751=[]))
|
||||
jira_issue_2 = MagicMock(fields=MagicMock(customfield_12751=[], created=self.created))
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_2)
|
||||
self.assertEqual(bf_issue.assigned_team, under_test.UNASSIGNED_LABEL)
|
||||
|
||||
jira_issue_3 = MagicMock(fields=MagicMock())
|
||||
jira_issue_3 = MagicMock(fields=MagicMock(created=self.created))
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_3)
|
||||
self.assertEqual(bf_issue.assigned_team, under_test.UNASSIGNED_LABEL)
|
||||
|
||||
def test_parse_evergreen_projects_from_jira_issue(self):
|
||||
evergreen_projects = ["evg-project"]
|
||||
jira_issue_1 = MagicMock(fields=MagicMock(customfield_14278=evergreen_projects))
|
||||
jira_issue_1 = MagicMock(
|
||||
fields=MagicMock(customfield_14278=evergreen_projects, created=self.created)
|
||||
)
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_1)
|
||||
self.assertEqual(bf_issue.evergreen_projects, evergreen_projects)
|
||||
|
||||
jira_issue_2 = MagicMock(fields=MagicMock(customfield_14278=[]))
|
||||
jira_issue_2 = MagicMock(fields=MagicMock(customfield_14278=[], created=self.created))
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_2)
|
||||
self.assertEqual(bf_issue.evergreen_projects, [])
|
||||
|
||||
jira_issue_3 = MagicMock(fields=MagicMock())
|
||||
jira_issue_3 = MagicMock(fields=MagicMock(created=self.created))
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_3)
|
||||
self.assertEqual(bf_issue.evergreen_projects, [])
|
||||
|
||||
def test_parse_bf_temperature_from_jira_issue(self):
|
||||
bf_temperature = "hot"
|
||||
jira_issue_1 = MagicMock(fields=MagicMock(customfield_24859=bf_temperature))
|
||||
jira_issue_1 = MagicMock(
|
||||
fields=MagicMock(customfield_24859=bf_temperature, created=self.created)
|
||||
)
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_1)
|
||||
self.assertEqual(bf_issue.temperature, under_test.BfTemperature.HOT)
|
||||
|
||||
bf_temperature = "cold"
|
||||
jira_issue_2 = MagicMock(fields=MagicMock(customfield_24859=bf_temperature))
|
||||
jira_issue_2 = MagicMock(
|
||||
fields=MagicMock(customfield_24859=bf_temperature, created=self.created)
|
||||
)
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_2)
|
||||
self.assertEqual(bf_issue.temperature, under_test.BfTemperature.COLD)
|
||||
|
||||
jira_issue_3 = MagicMock(fields=MagicMock())
|
||||
jira_issue_3 = MagicMock(fields=MagicMock(created=self.created))
|
||||
bf_issue = under_test.BfIssue.from_jira_issue(jira_issue_3)
|
||||
self.assertEqual(bf_issue.temperature, under_test.BfTemperature.NONE)
|
||||
|
@ -1,12 +1,14 @@
|
||||
overall_thresholds:
|
||||
hot_bf_count: 15
|
||||
cold_bf_count: 50
|
||||
perf_bf_count: 15
|
||||
hot_bf_count: 30
|
||||
cold_bf_count: 100
|
||||
perf_bf_count: 30
|
||||
include_bfs_older_than_hours: 168 # 7 days
|
||||
|
||||
team_default_thresholds:
|
||||
hot_bf_count: 3
|
||||
cold_bf_count: 10
|
||||
perf_bf_count: 5
|
||||
include_bfs_older_than_hours: 48
|
||||
|
||||
team_groups:
|
||||
######################################################################
|
||||
@ -14,11 +16,48 @@ team_groups:
|
||||
######################################################################
|
||||
# - name: Group Name
|
||||
# teams:
|
||||
# - Team Name 1 # Should exactly match "Assigned Teams" field value
|
||||
# - Team Name 1 # Should exactly match "Assigned Teams" Jira BF field value
|
||||
# - Team Name 2
|
||||
# - Team Name 3
|
||||
# thresholds:
|
||||
# hot_bf_count: 3
|
||||
# cold_bf_count: 10
|
||||
# perf_bf_count: 5
|
||||
# include_bfs_older_than_hours: 168 # 7 days
|
||||
######################################################################
|
||||
|
||||
# Core Server Team VP orgs as described in https://wiki.corp.mongodb.com/pages/viewpage.action?spaceKey=KERNEL&title=Server+Home
|
||||
- name: "Clusters & Integrations"
|
||||
teams:
|
||||
- Server Security
|
||||
- Networking & Observability
|
||||
- Workload Scheduling
|
||||
- Server Programmability
|
||||
- Cluster Scalability
|
||||
- Catalog and Routing
|
||||
thresholds:
|
||||
hot_bf_count: 15
|
||||
cold_bf_count: 50
|
||||
perf_bf_count: 15
|
||||
include_bfs_older_than_hours: 168 # 7 days
|
||||
- name: "Durable Transactions & Availability"
|
||||
teams:
|
||||
- Replication
|
||||
- Storage Execution
|
||||
- RSS Sydney
|
||||
- Storage Engines
|
||||
thresholds:
|
||||
hot_bf_count: 15
|
||||
cold_bf_count: 50
|
||||
perf_bf_count: 15
|
||||
include_bfs_older_than_hours: 168 # 7 days
|
||||
- name: "Query"
|
||||
teams:
|
||||
- Query Execution
|
||||
- Query Optimization
|
||||
- Query Integration
|
||||
thresholds:
|
||||
hot_bf_count: 15
|
||||
cold_bf_count: 50
|
||||
perf_bf_count: 15
|
||||
include_bfs_older_than_hours: 168 # 7 days
|
||||
|
@ -1103,26 +1103,6 @@ tasks:
|
||||
- func: "set up venv"
|
||||
- func: "upload pip requirements"
|
||||
- func: "configure evergreen api credentials"
|
||||
# Attempting to download build status file that was uploaded by previous run of this task
|
||||
# `remote_file` location should match uploading file location in steps below
|
||||
# `optional: true` is to pass this step when this task is running for the first time
|
||||
- command: s3.get
|
||||
params:
|
||||
aws_key: ${aws_key}
|
||||
aws_secret: ${aws_secret}
|
||||
remote_file: ${project}/monitor_build_status/${branch_name}/build_status_latest_file.txt
|
||||
bucket: mciuploads
|
||||
local_file: src/input_build_status_file.txt
|
||||
optional: true
|
||||
# The same as above but for patches for testing purposes
|
||||
- command: s3.get
|
||||
params:
|
||||
aws_key: ${aws_key}
|
||||
aws_secret: ${aws_secret}
|
||||
remote_file: ${project}/monitor_build_status_testing/${branch_name}/build_status_latest_file_testing.txt
|
||||
bucket: mciuploads
|
||||
local_file: src/input_build_status_file_testing.txt
|
||||
optional: true
|
||||
- command: subprocess.exec
|
||||
type: test
|
||||
params:
|
||||
@ -1134,41 +1114,6 @@ tasks:
|
||||
JIRA_AUTH_ACCESS_TOKEN_SECRET: ${jira_auth_access_token_secret}
|
||||
JIRA_AUTH_CONSUMER_KEY: ${jira_auth_consumer_key}
|
||||
JIRA_AUTH_KEY_CERT: ${jira_auth_key_cert}
|
||||
# Uploading build status file after each run of this task to be consumed by the next run
|
||||
# `remote_file` location should match downloading file location in steps above
|
||||
- command: s3.put
|
||||
params:
|
||||
aws_key: ${aws_key}
|
||||
aws_secret: ${aws_secret}
|
||||
local_file: src/output_build_status_file.txt
|
||||
remote_file: ${project}/monitor_build_status/${branch_name}/build_status_latest_file.txt
|
||||
bucket: mciuploads
|
||||
permissions: public-read
|
||||
content_type: text/plain
|
||||
display_name: Build Status Latest
|
||||
patchable: false
|
||||
# The same as above but for patches for testing purposes to avoid overriding the file
|
||||
# that is used on the waterfall runs
|
||||
# However `remote_file` location could be changed here to intentionally override the file
|
||||
# that is used on the waterfall from the patch in case of emergency, e.g. build status was
|
||||
# evaluated incorrectly on the waterfall and incorrect status was uploaded on S3
|
||||
- command: s3.put
|
||||
params:
|
||||
aws_key: ${aws_key}
|
||||
aws_secret: ${aws_secret}
|
||||
local_file: src/output_build_status_file_testing.txt
|
||||
remote_file: ${project}/monitor_build_status_testing/${branch_name}/build_status_latest_file_testing.txt
|
||||
bucket: mciuploads
|
||||
permissions: public-read
|
||||
content_type: text/plain
|
||||
display_name: Build Status Latest Patch Testing
|
||||
patch_only: true
|
||||
- command: subprocess.exec
|
||||
type: test
|
||||
params:
|
||||
binary: bash
|
||||
args:
|
||||
- "src/evergreen/monitor_build_status_exit.sh"
|
||||
|
||||
- name: monitor_mongo_fork_10gen
|
||||
tags: ["assigned_to_jira_team_devprod_correctness", "auxiliary"]
|
||||
|
@ -1,16 +0,0 @@
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)"
|
||||
. "$DIR/prelude.sh"
|
||||
|
||||
set -o errexit
|
||||
set -o verbose
|
||||
|
||||
cd src
|
||||
|
||||
if [ -f monitor_build_status_command_exit_code.txt ]; then
|
||||
exit_code=$(cat monitor_build_status_command_exit_code.txt)
|
||||
else
|
||||
exit_code=0
|
||||
fi
|
||||
|
||||
echo "Exiting monitor_build_status with code $exit_code"
|
||||
exit "$exit_code"
|
@ -8,15 +8,9 @@ cd src
|
||||
activate_venv
|
||||
|
||||
command_invocation="$python buildscripts/monitor_build_status/cli.py"
|
||||
if [ "${is_patch}" = "true" ]; then
|
||||
command_invocation="$command_invocation --input-status-file input_build_status_file_testing.txt"
|
||||
command_invocation="$command_invocation --output-status-file output_build_status_file_testing.txt"
|
||||
else
|
||||
if [ "${is_patch}" != "true" ]; then
|
||||
command_invocation="$command_invocation --notify"
|
||||
fi
|
||||
|
||||
echo "Verbatim monitor_build_status invocation: ${command_invocation}"
|
||||
set +o errexit
|
||||
eval "${command_invocation}"
|
||||
echo "$?" > monitor_build_status_command_exit_code.txt
|
||||
set -o errexit
|
||||
|
Loading…
Reference in New Issue
Block a user