def testAnalyzeCompileFailurePipelineNotAbortedIfWithoutError(self, mock_mon): master_name = 'm' builder_name = 'b' build_number = 124 self._SetupAnalysis( master_name, builder_name, build_number, status=analysis_status.COMPLETED) pipeline_input = AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable({}), build_completed=False, force=True) root_pipeline = AnalyzeCompileFailurePipeline(pipeline_input) root_pipeline.OnAbort(pipeline_input) analysis = WfAnalysis.Get(master_name, builder_name, build_number) self.assertIsNotNone(analysis) self.assertNotEqual(analysis_status.ERROR, analysis.status) self.assertFalse(mock_mon.called)
def testCompileNotSupport(self): failure_info = copy.deepcopy(_COMPILE_FAILURE_INFO) failure_info['failed_steps']['compile']['supported'] = False self.assertEqual({}, extract_compile_signal.ExtractSignalsForCompileFailure( CompileFailureInfo.FromSerializable(failure_info), None))
def testAnalyzeCompileFailurePipelineAbortedIfWithError(self, mock_mon): master_name = 'm' builder_name = 'b' build_number = 124 self._SetupAnalysis( master_name, builder_name, build_number, status=analysis_status.RUNNING) pipeline_input = AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable({}), build_completed=False, force=True) root_pipeline = AnalyzeCompileFailurePipeline(pipeline_input) root_pipeline.OnAbort(pipeline_input) analysis = WfAnalysis.Get(master_name, builder_name, build_number) self.assertIsNotNone(analysis) self.assertEqual(analysis_status.ERROR, analysis.status) self.assertIsNone(analysis.result_status) self.assertTrue(analysis.aborted) mock_mon.assert_called_once_with(master_name, builder_name, analysis_status.ERROR, analysis_approach_type.HEURISTIC)
def testAnalyzeCompileFailurePipelineStartTryJob(self, mocked_pipeline, mock_log, mock_mon): master_name = 'm' builder_name = 'b' build_number = 124 failure_info = { 'failed_steps': { 'compile': { 'last_pass': 122, 'current_failure': 123, 'first_failure': 123 } } } self._SetupAnalysis( master_name, builder_name, build_number, status=analysis_status.RUNNING, signals={}, failure_info=failure_info) pipeline_input = AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable({}), build_completed=False, force=False) root_pipeline = AnalyzeCompileFailurePipeline(pipeline_input) root_pipeline.OnAbort(pipeline_input) heuristic_result = CompileHeuristicAnalysisOutput.FromSerializable({ 'failure_info': failure_info, 'signals': {}, 'heuristic_result': None }) start_try_job_params = StartCompileTryJobInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), heuristic_result=heuristic_result, build_completed=False, force=False) mocked_pipeline.assert_called_once_with(start_try_job_params) mocked_pipeline.assert_has_calls( [mock.call().start(queue_name=constants.WATERFALL_ANALYSIS_QUEUE)]) mock_log.assert_called_once_with( 'A try job pipeline for build %s, %s, %s starts after heuristic ' 'analysis was aborted. Check pipeline at: %s.', master_name, builder_name, build_number, root_pipeline.pipeline_status_path) mock_mon.assert_called_once_with(master_name, builder_name, analysis_status.ERROR, analysis_approach_type.HEURISTIC)
def GetCompileFailureInfo(self, context, build, first_failures_in_current_build): """Creates structured object expected by heuristic analysis code.""" # As per common/waterfall/failure_type.py LEGACY_COMPILE_TYPE = 0x08 return CompileFailureInfo.FromSerializable({ 'failed_steps': { 'compile': { 'supported': True, 'last_pass': first_failures_in_current_build['last_passed_build'] ['number'], 'current_failure': build.number, 'first_failure': build.number, }, }, 'master_name': build.input.properties['mastername'], 'builder_name': build.builder.builder, 'build_number': build.number, 'parent_mastername': None, # These only apply to some testers. 'parent_buildername': None, 'builds': { build.number: { # Construct a list of revisions since the last passing build. 'blame_list': git.GetCommitsBetweenRevisionsInOrder( first_failures_in_current_build['last_passed_build'] ['commit_id'], context.gitiles_id, ascending=False), 'chromium_revision': context.gitiles_id, }, }, 'failure_type': LEGACY_COMPILE_TYPE, 'failed': True, 'chromium_revision': context.gitiles_id, 'is_luci': True, 'buildbucket_bucket': 'luci.%s.%s' % (build.builder.project, build.builder.bucket), 'buildbucket_id': str(build.id), })
def testBuildFailurePipelineFlow(self): master_name = 'm' builder_name = 'b' build_number = 124 current_failure_info = {} self._SetupAnalysis(master_name, builder_name, build_number) heuristic_params = CompileHeuristicAnalysisParameters.FromSerializable({ 'failure_info': current_failure_info, 'build_completed': False }) heuristic_output = CompileHeuristicAnalysisOutput.FromSerializable({ 'failure_info': None, 'signals': None, 'heuristic_result': {} }) self.MockSynchronousPipeline( analyze_compile_failure_pipeline.HeuristicAnalysisForCompilePipeline, heuristic_params, heuristic_output) start_try_job_params = StartCompileTryJobInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), heuristic_result=heuristic_output, build_completed=False, force=False) self.MockGeneratorPipeline( analyze_compile_failure_pipeline.StartCompileTryJobPipeline, start_try_job_params, False) report_event_input = report_event_pipeline.ReportEventInput( analysis_urlsafe_key=WfAnalysis.Get(master_name, builder_name, build_number).key.urlsafe()) self.MockGeneratorPipeline( report_event_pipeline.ReportAnalysisEventPipeline, report_event_input, None) pipeline_input = AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable( current_failure_info), build_completed=False, force=False) root_pipeline = AnalyzeCompileFailurePipeline(pipeline_input) root_pipeline.start(queue_name=constants.DEFAULT_QUEUE) self.execute_queued_tasks() analysis = WfAnalysis.Get(master_name, builder_name, build_number) self.assertEqual(analysis_status.RUNNING, analysis.status)
def testGetCompileStepSignalFromNinjaJsonOutput(self, _): master_name = 'm' builder_name = 'b' build_number = 123 self._CreateAndSaveWfAnanlysis(master_name, builder_name, build_number) signals = extract_compile_signal.ExtractSignalsForCompileFailure( CompileFailureInfo.FromSerializable(_COMPILE_FAILURE_INFO), None) expected_failed_edges = [{ 'output_nodes': ['a/b.o'], 'rule': 'CXX', 'dependencies': ['b.h', 'b.c'] }] self.assertEqual(expected_failed_edges, signals['compile']['failed_edges'])
def testAnalyzeCompileFailureNotSupported(self, _): failure_info = { 'master_name': 'm', 'builder_name': 'b', 'build_number': 123, 'failed_steps': { 'compile': { 'current_failure': 123, 'first_failure': 121 } }, 'builds': {} } analysis_result, _ = compile_failure_analysis.AnalyzeCompileFailure( CompileFailureInfo.FromSerializable(failure_info), None, None, {'compile': {}}) self.assertEqual({'failures': []}, analysis_result)
def testUpdateAbortedAnalysisNoTryJob(self): master_name = 'm' builder_name = 'b' build_number = 124 parameter = AnalyzeCompileFailureInput( build_key=BuildKey(master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable({}), build_completed=False, force=True) WfAnalysis.Create(master_name, builder_name, build_number).put() analysis, run_try_job, heuristic_aborted = ( build_failure_analysis.UpdateAbortedAnalysis(parameter)) self.assertEqual(analysis_status.ERROR, analysis.status) self.assertTrue(analysis.aborted) self.assertFalse(run_try_job) self.assertTrue(heuristic_aborted)
def testGetGoodRevisionNoLastPass(self): failed_steps = { 'a': { 'current_failure': 124, 'first_failure': 124, } } builds = { 124: { 'chromium_revision': '124_git_hash', 'blame_list': ['124_git_hash'] } } failed_steps = BaseFailedSteps.FromSerializable(failed_steps) builds = FailureInfoBuilds.FromSerializable(builds) failure_info = CompileFailureInfo( build_number=124, failed_steps=failed_steps, builds=builds) self.assertIsNone(ci_failure.GetGoodRevision(failure_info))
def testCompileNotInFailedSteps(self): failure_info = { 'master_name': 'm', 'builder_name': 'b', 'build_number': 123, 'failed': True, 'chromium_revision': 'a_git_hash', 'failed_steps': { 'a': { 'last_pass': 122, 'current_failure': 123, 'first_failure': 123, } } } self.assertEqual({}, extract_compile_signal.ExtractSignalsForCompileFailure( CompileFailureInfo.FromSerializable(failure_info), None))
def testOnFinalized(self, mock_mon): master_name = 'm' builder_name = 'b' build_number = 124 self._SetupAnalysis( master_name, builder_name, build_number, status=analysis_status.COMPLETED) pipeline_input = AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable({}), build_completed=False, force=True) root_pipeline = AnalyzeCompileFailurePipeline(pipeline_input) root_pipeline.OnFinalized(pipeline_input) mock_mon.assert_called_once_with({'type': 'compile'})
def testCompileStepSignalFromCachedStepLog(self): master_name = 'm' builder_name = 'b' build_number = 123 step_name = 'compile' step = WfStep.Create(master_name, builder_name, build_number, step_name) step.log_data = _NINJA_OUTPUT_JSON step.put() self._CreateAndSaveWfAnanlysis(master_name, builder_name, build_number) signals = extract_compile_signal.ExtractSignalsForCompileFailure( CompileFailureInfo.FromSerializable(_COMPILE_FAILURE_INFO), None) expected_failed_edges = [{ 'output_nodes': ['a/b.o'], 'rule': 'CXX', 'dependencies': ['b.h', 'b.c'] }] self.assertEqual(expected_failed_edges, signals['compile']['failed_edges'])
def testStartCompilePipelineForNewAnalysis(self, mock_info): master_name = 'm' builder_name = 'b' build_number = 124 failure_info = { 'failed': True, 'chromium_revision': 'rev', 'failure_type': failure_type.COMPILE } mock_info.return_value = failure_info, True compile_pipeline_input = ( build_failure_analysis_pipelines.AnalyzeCompileFailureInput( build_key=BuildKey( master_name=master_name, builder_name=builder_name, build_number=build_number), current_failure_info=CompileFailureInfo.FromSerializable( failure_info), build_completed=False, force=False)) self.MockGeneratorPipeline( build_failure_analysis_pipelines.AnalyzeCompileFailurePipeline, compile_pipeline_input, None) build_failure_analysis_pipelines.ScheduleAnalysisIfNeeded( master_name, builder_name, build_number, failed_steps=['a'], build_completed=False, force=False, queue_name=constants.DEFAULT_QUEUE) analysis = WfAnalysis.Get(master_name, builder_name, build_number) self.assertIsNotNone(analysis)
def testGetGoodRevision(self): failed_steps = { 'a': { 'current_failure': 124, 'first_failure': 124, 'last_pass': 123 }, 'b': { 'current_failure': 124, 'first_failure': 124, }, 'c': { 'current_failure': 124, 'first_failure': 123, 'last_pass': 122 }, } builds = { 122: { 'chromium_revision': '122_git_hash', 'blame_list': ['122_git_hash'] }, 123: { 'chromium_revision': '123_git_hash', 'blame_list': ['123_git_hash'] }, 124: { 'chromium_revision': '124_git_hash', 'blame_list': ['124_git_hash'] } } failed_steps = BaseFailedSteps.FromSerializable(failed_steps) builds = FailureInfoBuilds.FromSerializable(builds) failure_info = CompileFailureInfo( build_number=124, failed_steps=failed_steps, builds=builds) self.assertEqual('122_git_hash', ci_failure.GetGoodRevision(failure_info))
def testAnalyzeCompileFailureNoSignal(self, mock_logging): failure_info = {'failed_steps': {'a': {}}} analysis_result, _ = compile_failure_analysis.AnalyzeCompileFailure( CompileFailureInfo.FromSerializable(failure_info), None, None, {}) self.assertEqual({'failures': []}, analysis_result)
def testHeuristicAnalysisForCompile(self, mock_result, mock_signals, mock_failure_info, mock_mon, *_): failure_info = { 'build_number': 213, 'master_name': 'chromium.win', 'builder_name': 'WinMSVC64 (dbg)', 'parent_mastername': None, 'parent_buildername': None, 'failed_steps': { 'compile': { 'last_pass': 212, 'current_failure': 213, 'first_failure': 213 } }, 'builds': { '212': { 'blame_list': ['3045acb501991e37fb2416ab8816d2ff4e66735f',], 'chromium_revision': 'c7388ba52388421e91c113ed807dec16b830c45b' }, '213': { 'blame_list': ['e282b48ad7a9715d132c649fe1aff9dde0347b1c',], 'chromium_revision': '2fefee0825b80ec3ebec5c661526818da9490180' } }, 'failure_type': 8, 'failed': True, 'chromium_revision': '2fefee0825b80ec3ebec5c661526818da9490180', } signals = { 'compile': { 'failed_edges': [{ 'dependencies': [ 'third_party/webrtc/media/base/codec.h', 'third_party/webrtc/rtc_base/sanitizer.h', ], 'output_nodes': ['obj/third_party/webrtc/media//file.obj'], 'rule': 'CXX' }], 'files': { 'c:/b/c/b/win/src/third_party/webrtc/media/engine/file.cc': [ 76 ] }, 'failed_targets': [{ 'source': '../../third_party/webrtc/media/engine/target1.cc', 'target': 'obj/third_party/webrtc/media//file.obj' }], 'failed_output_nodes': [ 'obj/third_party/webrtc/media/rtc_audio_video/fon.obj' ], 'keywords': {} } } mock_signals.return_value = signals heuristic_result = { 'failures': [{ 'first_failure': 213, 'supported': True, 'suspected_cls': [{ 'commit_position': 517979, 'url': 'url/0366f1a82a0d2c4e0b82a3632e1dff5ee0b35690', 'hints': { 'add a.cc': 5 }, 'score': 5, 'build_number': 213, 'revision': '0366f1a82a0d2c4e0b82a3632e1dff5ee0b35690', 'repo_name': 'chromium' }], 'step_name': 'compile', 'last_pass': 212, 'new_compile_suspected_cls': [{ 'commit_position': 517979, 'url': 'url/0366f1a82a0d2c4e0b82a3632e1dff5ee0b35690', 'hints': { 'add a.cc': 5 }, 'score': 5, 'build_number': 213, 'revision': '0366f1a82a0d2c4e0b82a3632e1dff5ee0b35690', 'repo_name': 'chromium' }], 'use_ninja_dependencies': True }] } mock_result.return_value = heuristic_result, [] mock_failure_info.return_value = CompileFailureInfo.FromSerializable( failure_info) WfAnalysis.Create('chromium.win', 'WinMSVC64 (dbg)', 213).put() heuristic_params = CompileHeuristicAnalysisParameters( failure_info=CompileFailureInfo.FromSerializable(failure_info), build_completed=True) result = compile_failure_analysis.HeuristicAnalysisForCompile( heuristic_params) expected_result = { 'failure_info': failure_info, 'signals': signals, 'heuristic_result': heuristic_result } self.assertEqual( result, CompileHeuristicAnalysisOutput.FromSerializable(expected_result)) mock_mon.assert_called_once_with('chromium.win', 'WinMSVC64 (dbg)', analysis_status.COMPLETED, analysis_approach_type.HEURISTIC)
def testFailedToGetFailureLog(self, *_): self.assertEqual( {}, extract_compile_signal.ExtractSignalsForCompileFailure( CompileFailureInfo.FromSerializable(_COMPILE_FAILURE_INFO), None))
def testAnalyzeCompileFailureByDependencies(self): failure_info = { 'master_name': 'm', 'builder_name': 'b', 'build_number': 123, 'failure_type': failure_type.COMPILE, 'failed': True, 'chromium_revision': 'r99_2', 'failed_steps': { 'compile': { 'current_failure': 99, 'first_failure': 98, 'supported': True, }, }, 'builds': { 99: { 'blame_list': ['r99_1', 'r99_2'], }, 98: { 'blame_list': ['r98_1'], }, 97: {} } } deps_info = {} failure_signals_json = { 'compile': { 'files': {}, 'failed_edges': [{ 'dependencies': ['src/a/b/f99_2.cc'] }] }, } expected_analysis_result = { 'failures': [{ 'step_name': 'compile', 'supported': True, 'first_failure': 98, 'last_pass': None, 'suspected_cls': [{ 'build_number': 99, 'repo_name': 'chromium', 'revision': 'r99_2', 'commit_position': None, 'url': None, 'score': 2, 'hints': { ('modified f99_2.cc (and it was in' ' dependencies found by ninja)'): 2, }, }], 'new_compile_suspected_cls': [{ 'build_number': 99, 'repo_name': 'chromium', 'revision': 'r99_2', 'commit_position': None, 'url': None, 'score': 2, 'hints': { ('modified f99_2.cc (and it was in' ' dependencies found by ninja)'): 2, }, }], 'use_ninja_dependencies': True, }] } expected_suspected_cl = [{ 'repo_name': 'chromium', 'revision': 'r99_2', 'commit_position': None, 'url': None, 'failures': { 'compile': [] }, 'top_score': 2 }] analysis_result, suspected_cls = ( compile_failure_analysis.AnalyzeCompileFailure( CompileFailureInfo.FromSerializable(failure_info), _SAMPLE_CHANGE_LOG, deps_info, failure_signals_json)) self.assertEqual(expected_analysis_result, analysis_result) self.assertEqual(sorted(expected_suspected_cl), sorted(suspected_cls))
def testStopLookingBackIfFindTheFirstBuild(self, mock_search_builds, *_): master_name = 'm' builder_name = 'b' build_number = 2 failed_steps = { 'a_tests': { 'current_failure': 2, 'first_failure': 2, 'supported': True }, 'unit_tests': { 'current_failure': 2, 'first_failure': 2, 'supported': True } } builds = {'2': {'chromium_revision': 'rev2', 'blame_list': ['rev2']}} failed_steps = BaseFailedSteps.FromSerializable(failed_steps) builds = FailureInfoBuilds.FromSerializable(builds) build = WfBuild.Create(master_name, builder_name, build_number) build.build_id = '80000000124' build.completed = True build.put() self._CreateAndSaveWfAnanlysis(master_name, builder_name, build_number, analysis_status.RUNNING) # Setup build data for builds: step1 = Step(name='a_tests', status=common_pb2.FAILURE) log = step1.logs.add() log.name = 'stdout' step2 = Step(name='unit_tests', status=common_pb2.FAILURE) log = step2.logs.add() log.name = 'stdout' build_1 = Build(number=1, status=common_pb2.FAILURE, id=80000000001) build_1.steps.extend([step1, step2]) build_1.input.gitiles_commit.id = 'rev1' build_0 = Build(number=0, status=common_pb2.FAILURE, id=80000000000) build_0.steps.extend([step1, step2]) build_0.input.gitiles_commit.id = 'rev0' mock_search_builds.side_effect = [ SearchBuildsResponse(builds=[build_1]), SearchBuildsResponse(builds=[build_0]), SearchBuildsResponse(builds=[]) ] expected_failed_steps = { 'a_tests': { 'current_failure': 2, 'first_failure': 0, 'last_pass': None, 'supported': True }, 'unit_tests': { 'current_failure': 2, 'first_failure': 0, 'last_pass': None, 'supported': True } } expected_builds = { 2: { 'chromium_revision': 'rev2', 'blame_list': ['rev2'] }, 1: { 'chromium_revision': 'rev1', 'blame_list': ['rev1'] }, 0: { 'chromium_revision': 'rev0', 'blame_list': ['rev0'] }, } failure_info = CompileFailureInfo(failed_steps=failed_steps, builds=builds) ci_failure.CheckForFirstKnownFailure(master_name, builder_name, build_number, failure_info) self.assertEqual(expected_failed_steps, failure_info.failed_steps.ToSerializable()) self.assertEqual(expected_builds, failure_info.builds.ToSerializable())
def ScheduleAnalysisIfNeeded(master_name, builder_name, build_number, failed_steps=None, build_completed=False, force=False, queue_name=constants.DEFAULT_QUEUE): """Schedules an analysis if needed and returns the build analysis. When the build failure was already analyzed and a new analysis is scheduled, the returned WfAnalysis will still have the result of last completed analysis. Args: master_name (str): The master name of the failed build. builder_name (str): The builder name of the failed build. build_number (int): The build number of the failed build. failed_steps (list): The names of all failed steps reported for the build. build_completed (bool): Indicate whether the build is completed. force (bool): If True, a fresh new analysis will be triggered even when an old one was completed already; otherwise bail out. queue_name (str): The task queue to be used for pipeline tasks. Returns: A WfAnalysis instance. """ if NeedANewAnalysis(master_name, builder_name, build_number, failed_steps, build_completed, force): failure_info, should_proceed = ci_failure.GetBuildFailureInfo( master_name, builder_name, build_number) if not should_proceed: return WfAnalysis.Get(master_name, builder_name, build_number) build_key = BuildKey(master_name=master_name, builder_name=builder_name, build_number=build_number) if failure_info['failure_type'] == failure_type.COMPILE: # Use new compile pipelines. # TODO(crbug/869684): Use a gauge metric to track intermittent statuses. compile_pipeline_input = AnalyzeCompileFailureInput( build_key=build_key, current_failure_info=CompileFailureInfo.FromSerializable( failure_info), build_completed=build_completed, force=force) pipeline_job = AnalyzeCompileFailurePipeline( compile_pipeline_input) else: # TODO(crbug/869684): Use a gauge metric to track intermittent statuses. test_pipeline_input = AnalyzeTestFailureInput( build_key=build_key, current_failure_info=TestFailureInfo.FromSerializable( failure_info), build_completed=build_completed, force=force) pipeline_job = AnalyzeTestFailurePipeline(test_pipeline_input) # Explicitly run analysis in the backend module "waterfall-backend". # Note: Just setting the target in queue.yaml does NOT work for pipeline # when deployed to App Engine, but it does work in dev-server locally. # A possible reason is that pipeline will pick a default target if none is # specified explicitly, and the default target is used rather than the one # in the queue.yaml file, but this contradicts the documentation in # https://cloud.google.com/appengine/docs/python/taskqueue/tasks#Task. pipeline_job.target = appengine_util.GetTargetNameForModule( constants.WATERFALL_BACKEND) pipeline_job.start(queue_name=queue_name) logging.info('An analysis was scheduled for build %s, %s, %s: %s', master_name, builder_name, build_number, pipeline_job.pipeline_status_path) else: logging.info('An analysis is not needed for build %s, %s, %s', master_name, builder_name, build_number) return WfAnalysis.Get(master_name, builder_name, build_number)
def testStopLookingBackIfAllFailedStepsPassedInLastBuild( self, mock_search_builds, *_): master_name = 'm' builder_name = 'b' build_number = 124 failed_steps = { 'a': { 'current_failure': 124, 'first_failure': 124, 'supported': True } } builds = {124: {'chromium_revision': 'rev124', 'blame_list': ['rev124']}} failed_steps = BaseFailedSteps.FromSerializable(failed_steps) builds = FailureInfoBuilds.FromSerializable(builds) build = WfBuild.Create(master_name, builder_name, build_number) build.build_id = '80000000124' build.completed = True build.put() self._CreateAndSaveWfAnanlysis(master_name, builder_name, build_number, analysis_status.RUNNING) build_123 = Build(number=123, status=common_pb2.FAILURE) step1 = Step(name='a', status=common_pb2.SUCCESS) log = step1.logs.add() log.name = 'stdout' step2 = Step(name='net_unittests', status=common_pb2.FAILURE) log = step2.logs.add() log.name = 'stdout' step3 = Step(name='unit_tests', status=common_pb2.FAILURE) log = step3.logs.add() log.name = 'stdout' build_123.steps.extend([step1, step2, step3]) build_123.input.gitiles_commit.id = 'rev123' mock_search_builds.side_effect = [SearchBuildsResponse(builds=[build_123])] expected_failed_steps = { 'a': { 'last_pass': 123, 'current_failure': 124, 'first_failure': 124, 'supported': True } } expected_builds = { 124: { 'chromium_revision': 'rev124', 'blame_list': ['rev124'] }, 123: { 'chromium_revision': 'rev123', 'blame_list': ['rev123'] } } failure_info = CompileFailureInfo(failed_steps=failed_steps, builds=builds) failure_info = ci_failure.CheckForFirstKnownFailure( master_name, builder_name, build_number, failure_info) self.assertEqual(expected_failed_steps, failure_info.failed_steps.ToSerializable()) self.assertEqual(expected_builds, failure_info.builds.ToSerializable())