Module materialize.scalability.regression_assessment

Expand source code Browse git
# Copyright Materialize, Inc. and contributors. All rights reserved.
#
# Use of this software is governed by the Business Source License
# included in the LICENSE file at the root of this repository.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0.
from __future__ import annotations

from materialize.docker import (
    get_mz_version_from_image_tag,
    is_image_tag_of_version,
)
from materialize.mz_version import MzVersion
from materialize.mzcompose.test_result import TestFailureDetails
from materialize.scalability.comparison_outcome import ComparisonOutcome
from materialize.scalability.endpoint import Endpoint
from materialize.version_ancestor_overrides import (
    ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
)
from materialize.version_list import (
    get_commits_of_accepted_regressions_between_versions,
)


class RegressionAssessment:
    def __init__(
        self,
        baseline_endpoint: Endpoint | None,
        comparison_outcome: ComparisonOutcome,
    ):
        self.baseline_endpoint = baseline_endpoint
        self.comparison_outcome = comparison_outcome
        self.endpoints_with_regressions_and_justifications: dict[
            Endpoint, str | None
        ] = {}
        self.determine_whether_regressions_are_justified()
        assert len(comparison_outcome.endpoints_with_regressions) == len(
            self.endpoints_with_regressions_and_justifications
        )

    def has_comparison_target(self) -> bool:
        return self.baseline_endpoint is not None

    def has_regressions(self) -> bool:
        return self.comparison_outcome.has_regressions()

    def has_unjustified_regressions(self):
        return any(
            justification is None
            for justification in self.endpoints_with_regressions_and_justifications.values()
        )

    def determine_whether_regressions_are_justified(self) -> None:
        if self.baseline_endpoint is None:
            return

        if not self.comparison_outcome.has_regressions():
            return

        if not self._endpoint_references_release_version(self.baseline_endpoint):
            # justified regressions require a version as comparison target
            self._mark_all_targets_with_regressions_as_unjustified()
            return

        baseline_version = get_mz_version_from_image_tag(
            self.baseline_endpoint.resolved_target()
        )

        for endpoint in self.comparison_outcome.endpoints_with_regressions:
            commits_with_accepted_regressions = (
                self.collect_accepted_regression_commits_of_endpoint(
                    endpoint, baseline_version
                )
            )

            if len(commits_with_accepted_regressions) > 0:
                self.endpoints_with_regressions_and_justifications[
                    endpoint
                ] = ", ".join(commits_with_accepted_regressions)
            else:
                self.endpoints_with_regressions_and_justifications[endpoint] = None

    def collect_accepted_regression_commits_of_endpoint(
        self, endpoint: Endpoint, baseline_version: MzVersion
    ) -> list[str]:
        """
        Collect known regressions (in form of commits) between the endpoint version and baseline version.
        @returns list of commits representing known regressions
        """
        if not self._endpoint_references_release_version(endpoint):
            # no explicit version referenced: not supported
            return []

        endpoint_version = get_mz_version_from_image_tag(endpoint.resolved_target())

        if baseline_version >= endpoint_version:
            # baseline more recent than endpoint: not supported, should not be relevant
            return []

        return get_commits_of_accepted_regressions_between_versions(
            ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
            since_version_exclusive=baseline_version,
            to_version_inclusive=endpoint_version,
        )

    def _mark_all_targets_with_regressions_as_unjustified(self) -> None:
        for endpoint in self.comparison_outcome.endpoints_with_regressions:
            self.endpoints_with_regressions_and_justifications[endpoint] = None

    def _endpoint_references_release_version(self, endpoint: Endpoint) -> bool:
        target = endpoint.resolved_target()
        return is_image_tag_of_version(target) and MzVersion.is_valid_version_string(
            target
        )

    def to_failure_details(self) -> list[TestFailureDetails]:
        failure_details = []

        assert self.baseline_endpoint is not None
        baseline_version = self.baseline_endpoint.try_load_version()
        for (
            endpoint_with_regression,
            justification,
        ) in self.endpoints_with_regressions_and_justifications.items():
            if justification is not None:
                continue

            regressions = self.comparison_outcome.get_regressions_by_endpoint(
                endpoint_with_regression
            )

            for regression in regressions:
                failure_details.append(
                    TestFailureDetails(
                        test_case_name_override=f"Workload '{regression.workload_name}'",
                        message=f"New regression against {baseline_version}",
                        details=str(regression),
                    )
                )

        return failure_details

Classes

class RegressionAssessment (baseline_endpoint: Endpoint | None, comparison_outcome: ComparisonOutcome)
Expand source code Browse git
class RegressionAssessment:
    def __init__(
        self,
        baseline_endpoint: Endpoint | None,
        comparison_outcome: ComparisonOutcome,
    ):
        self.baseline_endpoint = baseline_endpoint
        self.comparison_outcome = comparison_outcome
        self.endpoints_with_regressions_and_justifications: dict[
            Endpoint, str | None
        ] = {}
        self.determine_whether_regressions_are_justified()
        assert len(comparison_outcome.endpoints_with_regressions) == len(
            self.endpoints_with_regressions_and_justifications
        )

    def has_comparison_target(self) -> bool:
        return self.baseline_endpoint is not None

    def has_regressions(self) -> bool:
        return self.comparison_outcome.has_regressions()

    def has_unjustified_regressions(self):
        return any(
            justification is None
            for justification in self.endpoints_with_regressions_and_justifications.values()
        )

    def determine_whether_regressions_are_justified(self) -> None:
        if self.baseline_endpoint is None:
            return

        if not self.comparison_outcome.has_regressions():
            return

        if not self._endpoint_references_release_version(self.baseline_endpoint):
            # justified regressions require a version as comparison target
            self._mark_all_targets_with_regressions_as_unjustified()
            return

        baseline_version = get_mz_version_from_image_tag(
            self.baseline_endpoint.resolved_target()
        )

        for endpoint in self.comparison_outcome.endpoints_with_regressions:
            commits_with_accepted_regressions = (
                self.collect_accepted_regression_commits_of_endpoint(
                    endpoint, baseline_version
                )
            )

            if len(commits_with_accepted_regressions) > 0:
                self.endpoints_with_regressions_and_justifications[
                    endpoint
                ] = ", ".join(commits_with_accepted_regressions)
            else:
                self.endpoints_with_regressions_and_justifications[endpoint] = None

    def collect_accepted_regression_commits_of_endpoint(
        self, endpoint: Endpoint, baseline_version: MzVersion
    ) -> list[str]:
        """
        Collect known regressions (in form of commits) between the endpoint version and baseline version.
        @returns list of commits representing known regressions
        """
        if not self._endpoint_references_release_version(endpoint):
            # no explicit version referenced: not supported
            return []

        endpoint_version = get_mz_version_from_image_tag(endpoint.resolved_target())

        if baseline_version >= endpoint_version:
            # baseline more recent than endpoint: not supported, should not be relevant
            return []

        return get_commits_of_accepted_regressions_between_versions(
            ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
            since_version_exclusive=baseline_version,
            to_version_inclusive=endpoint_version,
        )

    def _mark_all_targets_with_regressions_as_unjustified(self) -> None:
        for endpoint in self.comparison_outcome.endpoints_with_regressions:
            self.endpoints_with_regressions_and_justifications[endpoint] = None

    def _endpoint_references_release_version(self, endpoint: Endpoint) -> bool:
        target = endpoint.resolved_target()
        return is_image_tag_of_version(target) and MzVersion.is_valid_version_string(
            target
        )

    def to_failure_details(self) -> list[TestFailureDetails]:
        failure_details = []

        assert self.baseline_endpoint is not None
        baseline_version = self.baseline_endpoint.try_load_version()
        for (
            endpoint_with_regression,
            justification,
        ) in self.endpoints_with_regressions_and_justifications.items():
            if justification is not None:
                continue

            regressions = self.comparison_outcome.get_regressions_by_endpoint(
                endpoint_with_regression
            )

            for regression in regressions:
                failure_details.append(
                    TestFailureDetails(
                        test_case_name_override=f"Workload '{regression.workload_name}'",
                        message=f"New regression against {baseline_version}",
                        details=str(regression),
                    )
                )

        return failure_details

Methods

def collect_accepted_regression_commits_of_endpoint(self, endpoint: Endpoint, baseline_version: MzVersion) ‑> list[str]

Collect known regressions (in form of commits) between the endpoint version and baseline version. @returns list of commits representing known regressions

Expand source code Browse git
def collect_accepted_regression_commits_of_endpoint(
    self, endpoint: Endpoint, baseline_version: MzVersion
) -> list[str]:
    """
    Collect known regressions (in form of commits) between the endpoint version and baseline version.
    @returns list of commits representing known regressions
    """
    if not self._endpoint_references_release_version(endpoint):
        # no explicit version referenced: not supported
        return []

    endpoint_version = get_mz_version_from_image_tag(endpoint.resolved_target())

    if baseline_version >= endpoint_version:
        # baseline more recent than endpoint: not supported, should not be relevant
        return []

    return get_commits_of_accepted_regressions_between_versions(
        ANCESTOR_OVERRIDES_FOR_SCALABILITY_REGRESSIONS,
        since_version_exclusive=baseline_version,
        to_version_inclusive=endpoint_version,
    )
def determine_whether_regressions_are_justified(self) ‑> None
Expand source code Browse git
def determine_whether_regressions_are_justified(self) -> None:
    if self.baseline_endpoint is None:
        return

    if not self.comparison_outcome.has_regressions():
        return

    if not self._endpoint_references_release_version(self.baseline_endpoint):
        # justified regressions require a version as comparison target
        self._mark_all_targets_with_regressions_as_unjustified()
        return

    baseline_version = get_mz_version_from_image_tag(
        self.baseline_endpoint.resolved_target()
    )

    for endpoint in self.comparison_outcome.endpoints_with_regressions:
        commits_with_accepted_regressions = (
            self.collect_accepted_regression_commits_of_endpoint(
                endpoint, baseline_version
            )
        )

        if len(commits_with_accepted_regressions) > 0:
            self.endpoints_with_regressions_and_justifications[
                endpoint
            ] = ", ".join(commits_with_accepted_regressions)
        else:
            self.endpoints_with_regressions_and_justifications[endpoint] = None
def has_comparison_target(self) ‑> bool
Expand source code Browse git
def has_comparison_target(self) -> bool:
    return self.baseline_endpoint is not None
def has_regressions(self) ‑> bool
Expand source code Browse git
def has_regressions(self) -> bool:
    return self.comparison_outcome.has_regressions()
def has_unjustified_regressions(self)
Expand source code Browse git
def has_unjustified_regressions(self):
    return any(
        justification is None
        for justification in self.endpoints_with_regressions_and_justifications.values()
    )
def to_failure_details(self) ‑> list[TestFailureDetails]
Expand source code Browse git
def to_failure_details(self) -> list[TestFailureDetails]:
    failure_details = []

    assert self.baseline_endpoint is not None
    baseline_version = self.baseline_endpoint.try_load_version()
    for (
        endpoint_with_regression,
        justification,
    ) in self.endpoints_with_regressions_and_justifications.items():
        if justification is not None:
            continue

        regressions = self.comparison_outcome.get_regressions_by_endpoint(
            endpoint_with_regression
        )

        for regression in regressions:
            failure_details.append(
                TestFailureDetails(
                    test_case_name_override=f"Workload '{regression.workload_name}'",
                    message=f"New regression against {baseline_version}",
                    details=str(regression),
                )
            )

    return failure_details