blob: 3d9355fc494aae8e3c4f916740ced0fd376a98c0 [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']))
225 self.jenkins.fakeAddHistory(name=self.name, number=self.number,
226 result='SUCCESS')
227 self.callback.jenkins_endpoint(FakeJenkinsEvent(
228 self.name, self.number, self.parameters,
229 'COMPLETED', 'SUCCESS'))
230 self.callback.jenkins_endpoint(FakeJenkinsEvent(
231 self.name, self.number, self.parameters,
232 'FINISHED', 'SUCCESS'))
233 self.jenkins.all_jobs.remove(self)
234
235
236class FakeJenkins(object):
237 log = logging.getLogger("zuul.test")
238
239 def __init__(self, *args, **kw):
240 self.queue = []
241 self.all_jobs = []
242 self.job_counter = {}
243 self.job_history = []
244 self.hold_jobs_in_queue = False
245 self.hold_jobs_in_build = False
246
247 def fakeEnqueue(self, job):
248 self.queue.append(job)
249
250 def fakeDequeue(self, job):
251 self.queue.remove(job)
252
253 def fakeAddHistory(self, **kw):
254 self.job_history.append(kw)
255
256 def fakeRelease(self, regex=None):
257 all_jobs = self.all_jobs[:]
258 self.log.debug("releasing jobs %s (%s)" % (regex, len(self.all_jobs)))
259 for job in all_jobs:
260 if not regex or re.match(regex, job.name):
261 self.log.debug("releasing job %s" % (job.parameters['UUID']))
262 job.release()
263 else:
264 self.log.debug("not releasing job %s" % (
265 job.parameters['UUID']))
266 self.log.debug("done releasing jobs %s (%s)" % (regex,
267 len(self.all_jobs)))
268
269 def fakeAllWaiting(self, regex=None):
270 all_jobs = self.all_jobs[:]
271 for job in all_jobs:
272 self.log.debug("job %s %s" % (job.parameters['UUID'],
273 job.isWaiting()))
274 if not job.isWaiting():
275 return False
276 return True
277
278 def build_job(self, name, parameters):
279 count = self.job_counter.get(name, 0)
280 count += 1
281 self.job_counter[name] = count
282 job = FakeJenkinsJob(self, self.callback, name, count, parameters)
283 self.all_jobs.append(job)
284 job.start()
285
286 def set_build_description(self, *args, **kw):
287 pass
288
289
290class FakeJenkinsCallback(zuul.launcher.jenkins.JenkinsCallback):
291 def start(self):
292 pass
293
294
295class testScheduler(unittest.TestCase):
296 log = logging.getLogger("zuul.test")
297
298 def setUp(self):
299 self.config = CONFIG
300 self.sched = zuul.scheduler.Scheduler()
301
302 def jenkinsFactory(*args, **kw):
303 self.fake_jenkins = FakeJenkins()
304 return self.fake_jenkins
305
306 def jenkinsCallbackFactory(*args, **kw):
307 self.fake_jenkins_callback = FakeJenkinsCallback(*args, **kw)
308 return self.fake_jenkins_callback
309
310 zuul.launcher.jenkins.ExtendedJenkins = jenkinsFactory
311 zuul.launcher.jenkins.JenkinsCallback = jenkinsCallbackFactory
312 self.jenkins = zuul.launcher.jenkins.Jenkins(self.config, self.sched)
313 self.fake_jenkins.callback = self.fake_jenkins_callback
314
315 zuul.lib.gerrit.Gerrit = FakeGerrit
316
317 self.gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
318 self.fake_gerrit = self.gerrit.gerrit
319
320 self.sched.setLauncher(self.jenkins)
321 self.sched.setTrigger(self.gerrit)
322
323 self.sched.start()
324 self.sched.reconfigure(self.config)
325 self.sched.resume()
326
327 def tearDown(self):
328 self.jenkins.stop()
329 self.gerrit.stop()
330 self.sched.stop()
331 self.sched.join()
332
333 def waitUntilSettled(self):
334 self.log.debug("Waiting until settled...")
335 start = time.time()
336 while True:
337 if time.time() - start > 10:
338 print 'queue status:',
339 print self.sched.trigger_event_queue.empty(),
340 print self.sched.result_event_queue.empty(),
341 print self.fake_gerrit.event_queue.empty(),
342 raise Exception("Timeout waiting for Zuul to settle")
343 self.fake_gerrit.event_queue.join()
344 self.sched.queue_lock.acquire()
345 if (self.sched.trigger_event_queue.empty() and
346 self.sched.result_event_queue.empty() and
347 self.fake_gerrit.event_queue.empty() and
348 self.fake_jenkins.fakeAllWaiting()):
349 self.sched.queue_lock.release()
350 self.log.debug("...settled.")
351 return
352 self.sched.queue_lock.release()
353 self.sched.wake_event.wait(0.1)
354
355 def test_jobs_launched(self):
356 "Test that jobs are launched and a change is merged"
357 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
358 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
359 self.waitUntilSettled()
360 jobs = self.fake_jenkins.job_history
361 job_names = [x['name'] for x in jobs]
362 assert 'project-merge' in job_names
363 assert 'project-test1' in job_names
364 assert 'project-test2' in job_names
365 assert jobs[0]['result'] == 'SUCCESS'
366 assert jobs[1]['result'] == 'SUCCESS'
367 assert jobs[2]['result'] == 'SUCCESS'
368 assert A.data['status'] == 'MERGED'
369
370 def test_parallel_changes(self):
371 "Test that changes are tested in parallel and merged in series"
372 self.fake_jenkins.hold_jobs_in_build = True
373 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
374 B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
375 C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
376
377 self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
378 self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
379 self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
380
381 self.waitUntilSettled()
382 jobs = self.fake_jenkins.all_jobs
383 assert len(jobs) == 1
384 assert jobs[0].name == 'project-merge'
385 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
386 'org/project:master:refs/changes/1/1/1')
387
388 self.fake_jenkins.fakeRelease('.*-merge')
389 self.waitUntilSettled()
390 assert len(jobs) == 3
391 assert jobs[0].name == 'project-test1'
392 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
393 'org/project:master:refs/changes/1/1/1')
394 assert jobs[1].name == 'project-test2'
395 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
396 'org/project:master:refs/changes/1/1/1')
397 assert jobs[2].name == 'project-merge'
398 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
399 'org/project:master:refs/changes/1/1/1^'
400 'org/project:master:refs/changes/1/2/1')
401
402 self.fake_jenkins.fakeRelease('.*-merge')
403 self.waitUntilSettled()
404 assert len(jobs) == 5
405 assert jobs[0].name == 'project-test1'
406 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
407 'org/project:master:refs/changes/1/1/1')
408 assert jobs[1].name == 'project-test2'
409 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
410 'org/project:master:refs/changes/1/1/1')
411
412 assert jobs[2].name == 'project-test1'
413 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
414 'org/project:master:refs/changes/1/1/1^'
415 'org/project:master:refs/changes/1/2/1')
416 assert jobs[3].name == 'project-test2'
417 assert (jobs[3].parameters['GERRIT_CHANGES'] ==
418 'org/project:master:refs/changes/1/1/1^'
419 'org/project:master:refs/changes/1/2/1')
420
421 assert jobs[4].name == 'project-merge'
422 assert (jobs[4].parameters['GERRIT_CHANGES'] ==
423 'org/project:master:refs/changes/1/1/1^'
424 'org/project:master:refs/changes/1/2/1^'
425 'org/project:master:refs/changes/1/3/1')
426
427 self.fake_jenkins.fakeRelease('.*-merge')
428 self.waitUntilSettled()
429 assert len(jobs) == 6
430 assert jobs[0].name == 'project-test1'
431 assert (jobs[0].parameters['GERRIT_CHANGES'] ==
432 'org/project:master:refs/changes/1/1/1')
433 assert jobs[1].name == 'project-test2'
434 assert (jobs[1].parameters['GERRIT_CHANGES'] ==
435 'org/project:master:refs/changes/1/1/1')
436
437 assert jobs[2].name == 'project-test1'
438 assert (jobs[2].parameters['GERRIT_CHANGES'] ==
439 'org/project:master:refs/changes/1/1/1^'
440 'org/project:master:refs/changes/1/2/1')
441 assert jobs[3].name == 'project-test2'
442 assert (jobs[3].parameters['GERRIT_CHANGES'] ==
443 'org/project:master:refs/changes/1/1/1^'
444 'org/project:master:refs/changes/1/2/1')
445
446 assert jobs[4].name == 'project-test1'
447 assert (jobs[4].parameters['GERRIT_CHANGES'] ==
448 'org/project:master:refs/changes/1/1/1^'
449 'org/project:master:refs/changes/1/2/1^'
450 'org/project:master:refs/changes/1/3/1')
451 assert jobs[5].name == 'project-test2'
452 assert (jobs[5].parameters['GERRIT_CHANGES'] ==
453 'org/project:master:refs/changes/1/1/1^'
454 'org/project:master:refs/changes/1/2/1^'
455 'org/project:master:refs/changes/1/3/1')
456
457 self.fake_jenkins.hold_jobs_in_build = False
458 self.fake_jenkins.fakeRelease()
459 self.waitUntilSettled()
460 assert len(jobs) == 0
461
462 jobs = self.fake_jenkins.job_history
463 assert len(jobs) == 9
464 assert A.data['status'] == 'MERGED'
465 assert B.data['status'] == 'MERGED'
466 assert C.data['status'] == 'MERGED'