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

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: fix nits Created 4 years, 2 months 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 15
16 import endpoints 16 import endpoints
17 from google.appengine.api import taskqueue 17 from google.appengine.api import taskqueue
18 from protorpc import messages 18 from protorpc import messages
19 from protorpc import remote 19 from protorpc import remote
20 20
21 from common import appengine_util 21 from common import appengine_util
22 from common import auth_util 22 from common import auth_util
23 from common import constants 23 from common import constants
24 from common import time_util 24 from common import time_util
25 from common.waterfall import failure_type 25 from common.waterfall import failure_type
26 from model import analysis_approach_type
26 from model.flake.flake_analysis_request import FlakeAnalysisRequest 27 from model.flake.flake_analysis_request import FlakeAnalysisRequest
28 from model.suspected_cl_confidence import SuspectedCLConfidence
27 from model.wf_analysis import WfAnalysis 29 from model.wf_analysis import WfAnalysis
30 from model.wf_suspected_cl import WfSuspectedCL
28 from model.wf_swarming_task import WfSwarmingTask 31 from model.wf_swarming_task import WfSwarmingTask
29 from model.wf_try_job import WfTryJob 32 from model.wf_try_job import WfTryJob
30 from waterfall import buildbot 33 from waterfall import buildbot
34 from waterfall import suspected_cl_util
31 from waterfall import waterfall_config 35 from waterfall import waterfall_config
32 from waterfall.flake import flake_analysis_service 36 from waterfall.flake import flake_analysis_service
33 from waterfall.flake import triggering_sources 37 from waterfall.flake import triggering_sources
34 38
35 39
36 # This is used by the underlying ProtoRpc when creating names for the ProtoRPC 40 # 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 41 # messages below. This package name will show up as a prefix to the message
38 # class names in the discovery doc and client libraries. 42 # class names in the discovery doc and client libraries.
39 package = 'FindIt' 43 package = 'FindIt'
40 44
41 45
42 # These subclasses of Message are basically definitions of Protocol RPC 46 # These subclasses of Message are basically definitions of Protocol RPC
43 # messages. https://cloud.google.com/appengine/docs/python/tools/protorpc/ 47 # messages. https://cloud.google.com/appengine/docs/python/tools/protorpc/
44 class _BuildFailure(messages.Message): 48 class _BuildFailure(messages.Message):
45 master_url = messages.StringField(1, required=True) 49 master_url = messages.StringField(1, required=True)
46 builder_name = messages.StringField(2, required=True) 50 builder_name = messages.StringField(2, required=True)
47 build_number = messages.IntegerField(3, variant=messages.Variant.INT32, 51 build_number = messages.IntegerField(3, variant=messages.Variant.INT32,
48 required=True) 52 required=True)
49 # All failed steps of the build reported by the client. 53 # All failed steps of the build reported by the client.
50 failed_steps = messages.StringField(4, repeated=True, required=False) 54 failed_steps = messages.StringField(4, repeated=True, required=False)
51 55
52 56
53 class _BuildFailureCollection(messages.Message): 57 class _BuildFailureCollection(messages.Message):
54 """Represents a request from a client, eg. builder_alerts.""" 58 """Represents a request from a client, eg. builder_alerts."""
55 builds = messages.MessageField(_BuildFailure, 1, repeated=True) 59 builds = messages.MessageField(_BuildFailure, 1, repeated=True)
56 60
57 61
62 class _AnalysisApproach(messages.Enum):
63 HEURISTIC = analysis_approach_type.HEURISTIC
64 TRY_JOB = analysis_approach_type.TRY_JOB
65
66
58 class _SuspectedCL(messages.Message): 67 class _SuspectedCL(messages.Message):
59 repo_name = messages.StringField(1, required=True) 68 repo_name = messages.StringField(1, required=True)
60 revision = messages.StringField(2, required=True) 69 revision = messages.StringField(2, required=True)
61 commit_position = messages.IntegerField(3, variant=messages.Variant.INT32) 70 commit_position = messages.IntegerField(3, variant=messages.Variant.INT32)
71 confidence = messages.IntegerField(4, variant=messages.Variant.INT32)
72 analysis_approach = messages.EnumField(_AnalysisApproach, 5)
62 73
63 74
64 class _AnalysisApproach(messages.Enum): 75 class _TryJobStatus(messages.Enum):
stgao 2016/10/19 00:43:08 More status here: 1. not supported 2. pending 3. e
chanli 2016/10/19 03:00:59 I was thinking the try job status is to let sherif
stgao 2016/10/21 01:51:22 OK, I agree 2-4 could be merged into RUNNING or FI
stgao 2016/10/22 00:24:14 Per discussion offline, we will go with the curren
65 HEURISTIC = 1 76 # Try job is pending or running. Can expect result from try job.
66 TRY_JOB = 2 77 RUNNING = 1
78 # There is no try job, try job completed or try job finished with error.
79 # Result from try job is ready or no need to continue waiting for it.
80 FINISHED = 2
67 81
68 82
69 class _BuildFailureAnalysisResult(messages.Message): 83 class _BuildFailureAnalysisResult(messages.Message):
70 master_url = messages.StringField(1, required=True) 84 master_url = messages.StringField(1, required=True)
71 builder_name = messages.StringField(2, required=True) 85 builder_name = messages.StringField(2, required=True)
72 build_number = messages.IntegerField(3, variant=messages.Variant.INT32, 86 build_number = messages.IntegerField(3, variant=messages.Variant.INT32,
73 required=True) 87 required=True)
74 step_name = messages.StringField(4, required=True) 88 step_name = messages.StringField(4, required=True)
75 is_sub_test = messages.BooleanField(5, variant=messages.Variant.BOOL, 89 is_sub_test = messages.BooleanField(5, variant=messages.Variant.BOOL,
76 required=True) 90 required=True)
77 test_name = messages.StringField(6) 91 test_name = messages.StringField(6)
78 first_known_failed_build_number = messages.IntegerField( 92 first_known_failed_build_number = messages.IntegerField(
79 7, variant=messages.Variant.INT32) 93 7, variant=messages.Variant.INT32)
80 suspected_cls = messages.MessageField(_SuspectedCL, 8, repeated=True) 94 suspected_cls = messages.MessageField(_SuspectedCL, 8, repeated=True)
81 analysis_approach = messages.EnumField(_AnalysisApproach, 9) 95 analysis_approach = messages.EnumField(_AnalysisApproach, 9)
96 try_job_status = messages.EnumField(_TryJobStatus, 10)
97 is_flaky_test = messages.BooleanField(11, variant=messages.Variant.BOOL)
82 98
83 99
84 class _BuildFailureAnalysisResultCollection(messages.Message): 100 class _BuildFailureAnalysisResultCollection(messages.Message):
85 """Represents a response to the client, eg. builder_alerts.""" 101 """Represents a response to the client, eg. builder_alerts."""
86 results = messages.MessageField(_BuildFailureAnalysisResult, 1, repeated=True) 102 results = messages.MessageField(_BuildFailureAnalysisResult, 1, repeated=True)
87 103
88 104
89 class _BuildStep(messages.Message): 105 class _BuildStep(messages.Message):
90 master_name = messages.StringField(1, required=True) 106 master_name = messages.StringField(1, required=True)
91 builder_name = messages.StringField(2, required=True) 107 builder_name = messages.StringField(2, required=True)
(...skipping 30 matching lines...) Expand all
122 payload=payload, target=target, 138 payload=payload, target=target,
123 queue_name=constants.WATERFALL_FAILURE_ANALYSIS_REQUEST_QUEUE) 139 queue_name=constants.WATERFALL_FAILURE_ANALYSIS_REQUEST_QUEUE)
124 140
125 141
126 # Create a Cloud Endpoints API. 142 # Create a Cloud Endpoints API.
127 # https://cloud.google.com/appengine/docs/python/endpoints/create_api 143 # https://cloud.google.com/appengine/docs/python/endpoints/create_api
128 @endpoints.api(name='findit', version='v1', description='FindIt API') 144 @endpoints.api(name='findit', version='v1', description='FindIt API')
129 class FindItApi(remote.Service): 145 class FindItApi(remote.Service):
130 """FindIt API v1.""" 146 """FindIt API v1."""
131 147
148 def _GetConfidenceAndApproachForCL(
149 self, repo_name, revision, confidences, build, first_failure):
150 cl = WfSuspectedCL.Get(repo_name, revision)
151 if not cl:
152 return None, None
153
154 master_name = buildbot.GetMasterNameFromUrl(build.master_url)
155 builder_name = build.builder_name
156 current_build = build.build_number
157
158 # If the CL is found by a try job, only the first failure will be recorded.
159 # So we might need to go to the first failure to get CL information.
160 build_info = (cl.GetBuildInfo(master_name, builder_name, current_build) or
161 cl.GetBuildInfo(master_name, builder_name, first_failure))
162
163 confidence = suspected_cl_util.GetSuspectedCLConfidenceScore(
164 confidences, build_info)
165
166 cl_approach = (
167 _AnalysisApproach.TRY_JOB if analysis_approach_type.TRY_JOB in
168 build_info['approaches'] else _AnalysisApproach.HEURISTIC)
169
170 return confidence, cl_approach
171
132 def _GenerateBuildFailureAnalysisResult( 172 def _GenerateBuildFailureAnalysisResult(
133 self, build, suspected_cls_in_result, step_name, 173 self, build, suspected_cls_in_result, step_name, first_failure, test_name,
134 first_failure, test_name=None, 174 analysis_approach, confidences, try_job_status, is_flaky_test):
135 analysis_approach=_AnalysisApproach.HEURISTIC): 175
136 suspected_cls = [] 176 suspected_cls = []
137 for suspected_cl in suspected_cls_in_result: 177 for suspected_cl in suspected_cls_in_result:
178 repo_name = suspected_cl['repo_name']
179 revision = suspected_cl['revision']
180 commit_position = suspected_cl['commit_position']
181 confidence, cl_approach = self._GetConfidenceAndApproachForCL(
182 repo_name, revision, confidences, build, first_failure)
183 cl_approach = cl_approach or analysis_approach
184
138 suspected_cls.append(_SuspectedCL( 185 suspected_cls.append(_SuspectedCL(
139 repo_name=suspected_cl['repo_name'], 186 repo_name=repo_name, revision=revision,
140 revision=suspected_cl['revision'], 187 commit_position=commit_position, confidence=confidence,
141 commit_position=suspected_cl['commit_position'])) 188 analysis_approach=cl_approach))
142 189
143 return _BuildFailureAnalysisResult( 190 return _BuildFailureAnalysisResult(
144 master_url=build.master_url, 191 master_url=build.master_url,
145 builder_name=build.builder_name, 192 builder_name=build.builder_name,
146 build_number=build.build_number, 193 build_number=build.build_number,
147 step_name=step_name, 194 step_name=step_name,
148 is_sub_test=test_name is not None, 195 is_sub_test=test_name is not None,
149 test_name=test_name, 196 test_name=test_name,
150 first_known_failed_build_number=first_failure, 197 first_known_failed_build_number=first_failure,
151 suspected_cls=suspected_cls, 198 suspected_cls=suspected_cls,
152 analysis_approach=analysis_approach) 199 analysis_approach=analysis_approach,
200 try_job_status=try_job_status,
201 is_flaky_test=is_flaky_test)
153 202
154 def _GetCulpritFromTryJob( 203 def _GetStatusAndCulpritFromTryJob(
155 self, try_job_map, build_failure_type, step_name, test_name=None): 204 self, try_job_map, build_failure_type, step_name, test_name=None):
156 """Returns the culprit found by try-job for the given step or test.""" 205 """Returns the culprit found by try-job for the given step or test."""
157 if not try_job_map: 206 if not try_job_map:
158 return None 207 return _TryJobStatus.FINISHED, None
159 208
160 if test_name is None: 209 if test_name is None:
161 try_job_key = try_job_map.get(step_name) 210 try_job_key = try_job_map.get(step_name)
162 else: 211 else:
163 try_job_key = try_job_map.get(step_name, {}).get(test_name) 212 try_job_key = try_job_map.get(step_name, {}).get(test_name)
164 213
165 if not try_job_key: 214 if not try_job_key:
166 return None 215 return _TryJobStatus.FINISHED, None
167 216
168 try_job = WfTryJob.Get(*try_job_key.split('/')) 217 try_job = WfTryJob.Get(*try_job_key.split('/'))
169 if not try_job or not try_job.completed or try_job.failed: 218 if not try_job or try_job.failed:
170 return None 219 return _TryJobStatus.FINISHED, None
220
221 if not try_job.completed:
222 return _TryJobStatus.RUNNING, None
171 223
172 if build_failure_type == failure_type.COMPILE: 224 if build_failure_type == failure_type.COMPILE:
173 if not try_job.compile_results: # pragma: no cover. 225 if not try_job.compile_results: # pragma: no cover.
174 return None 226 return _TryJobStatus.FINISHED, None
175 return try_job.compile_results[-1].get('culprit', {}).get(step_name) 227 return (
228 _TryJobStatus.FINISHED,
229 try_job.compile_results[-1].get('culprit', {}).get(step_name))
176 230
177 if not try_job.test_results: # pragma: no cover. 231 if not try_job.test_results: # pragma: no cover.
178 return None 232 return _TryJobStatus.FINISHED, None
179 233
180 if test_name is None: 234 if test_name is None:
181 step_info = try_job.test_results[-1].get('culprit', {}).get(step_name) 235 step_info = try_job.test_results[-1].get('culprit', {}).get(step_name)
182 if not step_info or step_info.get('tests'): # pragma: no cover. 236 if not step_info or step_info.get('tests'): # pragma: no cover.
183 # TODO(chanli): For some steps like checkperms/sizes/etc, the culprit 237 # TODO(chanli): For some steps like checkperms/sizes/etc, the culprit
184 # finding try-job might have test-level results. 238 # finding try-job might have test-level results.
185 return None 239 return _TryJobStatus.FINISHED, None
186 return step_info 240 return _TryJobStatus.FINISHED, step_info
187 241
188 task = WfSwarmingTask.Get(*try_job_key.split('/'), step_name=step_name) 242 task = WfSwarmingTask.Get(*try_job_key.split('/'), step_name=step_name)
189 ref_name = (task.parameters.get('ref_name') if task and task.parameters 243 ref_name = (task.parameters.get('ref_name') if task and task.parameters
190 else None) 244 else None)
191 return try_job.test_results[-1].get('culprit', {}).get( 245 return (
192 ref_name or step_name, {}).get('tests', {}).get(test_name) 246 _TryJobStatus.FINISHED, try_job.test_results[-1].get('culprit', {}).get(
247 ref_name or step_name, {}).get('tests', {}).get(test_name))
248
249 def _CheckIsFlaky(self, try_job_map, step_name, test_name):
250 """Checks if the test is flaky."""
251 if not try_job_map or not test_name:
252 return False
253
254 try_job_key = try_job_map.get(step_name, {}).get(test_name)
255 if not try_job_key:
256 return False
257
258 swarming_task = WfSwarmingTask.Get(*try_job_key.split('/'),
stgao 2016/10/22 00:24:14 Do we have function for the encode and decode of k
chanli 2016/10/24 18:30:44 Done.
259 step_name=step_name)
260 if not swarming_task or not swarming_task.classified_tests:
261 return False
262
263 return test_name in swarming_task.classified_tests.get('flaky_tests', [])
193 264
194 def _PopulateResult( 265 def _PopulateResult(
195 self, results, build, try_job_map, build_failure_type, 266 self, results, build, try_job_map, build_failure_type,
196 heuristic_result, step_name, test_name=None): 267 heuristic_result, step_name, confidences, test_name=None):
197 """Appends an analysis result for the given step or test. 268 """Appends an analysis result for the given step or test.
198 269
199 Try-job results are always given priority over heuristic results. 270 Try-job results are always given priority over heuristic results.
200 """ 271 """
201 # Default to heuristic analysis. 272 # Default to heuristic analysis.
202 suspected_cls = heuristic_result['suspected_cls'] 273 suspected_cls = heuristic_result['suspected_cls']
203 analysis_approach = _AnalysisApproach.HEURISTIC 274 analysis_approach = _AnalysisApproach.HEURISTIC
204 275
205 # Check analysis result from try-job. 276 # Check if the test is flaky.
206 culprit = self._GetCulpritFromTryJob( 277 is_flaky_test = self._CheckIsFlaky(try_job_map, step_name, test_name)
207 try_job_map, build_failure_type, step_name, test_name=test_name)
208 if culprit:
209 suspected_cls = [culprit]
210 analysis_approach = _AnalysisApproach.TRY_JOB
211 278
212 if not suspected_cls: 279 if is_flaky_test:
280 suspected_cls = []
281 try_job_status = _TryJobStatus.FINISHED # There will be no try job.
282 else:
283 # Check analysis result from try-job.
284 try_job_status, culprit = self._GetStatusAndCulpritFromTryJob(
285 try_job_map, build_failure_type, step_name, test_name=test_name)
286 if culprit:
287 suspected_cls = [culprit]
288 analysis_approach = _AnalysisApproach.TRY_JOB
289
290 if not is_flaky_test and not suspected_cls:
213 return 291 return
214 292
215 results.append(self._GenerateBuildFailureAnalysisResult( 293 results.append(self._GenerateBuildFailureAnalysisResult(
stgao 2016/10/19 00:43:08 Do we have an estimation of how many ndb read in o
chanli 2016/10/19 03:00:59 For cases with the most reads: 1. 1 ndb read for c
stgao 2016/10/21 01:51:22 Acknowledged.
216 build, suspected_cls, step_name, heuristic_result['first_failure'], 294 build, suspected_cls, step_name, heuristic_result['first_failure'],
217 test_name=test_name, analysis_approach=analysis_approach)) 295 test_name, analysis_approach, confidences, try_job_status,
296 is_flaky_test))
218 297
219 def _GenerateResultsForBuild(self, build, heuristic_analysis, results): 298 def _GenerateResultsForBuild(
299 self, build, heuristic_analysis, results, confidences):
220 for failure in heuristic_analysis.result['failures']: 300 for failure in heuristic_analysis.result['failures']:
221 if failure.get('tests'): # Test-level analysis. 301 if failure.get('tests'): # Test-level analysis.
222 for test in failure['tests']: 302 for test in failure['tests']:
223 self._PopulateResult( 303 self._PopulateResult(
224 results, build, heuristic_analysis.failure_result_map, 304 results, build, heuristic_analysis.failure_result_map,
225 heuristic_analysis.failure_type, test, 305 heuristic_analysis.failure_type, test,
226 failure['step_name'], test_name=test['test_name']) 306 failure['step_name'], confidences, test_name=test['test_name'])
227 else: 307 else:
228 self._PopulateResult( 308 self._PopulateResult(
229 results, build, heuristic_analysis.failure_result_map, 309 results, build, heuristic_analysis.failure_result_map,
230 heuristic_analysis.failure_type, failure, failure['step_name']) 310 heuristic_analysis.failure_type, failure, failure['step_name'],
311 confidences)
231 312
232 @endpoints.method( 313 @endpoints.method(
233 _BuildFailureCollection, _BuildFailureAnalysisResultCollection, 314 _BuildFailureCollection, _BuildFailureAnalysisResultCollection,
234 path='buildfailure', name='buildfailure') 315 path='buildfailure', name='buildfailure')
235 def AnalyzeBuildFailures(self, request): 316 def AnalyzeBuildFailures(self, request):
236 """Returns analysis results for the given build failures in the request. 317 """Returns analysis results for the given build failures in the request.
237 318
238 Analysis of build failures will be triggered automatically on demand. 319 Analysis of build failures will be triggered automatically on demand.
239 320
240 Args: 321 Args:
241 request (_BuildFailureCollection): A list of build failures. 322 request (_BuildFailureCollection): A list of build failures.
242 323
243 Returns: 324 Returns:
244 _BuildFailureAnalysisResultCollection 325 _BuildFailureAnalysisResultCollection
245 A list of analysis results for the given build failures. 326 A list of analysis results for the given build failures.
246 """ 327 """
247 results = [] 328 results = []
248 supported_builds = [] 329 supported_builds = []
330 confidences = SuspectedCLConfidence.Get()
249 331
250 for build in request.builds: 332 for build in request.builds:
251 master_name = buildbot.GetMasterNameFromUrl(build.master_url) 333 master_name = buildbot.GetMasterNameFromUrl(build.master_url)
252 if not (master_name and waterfall_config.MasterIsSupported(master_name)): 334 if not (master_name and waterfall_config.MasterIsSupported(master_name)):
253 logging.info('%s/%s/%s is not supported', 335 logging.info('%s/%s/%s is not supported',
254 build.master_url, build.builder_name, build.build_number) 336 build.master_url, build.builder_name, build.build_number)
255 continue 337 continue
256 338
257 supported_builds.append({ 339 supported_builds.append({
258 'master_name': master_name, 340 'master_name': master_name,
259 'builder_name': build.builder_name, 341 'builder_name': build.builder_name,
260 'build_number': build.build_number, 342 'build_number': build.build_number,
261 'failed_steps': build.failed_steps, 343 'failed_steps': build.failed_steps,
262 }) 344 })
263 345
264 # If the build failure was already analyzed and a new analysis is 346 # If the build failure was already analyzed and a new analysis is
265 # scheduled to analyze new failed steps, the returned WfAnalysis will 347 # scheduled to analyze new failed steps, the returned WfAnalysis will
266 # still have the result from last completed analysis. 348 # still have the result from last completed analysis.
267 # If there is no analysis yet, no result is returned. 349 # If there is no analysis yet, no result is returned.
268 heuristic_analysis = WfAnalysis.Get( 350 heuristic_analysis = WfAnalysis.Get(
269 master_name, build.builder_name, build.build_number) 351 master_name, build.builder_name, build.build_number)
270 if not heuristic_analysis: 352 if not heuristic_analysis:
271 continue 353 continue
272 354
273 if heuristic_analysis.failed or not heuristic_analysis.result: 355 if heuristic_analysis.failed or not heuristic_analysis.result:
274 # Bail out if the analysis failed or there is no result yet. 356 # Bail out if the analysis failed or there is no result yet.
275 continue 357 continue
276 358
277 self._GenerateResultsForBuild(build, heuristic_analysis, results) 359 self._GenerateResultsForBuild(
360 build, heuristic_analysis, results, confidences)
278 361
279 logging.info('%d build failure(s), while %d are supported', 362 logging.info('%d build failure(s), while %d are supported',
280 len(request.builds), len(supported_builds)) 363 len(request.builds), len(supported_builds))
281 try: 364 try:
282 _TriggerNewAnalysesOnDemand(supported_builds) 365 _TriggerNewAnalysesOnDemand(supported_builds)
283 except Exception: # pragma: no cover. 366 except Exception: # pragma: no cover.
284 # If we fail to post a task to the task queue, we ignore and wait for next 367 # If we fail to post a task to the task queue, we ignore and wait for next
285 # request. 368 # request.
286 logging.exception('Failed to add analysis request to task queue: %s', 369 logging.exception('Failed to add analysis request to task queue: %s',
287 repr(supported_builds)) 370 repr(supported_builds))
(...skipping 18 matching lines...) Expand all
306 logging.info('Flake: %s', CreateFlakeAnalysisRequest(request)) 389 logging.info('Flake: %s', CreateFlakeAnalysisRequest(request))
307 analysis_triggered = flake_analysis_service.ScheduleAnalysisForFlake( 390 analysis_triggered = flake_analysis_service.ScheduleAnalysisForFlake(
308 CreateFlakeAnalysisRequest(request), user_email, is_admin, 391 CreateFlakeAnalysisRequest(request), user_email, is_admin,
309 triggering_sources.FINDIT_API) 392 triggering_sources.FINDIT_API)
310 393
311 if analysis_triggered is None: 394 if analysis_triggered is None:
312 raise endpoints.UnauthorizedException( 395 raise endpoints.UnauthorizedException(
313 'No permission for a new analysis! User is %s' % user_email) 396 'No permission for a new analysis! User is %s' % user_email)
314 397
315 return _FlakeAnalysis(analysis_triggered=analysis_triggered) 398 return _FlakeAnalysis(analysis_triggered=analysis_triggered)
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