blob: edae02ecb7bb24100ffead4d425439d0b7e26ed2 [file] [log] [blame]
James E. Blairb0fcae42012-07-17 11:12:10 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import unittest
18import ConfigParser
19import os
20import Queue
21import logging
22import json
23import threading
24import time
25import pprint
26import re
27
28import zuul
29import zuul.scheduler
30import zuul.launcher.jenkins
31import zuul.trigger.gerrit
32
33FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
34 'fixtures')
35CONFIG = ConfigParser.ConfigParser()
36CONFIG.read(os.path.join(FIXTURE_DIR, "zuul.conf"))
37
38CONFIG.set('zuul', 'layout_config',
39 os.path.join(FIXTURE_DIR, "layout.yaml"))
40
41logging.basicConfig(level=logging.DEBUG)
42
43
44class FakeChange(object):
45 categories = {'APRV': 'Approved',
46 'CRVW': 'Code-Review',
47 'VRFY': 'Verified'}
48
49 def __init__(self, number, project, branch, subject, status='NEW'):
50 self.patchsets = []
51 self.submit_records = []
52 self.number = number
53 self.project = project
54 self.branch = branch
55 self.subject = subject
56 self.latest_patchset = 0
57 self.data = {
58 'branch': branch,
59 'comments': [],
60 'commitMessage': subject,
61 'createdOn': time.time(),
62 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
63 'lastUpdated': time.time(),
64 'number': str(number),
65 'open': True,
66 'owner': {'email': 'user@example.com',
67 'name': 'User Name',
68 'username': 'username'},
69 'patchSets': self.patchsets,
70 'project': project,
71 'status': status,
72 'subject': subject,
73 'submitRecords': self.submit_records,
74 'url': 'https://hostname/%s' % number}
75
76 self.addPatchset()
77
78 def addPatchset(self, files=None):
79 self.latest_patchset += 1
80 d = {'approvals': [],
81 'createdOn': time.time(),
82 'files': [{'file': '/COMMIT_MSG',
83 'type': 'ADDED'},
84 {'file': 'README',
85 'type': 'MODIFIED'}],
86 'number': self.latest_patchset,
87 'ref': 'refs/changes/1/%s/%s' % (self.number,
88 self.latest_patchset),
89 'revision':
90 'aa69c46accf97d0598111724a38250ae76a22c87',
91 'uploader': {'email': 'user@example.com',
92 'name': 'User name',
93 'username': 'user'}}
94 self.data['currentPatchSet'] = d
95 self.patchsets.append(d)
96
97 def addApproval(self, category, value):
98 event = {'approvals': [{'description': self.categories[category],
99 'type': category,
100 'value': str(value)}],
101 'author': {'email': 'user@example.com',
102 'name': 'User Name',
103 'username': 'username'},
104 'change': {'branch': self.branch,
105 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
106 'number': str(self.number),
107 'owner': {'email': 'user@example.com',
108 'name': 'User Name',
109 'username': 'username'},
110 'project': self.project,
111 'subject': self.subject,
112 'topic': 'master',
113 'url': 'https://hostname/459'},
114 'comment': '',
115 'patchSet': self.patchsets[-1],
116 'type': 'comment-added'}
117 return json.loads(json.dumps(event))
118
119 def query(self):
120 return json.loads(json.dumps(self.data))
121
122 def setMerged(self):
123 self.data['status'] = 'MERGED'
124 self.open = False
125
126
127class FakeGerrit(object):
128 def __init__(self, *args, **kw):
129 self.event_queue = Queue.Queue()
130 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
131 self.change_number = 0
132 self.changes = {}
133
134 def addFakeChange(self, project, branch, subject):
135 self.change_number += 1
136 c = FakeChange(self.change_number, project, branch, subject)
137 self.changes[self.change_number] = c
138 return c
139
140 def addEvent(self, data):
141 return self.event_queue.put(data)
142
143 def getEvent(self):
144 return self.event_queue.get()
145
146 def eventDone(self):
147 self.event_queue.task_done()
148
149 def review(self, project, changeid, message, action):
150 if 'submit' in action:
151 number, ps = changeid.split(',')
152 change = self.changes[int(number)]
153 change.setMerged()
154
155 def query(self, number):
156 change = self.changes[int(number)]
157 return change.query()
158
159 def startWatching(self, *args, **kw):
160 pass
161
162
163class FakeJenkinsEvent(object):
164 def __init__(self, name, number, parameters, phase, status=None):
165 data = {'build':
166 {'full_url': 'https://server/job/%s/%s/' % (name, number),
167 'number': number,
168 'parameters': parameters,
169 'phase': phase,
170 'url': 'job/%s/%s/' % (name, number)},
171 'name': name,
172 'url': 'job/%s/' % name}
173 if status:
174 data['build']['status'] = status
175 self.body = json.dumps(data)
176
177
178class FakeJenkinsJob(threading.Thread):
179 log = logging.getLogger("zuul.test")
180
181 def __init__(self, jenkins, callback, name, number, parameters):
182 threading.Thread.__init__(self)
183 self.jenkins = jenkins
184 self.callback = callback
185 self.name = name
186 self.number = number
187 self.parameters = parameters
188 self.wait_condition = threading.Condition()
189 self.waiting = False
190
191 def release(self):
192 self.wait_condition.acquire()
193 self.wait_condition.notify()
194 self.waiting = False
195 self.log.debug("Job %s released" % (self.parameters['UUID']))
196 self.wait_condition.release()
197
198 def isWaiting(self):
199 self.wait_condition.acquire()
200 if self.waiting:
201 ret = True
202 else:
203 ret = False
204 self.wait_condition.release()
205 return ret
206
207 def _wait(self):
208 self.wait_condition.acquire()
209 self.waiting = True
210 self.log.debug("Job %s waiting" % (self.parameters['UUID']))
211 self.wait_condition.wait()
212 self.wait_condition.release()
213
214 def run(self):
215 self.jenkins.fakeEnqueue(self)
216 if self.jenkins.hold_jobs_in_queue:
217 self._wait()
218 self.jenkins.fakeDequeue(self)
219 self.callback.jenkins_endpoint(FakeJenkinsEvent(
220 self.name, self.number, self.parameters,
221 'STARTED'))
222 if self.jenkins.hold_jobs_in_build:
223 self._wait()
224 self.log.debug("Job %s continuing" % (self.parameters['UUID']))
James E. Blairb02a3bb2012-07-30 17:49:55 -0700225
226 result = 'SUCCESS'
227 if self.jenkins.fakeShouldFailTest(
228 self.name,
229 self.parameters['GERRIT_CHANGES']):
230 result = 'FAILURE'
231
James E. Blairb0fcae42012-07-17 11:12:10 -0700232 self.jenkins.fakeAddHistory(name=self.name, number=self.number,
James E. Blairb02a3bb2012-07-30 17:49:55 -0700233 result=result)
James E. Blairb0fcae42012-07-17 11:12:10 -0700234 self.callback.jenkins_endpoint(FakeJenkinsEvent(
235 self.name, self.number, self.parameters,
James E. Blairb02a3bb2012-07-30 17:49:55 -0700236 'COMPLETED', result))
James E. Blairb0fcae42012-07-17 11:12:10 -0700237 self.callback.jenkins_endpoint(FakeJenkinsEvent(
238 self.name, self.number, self.parameters,
James E. Blairb02a3bb2012-07-30 17:49:55 -0700239 'FINISHED', result))
James E. Blairb0fcae42012-07-17 11:12:10 -0700240 self.jenkins.all_jobs.remove(self)
241
242
243class FakeJenkins(object):
244 log = logging.getLogger("zuul.test")
245
246 def __init__(self, *args, **kw):
247 self.queue = []
248 self.all_jobs = []
249 self.job_counter = {}
250 self.job_history = []
251 self.hold_jobs_in_queue = False
252 self.hold_jobs_in_build = False
James E. Blairb02a3bb2012-07-30 17:49:55 -0700253 self.fail_tests = {}
James E. Blairb0fcae42012-07-17 11:12:10 -0700254
255 def fakeEnqueue(self, job):
256 self.queue.append(job)
257
258 def fakeDequeue(self, job):
259 self.queue.remove(job)
260
261 def fakeAddHistory(self, **kw):
262 self.job_history.append(kw)
263
264 def fakeRelease(self, regex=None):
265 all_jobs = self.all_jobs[:]
266 self.log.debug("releasing jobs %s (%s)" % (regex, len(self.all_jobs)))
267 for job in all_jobs:
268 if not regex or re.match(regex, job.name):
269 self.log.debug("releasing job %s" % (job.parameters['UUID']))
270 job.release()
271 else:
272 self.log.debug("not releasing job %s" % (
273 job.parameters['UUID']))
274 self.log.debug("done releasing jobs %s (%s)" % (regex,
275 len(self.all_jobs)))
276
277 def fakeAllWaiting(self, regex=None):
278 all_jobs = self.all_jobs[:]
279 for job in all_jobs:
280 self.log.debug("job %s %s" % (job.parameters['UUID'],
281 job.isWaiting()))
282 if not job.isWaiting():
283 return False
284 return True
285
James E. Blairb02a3bb2012-07-30 17:49:55 -0700286 def fakeAddFailTest(self, name, change):
287 l = self.fail_tests.get(name, [])
288 l.append(change)
289 self.fail_tests[name] = l
290
291 def fakeShouldFailTest(self, name, changes):
292 l = self.fail_tests.get(name, [])
293 for change in l:
294 if change in changes:
295 return True
296 return False
297
James E. Blairb0fcae42012-07-17 11:12:10 -0700298 def build_job(self, name, parameters):
299 count = self.job_counter.get(name, 0)
300 count += 1
301 self.job_counter[name] = count
302 job = FakeJenkinsJob(self, self.callback, name, count, parameters)
303 self.all_jobs.append(job)
304 job.start()
305
306 def set_build_description(self, *args, **kw):
307 pass
308
309
310class FakeJenkinsCallback(zuul.launcher.jenkins.JenkinsCallback):
311 def start(self):
312 pass
313
314
315class testScheduler(unittest.TestCase):
316 log = logging.getLogger("zuul.test")
317
318 def setUp(self):
319 self.config = CONFIG
320 self.sched = zuul.scheduler.Scheduler()
321
322 def jenkinsFactory(*args, **kw):
323 self.fake_jenkins = FakeJenkins()
324 return self.fake_jenkins
325
326 def jenkinsCallbackFactory(*args, **kw):
327 self.fake_jenkins_callback = FakeJenkinsCallback(*args, **kw)
328 return self.fake_jenkins_callback
329
330 zuul.launcher.jenkins.ExtendedJenkins = jenkinsFactory
331 zuul.launcher.jenkins.JenkinsCallback = jenkinsCallbackFactory
332 self.jenkins = zuul.launcher.jenkins.Jenkins(self.config, self.sched)
333 self.fake_jenkins.callback = self.fake_jenkins_callback
334
335 zuul.lib.gerrit.Gerrit = FakeGerrit
336
337 self.gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
338 self.fake_gerrit = self.gerrit.gerrit
339
340 self.sched.setLauncher(self.jenkins)
341 self.sched.setTrigger(self.gerrit)
342
343 self.sched.start()
344 self.sched.reconfigure(self.config)
345 self.sched.resume()
346
347 def tearDown(self):
348 self.jenkins.stop()
349 self.gerrit.stop()
350 self.sched.stop()
351 self.sched.join()
352
353 def waitUntilSettled(self):
354 self.log.debug("Waiting until settled...")
355 start = time.time()
356 while True:
357 if time.time() - start > 10:
358 print 'queue status:',
359 print self.sched.trigger_event_queue.empty(),
360 print self.sched.result_event_queue.empty(),
361 print self.fake_gerrit.event_queue.empty(),
362 raise Exception("Timeout waiting for Zuul to settle")
363 self.fake_gerrit.event_queue.join()
364 self.sched.queue_lock.acquire()
365 if (self.sched.trigger_event_queue.empty() and
366 self.sched.result_event_queue.empty() and
367 self.fake_gerrit.event_queue.empty() and
368 self.fake_jenkins.fakeAllWaiting()):
369 self.sched.queue_lock.release()
370 self.log.debug("...settled.")
371 return
372 self.sched.queue_lock.release()
373 self.sched.wake_event.wait(0.1)
374
375 def test_jobs_launched(self):
376 "Test that jobs are launched and a change is merged"
377 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
378 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
379 self.waitUntilSettled()
380 jobs = self.fake_jenkins.job_history
381 job_names = [x['name'] for x in jobs]
382 assert 'project-merge' in job_names
383 assert 'project-test1' in job_names
384 assert 'project-test2' in job_names
385 assert jobs[0]['result'] == 'SUCCESS'
386 assert jobs[1]['result'] == 'SUCCESS'
387 assert jobs[2]['result'] == 'SUCCESS'
388 assert A.data['status'] == 'MERGED'
389
390 def test_parallel_changes(self):
391 "Test that changes are tested in parallel and merged in series"
392 self.fake_jenkins.hold_jobs_in_build = True
393 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
394 B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
395 C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
396
397 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
398 self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
399 self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
400
401 self.waitUntilSettled()
402 jobs = self.fake_jenkins.all_jobs
403 assert len(jobs) == 1
404 assert jobs[0].name == 'project-merge'
405 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
406 'org/project:master:refs/changes/1/1/1')
407
408 self.fake_jenkins.fakeRelease('.*-merge')
409 self.waitUntilSettled()
410 assert len(jobs) == 3
411 assert jobs[0].name == 'project-test1'
412 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
413 'org/project:master:refs/changes/1/1/1')
414 assert jobs[1].name == 'project-test2'
415 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
416 'org/project:master:refs/changes/1/1/1')
417 assert jobs[2].name == 'project-merge'
418 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
419 'org/project:master:refs/changes/1/1/1^'
420 'org/project:master:refs/changes/1/2/1')
421
422 self.fake_jenkins.fakeRelease('.*-merge')
423 self.waitUntilSettled()
424 assert len(jobs) == 5
425 assert jobs[0].name == 'project-test1'
426 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
427 'org/project:master:refs/changes/1/1/1')
428 assert jobs[1].name == 'project-test2'
429 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
430 'org/project:master:refs/changes/1/1/1')
431
432 assert jobs[2].name == 'project-test1'
433 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
434 'org/project:master:refs/changes/1/1/1^'
435 'org/project:master:refs/changes/1/2/1')
436 assert jobs[3].name == 'project-test2'
437 assert (jobs[3].parameters['GERRIT_CHANGES'] ==
438 'org/project:master:refs/changes/1/1/1^'
439 'org/project:master:refs/changes/1/2/1')
440
441 assert jobs[4].name == 'project-merge'
442 assert (jobs[4].parameters['GERRIT_CHANGES'] ==
443 'org/project:master:refs/changes/1/1/1^'
444 'org/project:master:refs/changes/1/2/1^'
445 'org/project:master:refs/changes/1/3/1')
446
447 self.fake_jenkins.fakeRelease('.*-merge')
448 self.waitUntilSettled()
449 assert len(jobs) == 6
450 assert jobs[0].name == 'project-test1'
451 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
452 'org/project:master:refs/changes/1/1/1')
453 assert jobs[1].name == 'project-test2'
454 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
455 'org/project:master:refs/changes/1/1/1')
456
457 assert jobs[2].name == 'project-test1'
458 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
459 'org/project:master:refs/changes/1/1/1^'
460 'org/project:master:refs/changes/1/2/1')
461 assert jobs[3].name == 'project-test2'
462 assert (jobs[3].parameters['GERRIT_CHANGES'] ==
463 'org/project:master:refs/changes/1/1/1^'
464 'org/project:master:refs/changes/1/2/1')
465
466 assert jobs[4].name == 'project-test1'
467 assert (jobs[4].parameters['GERRIT_CHANGES'] ==
468 'org/project:master:refs/changes/1/1/1^'
469 'org/project:master:refs/changes/1/2/1^'
470 'org/project:master:refs/changes/1/3/1')
471 assert jobs[5].name == 'project-test2'
472 assert (jobs[5].parameters['GERRIT_CHANGES'] ==
473 'org/project:master:refs/changes/1/1/1^'
474 'org/project:master:refs/changes/1/2/1^'
475 'org/project:master:refs/changes/1/3/1')
476
477 self.fake_jenkins.hold_jobs_in_build = False
478 self.fake_jenkins.fakeRelease()
479 self.waitUntilSettled()
480 assert len(jobs) == 0
481
482 jobs = self.fake_jenkins.job_history
483 assert len(jobs) == 9
484 assert A.data['status'] == 'MERGED'
485 assert B.data['status'] == 'MERGED'
486 assert C.data['status'] == 'MERGED'
James E. Blairb02a3bb2012-07-30 17:49:55 -0700487
488 def test_failed_changes(self):
489 "Test that a change behind a failed change is retested"
490 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
491 B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
492
493 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
494 self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
495
496 self.fake_jenkins.fakeAddFailTest(
497 'project-test1',
498 'org/project:master:refs/changes/1/1/1')
499
500 self.waitUntilSettled()
501 jobs = self.fake_jenkins.job_history
502 assert len(jobs) > 6
503 assert A.data['status'] == 'NEW'
504 assert B.data['status'] == 'MERGED'
505
506 def test_independent_queues(self):
507 "Test that changes end up in the right queues"
508 self.fake_jenkins.hold_jobs_in_build = True
509 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
510 B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
511 C = self.fake_gerrit.addFakeChange('org/project2', 'master', 'C')
512
513 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
514 self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
515 self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
516
517 jobs = self.fake_jenkins.all_jobs
518 self.waitUntilSettled()
519
520 # There should be one merge job at the head of each queue running
521 assert len(jobs) == 2
522 assert jobs[0].name == 'project-merge'
523 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
524 'org/project:master:refs/changes/1/1/1')
525 assert jobs[1].name == 'project1-merge'
526 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
527 'org/project1:master:refs/changes/1/2/1')
528
529 # Release the current merge jobs
530 self.fake_jenkins.fakeRelease('.*-merge')
531 self.waitUntilSettled()
532 # Release the merge job for project2 which is behind project1
533 self.fake_jenkins.fakeRelease('.*-merge')
534 self.waitUntilSettled()
535
536 # All the test jobs should be running:
537 # project1 (3) + project2 (3) + project (2) = 8
538 assert len(jobs) == 8
539
540 self.fake_jenkins.fakeRelease()
541 self.waitUntilSettled()
542 assert len(jobs) == 0
543
544 jobs = self.fake_jenkins.job_history
545 assert len(jobs) == 11
546 assert A.data['status'] == 'MERGED'
547 assert B.data['status'] == 'MERGED'
548 assert C.data['status'] == 'MERGED'