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

Side by Side Diff: commit-queue/verification/try_server.py

Issue 135363007: Delete public commit queue to avoid confusion after move to internal repo (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/
Patch Set: Created 6 years, 10 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 | Annotate | Revision Log
« no previous file with comments | « commit-queue/verification/try_job_steps.py ('k') | commit-queue/workdir/README » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # coding=utf8
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """Sends patches to the Try server and reads back results.
6
7 - TryJobs contains TryJob, one per try job on a builder.
8 - TryRunnerBase contains the common logic to send try jobs and responds to the
9 try job results.
10 - TryRunnerSvn uses svn plus /json polling on the try server for status updates.
11 """
12
13 import logging
14 import os
15 import re
16 import time
17 import urllib2
18
19 import find_depot_tools # pylint: disable=W0611
20 import presubmit_support
21 import trychange
22
23 import buildbot_json
24 import model
25 from verification import base
26
27
28 # We don't want to have trychange use gcl so block it.
29 trychange.gcl = None
30 # Hack out trychange logging.info()
31 trychange.logging = logging.getLogger('trychange')
32 trychange.logging.setLevel(logging.WARNING)
33
34
35 def or_3_way(a, b):
36 """Returns highest value, where True > False > None."""
37 return None if (a == b == None) else (a or b)
38
39
40 def parse_gclient_rev(rev):
41 """Returns the absolute number of a gclient revision.
42
43 It strips off the solution.
44 """
45 if rev is None:
46 return rev
47 return str(rev).split('@')[-1]
48
49
50 def unambiguous_revision(checkout, revision):
51 """Returns if a revision is unambiguous for the checkout.
52
53 HEAD, date or branch name are ambiguous.
54 """
55 revision = parse_gclient_rev(revision)
56 if not revision:
57 return False
58 name = checkout.__class__.__name__
59 if revision.isdigit() and 'Svn' in name:
60 # GitSvn should accept revision numbers?
61 return True
62 if re.match(r'^[a-f0-9]{5,20}$', revision) and 'Git' in name:
63 return True
64 return False
65
66
67 class TryJob(model.PersistentMixIn):
68 """Represents a try job for a pending commit.
69
70 This data can be regenerated by parsing all the try job names but it is a bit
71 hard on the try server.
72
73 TODO(maruel): Should use __getstate__(), __setstate__() and __reduce__().
74 """
75 builder = unicode
76 build = (None, int)
77 revision = (None, int)
78 result = (None, int)
79 sent = float
80 failed_steps = list
81 clobber = bool
82 name = (None, unicode)
83 # Number of retries for this configuration.
84 tries = int
85 tests = list
86
87 def __init__(self, **kwargs):
88 kwargs.setdefault('sent', time.time())
89 super(TryJob, self).__init__(**kwargs)
90
91 def get_state(self):
92 if self.result in (
93 buildbot_json.SUCCESS, buildbot_json.WARNINGS, buildbot_json.SKIPPED):
94 return base.SUCCEEDED
95 elif self.result in (
96 buildbot_json.FAILURE, buildbot_json.EXCEPTION, buildbot_json.RETRY):
97 return base.FAILED
98 else:
99 assert self.result == None
100 return base.PROCESSING
101
102
103 class TryJobs(base.IVerifierStatus):
104 """A set of try jobs that were sent for a specific patch."""
105 # An array of TryJob objects.
106 try_jobs = list
107 # When NOTRY=true is specified.
108 skipped = bool
109
110 def get_state(self):
111 if self.skipped:
112 return base.SUCCEEDED
113 if not self.try_jobs:
114 return base.PROCESSING
115 states = set(i.get_state() for i in self.try_jobs)
116 assert states.issubset(base.VALID_STATES)
117 return max(states)
118
119 def why_not(self):
120 pass
121
122
123 def steps_quality(steps):
124 if not steps:
125 return None
126 return all(v in (True, None) for v in steps)
127
128
129 class StepDb(object):
130 """Calculate statistics about all steps for each revisions."""
131 max_cache = 200
132
133 def __init__(self, builders, buildbot, checkout):
134 self._need_full = True
135 self.builders = builders
136 self.buildbot = buildbot
137 self.checkout = checkout
138
139 def need_full(self):
140 result = self._need_full
141 self._need_full = False
142 return result
143
144 def revision_quality_builder_steps(self, builder, revision):
145 """Calculates the occurence of a successful step execution, for a specific
146 builder, for builds on a specific revision.
147
148 The return value is a tuple of two elements:
149 1. array of True/False/None, one value per step in a build. True means the
150 step passed at least once. False means the step always failed. None
151 means the step never ran for this revision on this builder.
152 2. Number of builds that ran on this builder at this revision.
153 """
154 revision = str(revision)
155 steps = None
156 nb_builds = 0
157 for build in self.buildbot.builders[builder].builds.cached_children:
158 if parse_gclient_rev(build.revision) != revision:
159 continue
160 nb_builds += 1
161 if not (not steps or len(steps) == len(build.steps)):
162 logging.warn('Invalid build %s' % build)
163 continue
164 if not steps or len(steps) != len(build.steps):
165 # If the number of steps changed after a master restart, we need to
166 # ditch the previous steps.
167 # One workaround is to key by name but it's not worth the effort here,
168 # the worst case is that previous builds that could be considered good
169 # will be ignored, making it impossible to determine a lkgr.
170 steps = [None] * len(build.steps)
171 for step in build.steps:
172 steps[step.number] = or_3_way(
173 steps[step.number], step.simplified_result)
174 return steps, nb_builds
175
176 def last_good_revision_builder(self, builder):
177 """Returns LKGR for this builder or None if no revision was found.
178
179 For a single revision, for each step, make sure step either passed at least
180 once or never ran.
181
182 For example, if build 1 has [True, True, False, None] and build 2 has [True,
183 False, True, None], the revision is known to be good since each step run
184 either succeeded one time or never ran.
185 """
186 state = {}
187 for build in self.buildbot.builders[builder].builds.cached_children:
188 if not unambiguous_revision(self.checkout, build.revision):
189 # Ignore all builds that doesn't use revision numbers. It could be
190 # instead svn date format {2011-01-30}, 'HEAD', 'BASE', etc.
191 continue
192 build_rev = parse_gclient_rev(build.revision)
193 state.setdefault(build_rev, [None] * len(build.steps))
194 for step in build.steps:
195 if len(state[build_rev]) <= step.number:
196 continue
197 state[build_rev][step.number] = or_3_way(
198 state[build_rev][step.number],
199 step.simplified_result)
200
201 # Insert a None item. It will be returned if no revision was found.
202 revisions = sorted(
203 int(revision) for revision in state
204 if (all(v in (True, None) for v in state[revision])))
205 if not revisions:
206 return None
207 return revisions[-1]
208
209
210 class TryRunnerBase(base.VerifierCheckout):
211 """Stateless communication with a try server.
212
213 Sends try jobs and reads try job status.
214
215 Analysis goes as following:
216 - compile step is not flaky. compile.py already takes care of most flakiness
217 and clobber build is done by default. If compile step fails, try again with
218 clobber=True
219 - test steps are flaky and can be retried as necessary.
220 """
221 name = 'try server'
222
223 # A try job sent this long ago and that hasn't started yet is deemed to be
224 # lost.
225 lost_try_job_delay = 15*60
226
227 # Only updates a job status once every 60 seconds.
228 update_latency = 60
229
230 def __init__(
231 self, context_obj, try_server_url, commit_user,
232 builders_and_tests, ignored_steps, solution):
233 super(TryRunnerBase, self).__init__(context_obj)
234 self.commit_user = commit_user
235 self.try_server_url = try_server_url
236 self.builders_and_tests = builders_and_tests
237 self.ignored_steps = set(ignored_steps)
238 self.last_update = time.time() - self.update_latency
239 self.solution = solution
240
241 def verify(self, pending):
242 """Sends a try job to the try server and returns a TryJob list."""
243 jobs = pending.verifications.setdefault(self.name, TryJobs())
244 if jobs.try_jobs:
245 logging.warning(
246 'Already tried jobs. Let it go. At worst, it\'ll time out soon.')
247 return
248
249 jobs.try_jobs = jobs.try_jobs or []
250 if self._is_skip_try_job(pending):
251 # Do not run try job for it.
252 jobs.skipped = True
253 return
254
255 new_jobs = [
256 TryJob(
257 builder=builder,
258 tests=self.builders_and_tests[builder],
259 revision=pending.revision,
260 clobber=False)
261 for builder in sorted(self.builders_and_tests)
262 ]
263 jobs.try_jobs.extend(new_jobs)
264 self._send_jobs(
265 pending,
266 new_jobs,
267 False,
268 self.builders_and_tests,
269 unicode(pending.pending_name()))
270 # Slightly postpone next check.
271 self.last_update = min(
272 time.time(), self.last_update + (self.update_latency / 4))
273
274 def update_status(self, queue):
275 """Grabs the current status of all try jobs and update self.queue.
276
277 Note: it would be more efficient to be event based.
278 """
279 if not queue:
280 logging.debug('The list is empty, nothing to do')
281 return
282
283 if time.time() - self.last_update < self.update_latency:
284 logging.debug('TS: Throttling updates')
285 return
286 self.last_update = time.time()
287
288 self._update_statuses(queue)
289
290 def _send_jobs(
291 self, pending, jobs, need_prepare, builders_and_tests, job_name):
292 """Prepares the TryJobs instance |jobs| to send try jobs to the try server.
293
294 Sending try jobs is deferred to self._send_job().
295
296 Arguments:
297 - pending: pending_manager.Pending instance.
298 - jobs: List of TryJob instances to be executed.
299 - need_prepare: The checkout needs to have the patch applied, e.g. this
300 function is called from within update_status().
301 - builders_and_tests: dict('builder': ['test1', 'test2']) for try jobs to
302 run. Can be self.builders_and_tests or a smaller subset when retrying
303 jobs.
304 - job_name: Job name to use, may have suffix like "retry".
305 """
306 for job in jobs:
307 job.tries = job.tries or 0
308 job.tries += 1
309 if job.tries > 4:
310 raise base.DiscardPending(
311 pending,
312 ('The commit queue went berserk retrying too often for a\n'
313 'seemingly flaky test. Builder is %s, revision is %s, job name\n'
314 'was %s.') % (job.builder, job.revision, job_name))
315
316 builders = sorted(job.builder for job in jobs)
317 assert len(set(builders)) == len(builders)
318
319 revision = set(job.revision for job in jobs)
320 assert len(revision) == 1
321 revision = revision.pop()
322
323 clobber = set(job.clobber for job in jobs)
324 assert len(clobber) == 1
325 clobber = clobber.pop()
326
327 for job in jobs:
328 job.result = None
329 job.build = None
330 job.name = job_name
331 job.tests = builders_and_tests[job.builder]
332
333 if need_prepare:
334 self._prepare(pending, revision)
335 self._send_job(pending, revision, clobber, builders_and_tests, job_name)
336 for builder in builders:
337 # Signal a new try job was sent.
338 info = {
339 'builder': builder,
340 'clobber': job.clobber,
341 'job_name': job_name,
342 'revision': revision,
343 }
344 self.send_status(pending, info)
345 for job in jobs:
346 job.sent = time.time()
347
348 def _build_status_url(self, job):
349 """Html url for this try job."""
350 assert job.build is not None, str(job)
351 return '%s/buildstatus?builder=%s&number=%s' % (
352 self.try_server_url.rstrip('/'), job.builder, job.build)
353
354 def _error_msg(self, name, job, failed_steps):
355 """Constructs the error message."""
356 def steps_to_str(steps):
357 if len(steps) > 1:
358 return 'steps "%s"' % ', '.join(steps)
359 elif steps:
360 return 'step "%s"' % steps[0]
361 else:
362 return ''
363
364 msg = u'Try job failure for %s on %s for %s' % (
365 name, job.builder, steps_to_str(failed_steps))
366 if job.clobber:
367 msg += ' (clobber build)'
368 msg += '.'
369 if job.failed_steps:
370 msg += u'\nIt\'s a second try, previously, %s failed.' % (
371 steps_to_str(job.failed_steps))
372 msg += '\n%s' % self._build_status_url(job)
373 logging.info(msg)
374 return msg
375
376 def _handle_try_job(self, pending, jobs, job, build):
377 """Determines if the try job is a good signal to commit the patch."""
378 if build.simplified_result is None:
379 # The build hasn't completed yet.
380 return
381 assert job.result is None
382 assert job.build is not None
383 job.result = build.result
384 # Warning: This code assumes that steps do not abort build on failure.
385 failed_steps = list(set(
386 step.name for step in build.steps if step.simplified_result is False
387 ) - self.ignored_steps)
388 # If the failed steps are only ignored steps like update_scripts or
389 # cleanup_temp, still consider the job as a success. As such, do not use
390 # build.result.
391 if (not failed_steps and
392 all(build.steps[s].simplified_result for s in job.tests
393 if s in build.steps.keys)):
394 job.result = buildbot_json.SUCCESS
395
396 # Signal to the dashboard a try job completed.
397 info = {
398 'build': build.number,
399 'builder': job.builder,
400 'duration': build.duration,
401 'job_name': job.name,
402 'result': job.result,
403 'revision': job.revision,
404 'url': self._build_status_url(job),
405 }
406 self.send_status(pending, info)
407
408 if job.get_state() != base.FAILED:
409 assert not failed_steps
410 logging.info(u'Try job status for %s on %s: %s\n%s' % (
411 job.name,
412 job.builder,
413 job.result,
414 self._build_status_url(job)))
415 return
416
417 msg = self._error_msg(job.name, job, failed_steps)
418 quality = self._get_quality(job.builder, int(job.revision))
419
420 def retry(msg2, tests=None):
421 """Retry a try job. Will use LKGR if quality is bad."""
422 if not quality:
423 lkgr = self.get_lkgr(job.builder)
424 if lkgr is None:
425 logging.error('lkgr should never be None.')
426 fail('Couldn\'t find a good revision, aborting.')
427 return
428 job.revision = lkgr
429 logging.info(
430 'Retrying %s on %s, %s; rev=%s; %s' %
431 (job.name, job.builder, str(tests), job.revision, msg2))
432 job.failed_steps = failed_steps
433 tests = tests or job.tests
434 self._send_jobs(
435 pending, [job], True, {job.builder: tests}, u'%s (retry)' % job.name)
436
437 def fail(msg2):
438 jobs.error_message = '%s\n%s' % (msg, msg2)
439 logging.info(jobs.error_message)
440 job.failed_steps = failed_steps
441
442 if 'update' in failed_steps:
443 # Look at update quality specifically since it's a special step.
444 return fail(
445 '\nStep "update" is always a major failure.\n'
446 'Look at the try server FAQ for more details.')
447
448 if 'compile' in failed_steps:
449 if not job.clobber:
450 # Note: this resets previous test failure if there has been on the
451 # second previous try. This is fine since a slave could be broken.
452 job.clobber = True
453 return retry('retry compile with clobber')
454
455 return fail('')
456
457 if quality:
458 if job.failed_steps:
459 # The job had already failed.
460 return fail('')
461
462 return retry('Quality but first try', failed_steps)
463
464 # TODO(maruel): It would make sense to do a clobber build to see if the
465 # revision is indeed broken, since this algorithm assumes that the try
466 # server is continuously used for recent revisions!
467 # The revision looks like it's broken, retry with lkgr instead.
468 return retry('No quality, no idea', failed_steps)
469
470 @staticmethod
471 def _is_skip_try_job(pending):
472 """Returns True if a description contains NOTRY=true."""
473 match = re.search(r'^NOTRY=(.*)$', pending.description, re.MULTILINE)
474 return match and match.group(1).lower() == 'true'
475
476 def _prepare(self, pending, revision):
477 """Prepares the checkout by applying the patch."""
478 raise NotImplementedError()
479
480 def _get_quality(self, builder, revision):
481 """Gets quality about a revision job."""
482 raise NotImplementedError()
483
484 def get_lkgr(self, builder):
485 """Gets the last known good revision."""
486 raise NotImplementedError()
487
488 def _send_job(self, pending, revision, clobber, builders_and_tests, job_name):
489 """Sends a try job."""
490 raise NotImplementedError()
491
492 def _update_statuses(self, queue):
493 """Updates TryJob status for all the Pending instances in the queue.
494
495 Calls to this function are throttled.
496 """
497 raise NotImplementedError()
498
499
500 class TryRunnerSvn(TryRunnerBase):
501 """Uses SVN to send the try job.
502
503 Keeps a database of steps for each revision for each builder that ever passed,
504 to know if it is possible for a step to pass. When unsure, it sends an empty
505 build for the said revsion to determine if the revision is simply broken.
506
507 TODO(maruel): Ask the main server for details? Still doesn't cover well flaky
508 tests.
509 """
510 def __init__(
511 self, context_obj, try_server_url, commit_user,
512 builders_and_tests, ignored_steps, solution,
513 extra_flags, lkgr):
514 super(TryRunnerSvn, self).__init__(
515 context_obj, try_server_url, commit_user,
516 builders_and_tests, ignored_steps, solution)
517 self.status = buildbot_json.Buildbot(self.try_server_url)
518 self.step_db = StepDb(
519 self.builders_and_tests.keys(), self.status, self.context.checkout)
520 self.extra_flags = extra_flags or []
521 self.lkgr = lkgr
522
523 def _prepare(self, pending, revision):
524 """Running from inside update_status(), the patch wasn't applied. Do it now.
525 """
526 pending.revision = revision
527 pending.apply_patch(self.context, True)
528
529 def _get_quality(self, builder, revision):
530 steps, _ = self.step_db.revision_quality_builder_steps(builder, revision)
531 return steps_quality(steps)
532
533 def get_lkgr(self, builder):
534 return max(self.step_db.last_good_revision_builder(builder), self.lkgr())
535
536 def _send_job(self, pending, revision, clobber, builders_and_tests, job_name):
537 """Sends a try job."""
538 assert revision
539 cmd = [
540 '--no_search',
541 '--revision', '%s@%s' % (self.solution, revision),
542 '--name', job_name,
543 '--user', self.commit_user.split('@', 1)[0],
544 '--email', self.commit_user,
545 '--rietveld_url', self._patch_url(pending),
546 '--issue', str(pending.issue),
547 '--patchset', str(pending.patchset)
548 ]
549 cmd.extend(self.extra_flags)
550 for builder in sorted(builders_and_tests):
551 cmd.append('--bot')
552 tests = builders_and_tests[builder]
553 if tests:
554 cmd.append('%s:%s' % (builder, ','.join(tests)))
555 else:
556 cmd.append(builder)
557 if clobber:
558 cmd.append('--clobber')
559 # TODO(maruel): use GitChange when relevant.
560 change = presubmit_support.SvnChange(
561 job_name,
562 pending.description,
563 self.context.checkout.project_path,
564 [('M', f) for f in pending.files],
565 pending.issue,
566 pending.patchset,
567 pending.owner)
568 prev_dir = os.getcwd()
569 try:
570 os.chdir(self.context.checkout.project_path)
571 trychange.TryChange(
572 cmd,
573 change,
574 swallow_exception=True)
575 except SystemExit as e:
576 logging.error(
577 '_send_job(%s, %s, %s, %s, %s) failed!' % (
578 pending.pending_name(), revision, clobber, builders_and_tests,
579 job_name))
580 raise base.DiscardPending(
581 pending,
582 'Failed to send try job %s: %s' % (job_name, e))
583 finally:
584 os.chdir(prev_dir)
585
586 def _reset_cache(self, queue):
587 """Resets the cache of self.status and self.step_db so the next requests
588 are more efficient.
589 """
590 self.status.discard()
591
592 jobs_to_update = []
593 for _, jobs in self.loop(queue, TryJobs, True):
594 jobs_to_update.extend(
595 job for job in jobs.try_jobs if job.get_state() == base.PROCESSING)
596
597 # First determine what data is needed.
598 builds_to_cache = {}
599 if self.step_db.need_full():
600 logging.info('Fetching all try jobs status to fetch good revisions')
601 builders_to_cache = self.builders_and_tests.keys()
602 else:
603 builders_to_cache = set()
604 for job in jobs_to_update:
605 if job.build is None:
606 builders_to_cache.add(job.builder)
607 else:
608 if job.get_state() == base.PROCESSING:
609 builds_to_cache.setdefault(job.builder, []).append(job.build)
610
611 # Simplify testing.
612 builders_to_cache = sorted(builders_to_cache)
613
614 # Reduce the number of requests by caching all the needed builders in one
615 # shot when some jobs weren't started yet.
616 if builders_to_cache:
617 self.status.builders.cache_partial(builders_to_cache)
618
619 for builder in builders_to_cache:
620 self.status.builders[builder].builds.cache()
621 # Filter out jobs that were retrieved.
622 if builder in builds_to_cache:
623 del builds_to_cache[builder]
624
625 # Cache remaining builds. Sort to make testing simpler.
626 for builder, builds in sorted(
627 builds_to_cache.iteritems(), key=lambda x: x[0]):
628 self.status.builders[builder].builds.cache_partial(builds)
629
630 def _update_statuses(self, queue):
631 self._reset_cache(queue)
632 for pending, jobs in self.loop(queue, TryJobs, True):
633 for job in jobs.try_jobs:
634 if job.get_state() != base.PROCESSING:
635 continue
636 self._update_status(pending, jobs, job)
637
638 def _update_status(self, pending, jobs, job):
639 """There's one TryJob per builder."""
640 # TODO(maruel): There should be differentiation when there's multiple
641 # jobs for a single builder.
642 build = None
643 try:
644 if job.build is None:
645 build = self._find_job(job)
646 if build:
647 # Signal a try job was found.
648 info = {
649 'build': build.number,
650 'builder': job.builder,
651 'job_name': job.name,
652 'revision': job.revision,
653 'url': self._build_status_url(job),
654 }
655 self.send_status(pending, info)
656 else:
657 try:
658 build = self.status.builders[job.builder].builds[job.build]
659 except KeyError:
660 # May happen when there is a huge backlog and the build is not
661 # cached anymore.
662 build = None
663 except urllib2.HTTPError as e:
664 logging.error(str(e))
665 return
666
667 if build is not None:
668 self._handle_try_job(pending, jobs, job, build)
669 else:
670 # A job needs to be sent again if it has been sent more than
671 # self.lost_try_job_delay ago.
672 builder = self.status.builders[job.builder]
673 pending_builds = builder.data.get('pendingBuilds', 0)
674 if (time.time() - job.sent) > self.lost_try_job_delay:
675 if pending_builds:
676 job_names = [
677 data.get('reason', '') for data in builder.pending_builds.data
678 ]
679 if job.name in job_names:
680 # It's pending, move on.
681 return
682
683 # The job went to /dev/null. For example, the master may have
684 # restarted, the svn server may have a fluke, network may have had a
685 # short downtime, etc. Delete the previous job.
686 # Resend exactly the same job.
687 tests = job.tests
688 if not tests:
689 if not job.builder in self.builders_and_tests:
690 # This means the builder was removed. Skip it.
691 logging.warn(
692 ( 'Wanted to retry %s but it\'s not a requirement anymore. '
693 'Ignoring it!') % job.builder)
694 job.result = buildbot_json.SKIPPED
695 return
696
697 tests = self.builders_and_tests[job.builder]
698 self._send_jobs(
699 pending,
700 [job],
701 True,
702 {job.builder:tests},
703 u'%s (previous was lost)' % job.name)
704
705 def _find_job(self, job):
706 """Searches on the try server if the try job for |job| has started."""
707 revision = '%s@%s' % (self.solution, job.revision)
708 # TODO(maruel): Strip this off.
709 job_name = job.name.split(':', 1)[-1]
710 logging.debug('Searching for job.reason = %s @ %s' % (job_name, revision))
711 for build in self.status.builders[job.builder].builds:
712 blame = build.data.get('blame', [])
713 logging.debug(
714 'Build.reason = %s @ %s; blame: %s' % (
715 build.reason, build.revision, ','.join(blame)))
716 if (build.reason == job_name and
717 str(build.revision) == revision and
718 len(blame) == 1 and
719 blame[0] == self.commit_user):
720 # Note the build number to remember it started.
721 logging.info('Found build %d for job %s' % (build.number, job_name))
722 job.build = build.number
723 return build
724 return None
725
726 def _patch_url(self, pending):
727 return ('%s/download/issue%d_%d.diff' %
728 (self.context.rietveld.url, pending.issue, pending.patchset))
OLDNEW
« no previous file with comments | « commit-queue/verification/try_job_steps.py ('k') | commit-queue/workdir/README » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698