Exemple #1
0
def test_hgmo_json_data(responses):
    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/rev/abcdef?style=json",
        json={"backedoutby": "123456"},
        status=200,
    )

    h = HGMO("abcdef")
    assert h.get("backedoutby") == "123456"
Exemple #2
0
def test_hgmo_cache():
    # HGMO.create() uses a cache.
    h1 = HGMO.create("abcdef", "autoland")
    h2 = HGMO.create("abcdef", "autoland")
    assert h1 == h2

    # Instantiating directly ignores the cache.
    h1 = HGMO("abcdef", "autoland")
    h2 = HGMO("abcdef", "autoland")
    assert h1 != h2
Exemple #3
0
def test_hgmo_is_backout(responses):
    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={"changesets": [{"backsoutnodes": []}]},
        status=200,
    )

    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={"changesets": [{"backsoutnodes": ["123456"]}]},
        status=200,
    )

    h = HGMO("abcdef")
    assert not h.is_backout
    assert h.changesets[0]["backsoutnodes"] == []

    h = HGMO("abcdef")
    assert h.is_backout
    assert h.changesets[0]["backsoutnodes"] == ["123456"]
Exemple #4
0
    def __init__(self, revs, branch="autoland"):
        """A representation of a single push.

        Args:
            revs (list): List of revisions of commits in the push (top-most is the first element).
            branch (str): Branch to look on (default: autoland).
        """
        if isinstance(revs, str):
            revs = [revs]

        self.revs = revs
        self.branch = branch
        self._hgmo = HGMO.create(self.rev, branch=self.branch)
        self.index = BASE_INDEX.format(branch=self.branch, rev=self.rev)

        self._id = None
        self._date = None
Exemple #5
0
    def __init__(self, revs, branch="autoland"):
        if isinstance(revs, str):
            self._revs = None
            revs = [revs]
        else:
            self._revs = revs

        self.branch = branch
        self._hgmo = HGMO.create(revs[0], branch=self.branch)

        self._id = None
        self._date = None

        # Need to use full hash in the index.
        if len(revs[0]) == 40:
            self.rev = revs[0]
        else:
            self.rev = self._hgmo["node"]

        self.index = BASE_INDEX.format(branch=self.branch, rev=self.rev)
Exemple #6
0
    def __init__(self, revs, branch="autoland"):
        if isinstance(revs, str):
            self._revs = None
            revs = [revs]
        else:
            self._revs = revs

        self.branch = branch
        self._hgmo = HGMO.create(revs[0], branch=self.branch)

        self._id = None
        self._date = None

        # Need to use full hash in the index.
        if len(revs[0]) == 40:
            self.rev = revs[0]
        else:
            self.rev = self._hgmo["node"]

        self.index = BASE_INDEX.format(branch=self.branch, rev=self.rev)
        # Unique identifier for a Push across branches
        self.push_uuid = "{}/{}".format(self.branch, self.rev)
Exemple #7
0
    def get_candidate_regressions(self, runnable_type):
        """Retrieve the set of "runnables" that are regression candidates for this push.

        A "runnable" can be any group of tests, e.g. a label, a manifest across platforms,
        a manifest on a given platform.

        A candidate regression is any runnable for which at least one
        associated task failed (therefore including intermittents), and which
        is either not classified or fixed by commit.

        Returns:
            set: Set of runnable names (str).
        """
        failclass = ("not classified", "fixed by commit")

        passing_runnables = set()
        candidate_regressions = {}

        count = 0
        other = self
        while count < MAX_DEPTH + 1:
            for name, summary in getattr(other, f"{runnable_type}_summaries").items():
                if name in passing_runnables:
                    # It passed in one of the pushes between the current and its
                    # children, so it is definitely not a regression in the current.
                    continue

                if summary.status == Status.PASS:
                    passing_runnables.add(name)
                    continue

                if all(c not in failclass for c, n in summary.classifications):
                    passing_runnables.add(name)
                    continue

                fixed_by_commit_classification_notes = [
                    n[:12]
                    for c, n in summary.classifications
                    if c == "fixed by commit"
                    if n is not None
                ]
                if len(fixed_by_commit_classification_notes) > 0:
                    if (
                        self.backedout
                        and self.backedoutby[:12]
                        in fixed_by_commit_classification_notes
                    ):
                        # If the failure was classified as fixed by commit, and the fixing commit
                        # is a backout of the current push, it is definitely a regression of the
                        # current push.
                        candidate_regressions[name] = (-math.inf, summary.status)
                        continue
                    elif all(
                        HGMO.create(n, branch=self.branch).is_backout
                        for n in fixed_by_commit_classification_notes
                    ):
                        # If the failure was classified as fixed by commit, and the fixing commit
                        # is a backout of another push, it is definitely not a regression of the
                        # current push.
                        passing_runnables.add(name)
                        continue

                if name in candidate_regressions:
                    # It failed in one of the pushes between the current and its
                    # children, we don't want to increase the previous distance.
                    continue

                candidate_regressions[name] = (count, summary.status)

            other = other.child
            count += 1

        return candidate_regressions
Exemple #8
0
    def _is_classified_as_cause(self, first_appareance_push, classifications):
        """Checks a 'fixed by commit' classification to figure out what push it references.

        Returns:
            bool or None: True, if the classification references this push.
                          False, if the classification references another push.
                          None, if it is not clear what the classification references.
        """
        fixed_by_commit_classification_notes = set(n[:12]
                                                   for c, n in classifications
                                                   if c == "fixed by commit"
                                                   if n is not None)

        if len(fixed_by_commit_classification_notes) == 0:
            return None

        # If the failure was classified as fixed by commit, and the fixing commit
        # is a backout of the current push, it is definitely a regression of the
        # current push.
        # If the failure was classified as fixed by commit, and the fixing commit
        # is a backout of another push, it is definitely not a regression of the
        # current push.
        # Unless some condition holds which makes us doubt about the correctness of the
        # classification.
        # - the backout commit also backs out one of the commits of this push;
        # - the other push backed-out by the commit which is mentioned in the classification
        #   is landed after the push where the failure occurs (so, it can't have caused it);
        # - the backout push also contains a commit backing out one of the commits of this push.
        for classification_note in fixed_by_commit_classification_notes:
            try:
                fix_hgmo = HGMO.create(classification_note, branch=self.branch)
                if len(fix_hgmo.backouts) == 0:
                    continue
            except PushNotFound:
                logger.warning(
                    f"Classification note ({classification_note}) references a revision which does not exist on push {first_appareance_push.rev}"
                )
                return None

            self_fix = None
            other_fixes = set()

            # If the backout commit also backs out one of the commits of this push, then
            # we can consider it as a regression of this push.
            # NOTE: this should never happen in practice because of current development
            # practices.
            for backout, backedouts in fix_hgmo.backouts.items():
                if backout[:12] != classification_note[:12]:
                    continue

                if any(rev[:12] in
                       {backedout[:12]
                        for backedout in backedouts} for rev in self.revs):
                    self_fix = backout[:12]
                    break

            # Otherwise, if the backout push also contains the backout commit of this push,
            # we can consider it as a regression of this push.
            if self.backedout:
                for backout in fix_hgmo.backouts:
                    if backout[:12] == self.backedoutby[:12]:
                        self_fix = backout[:12]
                        break

            # If one of the commits in the backout push is a bustage fix, then we could
            # consider it as a regression of this push.
            if self_fix is None:
                for bug in self.bugs:
                    if bug in fix_hgmo.bugs_without_backouts:
                        self_fix = fix_hgmo.bugs_without_backouts[bug][:12]
                        break

            # Otherwise, as long as the commit which was backed-out was landed **before**
            # the appearance of this failure, we can be sure it was its cause and so
            # the current push is not at fault.
            # TODO: We should actually check if the failure was already happening in the parents
            # and compare the backout push ID with the the ID of first parent where it failed.
            for backout, backedouts in fix_hgmo.backouts.items():
                if self.backedout and backout[:12] == self.backedoutby[:12]:
                    continue

                if any(
                        HGMO.create(backedout, branch=self.branch).pushid <=
                        first_appareance_push.id for backedout in backedouts):
                    other_fixes.add(backout[:12])

            # If the backout push contains a bustage fix of another push, then we could
            # consider it as a regression of another push.
            if len(fix_hgmo.bugs_without_backouts) > 0:
                other_parent = first_appareance_push
                for i in range(MAX_DEPTH):
                    if other_parent != self:
                        for bug in other_parent.bugs:
                            if bug in fix_hgmo.bugs_without_backouts:
                                other_fixes.add(
                                    fix_hgmo.bugs_without_backouts[bug][:12])

                    other_parent = other_parent.parent

            if self_fix and other_fixes:
                # If the classification points to a commit in the middle of the backout push and not the backout push head,
                # we can consider the classification to be correct.
                if (self_fix != fix_hgmo.pushhead[:12]
                        and classification_note[:12] == self_fix
                        and classification_note[:12] not in other_fixes):
                    return True
                elif any(other_fix != fix_hgmo.pushhead[:12]
                         and classification_note[:12] == other_fix
                         and classification_note[:12] != self_fix
                         for other_fix in other_fixes):
                    return False

                return None

            if self_fix:
                return True

            if other_fixes:
                return False

        return None
Exemple #9
0
    def parent(self):
        """Returns the parent push of this push.

        Returns:
            Push: A `Push` instance representing the parent push.

        Raises:
            :class:`~mozci.errors.ParentPushNotFound`: When no suitable parent
                push can be detected.
        """
        # Mozilla-unified and try allow multiple heads, so we can't rely on
        # `self.id - 1` to be the parent.
        if self.branch not in ("mozilla-unified", "try"):
            return self.create_push(self.id - 1)

        changesets = [
            c for c in self._hgmo.changesets if c.get("phase") == "draft"
        ]
        if not changesets:
            # Supports mozilla-unified as well as older automationrelevance
            # files that don't contain the phase.
            changesets = self._hgmo.changesets

        parents = changesets[0]["parents"]
        if len(parents) > 1:
            raise ParentPushNotFound("merge commits are unsupported",
                                     rev=self.rev,
                                     branch=self.branch)

        # Search for this revision in the following repositories. We search
        # autoland last as it would run the fewest tasks, so a push from one of
        # the other repositories would be preferable.
        branches = ("mozilla-central", "mozilla-beta", "mozilla-release",
                    "autoland")
        found_on = []
        parent_rev = parents[0]
        for branch in branches:
            try:
                hgmo = HGMO.create(parent_rev, branch=branch)
                head = hgmo.changesets[0]["pushhead"]
            except PushNotFound:
                continue

            found_on.append(branch)

            # Revision exists in repo but is not the 'pushhead', so keep searching.
            if head != parent_rev:
                continue

            return Push(parent_rev, branch=branch)

        if found_on:
            branches = found_on
            reason = "was not a push head"
        else:
            # This should be extremely rare (if not impossible).
            reason = "was not found"

        branches = [
            b[len("mozilla-"):] if b.startswith("mozilla-") else b
            for b in branches
        ]
        msg = f"parent revision '{parent_rev[:12]}' {reason} on any of {', '.join(branches)}"
        raise ParentPushNotFound(msg, rev=self.rev, branch=self.branch)
Exemple #10
0
def test_hgmo_backouts(responses):
    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={
            "changesets": [{
                "node": "789",
                "backsoutnodes": [],
                "pushhead": "789"
            }]
        },
        status=200,
    )

    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={
            "changesets": [{
                "node": "789",
                "backsoutnodes": [{
                    "node": "123456"
                }],
                "pushhead": "789",
            }]
        },
        status=200,
    )

    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={
            "changesets": [
                {
                    "node": "789",
                    "backsoutnodes": [{
                        "node": "123456"
                    }],
                    "pushhead": "789",
                },
                {
                    "node": "jkl",
                    "backsoutnodes": [{
                        "node": "asd"
                    }, {
                        "node": "fgh"
                    }]
                },
            ]
        },
        status=200,
    )

    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/abcdef",
        json={
            "changesets": [
                {
                    "node": "789",
                    "backsoutnodes": [{
                        "node": "123456"
                    }],
                    "pushhead": "ghi",
                },
            ]
        },
        status=200,
    )

    responses.add(
        responses.GET,
        "https://hg.mozilla.org/integration/autoland/json-automationrelevance/ghi",
        json={
            "changesets": [
                {
                    "node": "ghi",
                    "backsoutnodes": [{
                        "node": "789"
                    }],
                    "pushhead": "ghi"
                },
                {
                    "node": "789",
                    "backsoutnodes": [{
                        "node": "123456"
                    }],
                    "pushhead": "ghi",
                },
            ]
        },
        status=200,
    )

    h = HGMO("abcdef")
    assert h.backouts == {}
    assert h.changesets[0]["backsoutnodes"] == []

    h = HGMO("abcdef")
    assert h.backouts == {"789": ["123456"]}
    assert h.changesets[0]["backsoutnodes"] == [{"node": "123456"}]

    h = HGMO("abcdef")
    assert h.backouts == {"789": ["123456"], "jkl": ["asd", "fgh"]}
    assert h.changesets[0]["backsoutnodes"] == [{"node": "123456"}]
    assert h.changesets[1]["backsoutnodes"] == [{
        "node": "asd"
    }, {
        "node": "fgh"
    }]

    h = HGMO("abcdef")
    assert h.backouts == {"789": ["123456"], "ghi": ["789"]}
    assert h.changesets[0]["backsoutnodes"] == [{"node": "123456"}]