blob: 0aa30daa14a8fffbd785cb89bcbea83164e28c5c [file] [log] [blame]
# Copyright 2018 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.
import datetime
import json
import logging
from urlparse import urlparse
from google.appengine.ext import ndb
from google.protobuf.timestamp_pb2 import Timestamp
from libs import analysis_status
from libs import time_util
from model.flake.analysis.master_flake_analysis import MasterFlakeAnalysis
from model.proto.gen import findit_pb2
from model.proto.gen.compile_analysis_pb2 import CompileAnalysisCompletionEvent
from model.proto.gen.test_analysis_pb2 import TestAnalysisCompletionEvent
from model.wf_suspected_cl import WfSuspectedCL
from model.wf_try_job import WfTryJob
from services import bigquery_helper
from services.flake_failure.pass_rate_util import IsFullyStable
# Constants to report events to.
_PROJECT_ID = 'findit-for-me'
_DATASET_ID = 'events'
_TABLE_ID_TEST = 'test'
_TABLE_ID_COMPILE = 'compile'
# Culprit constants.
_DEFAULT_HOST = 'chromium-review.googlesource.com'
_DEFAULT_PROJECT = 'chromium'
_DEFAULT_REF = 'refs/heads/master'
# Start of unix epoch time.
_EPOCH_START = datetime.datetime.utcfromtimestamp(0)
def ReportCompileFailureAnalysisCompletionEvent(analysis):
"""Creates a proto from analysis and sends it to Bigquery.
Extracts compile information into a schema proto and sends to BQ.
"""
proto = CreateCompileFailureAnalysisCompletionEvent(analysis)
event_id = analysis.key.urlsafe()
return bigquery_helper.ReportEventsToBigquery(
[(proto, event_id)], _PROJECT_ID, _DATASET_ID, _TABLE_ID_COMPILE)
def ReportTestFailureAnalysisCompletionEvent(analysis):
"""Creates a proto from analysis and sends it to Bigquery.
Extracts test failure information into schema protos and sends them to BQ.
"""
events_and_ids = []
for proto in CreateTestFailureAnalysisCompletionEvent(analysis):
events_and_ids.append((proto, analysis.key.urlsafe() + proto.test_name))
if not events_and_ids: # If there are no events, return.
return None
return bigquery_helper.ReportEventsToBigquery(events_and_ids, _PROJECT_ID,
_DATASET_ID, _TABLE_ID_TEST)
def ReportTestFlakeAnalysisCompletionEvent(analysis):
"""Creates a proto from analysis and sends it to Bigquery.
Extracts test flake information into schema protos and sends it to BQ.
"""
proto = CreateTestFlakeAnalysisCompletionEvent(analysis)
event_id = analysis.key.urlsafe()
return bigquery_helper.ReportEventsToBigquery(
[(proto, event_id)], _PROJECT_ID, _DATASET_ID, _TABLE_ID_TEST)
def _ExtractBaseAnalysisInfo(analysis, event):
"""Extracts base information and stores it in a proto.
Args:
analysis (BaseBuildModel, BaseAnalysis): Analysis to be extracted from.
event (TestAnalysisCompletionEvent,
CompileAnalysisCompletionEvent): Event proto to be written to.
Returns:
Event proto given as an argument.
"""
if isinstance(analysis, MasterFlakeAnalysis):
master_name = analysis.original_master_name or analysis.master_name
builder_name = analysis.original_builder_name or analysis.builder_name
build_number = analysis.original_build_number or analysis.build_number
else:
master_name = analysis.master_name
builder_name = analysis.builder_name
build_number = analysis.build_number
event.analysis_info.master_name = master_name
event.analysis_info.builder_name = builder_name
event.analysis_info.detected_build_number = build_number
return event
def _ExtractAnalysisTimestampInfo(analysis, event):
"""Extracts general information and stores it in a proto.
Args:
analysis (BaseBuildModel, BaseAnalysis): Analysis to be extracted from.
event (TestAnalysisCompletionEvent,
CompileAnalysisCompletionEvent): Event proto to be written to.
Returns:
Event proto given as an argument.
"""
def unix_time(dt):
return int((dt - _EPOCH_START).total_seconds())
seconds = unix_time(analysis.start_time)
event.analysis_info.timestamp.started.FromSeconds(seconds)
seconds = unix_time(time_util.GetUTCNow())
event.analysis_info.timestamp.completed.FromSeconds(seconds)
return event
def _ExtractGeneralAnalysisInfo(analysis, event):
_ExtractBaseAnalysisInfo(analysis, event)
_ExtractAnalysisTimestampInfo(analysis, event)
def _ExtractSuspectsForWfAnalysis(analysis, event):
"""Extracts information from Wf analysis and stores it in a proto.
Args:
(WfAnalysis): Analysis to be extracted from.
(*AnalysisCompletionEventProto): Event proto to be written to.
Returns:
Event proto given as an argument.
"""
suspects = analysis.suspected_cls or []
for suspect in suspects:
# Skips the culprit which is also included in this list.
# top_score here will be None in the culprit case, so a default value
# of 1 is used instead.
if suspect.get('top_score', 1) is None or suspect.get('failures'):
continue
commit = event.analysis_info.suspects.add()
commit.host = (urlparse(suspect.get('url', '')).netloc or _DEFAULT_HOST)
commit.project = suspect['repo_name']
commit.ref = _DEFAULT_REF
commit.revision = suspect['revision']
return event
def _ExtractCulpritForDict(culprit, event):
"""Extracts information from culprit and stores it in proto.
Args:
(dict): Dictionary with culprit info (compile results or test results)
(*AnalysisCompletionEventProto): Event proto to be written to.
"""
event.analysis_info.culprit.host = (
urlparse(culprit['url']).netloc or _DEFAULT_HOST)
event.analysis_info.culprit.project = culprit['repo_name']
event.analysis_info.culprit.ref = _DEFAULT_REF
event.analysis_info.culprit.revision = culprit['revision']
def _SetActionsForEvent(event):
"""Sets the actions for an analysis event.
Args:
(*AnalysisCompletionEventProto): Event proto to be written to.
"""
if event.analysis_info.culprit.host:
# If there's a culprit.host, then the SuspectedCL exists.
culprit_cl = WfSuspectedCL.Get(event.analysis_info.culprit.project,
event.analysis_info.culprit.revision)
assert culprit_cl
if culprit_cl.revert_submission_status == analysis_status.COMPLETED:
event.analysis_info.actions.append(findit_pb2.REVERT_SUBMITTED)
if culprit_cl.revert_status == analysis_status.COMPLETED:
event.analysis_info.actions.append(findit_pb2.REVERT_CREATED)
if culprit_cl.cr_notification_status == analysis_status.COMPLETED:
event.analysis_info.actions.append(findit_pb2.CL_COMMENTED)
def _SetOutcomesForEvent(event):
"""Sets the outcomes for an analysis event.
Args:
(*AnalysisCompletionEventProto): Event proto to be written to.
"""
if event.analysis_info.culprit.host:
# Culprit was identified from a regression range.
event.analysis_info.outcomes.append(findit_pb2.CULPRIT)
if event.analysis_info.suspects:
# Suspects were identified from a regression range but no culprit was found.
event.analysis_info.outcomes.append(findit_pb2.SUSPECT)
def CreateCompileFailureAnalysisCompletionEvent(analysis):
"""Transforms a compile failure analysis to an event proto.
Args:
analysis (WfAnalysis): The analysis to be transformed.
Returns:
(CompileAnalysisCompletionEvent) Proto used to report to BQ table.
"""
event = CompileAnalysisCompletionEvent()
_ExtractGeneralAnalysisInfo(analysis, event)
event.analysis_info.step_name = 'compile'
if (analysis.failure_info and analysis.failure_info.get('failed_steps') and
analysis.failure_info['failed_steps'].get('compile') and
analysis.failure_info['failed_steps']['compile'].get('first_failure')):
event.analysis_info.culprit_build_number = (
analysis.failure_info['failed_steps']['compile']['first_failure'])
try_job = WfTryJob.Get(analysis.master_name, analysis.builder_name,
event.analysis_info.culprit_build_number)
# Culprit.
if (try_job and try_job.compile_results and
try_job.compile_results[-1].get('culprit') and
try_job.compile_results[-1]['culprit'].get('compile')):
_ExtractCulpritForDict(try_job.compile_results[-1]['culprit']['compile'],
event)
event = _ExtractSuspectsForWfAnalysis(analysis, event)
if (analysis.signals and analysis.signals.get('compile') and
analysis.signals['compile'].get('failed_edges')):
# Use a set to avoid adding duplicates here.
rules = set()
for edge in analysis.signals['compile']['failed_edges']:
rules.add(edge['rule'])
event.failed_build_rules.extend(rules)
# Outcomes.
_SetOutcomesForEvent(event)
# Actions.
_SetActionsForEvent(event)
return event
def CreateTestFailureAnalysisCompletionEvent(analysis):
"""Transforms a test failure analysis into an event proto.
Args:
analysis (WfAnalysis): The analysis to be transformed.
Returns:
([TestAnalysisCompletionEvent]) Proto used to report to BQ table.
"""
events = []
for step in analysis.failure_info.get('failed_steps', {}):
for test in analysis.failure_info['failed_steps'][step].get('tests') or {}:
if analysis.flaky_tests and test in analysis.flaky_tests.get(step, []):
# The test is flaky, should report it in flake analysis.
continue
# If the failure result mapping isn't there, then bailout since it
# contains required information.
if (not analysis.failure_result_map or
not analysis.failure_result_map.get(step) or
not analysis.failure_result_map[step].get(test)):
continue
event = TestAnalysisCompletionEvent()
event.flake = False
_ExtractGeneralAnalysisInfo(analysis, event)
event.analysis_info.step_name = step
event.test_name = test
# Extract master/builder/build_number from failure_result_Map.
master, builder, build_number = (
analysis.failure_result_map[step][test].split('/'))
event.analysis_info.culprit_build_number = int(build_number)
# Culprit.
try_job = WfTryJob.Get(master, builder, build_number)
if (try_job and try_job.test_results and
try_job.test_results[-1].get('culprit') and
try_job.test_results[-1]['culprit'].get(step) and
try_job.test_results[-1]['culprit'][step].get('tests') and
try_job.test_results[-1]['culprit'][step]['tests'].get(test)):
_ExtractCulpritForDict(
try_job.test_results[-1]['culprit'][step]['tests'][test], event)
event = _ExtractSuspectsForWfAnalysis(analysis, event)
# Outcomes.
_SetOutcomesForEvent(event)
# Actions.
_SetActionsForEvent(event)
events.append(event)
return events
def CreateTestFlakeAnalysisCompletionEvent(analysis):
"""Transforms a flake analysis to an event proto.
Args:
analysis (MasterFlakeAnalysis): The analysis to be transformed.
Returns:
(TestAnalysisCompletionEvent) Proto used to report to BQ table.
"""
event = TestAnalysisCompletionEvent()
event.flake = True
_ExtractGeneralAnalysisInfo(analysis, event)
event.analysis_info.step_name = (
analysis.original_step_name or analysis.step_name)
event.test_name = analysis.original_test_name or analysis.test_name
culprit_key = analysis.culprit_urlsafe_key
culprit = None
if culprit_key:
culprit = ndb.Key(urlsafe=culprit_key).get()
assert culprit
event.analysis_info.culprit.host = _DEFAULT_HOST
event.analysis_info.culprit.project = _DEFAULT_PROJECT
event.analysis_info.culprit.ref = _DEFAULT_REF
event.analysis_info.culprit.confidence = analysis.confidence_in_culprit
event.analysis_info.culprit.revision = culprit.revision
suspect_keys = analysis.suspect_urlsafe_keys or []
suspects = [
ndb.Key(urlsafe=suspect_key).get() for suspect_key in suspect_keys
]
for suspect in suspects:
commit = event.analysis_info.suspects.add()
commit.host = _DEFAULT_HOST
commit.project = _DEFAULT_PROJECT
commit.ref = _DEFAULT_REF
commit.revision = suspect.revision
# Outcomes.
# TODO(crbug.com/805243): Track these outcomes explicitly in
# master_flake_analysis.
if suspects:
# Heuristic analysis suggested suspects.
event.analysis_info.outcomes.append(findit_pb2.SUSPECT)
if culprit:
# Culprit was identified from a regression range.
event.analysis_info.outcomes.append(findit_pb2.CULPRIT)
else:
if len(analysis.data_points) > 1:
# Long standing flake.
event.analysis_info.outcomes.append(findit_pb2.REPRODUCIBLE)
elif (analysis.data_points and
IsFullyStable(analysis.data_points[0].pass_rate)):
# More than one datapoint is required for a reproducible result.
event.analysis_info.outcomes.append(findit_pb2.NOT_REPRODUCIBLE)
# Actions.
if analysis.has_commented_on_cl:
event.analysis_info.actions.append(findit_pb2.CL_COMMENTED)
if analysis.has_commented_on_bug:
event.analysis_info.actions.append(findit_pb2.BUG_COMMENTED)
if analysis.has_filed_bug:
event.analysis_info.actions.append(findit_pb2.BUG_CREATED)
if analysis.has_created_autorevert:
event.analysis_info.actions.append(findit_pb2.REVERT_CREATED)
if analysis.has_submitted_autorevert:
event.analysis_info.actions.append(findit_pb2.REVERT_SUBMITTED)
return event