Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(383)

Side by Side Diff: appengine/findit/findit_api.py

Issue 2425853005: [Findit] Modify Findit API to return more information to Sheriff-O-Matic. (Closed)
Patch Set: rebase Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | appengine/findit/handlers/build_failure.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2015 The Chromium Authors. All rights reserved. 1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """This module is to provide Findit service APIs through Cloud Endpoints: 5 """This module is to provide Findit service APIs through Cloud Endpoints:
6 6
7 Current APIs include: 7 Current APIs include:
8 1. Analysis of compile/test failures in Chromium waterfalls. 8 1. Analysis of compile/test failures in Chromium waterfalls.
9 Analyzes failures and detects suspected CLs. 9 Analyzes failures and detects suspected CLs.
10 2. Analysis of flakes on Commit Queue. 10 2. Analysis of flakes on Commit Queue.
11 """ 11 """
12 12
13 import json 13 import json
14 import logging 14 import logging
15 import pickle 15 import pickle
16 16
17 import endpoints 17 import endpoints
18 from google.appengine.api import taskqueue 18 from google.appengine.api import taskqueue
19 from protorpc import messages 19 from protorpc import messages
20 from protorpc import remote 20 from protorpc import remote
21 21
22 from common import appengine_util 22 from common import appengine_util
23 from common import auth_util 23 from common import auth_util
24 from common import constants 24 from common import constants
25 from common import time_util 25 from common import time_util
26 from common.waterfall import failure_type 26 from common.waterfall import failure_type
27 from model import analysis_approach_type
27 from model.flake.flake_analysis_request import FlakeAnalysisRequest 28 from model.flake.flake_analysis_request import FlakeAnalysisRequest
29 from model.suspected_cl_confidence import SuspectedCLConfidence
28 from model.wf_analysis import WfAnalysis 30 from model.wf_analysis import WfAnalysis
31 from model.wf_suspected_cl import WfSuspectedCL
29 from model.wf_swarming_task import WfSwarmingTask 32 from model.wf_swarming_task import WfSwarmingTask
30 from model.wf_try_job import WfTryJob 33 from model.wf_try_job import WfTryJob
34 from waterfall import build_util
31 from waterfall import buildbot 35 from waterfall import buildbot
36 from waterfall import suspected_cl_util
32 from waterfall import waterfall_config 37 from waterfall import waterfall_config
33 from waterfall.flake import flake_analysis_service 38 from waterfall.flake import flake_analysis_service
34 39
35 40
36 # This is used by the underlying ProtoRpc when creating names for the ProtoRPC 41 # This is used by the underlying ProtoRpc when creating names for the ProtoRPC
37 # messages below. This package name will show up as a prefix to the message 42 # messages below. This package name will show up as a prefix to the message
38 # class names in the discovery doc and client libraries. 43 # class names in the discovery doc and client libraries.
39 package = 'FindIt' 44 package = 'FindIt'
40 45
41 46
42 # These subclasses of Message are basically definitions of Protocol RPC 47 # These subclasses of Message are basically definitions of Protocol RPC
43 # messages. https://cloud.google.com/appengine/docs/python/tools/protorpc/ 48 # messages. https://cloud.google.com/appengine/docs/python/tools/protorpc/
44 class _BuildFailure(messages.Message): 49 class _BuildFailure(messages.Message):
45 master_url = messages.StringField(1, required=True) 50 master_url = messages.StringField(1, required=True)
46 builder_name = messages.StringField(2, required=True) 51 builder_name = messages.StringField(2, required=True)
47 build_number = messages.IntegerField(3, variant=messages.Variant.INT32, 52 build_number = messages.IntegerField(3, variant=messages.Variant.INT32,
48 required=True) 53 required=True)
49 # All failed steps of the build reported by the client. 54 # All failed steps of the build reported by the client.
50 failed_steps = messages.StringField(4, repeated=True, required=False) 55 failed_steps = messages.StringField(4, repeated=True, required=False)
51 56
52 57
53 class _BuildFailureCollection(messages.Message): 58 class _BuildFailureCollection(messages.Message):
54 """Represents a request from a client, eg. builder_alerts.""" 59 """Represents a request from a client, eg. builder_alerts."""
55 builds = messages.MessageField(_BuildFailure, 1, repeated=True) 60 builds = messages.MessageField(_BuildFailure, 1, repeated=True)
56 61
57 62
63 class _AnalysisApproach(messages.Enum):
64 HEURISTIC = analysis_approach_type.HEURISTIC
65 TRY_JOB = analysis_approach_type.TRY_JOB
66
67
58 class _SuspectedCL(messages.Message): 68 class _SuspectedCL(messages.Message):
59 repo_name = messages.StringField(1, required=True) 69 repo_name = messages.StringField(1, required=True)
60 revision = messages.StringField(2, required=True) 70 revision = messages.StringField(2, required=True)
61 commit_position = messages.IntegerField(3, variant=messages.Variant.INT32) 71 commit_position = messages.IntegerField(3, variant=messages.Variant.INT32)
72 confidence = messages.IntegerField(4, variant=messages.Variant.INT32)
73 analysis_approach = messages.EnumField(_AnalysisApproach, 5)
62 74
63 75
64 class _AnalysisApproach(messages.Enum): 76 class _TryJobStatus(messages.Enum):
65 HEURISTIC = 1 77 # Try job is pending or running. Can expect result from try job.
66 TRY_JOB = 2 78 RUNNING = 1
79 # There is no try job, try job completed or try job finished with error.
80 # Result from try job is ready or no need to continue waiting for it.
81 FINISHED = 2
67 82
68 83
69 class _BuildFailureAnalysisResult(messages.Message): 84 class _BuildFailureAnalysisResult(messages.Message):
70 master_url = messages.StringField(1, required=True) 85 master_url = messages.StringField(1, required=True)
71 builder_name = messages.StringField(2, required=True) 86 builder_name = messages.StringField(2, required=True)
72 build_number = messages.IntegerField(3, variant=messages.Variant.INT32, 87 build_number = messages.IntegerField(3, variant=messages.Variant.INT32,
73 required=True) 88 required=True)
74 step_name = messages.StringField(4, required=True) 89 step_name = messages.StringField(4, required=True)
75 is_sub_test = messages.BooleanField(5, variant=messages.Variant.BOOL, 90 is_sub_test = messages.BooleanField(5, variant=messages.Variant.BOOL,
76 required=True) 91 required=True)
77 test_name = messages.StringField(6) 92 test_name = messages.StringField(6)
78 first_known_failed_build_number = messages.IntegerField( 93 first_known_failed_build_number = messages.IntegerField(
79 7, variant=messages.Variant.INT32) 94 7, variant=messages.Variant.INT32)
80 suspected_cls = messages.MessageField(_SuspectedCL, 8, repeated=True) 95 suspected_cls = messages.MessageField(_SuspectedCL, 8, repeated=True)
81 analysis_approach = messages.EnumField(_AnalysisApproach, 9) 96 analysis_approach = messages.EnumField(_AnalysisApproach, 9)
97 try_job_status = messages.EnumField(_TryJobStatus, 10)
98 is_flaky_test = messages.BooleanField(11, variant=messages.Variant.BOOL)
82 99
83 100
84 class _BuildFailureAnalysisResultCollection(messages.Message): 101 class _BuildFailureAnalysisResultCollection(messages.Message):
85 """Represents a response to the client, eg. builder_alerts.""" 102 """Represents a response to the client, eg. builder_alerts."""
86 results = messages.MessageField(_BuildFailureAnalysisResult, 1, repeated=True) 103 results = messages.MessageField(_BuildFailureAnalysisResult, 1, repeated=True)
87 104
88 105
89 class _BuildStep(messages.Message): 106 class _BuildStep(messages.Message):
90 master_name = messages.StringField(1, required=True) 107 master_name = messages.StringField(1, required=True)
91 builder_name = messages.StringField(2, required=True) 108 builder_name = messages.StringField(2, required=True)
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
132 payload=payload, target=target, 149 payload=payload, target=target,
133 queue_name=constants.WATERFALL_FLAKE_ANALYSIS_REQUEST_QUEUE) 150 queue_name=constants.WATERFALL_FLAKE_ANALYSIS_REQUEST_QUEUE)
134 151
135 152
136 # Create a Cloud Endpoints API. 153 # Create a Cloud Endpoints API.
137 # https://cloud.google.com/appengine/docs/python/endpoints/create_api 154 # https://cloud.google.com/appengine/docs/python/endpoints/create_api
138 @endpoints.api(name='findit', version='v1', description='FindIt API') 155 @endpoints.api(name='findit', version='v1', description='FindIt API')
139 class FindItApi(remote.Service): 156 class FindItApi(remote.Service):
140 """FindIt API v1.""" 157 """FindIt API v1."""
141 158
159 def _GetConfidenceAndApproachForCL(
160 self, repo_name, revision, confidences, build, first_failure):
161 cl = WfSuspectedCL.Get(repo_name, revision)
162 if not cl:
163 return None, None
164
165 master_name = buildbot.GetMasterNameFromUrl(build.master_url)
166 builder_name = build.builder_name
167 current_build = build.build_number
168
169 # If the CL is found by a try job, only the first failure will be recorded.
170 # So we might need to go to the first failure to get CL information.
171 build_info = (cl.GetBuildInfo(master_name, builder_name, current_build) or
172 cl.GetBuildInfo(master_name, builder_name, first_failure))
173
174 confidence = suspected_cl_util.GetSuspectedCLConfidenceScore(
175 confidences, build_info)
176
177 cl_approach = (
178 _AnalysisApproach.TRY_JOB if analysis_approach_type.TRY_JOB in
179 build_info['approaches'] else _AnalysisApproach.HEURISTIC)
180
181 return confidence, cl_approach
182
142 def _GenerateBuildFailureAnalysisResult( 183 def _GenerateBuildFailureAnalysisResult(
143 self, build, suspected_cls_in_result, step_name, 184 self, build, suspected_cls_in_result, step_name, first_failure, test_name,
144 first_failure, test_name=None, 185 analysis_approach, confidences, try_job_status, is_flaky_test):
145 analysis_approach=_AnalysisApproach.HEURISTIC): 186
146 suspected_cls = [] 187 suspected_cls = []
147 for suspected_cl in suspected_cls_in_result: 188 for suspected_cl in suspected_cls_in_result:
189 repo_name = suspected_cl['repo_name']
190 revision = suspected_cl['revision']
191 commit_position = suspected_cl['commit_position']
192 confidence, cl_approach = self._GetConfidenceAndApproachForCL(
193 repo_name, revision, confidences, build, first_failure)
194 cl_approach = cl_approach or analysis_approach
195
148 suspected_cls.append(_SuspectedCL( 196 suspected_cls.append(_SuspectedCL(
149 repo_name=suspected_cl['repo_name'], 197 repo_name=repo_name, revision=revision,
150 revision=suspected_cl['revision'], 198 commit_position=commit_position, confidence=confidence,
151 commit_position=suspected_cl['commit_position'])) 199 analysis_approach=cl_approach))
152 200
153 return _BuildFailureAnalysisResult( 201 return _BuildFailureAnalysisResult(
154 master_url=build.master_url, 202 master_url=build.master_url,
155 builder_name=build.builder_name, 203 builder_name=build.builder_name,
156 build_number=build.build_number, 204 build_number=build.build_number,
157 step_name=step_name, 205 step_name=step_name,
158 is_sub_test=test_name is not None, 206 is_sub_test=test_name is not None,
159 test_name=test_name, 207 test_name=test_name,
160 first_known_failed_build_number=first_failure, 208 first_known_failed_build_number=first_failure,
161 suspected_cls=suspected_cls, 209 suspected_cls=suspected_cls,
162 analysis_approach=analysis_approach) 210 analysis_approach=analysis_approach,
211 try_job_status=try_job_status,
212 is_flaky_test=is_flaky_test)
163 213
164 def _GetCulpritFromTryJob( 214 def _GetStatusAndCulpritFromTryJob(
165 self, try_job_map, build_failure_type, step_name, test_name=None): 215 self, try_job_map, build_failure_type, step_name, test_name=None):
166 """Returns the culprit found by try-job for the given step or test.""" 216 """Returns the culprit found by try-job for the given step or test."""
167 if not try_job_map: 217 if not try_job_map:
168 return None 218 return _TryJobStatus.FINISHED, None
169 219
170 if test_name is None: 220 if test_name is None:
171 try_job_key = try_job_map.get(step_name) 221 try_job_key = try_job_map.get(step_name)
172 else: 222 else:
173 try_job_key = try_job_map.get(step_name, {}).get(test_name) 223 try_job_key = try_job_map.get(step_name, {}).get(test_name)
174 224
175 if not try_job_key: 225 if not try_job_key:
176 return None 226 return _TryJobStatus.FINISHED, None
177 227
178 try_job = WfTryJob.Get(*try_job_key.split('/')) 228 try_job = WfTryJob.Get(*build_util.GetBuildInfoFromId(try_job_key))
179 if not try_job or not try_job.completed or try_job.failed: 229 if not try_job or try_job.failed:
180 return None 230 return _TryJobStatus.FINISHED, None
231
232 if not try_job.completed:
233 return _TryJobStatus.RUNNING, None
181 234
182 if build_failure_type == failure_type.COMPILE: 235 if build_failure_type == failure_type.COMPILE:
183 if not try_job.compile_results: # pragma: no cover. 236 if not try_job.compile_results: # pragma: no cover.
184 return None 237 return _TryJobStatus.FINISHED, None
185 return try_job.compile_results[-1].get('culprit', {}).get(step_name) 238 return (
239 _TryJobStatus.FINISHED,
240 try_job.compile_results[-1].get('culprit', {}).get(step_name))
186 241
187 if not try_job.test_results: # pragma: no cover. 242 if not try_job.test_results: # pragma: no cover.
188 return None 243 return _TryJobStatus.FINISHED, None
189 244
190 if test_name is None: 245 if test_name is None:
191 step_info = try_job.test_results[-1].get('culprit', {}).get(step_name) 246 step_info = try_job.test_results[-1].get('culprit', {}).get(step_name)
192 if not step_info or step_info.get('tests'): # pragma: no cover. 247 if not step_info or step_info.get('tests'): # pragma: no cover.
193 # TODO(chanli): For some steps like checkperms/sizes/etc, the culprit 248 # TODO(chanli): For some steps like checkperms/sizes/etc, the culprit
194 # finding try-job might have test-level results. 249 # finding try-job might have test-level results.
195 return None 250 return _TryJobStatus.FINISHED, None
196 return step_info 251 return _TryJobStatus.FINISHED, step_info
197 252
198 task = WfSwarmingTask.Get(*try_job_key.split('/'), step_name=step_name) 253 task = WfSwarmingTask.Get(*build_util.GetBuildInfoFromId(try_job_key),
254 step_name=step_name)
199 ref_name = (task.parameters.get('ref_name') if task and task.parameters 255 ref_name = (task.parameters.get('ref_name') if task and task.parameters
200 else None) 256 else None)
201 return try_job.test_results[-1].get('culprit', {}).get( 257 return (
202 ref_name or step_name, {}).get('tests', {}).get(test_name) 258 _TryJobStatus.FINISHED, try_job.test_results[-1].get('culprit', {}).get(
259 ref_name or step_name, {}).get('tests', {}).get(test_name))
260
261 def _CheckIsFlaky(self, try_job_map, step_name, test_name):
262 """Checks if the test is flaky."""
263 if not try_job_map or not test_name:
264 return False
265
266 try_job_key = try_job_map.get(step_name, {}).get(test_name)
267 if not try_job_key:
268 return False
269
270 swarming_task = WfSwarmingTask.Get(
271 *build_util.GetBuildInfoFromId(try_job_key), step_name=step_name)
272 if not swarming_task or not swarming_task.classified_tests:
273 return False
274
275 return test_name in swarming_task.classified_tests.get('flaky_tests', [])
203 276
204 def _PopulateResult( 277 def _PopulateResult(
205 self, results, build, try_job_map, build_failure_type, 278 self, results, build, try_job_map, build_failure_type,
206 heuristic_result, step_name, test_name=None): 279 heuristic_result, step_name, confidences, test_name=None):
207 """Appends an analysis result for the given step or test. 280 """Appends an analysis result for the given step or test.
208 281
209 Try-job results are always given priority over heuristic results. 282 Try-job results are always given priority over heuristic results.
210 """ 283 """
211 # Default to heuristic analysis. 284 # Default to heuristic analysis.
212 suspected_cls = heuristic_result['suspected_cls'] 285 suspected_cls = heuristic_result['suspected_cls']
213 analysis_approach = _AnalysisApproach.HEURISTIC 286 analysis_approach = _AnalysisApproach.HEURISTIC
214 287
215 # Check analysis result from try-job. 288 # Check if the test is flaky.
216 culprit = self._GetCulpritFromTryJob( 289 is_flaky_test = self._CheckIsFlaky(try_job_map, step_name, test_name)
217 try_job_map, build_failure_type, step_name, test_name=test_name)
218 if culprit:
219 suspected_cls = [culprit]
220 analysis_approach = _AnalysisApproach.TRY_JOB
221 290
222 if not suspected_cls: 291 if is_flaky_test:
292 suspected_cls = []
293 try_job_status = _TryJobStatus.FINISHED # There will be no try job.
294 else:
295 # Check analysis result from try-job.
296 try_job_status, culprit = self._GetStatusAndCulpritFromTryJob(
297 try_job_map, build_failure_type, step_name, test_name=test_name)
298 if culprit:
299 suspected_cls = [culprit]
300 analysis_approach = _AnalysisApproach.TRY_JOB
301
302 if not is_flaky_test and not suspected_cls:
223 return 303 return
224 304
225 results.append(self._GenerateBuildFailureAnalysisResult( 305 results.append(self._GenerateBuildFailureAnalysisResult(
226 build, suspected_cls, step_name, heuristic_result['first_failure'], 306 build, suspected_cls, step_name, heuristic_result['first_failure'],
227 test_name=test_name, analysis_approach=analysis_approach)) 307 test_name, analysis_approach, confidences, try_job_status,
308 is_flaky_test))
228 309
229 def _GenerateResultsForBuild(self, build, heuristic_analysis, results): 310 def _GenerateResultsForBuild(
311 self, build, heuristic_analysis, results, confidences):
230 for failure in heuristic_analysis.result['failures']: 312 for failure in heuristic_analysis.result['failures']:
231 if failure.get('tests'): # Test-level analysis. 313 if failure.get('tests'): # Test-level analysis.
232 for test in failure['tests']: 314 for test in failure['tests']:
233 self._PopulateResult( 315 self._PopulateResult(
234 results, build, heuristic_analysis.failure_result_map, 316 results, build, heuristic_analysis.failure_result_map,
235 heuristic_analysis.failure_type, test, 317 heuristic_analysis.failure_type, test,
236 failure['step_name'], test_name=test['test_name']) 318 failure['step_name'], confidences, test_name=test['test_name'])
237 else: 319 else:
238 self._PopulateResult( 320 self._PopulateResult(
239 results, build, heuristic_analysis.failure_result_map, 321 results, build, heuristic_analysis.failure_result_map,
240 heuristic_analysis.failure_type, failure, failure['step_name']) 322 heuristic_analysis.failure_type, failure, failure['step_name'],
323 confidences)
241 324
242 @endpoints.method( 325 @endpoints.method(
243 _BuildFailureCollection, _BuildFailureAnalysisResultCollection, 326 _BuildFailureCollection, _BuildFailureAnalysisResultCollection,
244 path='buildfailure', name='buildfailure') 327 path='buildfailure', name='buildfailure')
245 def AnalyzeBuildFailures(self, request): 328 def AnalyzeBuildFailures(self, request):
246 """Returns analysis results for the given build failures in the request. 329 """Returns analysis results for the given build failures in the request.
247 330
248 Analysis of build failures will be triggered automatically on demand. 331 Analysis of build failures will be triggered automatically on demand.
249 332
250 Args: 333 Args:
251 request (_BuildFailureCollection): A list of build failures. 334 request (_BuildFailureCollection): A list of build failures.
252 335
253 Returns: 336 Returns:
254 _BuildFailureAnalysisResultCollection 337 _BuildFailureAnalysisResultCollection
255 A list of analysis results for the given build failures. 338 A list of analysis results for the given build failures.
256 """ 339 """
257 results = [] 340 results = []
258 supported_builds = [] 341 supported_builds = []
342 confidences = SuspectedCLConfidence.Get()
259 343
260 for build in request.builds: 344 for build in request.builds:
261 master_name = buildbot.GetMasterNameFromUrl(build.master_url) 345 master_name = buildbot.GetMasterNameFromUrl(build.master_url)
262 if not (master_name and waterfall_config.MasterIsSupported(master_name)): 346 if not (master_name and waterfall_config.MasterIsSupported(master_name)):
263 logging.info('%s/%s/%s is not supported', 347 logging.info('%s/%s/%s is not supported',
264 build.master_url, build.builder_name, build.build_number) 348 build.master_url, build.builder_name, build.build_number)
265 continue 349 continue
266 350
267 supported_builds.append({ 351 supported_builds.append({
268 'master_name': master_name, 352 'master_name': master_name,
269 'builder_name': build.builder_name, 353 'builder_name': build.builder_name,
270 'build_number': build.build_number, 354 'build_number': build.build_number,
271 'failed_steps': build.failed_steps, 355 'failed_steps': build.failed_steps,
272 }) 356 })
273 357
274 # If the build failure was already analyzed and a new analysis is 358 # If the build failure was already analyzed and a new analysis is
275 # scheduled to analyze new failed steps, the returned WfAnalysis will 359 # scheduled to analyze new failed steps, the returned WfAnalysis will
276 # still have the result from last completed analysis. 360 # still have the result from last completed analysis.
277 # If there is no analysis yet, no result is returned. 361 # If there is no analysis yet, no result is returned.
278 heuristic_analysis = WfAnalysis.Get( 362 heuristic_analysis = WfAnalysis.Get(
279 master_name, build.builder_name, build.build_number) 363 master_name, build.builder_name, build.build_number)
280 if not heuristic_analysis: 364 if not heuristic_analysis:
281 continue 365 continue
282 366
283 if heuristic_analysis.failed or not heuristic_analysis.result: 367 if heuristic_analysis.failed or not heuristic_analysis.result:
284 # Bail out if the analysis failed or there is no result yet. 368 # Bail out if the analysis failed or there is no result yet.
285 continue 369 continue
286 370
287 self._GenerateResultsForBuild(build, heuristic_analysis, results) 371 self._GenerateResultsForBuild(
372 build, heuristic_analysis, results, confidences)
288 373
289 logging.info('%d build failure(s), while %d are supported', 374 logging.info('%d build failure(s), while %d are supported',
290 len(request.builds), len(supported_builds)) 375 len(request.builds), len(supported_builds))
291 try: 376 try:
292 _AsyncProcessFailureAnalysisRequests(supported_builds) 377 _AsyncProcessFailureAnalysisRequests(supported_builds)
293 except Exception: # pragma: no cover. 378 except Exception: # pragma: no cover.
294 # If we fail to post a task to the task queue, we ignore and wait for next 379 # If we fail to post a task to the task queue, we ignore and wait for next
295 # request. 380 # request.
296 logging.exception('Failed to add analysis request to task queue: %s', 381 logging.exception('Failed to add analysis request to task queue: %s',
297 repr(supported_builds)) 382 repr(supported_builds))
(...skipping 24 matching lines...) Expand all
322 407
323 try: 408 try:
324 _AsyncProcessFlakeReport(flake_analysis_request, user_email, is_admin) 409 _AsyncProcessFlakeReport(flake_analysis_request, user_email, is_admin)
325 queued = True 410 queued = True
326 except Exception: 411 except Exception:
327 # Ignore the report when fail to queue it for async processing. 412 # Ignore the report when fail to queue it for async processing.
328 queued = False 413 queued = False
329 logging.exception('Failed to queue flake report for async processing') 414 logging.exception('Failed to queue flake report for async processing')
330 415
331 return _FlakeAnalysis(queued=queued) 416 return _FlakeAnalysis(queued=queued)
OLDNEW
« no previous file with comments | « no previous file | appengine/findit/handlers/build_failure.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698