<reponame>allaparthi/monorail
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from google.appengine.ext import ndb


class GitilesCommit(ndb.Model):
  """A class for a gitiles commit."""

  @classmethod
  def _CreateKey(cls, gitiles_host, gitiles_project, gitiles_ref, gitiles_id):
    return ndb.Key(
        cls.__name__, '{}/{}/{}/{}'.format(gitiles_host, gitiles_project,
                                           gitiles_ref, gitiles_id))

  @classmethod
  def Get(cls, gitiles_host, gitiles_project, gitiles_ref, gitiles_id):
    return cls._CreateKey(gitiles_host, gitiles_project, gitiles_ref,
                          gitiles_id).get()

  @classmethod
  def Create(cls, gitiles_host, gitiles_project, gitiles_ref, gitiles_id,
             commit_position):
    """Creates an entity for a commit."""
    return cls(
        key=cls._CreateKey(gitiles_host, gitiles_project, gitiles_ref,
                           gitiles_id),
        gitiles_host=gitiles_host,
        gitiles_project=gitiles_project,
        gitiles_ref=gitiles_ref,
        gitiles_id=gitiles_id,
        commit_position=commit_position)

  # Gitiles hostname, e.g. 'chromium.googlesource.com'.
  gitiles_host = ndb.StringProperty(required=True)

  # Project name the commit belongs to, e.g. 'chromium/src'.
  gitiles_project = ndb.StringProperty(required=True)

  # Associated git ref of the commit, e.g. 'refs/heads/master'.
  # NOT a branch name: must start with 'refs/'.
  # If not specified when query build, default to 'refs/heads/master'.
  gitiles_ref = ndb.StringProperty(required=True)

  # SHA1 of the change.
  # May be called as 'revision' or 'git_hash' somewhere else.
  gitiles_id = ndb.StringProperty(required=True)

  # Integer identifier of a commit, required to sort commits.
  commit_position = ndb.IntegerProperty(required=True)


class Hint(ndb.Model):
  """Describes a hint produced by heuristic analysis and its score.

  I.e. A fact about a commit that makes it a suspect.
  """

  # A short, human-readable string that concisely describes a fact about the
  # suspect. e.g. 'add a/b/x.cc'
  content = ndb.StringProperty(indexed=False)

  # A score value for the hint, where a higher score means a stronger signal
  # that the suspect is indeed responsible for a given failure.
  score = ndb.IntegerProperty(indexed=False)


class Suspect(GitilesCommit):
  """Identifies a commit that is suspected of being a culprit for a failure."""

  # Facts about this commit that make it a suspect.
  hints = ndb.StructuredProperty(Hint, repeated=True)

  @property
  def score(self):
    return sum(h.score for h in self.hints)

  @classmethod
  def GetOrCreate(cls,
                  gitiles_host,
                  gitiles_project,
                  gitiles_ref,
                  gitiles_id,
                  commit_position=None,
                  hints=None):
    """Gets or Creates a Suspect entity.

    This also updates the entity if needed.
    """
    updated = False
    suspect = cls.Get(gitiles_host, gitiles_project, gitiles_ref, gitiles_id)
    if not suspect:
      suspect = cls.Create(gitiles_host, gitiles_project, gitiles_ref,
                           gitiles_id, commit_position)
      updated = True

    if hints:
      suspect.hints = suspect.hints or []
      for content, score in hints.iteritems():
        new_hint = Hint(content=content, score=score)
        if new_hint not in suspect.hints:
          suspect.hints.append(new_hint)
          updated = True

    if updated:
      suspect.put()
    return suspect


class Culprit(GitilesCommit):
  """Base class for a suspected or culprit commit."""
  # Urlsafe_keys to atom failures this culprit is responsible for.
  # Uses urlsafe_keys so that it can accept both compile and test failures.
  failure_urlsafe_keys = ndb.StringProperty(repeated=True)


  @classmethod
  # pylint: disable=arguments-differ
  def Create(cls,
             gitiles_host,
             gitiles_project,
             gitiles_ref,
             gitiles_id,
             commit_position,
             failure_urlsafe_keys=None):
    """Creates an entity for a culprit."""
    return cls(
        key=cls._CreateKey(gitiles_host, gitiles_project, gitiles_ref,
                           gitiles_id),
        gitiles_host=gitiles_host,
        gitiles_project=gitiles_project,
        gitiles_ref=gitiles_ref,
        gitiles_id=gitiles_id,
        commit_position=commit_position,
        failure_urlsafe_keys=failure_urlsafe_keys or [])

  @classmethod
  def GetOrCreate(cls,
                  gitiles_host,
                  gitiles_project,
                  gitiles_ref,
                  gitiles_id,
                  commit_position=None,
                  failure_urlsafe_keys=None):
    """Gets or Creates a Culprit entity.

    If failure_urlsafe_keys provided, update the culprit as well.
    """
    updated = False
    culprit = cls.Get(gitiles_host, gitiles_project, gitiles_ref, gitiles_id)
    if not culprit:
      culprit = cls.Create(gitiles_host, gitiles_project, gitiles_ref,
                           gitiles_id, commit_position)
      updated = True

    if failure_urlsafe_keys:
      culprit.failure_urlsafe_keys = list(
          set(culprit.failure_urlsafe_keys) | set(failure_urlsafe_keys))
      updated = True

    if updated:
      culprit.put()
    return culprit
