400 lines
12 KiB
Python
400 lines
12 KiB
Python
import sys
|
|
from typing import List, Optional
|
|
|
|
import pytest
|
|
|
|
from ray_release.bazel import bazel_runfile
|
|
from ray_release.configs.global_config import init_global_config
|
|
from ray_release.result import (
|
|
Result,
|
|
ResultStatus,
|
|
)
|
|
from ray_release.test import (
|
|
Test,
|
|
TestResult,
|
|
TestState,
|
|
)
|
|
from ray_release.test_automation.ci_state_machine import (
|
|
CONTINUOUS_FAILURE_TO_FLAKY,
|
|
CONTINUOUS_PASSING_TO_PASSING,
|
|
FAILING_TO_FLAKY_MESSAGE,
|
|
JAILED_MESSAGE,
|
|
JAILED_TAG,
|
|
CITestStateMachine,
|
|
)
|
|
from ray_release.test_automation.release_state_machine import ReleaseTestStateMachine
|
|
from ray_release.test_automation.state_machine import (
|
|
NO_TEAM,
|
|
WEEKLY_RELEASE_BLOCKER_TAG,
|
|
TestStateMachine,
|
|
)
|
|
|
|
init_global_config(bazel_runfile("release/ray_release/configs/oss_config.yaml"))
|
|
|
|
|
|
class MockLabel:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
|
|
|
|
class MockIssue:
|
|
def __init__(
|
|
self,
|
|
number: int,
|
|
title: str,
|
|
state: str = "open",
|
|
labels: Optional[List[MockLabel]] = None,
|
|
):
|
|
self.number = number
|
|
self.title = title
|
|
self.state = state
|
|
self.labels = labels or []
|
|
self.comments = []
|
|
|
|
def edit(
|
|
self, state: str = None, labels: List[MockLabel] = None, title: str = None
|
|
):
|
|
if state:
|
|
self.state = state
|
|
if labels:
|
|
self.labels = labels
|
|
if title:
|
|
self.title = title
|
|
if state:
|
|
self.state = state
|
|
|
|
def create_comment(self, comment: str):
|
|
self.comments.append(comment)
|
|
|
|
def get_labels(self):
|
|
return self.labels
|
|
|
|
|
|
class MockIssueDB:
|
|
issue_id = 1
|
|
issue_db = {}
|
|
|
|
|
|
class MockRepo:
|
|
def create_issue(self, labels: List[str], title: str, *args, **kwargs):
|
|
label_objs = [MockLabel(label) for label in labels]
|
|
issue = MockIssue(MockIssueDB.issue_id, title=title, labels=label_objs)
|
|
MockIssueDB.issue_db[MockIssueDB.issue_id] = issue
|
|
MockIssueDB.issue_id += 1
|
|
return issue
|
|
|
|
def get_issue(self, number: int):
|
|
return MockIssueDB.issue_db[number]
|
|
|
|
def get_issues(self, state: str, labels: List[MockLabel]) -> List[MockIssue]:
|
|
issues = []
|
|
for issue in MockIssueDB.issue_db.values():
|
|
if issue.state != state:
|
|
continue
|
|
issue_labels = [label.name for label in issue.labels]
|
|
if all(label.name in issue_labels for label in labels):
|
|
issues.append(issue)
|
|
|
|
return issues
|
|
|
|
def get_label(self, name: str):
|
|
return MockLabel(name)
|
|
|
|
|
|
class MockBuildkiteBuild:
|
|
def create_build(self, *args, **kwargs):
|
|
return {
|
|
"number": 1,
|
|
"jobs": [{"id": "1"}],
|
|
}
|
|
|
|
def list_all_for_pipeline(self, *args, **kwargs):
|
|
return []
|
|
|
|
|
|
class MockBuildkiteJob:
|
|
def unblock_job(self, *args, **kwargs):
|
|
return {}
|
|
|
|
|
|
class MockBuildkite:
|
|
def builds(self):
|
|
return MockBuildkiteBuild()
|
|
|
|
def jobs(self):
|
|
return MockBuildkiteJob()
|
|
|
|
|
|
TestStateMachine.ray_repo = MockRepo()
|
|
TestStateMachine.ray_buildkite = MockBuildkite()
|
|
|
|
|
|
def test_ci_empty_results():
|
|
test = Test(name="w00t", team="ci", state=TestState.FLAKY)
|
|
test.test_results = []
|
|
CITestStateMachine(test).move()
|
|
# do not change the state
|
|
assert test.get_state() == TestState.FLAKY
|
|
|
|
|
|
def test_ci_move_from_passing_to_flaky():
|
|
"""
|
|
Test the entire lifecycle of a CI test when it moves from passing to flaky.
|
|
"""
|
|
test = Test(name="w00t", team="ci")
|
|
# start from passing
|
|
assert test.get_state() == TestState.PASSING
|
|
|
|
# passing to flaky
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
] * 10
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.FLAKY
|
|
issue = MockIssueDB.issue_db[test.get(Test.KEY_GITHUB_ISSUE_NUMBER)]
|
|
assert issue.state == "open"
|
|
assert issue.title == "CI test w00t is flaky"
|
|
|
|
# flaky to jail
|
|
issue.edit(labels=[MockLabel(JAILED_TAG)])
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.JAILED
|
|
assert issue.comments[-1] == JAILED_MESSAGE
|
|
|
|
|
|
def test_ci_move_from_passing_to_failing_to_flaky():
|
|
"""
|
|
Test the entire lifecycle of a CI test when it moves from passing to failing.
|
|
Check that the conditions are met for each state transition. Also check that
|
|
gihub issues are created and closed correctly.
|
|
"""
|
|
test = Test(name="test", team="ci")
|
|
# start from passing
|
|
assert test.get_state() == TestState.PASSING
|
|
|
|
# passing to failing
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
]
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.FAILING
|
|
|
|
# failing to consistently failing
|
|
test.test_results.extend(
|
|
[
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
]
|
|
)
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
issue = MockIssueDB.issue_db[test.get(Test.KEY_GITHUB_ISSUE_NUMBER)]
|
|
assert issue.state == "open"
|
|
assert "ci-test" in [label.name for label in issue.labels]
|
|
|
|
# move from consistently failing to flaky
|
|
test.test_results.extend(
|
|
[TestResult.from_result(Result(status=ResultStatus.ERROR.value))]
|
|
* CONTINUOUS_FAILURE_TO_FLAKY
|
|
)
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.FLAKY
|
|
assert issue.comments[-1] == FAILING_TO_FLAKY_MESSAGE
|
|
|
|
# go back to passing
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
] * CONTINUOUS_PASSING_TO_PASSING
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.PASSING
|
|
assert test.get(Test.KEY_GITHUB_ISSUE_NUMBER) == issue.number
|
|
assert issue.state == "closed"
|
|
|
|
# go back to failing and reuse the github issue
|
|
test.test_results = 3 * [
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value))
|
|
]
|
|
CITestStateMachine(test).move()
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
assert test.get(Test.KEY_GITHUB_ISSUE_NUMBER) == issue.number
|
|
assert issue.state == "open"
|
|
|
|
|
|
def test_release_move_from_passing_to_failing():
|
|
test = Test(name="test", team="ci")
|
|
# Test original state
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
]
|
|
assert test.get_state() == TestState.PASSING
|
|
|
|
# Test moving from passing to failing
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.FAILING
|
|
assert test[Test.KEY_BISECT_BUILD_NUMBER] == 1
|
|
|
|
# Test moving from failing to consistently failing
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
assert test[Test.KEY_GITHUB_ISSUE_NUMBER] == MockIssueDB.issue_id - 1
|
|
|
|
|
|
def test_release_move_from_failing_to_consisently_failing():
|
|
test = Test(name="test", team="ci")
|
|
test[Test.KEY_BISECT_BUILD_NUMBER] = 1
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
]
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.FAILING
|
|
test[Test.KEY_BISECT_BLAMED_COMMIT] = "1234567890"
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
sm.comment_blamed_commit_on_github_issue()
|
|
issue = MockIssueDB.issue_db[test.get(Test.KEY_GITHUB_ISSUE_NUMBER)]
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
assert "Blamed commit: 1234567890" in issue.comments[0]
|
|
labels = [label.name for label in issue.get_labels()]
|
|
assert "ci" in labels
|
|
|
|
|
|
def test_release_move_from_failing_to_passing():
|
|
test = Test(name="test", team="ci")
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
]
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
assert test[Test.KEY_GITHUB_ISSUE_NUMBER] == MockIssueDB.issue_id - 1
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.PASSING
|
|
assert test.get(Test.KEY_BISECT_BUILD_NUMBER) is None
|
|
assert test.get(Test.KEY_BISECT_BLAMED_COMMIT) is None
|
|
|
|
|
|
def test_release_move_from_failing_to_jailed():
|
|
test = Test(name="test", team="ci")
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
]
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.CONSITENTLY_FAILING
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.JAILED
|
|
|
|
# Test moving from jailed to jailed
|
|
issue = MockIssueDB.issue_db[test.get(Test.KEY_GITHUB_ISSUE_NUMBER)]
|
|
issue.edit(state="closed")
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.ERROR.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.JAILED
|
|
assert issue.state == "open"
|
|
|
|
# Test moving from jailed to passing
|
|
test.test_results.insert(
|
|
0,
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.PASSING
|
|
assert issue.state == "closed"
|
|
|
|
|
|
def test_get_release_blockers() -> None:
|
|
MockIssueDB.issue_id = 1
|
|
MockIssueDB.issue_db = {}
|
|
TestStateMachine.ray_repo.create_issue(labels=["non-blocker"], title="non-blocker")
|
|
TestStateMachine.ray_repo.create_issue(
|
|
labels=[WEEKLY_RELEASE_BLOCKER_TAG], title="blocker"
|
|
)
|
|
issues = TestStateMachine.get_release_blockers()
|
|
assert len(issues) == 1
|
|
assert issues[0].title == "blocker"
|
|
|
|
|
|
def test_get_issue_owner() -> None:
|
|
issue = TestStateMachine.ray_repo.create_issue(labels=["core"], title="hi")
|
|
assert TestStateMachine.get_issue_owner(issue) == "core"
|
|
issue = TestStateMachine.ray_repo.create_issue(labels=["w00t"], title="bye")
|
|
assert TestStateMachine.get_issue_owner(issue) == NO_TEAM
|
|
|
|
|
|
def test_release_bisect_disabled(monkeypatch) -> None:
|
|
"""When bisect is disabled in config, no bisect build is triggered."""
|
|
import ray_release.test_automation.state_machine as sm_mod
|
|
|
|
real_get_global_config = sm_mod.get_global_config
|
|
|
|
def fake_get_global_config():
|
|
cfg = dict(real_get_global_config())
|
|
cfg["state_machine_bisect_disabled"] = True
|
|
return cfg
|
|
|
|
monkeypatch.setattr(sm_mod, "get_global_config", fake_get_global_config)
|
|
|
|
test = Test(name="bisect-off", team="ci")
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
]
|
|
assert test.get_state() == TestState.PASSING
|
|
test.test_results.insert(
|
|
0, TestResult.from_result(Result(status=ResultStatus.ERROR.value))
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.FAILING
|
|
# Bisect gated off -> no build number recorded.
|
|
assert test.get(Test.KEY_BISECT_BUILD_NUMBER) is None
|
|
|
|
|
|
def test_release_bisect_enabled_triggers(monkeypatch) -> None:
|
|
"""When bisect is enabled (default), a bisect build is triggered."""
|
|
test = Test(name="bisect-on", team="ci")
|
|
test.test_results = [
|
|
TestResult.from_result(Result(status=ResultStatus.SUCCESS.value)),
|
|
]
|
|
test.test_results.insert(
|
|
0, TestResult.from_result(Result(status=ResultStatus.ERROR.value))
|
|
)
|
|
sm = ReleaseTestStateMachine(test)
|
|
sm.move()
|
|
assert test.get_state() == TestState.FAILING
|
|
assert test[Test.KEY_BISECT_BUILD_NUMBER] == 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", __file__]))
|