regression_assessment.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. from __future__ import annotations
  10. from materialize.docker import (
  11. get_mz_version_from_image_tag,
  12. is_image_tag_of_release_version,
  13. )
  14. from materialize.mz_version import MzVersion
  15. from materialize.mzcompose.test_result import TestFailureDetails
  16. from materialize.scalability.endpoint.endpoint import Endpoint
  17. from materialize.scalability.result.comparison_outcome import ComparisonOutcome
  18. from materialize.version_ancestor_overrides import (
  19. ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
  20. )
  21. from materialize.version_list import (
  22. get_commits_of_accepted_regressions_between_versions,
  23. )
  24. class RegressionAssessment:
  25. def __init__(
  26. self,
  27. baseline_endpoint: Endpoint | None,
  28. comparison_outcome: ComparisonOutcome,
  29. ):
  30. self.baseline_endpoint = baseline_endpoint
  31. self.comparison_outcome = comparison_outcome
  32. self.endpoints_with_regressions_and_justifications: dict[
  33. Endpoint, str | None
  34. ] = {}
  35. self.determine_whether_regressions_are_justified()
  36. assert len(comparison_outcome.endpoints_with_regressions) == len(
  37. self.endpoints_with_regressions_and_justifications
  38. )
  39. def has_comparison_target(self) -> bool:
  40. return self.baseline_endpoint is not None
  41. def has_regressions(self) -> bool:
  42. return self.comparison_outcome.has_regressions()
  43. def has_unjustified_regressions(self):
  44. return any(
  45. justification is None
  46. for justification in self.endpoints_with_regressions_and_justifications.values()
  47. )
  48. def determine_whether_regressions_are_justified(self) -> None:
  49. if self.baseline_endpoint is None:
  50. return
  51. if not self.comparison_outcome.has_regressions():
  52. return
  53. if not self._endpoint_references_release_version(self.baseline_endpoint):
  54. # justified regressions require a version as comparison target
  55. self._mark_all_targets_with_regressions_as_unjustified()
  56. return
  57. baseline_version = get_mz_version_from_image_tag(
  58. self.baseline_endpoint.resolved_target()
  59. )
  60. for endpoint in self.comparison_outcome.endpoints_with_regressions:
  61. commits_with_accepted_regressions = (
  62. self.collect_accepted_regression_commits_of_endpoint(
  63. endpoint, baseline_version
  64. )
  65. )
  66. if len(commits_with_accepted_regressions) > 0:
  67. self.endpoints_with_regressions_and_justifications[endpoint] = (
  68. ", ".join(commits_with_accepted_regressions)
  69. )
  70. else:
  71. self.endpoints_with_regressions_and_justifications[endpoint] = None
  72. def collect_accepted_regression_commits_of_endpoint(
  73. self, endpoint: Endpoint, baseline_version: MzVersion
  74. ) -> list[str]:
  75. """
  76. Collect known regressions (in form of commits) between the endpoint version and baseline version.
  77. @returns list of commits representing known regressions
  78. """
  79. if not self._endpoint_references_release_version(endpoint):
  80. # no explicit version referenced: not supported
  81. return []
  82. endpoint_version = get_mz_version_from_image_tag(endpoint.resolved_target())
  83. if baseline_version >= endpoint_version:
  84. # baseline more recent than endpoint: not supported, should not be relevant
  85. return []
  86. return get_commits_of_accepted_regressions_between_versions(
  87. ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
  88. since_version_exclusive=baseline_version,
  89. to_version_inclusive=endpoint_version,
  90. )
  91. def _mark_all_targets_with_regressions_as_unjustified(self) -> None:
  92. for endpoint in self.comparison_outcome.endpoints_with_regressions:
  93. self.endpoints_with_regressions_and_justifications[endpoint] = None
  94. def _endpoint_references_release_version(self, endpoint: Endpoint) -> bool:
  95. target = endpoint.resolved_target()
  96. return is_image_tag_of_release_version(
  97. target
  98. ) and MzVersion.is_valid_version_string(target)
  99. def to_failure_details(self) -> list[TestFailureDetails]:
  100. failure_details = []
  101. assert self.baseline_endpoint is not None
  102. baseline_version = self.baseline_endpoint.try_load_version()
  103. for (
  104. endpoint_with_regression,
  105. justification,
  106. ) in self.endpoints_with_regressions_and_justifications.items():
  107. if justification is not None:
  108. continue
  109. regressions = self.comparison_outcome.get_regressions_by_endpoint(
  110. endpoint_with_regression
  111. )
  112. for regression in regressions:
  113. failure_details.append(
  114. TestFailureDetails(
  115. test_case_name_override=f"Workload '{regression.workload_name}'",
  116. message=f"New regression against {baseline_version}",
  117. details=str(regression),
  118. )
  119. )
  120. return failure_details