blob: 9dc412b5f1d546eed747d38618ddcd9c65b81ee2 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -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
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
35import threading
36import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060037import uuid
38
Clark Boylanb640e052014-04-03 16:41:46 -070039
40import git
41import gear
42import fixtures
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070044import statsd
45import testtools
Mike Heald8225f522014-11-21 09:52:33 +000046from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070047
Joshua Hesketh352264b2015-08-11 23:42:08 +100048import zuul.connection.gerrit
49import zuul.connection.smtp
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import zuul.connection.sql
Clark Boylanb640e052014-04-03 16:41:46 -070051import zuul.scheduler
52import zuul.webapp
53import zuul.rpclistener
54import zuul.launcher.gearman
55import zuul.lib.swift
Clark Boylanb640e052014-04-03 16:41:46 -070056import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070057import zuul.merger.merger
58import zuul.merger.server
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
106 categories = {'APRV': ('Approved', -1, 1),
107 'CRVW': ('Code-Review', -2, 2),
108 'VRFY': ('Verified', -2, 2)}
109
110 def __init__(self, gerrit, number, project, branch, subject,
111 status='NEW', upstream_root=None):
112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
145 self.addPatchset()
146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
149 def add_fake_change_to_repo(self, msg, fn, large):
150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
161 fn = os.path.join(path, fn)
162 f = open(fn, 'w')
163 f.write("test %s %s %s\n" %
164 (self.branch, self.number, self.latest_patchset))
165 f.close()
166 repo.index.add([fn])
167 else:
168 for fni in range(100):
169 fn = os.path.join(path, str(fni))
170 f = open(fn, 'w')
171 for ci in range(4096):
172 f.write(random.choice(string.printable))
173 f.close()
174 repo.index.add([fn])
175
176 r = repo.index.commit(msg)
177 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700178 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 repo.git.clean('-x', '-f', '-d')
180 repo.heads['master'].checkout()
181 return r
182
183 def addPatchset(self, files=[], large=False):
184 self.latest_patchset += 1
185 if files:
186 fn = files[0]
187 else:
James E. Blair97d902e2014-08-21 13:25:56 -0700188 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
190 c = self.add_fake_change_to_repo(msg, fn, large)
191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
195 for f in files:
196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
263 "approvals": [{"type": "Code-Review",
264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
James E. Blair8cce42e2016-10-18 08:18:36 -0700269 def getRefUpdatedEvent(self):
270 path = os.path.join(self.upstream_root, self.project)
271 repo = git.Repo(path)
272 oldrev = repo.heads[self.branch].commit.hexsha
273
274 event = {
275 "type": "ref-updated",
276 "submitter": {
277 "name": "User Name",
278 },
279 "refUpdate": {
280 "oldRev": oldrev,
281 "newRev": self.patchsets[-1]['revision'],
282 "refName": self.branch,
283 "project": self.project,
284 }
285 }
286 return event
287
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 def addApproval(self, category, value, username='reviewer_john',
289 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700290 if not granted_on:
291 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000292 approval = {
293 'description': self.categories[category][0],
294 'type': category,
295 'value': str(value),
296 'by': {
297 'username': username,
298 'email': username + '@example.com',
299 },
300 'grantedOn': int(granted_on)
301 }
Clark Boylanb640e052014-04-03 16:41:46 -0700302 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
303 if x['by']['username'] == username and x['type'] == category:
304 del self.patchsets[-1]['approvals'][i]
305 self.patchsets[-1]['approvals'].append(approval)
306 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000307 'author': {'email': 'author@example.com',
308 'name': 'Patchset Author',
309 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700310 'change': {'branch': self.branch,
311 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
312 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000313 'owner': {'email': 'owner@example.com',
314 'name': 'Change Owner',
315 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700316 'project': self.project,
317 'subject': self.subject,
318 'topic': 'master',
319 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000320 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700321 'patchSet': self.patchsets[-1],
322 'type': 'comment-added'}
323 self.data['submitRecords'] = self.getSubmitRecords()
324 return json.loads(json.dumps(event))
325
326 def getSubmitRecords(self):
327 status = {}
328 for cat in self.categories.keys():
329 status[cat] = 0
330
331 for a in self.patchsets[-1]['approvals']:
332 cur = status[a['type']]
333 cat_min, cat_max = self.categories[a['type']][1:]
334 new = int(a['value'])
335 if new == cat_min:
336 cur = new
337 elif abs(new) > abs(cur):
338 cur = new
339 status[a['type']] = cur
340
341 labels = []
342 ok = True
343 for typ, cat in self.categories.items():
344 cur = status[typ]
345 cat_min, cat_max = cat[1:]
346 if cur == cat_min:
347 value = 'REJECT'
348 ok = False
349 elif cur == cat_max:
350 value = 'OK'
351 else:
352 value = 'NEED'
353 ok = False
354 labels.append({'label': cat[0], 'status': value})
355 if ok:
356 return [{'status': 'OK'}]
357 return [{'status': 'NOT_READY',
358 'labels': labels}]
359
360 def setDependsOn(self, other, patchset):
361 self.depends_on_change = other
362 d = {'id': other.data['id'],
363 'number': other.data['number'],
364 'ref': other.patchsets[patchset - 1]['ref']
365 }
366 self.data['dependsOn'] = [d]
367
368 other.needed_by_changes.append(self)
369 needed = other.data.get('neededBy', [])
370 d = {'id': self.data['id'],
371 'number': self.data['number'],
372 'ref': self.patchsets[patchset - 1]['ref'],
373 'revision': self.patchsets[patchset - 1]['revision']
374 }
375 needed.append(d)
376 other.data['neededBy'] = needed
377
378 def query(self):
379 self.queried += 1
380 d = self.data.get('dependsOn')
381 if d:
382 d = d[0]
383 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
384 d['isCurrentPatchSet'] = True
385 else:
386 d['isCurrentPatchSet'] = False
387 return json.loads(json.dumps(self.data))
388
389 def setMerged(self):
390 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000391 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700392 return
393 if self.fail_merge:
394 return
395 self.data['status'] = 'MERGED'
396 self.open = False
397
398 path = os.path.join(self.upstream_root, self.project)
399 repo = git.Repo(path)
400 repo.heads[self.branch].commit = \
401 repo.commit(self.patchsets[-1]['revision'])
402
403 def setReported(self):
404 self.reported += 1
405
406
Joshua Hesketh352264b2015-08-11 23:42:08 +1000407class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
408 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700409
Joshua Hesketh352264b2015-08-11 23:42:08 +1000410 def __init__(self, connection_name, connection_config,
Jan Hruban6b71aff2015-10-22 16:58:08 +0200411 changes_db=None, queues_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000412 super(FakeGerritConnection, self).__init__(connection_name,
413 connection_config)
414
415 self.event_queue = queues_db
Clark Boylanb640e052014-04-03 16:41:46 -0700416 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
417 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000418 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700419 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200420 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700421
422 def addFakeChange(self, project, branch, subject, status='NEW'):
423 self.change_number += 1
424 c = FakeChange(self, self.change_number, project, branch, subject,
425 upstream_root=self.upstream_root,
426 status=status)
427 self.changes[self.change_number] = c
428 return c
429
Clark Boylanb640e052014-04-03 16:41:46 -0700430 def review(self, project, changeid, message, action):
431 number, ps = changeid.split(',')
432 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000433
434 # Add the approval back onto the change (ie simulate what gerrit would
435 # do).
436 # Usually when zuul leaves a review it'll create a feedback loop where
437 # zuul's review enters another gerrit event (which is then picked up by
438 # zuul). However, we can't mimic this behaviour (by adding this
439 # approval event into the queue) as it stops jobs from checking what
440 # happens before this event is triggered. If a job needs to see what
441 # happens they can add their own verified event into the queue.
442 # Nevertheless, we can update change with the new review in gerrit.
443
444 for cat in ['CRVW', 'VRFY', 'APRV']:
445 if cat in action:
Joshua Hesketh352264b2015-08-11 23:42:08 +1000446 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000447
448 if 'label' in action:
449 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000450 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000451
Clark Boylanb640e052014-04-03 16:41:46 -0700452 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000453
Clark Boylanb640e052014-04-03 16:41:46 -0700454 if 'submit' in action:
455 change.setMerged()
456 if message:
457 change.setReported()
458
459 def query(self, number):
460 change = self.changes.get(int(number))
461 if change:
462 return change.query()
463 return {}
464
James E. Blairc494d542014-08-06 09:23:52 -0700465 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700466 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800468 if query.startswith('change:'):
469 # Query a specific changeid
470 changeid = query[len('change:'):]
471 l = [change.query() for change in self.changes.values()
472 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700473 elif query.startswith('message:'):
474 # Query the content of a commit message
475 msg = query[len('message:'):].strip()
476 l = [change.query() for change in self.changes.values()
477 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800478 else:
479 # Query all open changes
480 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700481 return l
James E. Blairc494d542014-08-06 09:23:52 -0700482
Joshua Hesketh352264b2015-08-11 23:42:08 +1000483 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700484 pass
485
Joshua Hesketh352264b2015-08-11 23:42:08 +1000486 def getGitUrl(self, project):
487 return os.path.join(self.upstream_root, project.name)
488
Clark Boylanb640e052014-04-03 16:41:46 -0700489
490class BuildHistory(object):
491 def __init__(self, **kw):
492 self.__dict__.update(kw)
493
494 def __repr__(self):
495 return ("<Completed build, result: %s name: %s #%s changes: %s>" %
496 (self.result, self.name, self.number, self.changes))
497
498
499class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200500 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700501 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700502 self.url = url
503
504 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700505 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700506 path = res.path
507 project = '/'.join(path.split('/')[2:-2])
508 ret = '001e# service=git-upload-pack\n'
509 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
510 'multi_ack thin-pack side-band side-band-64k ofs-delta '
511 'shallow no-progress include-tag multi_ack_detailed no-done\n')
512 path = os.path.join(self.upstream_root, project)
513 repo = git.Repo(path)
514 for ref in repo.refs:
515 r = ref.object.hexsha + ' ' + ref.path + '\n'
516 ret += '%04x%s' % (len(r) + 4, r)
517 ret += '0000'
518 return ret
519
520
Clark Boylanb640e052014-04-03 16:41:46 -0700521class FakeStatsd(threading.Thread):
522 def __init__(self):
523 threading.Thread.__init__(self)
524 self.daemon = True
525 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
526 self.sock.bind(('', 0))
527 self.port = self.sock.getsockname()[1]
528 self.wake_read, self.wake_write = os.pipe()
529 self.stats = []
530
531 def run(self):
532 while True:
533 poll = select.poll()
534 poll.register(self.sock, select.POLLIN)
535 poll.register(self.wake_read, select.POLLIN)
536 ret = poll.poll()
537 for (fd, event) in ret:
538 if fd == self.sock.fileno():
539 data = self.sock.recvfrom(1024)
540 if not data:
541 return
542 self.stats.append(data[0])
543 if fd == self.wake_read:
544 return
545
546 def stop(self):
547 os.write(self.wake_write, '1\n')
548
549
550class FakeBuild(threading.Thread):
551 log = logging.getLogger("zuul.test")
552
553 def __init__(self, worker, job, number, node):
554 threading.Thread.__init__(self)
555 self.daemon = True
556 self.worker = worker
557 self.job = job
558 self.name = job.name.split(':')[1]
559 self.number = number
560 self.node = node
561 self.parameters = json.loads(job.arguments)
562 self.unique = self.parameters['ZUUL_UUID']
563 self.wait_condition = threading.Condition()
564 self.waiting = False
565 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500566 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700567 self.created = time.time()
568 self.description = ''
569 self.run_error = False
570
571 def release(self):
572 self.wait_condition.acquire()
573 self.wait_condition.notify()
574 self.waiting = False
575 self.log.debug("Build %s released" % self.unique)
576 self.wait_condition.release()
577
578 def isWaiting(self):
579 self.wait_condition.acquire()
580 if self.waiting:
581 ret = True
582 else:
583 ret = False
584 self.wait_condition.release()
585 return ret
586
587 def _wait(self):
588 self.wait_condition.acquire()
589 self.waiting = True
590 self.log.debug("Build %s waiting" % self.unique)
591 self.wait_condition.wait()
592 self.wait_condition.release()
593
594 def run(self):
595 data = {
596 'url': 'https://server/job/%s/%s/' % (self.name, self.number),
597 'name': self.name,
598 'number': self.number,
599 'manager': self.worker.worker_id,
600 'worker_name': 'My Worker',
601 'worker_hostname': 'localhost',
602 'worker_ips': ['127.0.0.1', '192.168.1.1'],
603 'worker_fqdn': 'zuul.example.org',
604 'worker_program': 'FakeBuilder',
605 'worker_version': 'v1.1',
606 'worker_extra': {'something': 'else'}
607 }
608
609 self.log.debug('Running build %s' % self.unique)
610
611 self.job.sendWorkData(json.dumps(data))
612 self.log.debug('Sent WorkData packet with %s' % json.dumps(data))
613 self.job.sendWorkStatus(0, 100)
614
615 if self.worker.hold_jobs_in_build:
616 self.log.debug('Holding build %s' % self.unique)
617 self._wait()
618 self.log.debug("Build %s continuing" % self.unique)
619
620 self.worker.lock.acquire()
621
622 result = 'SUCCESS'
623 if (('ZUUL_REF' in self.parameters) and
624 self.worker.shouldFailTest(self.name,
625 self.parameters['ZUUL_REF'])):
626 result = 'FAILURE'
627 if self.aborted:
628 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500629 if self.requeue:
630 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700631
632 if self.run_error:
633 work_fail = True
634 result = 'RUN_ERROR'
635 else:
636 data['result'] = result
Timothy Chavezb2332082015-08-07 20:08:04 -0500637 data['node_labels'] = ['bare-necessities']
638 data['node_name'] = 'foo'
Clark Boylanb640e052014-04-03 16:41:46 -0700639 work_fail = False
640
641 changes = None
642 if 'ZUUL_CHANGE_IDS' in self.parameters:
643 changes = self.parameters['ZUUL_CHANGE_IDS']
644
645 self.worker.build_history.append(
646 BuildHistory(name=self.name, number=self.number,
647 result=result, changes=changes, node=self.node,
648 uuid=self.unique, description=self.description,
James E. Blair456f2fb2016-02-09 09:29:33 -0800649 parameters=self.parameters,
Clark Boylanb640e052014-04-03 16:41:46 -0700650 pipeline=self.parameters['ZUUL_PIPELINE'])
651 )
652
653 self.job.sendWorkData(json.dumps(data))
654 if work_fail:
655 self.job.sendWorkFail()
656 else:
657 self.job.sendWorkComplete(json.dumps(data))
658 del self.worker.gearman_jobs[self.job.unique]
659 self.worker.running_builds.remove(self)
660 self.worker.lock.release()
661
662
663class FakeWorker(gear.Worker):
664 def __init__(self, worker_id, test):
665 super(FakeWorker, self).__init__(worker_id)
666 self.gearman_jobs = {}
667 self.build_history = []
668 self.running_builds = []
669 self.build_counter = 0
670 self.fail_tests = {}
671 self.test = test
672
673 self.hold_jobs_in_build = False
674 self.lock = threading.Lock()
675 self.__work_thread = threading.Thread(target=self.work)
676 self.__work_thread.daemon = True
677 self.__work_thread.start()
678
679 def handleJob(self, job):
680 parts = job.name.split(":")
681 cmd = parts[0]
682 name = parts[1]
683 if len(parts) > 2:
684 node = parts[2]
685 else:
686 node = None
687 if cmd == 'build':
688 self.handleBuild(job, name, node)
689 elif cmd == 'stop':
690 self.handleStop(job, name)
691 elif cmd == 'set_description':
692 self.handleSetDescription(job, name)
693
694 def handleBuild(self, job, name, node):
695 build = FakeBuild(self, job, self.build_counter, node)
696 job.build = build
697 self.gearman_jobs[job.unique] = job
698 self.build_counter += 1
699
700 self.running_builds.append(build)
701 build.start()
702
703 def handleStop(self, job, name):
704 self.log.debug("handle stop")
705 parameters = json.loads(job.arguments)
706 name = parameters['name']
707 number = parameters['number']
708 for build in self.running_builds:
709 if build.name == name and build.number == number:
710 build.aborted = True
711 build.release()
712 job.sendWorkComplete()
713 return
714 job.sendWorkFail()
715
716 def handleSetDescription(self, job, name):
717 self.log.debug("handle set description")
718 parameters = json.loads(job.arguments)
719 name = parameters['name']
720 number = parameters['number']
721 descr = parameters['html_description']
722 for build in self.running_builds:
723 if build.name == name and build.number == number:
724 build.description = descr
725 job.sendWorkComplete()
726 return
727 for build in self.build_history:
728 if build.name == name and build.number == number:
729 build.description = descr
730 job.sendWorkComplete()
731 return
732 job.sendWorkFail()
733
734 def work(self):
735 while self.running:
736 try:
737 job = self.getJob()
738 except gear.InterruptedError:
739 continue
740 try:
741 self.handleJob(job)
742 except:
743 self.log.exception("Worker exception:")
744
745 def addFailTest(self, name, change):
746 l = self.fail_tests.get(name, [])
747 l.append(change)
748 self.fail_tests[name] = l
749
750 def shouldFailTest(self, name, ref):
751 l = self.fail_tests.get(name, [])
752 for change in l:
753 if self.test.ref_has_change(ref, change):
754 return True
755 return False
756
757 def release(self, regex=None):
758 builds = self.running_builds[:]
759 self.log.debug("releasing build %s (%s)" % (regex,
760 len(self.running_builds)))
761 for build in builds:
762 if not regex or re.match(regex, build.name):
763 self.log.debug("releasing build %s" %
764 (build.parameters['ZUUL_UUID']))
765 build.release()
766 else:
767 self.log.debug("not releasing build %s" %
768 (build.parameters['ZUUL_UUID']))
769 self.log.debug("done releasing builds %s (%s)" %
770 (regex, len(self.running_builds)))
771
772
773class FakeGearmanServer(gear.Server):
774 def __init__(self):
775 self.hold_jobs_in_queue = False
776 super(FakeGearmanServer, self).__init__(0)
777
778 def getJobForConnection(self, connection, peek=False):
779 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
780 for job in queue:
781 if not hasattr(job, 'waiting'):
782 if job.name.startswith('build:'):
783 job.waiting = self.hold_jobs_in_queue
784 else:
785 job.waiting = False
786 if job.waiting:
787 continue
788 if job.name in connection.functions:
789 if not peek:
790 queue.remove(job)
791 connection.related_jobs[job.handle] = job
792 job.worker_connection = connection
793 job.running = True
794 return job
795 return None
796
797 def release(self, regex=None):
798 released = False
799 qlen = (len(self.high_queue) + len(self.normal_queue) +
800 len(self.low_queue))
801 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
802 for job in self.getQueue():
803 cmd, name = job.name.split(':')
804 if cmd != 'build':
805 continue
806 if not regex or re.match(regex, name):
807 self.log.debug("releasing queued job %s" %
808 job.unique)
809 job.waiting = False
810 released = True
811 else:
812 self.log.debug("not releasing queued job %s" %
813 job.unique)
814 if released:
815 self.wakeConnections()
816 qlen = (len(self.high_queue) + len(self.normal_queue) +
817 len(self.low_queue))
818 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
819
820
821class FakeSMTP(object):
822 log = logging.getLogger('zuul.FakeSMTP')
823
824 def __init__(self, messages, server, port):
825 self.server = server
826 self.port = port
827 self.messages = messages
828
829 def sendmail(self, from_email, to_email, msg):
830 self.log.info("Sending email from %s, to %s, with msg %s" % (
831 from_email, to_email, msg))
832
833 headers = msg.split('\n\n', 1)[0]
834 body = msg.split('\n\n', 1)[1]
835
836 self.messages.append(dict(
837 from_email=from_email,
838 to_email=to_email,
839 msg=msg,
840 headers=headers,
841 body=body,
842 ))
843
844 return True
845
846 def quit(self):
847 return True
848
849
850class FakeSwiftClientConnection(swiftclient.client.Connection):
851 def post_account(self, headers):
852 # Do nothing
853 pass
854
855 def get_auth(self):
856 # Returns endpoint and (unused) auth token
857 endpoint = os.path.join('https://storage.example.org', 'V1',
858 'AUTH_account')
859 return endpoint, ''
860
861
Joshua Heskethd78b4482015-09-14 16:56:34 -0600862class MySQLSchemaFixture(fixtures.Fixture):
863 def setUp(self):
864 super(MySQLSchemaFixture, self).setUp()
865
866 random_bits = ''.join(random.choice(string.ascii_lowercase +
867 string.ascii_uppercase)
868 for x in range(8))
869 self.name = '%s_%s' % (random_bits, os.getpid())
870 self.passwd = uuid.uuid4().hex
871 db = pymysql.connect(host="localhost",
872 user="openstack_citest",
873 passwd="openstack_citest",
874 db="openstack_citest")
875 cur = db.cursor()
876 cur.execute("create database %s" % self.name)
877 cur.execute(
878 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
879 (self.name, self.name, self.passwd))
880 cur.execute("flush privileges")
881
882 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
883 self.passwd,
884 self.name)
885 self.addDetail('dburi', testtools.content.text_content(self.dburi))
886 self.addCleanup(self.cleanup)
887
888 def cleanup(self):
889 db = pymysql.connect(host="localhost",
890 user="openstack_citest",
891 passwd="openstack_citest",
892 db="openstack_citest")
893 cur = db.cursor()
894 cur.execute("drop database %s" % self.name)
895 cur.execute("drop user '%s'@'localhost'" % self.name)
896 cur.execute("flush privileges")
897
898
Maru Newby3fe5f852015-01-13 04:22:14 +0000899class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700900 log = logging.getLogger("zuul.test")
901
902 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000903 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700904 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
905 try:
906 test_timeout = int(test_timeout)
907 except ValueError:
908 # If timeout value is invalid do not set a timeout.
909 test_timeout = 0
910 if test_timeout > 0:
911 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
912
913 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
914 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
915 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
916 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
917 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
918 os.environ.get('OS_STDERR_CAPTURE') == '1'):
919 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
920 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
921 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
922 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -0700923 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100924 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
925 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -0700926 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
927 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100928 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
929 log_level = logging.WARNING
930 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
931 log_level = logging.ERROR
932 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
933 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -0700934 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100935 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -0700936 format='%(asctime)s %(name)-32s '
937 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000938
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700939 # NOTE(notmorgan): Extract logging overrides for specific libraries
940 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
941 # each. This is used to limit the output during test runs from
942 # libraries that zuul depends on such as gear.
943 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
944
945 if log_defaults_from_env:
946 for default in log_defaults_from_env.split(','):
947 try:
948 name, level_str = default.split('=', 1)
949 level = getattr(logging, level_str, logging.DEBUG)
950 self.useFixture(fixtures.FakeLogger(
951 name=name,
952 level=level,
953 format='%(asctime)s %(name)-32s '
954 '%(levelname)-8s %(message)s'))
955 except ValueError:
956 # NOTE(notmorgan): Invalid format of the log default,
957 # skip and don't try and apply a logger for the
958 # specified module
959 pass
960
Maru Newby3fe5f852015-01-13 04:22:14 +0000961
962class ZuulTestCase(BaseTestCase):
963
964 def setUp(self):
965 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700966 if USE_TEMPDIR:
967 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000968 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
969 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700970 else:
971 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700972 self.test_root = os.path.join(tmp_root, "zuul-test")
973 self.upstream_root = os.path.join(self.test_root, "upstream")
974 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700975 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700976
977 if os.path.exists(self.test_root):
978 shutil.rmtree(self.test_root)
979 os.makedirs(self.test_root)
980 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700981 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700982
983 # Make per test copy of Configuration.
984 self.setup_config()
985 self.config.set('zuul', 'layout_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100986 os.path.join(FIXTURE_DIR,
987 self.config.get('zuul', 'layout_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700988 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700989 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700990
991 # For each project in config:
992 self.init_repo("org/project")
993 self.init_repo("org/project1")
994 self.init_repo("org/project2")
995 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700996 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -0700997 self.init_repo("org/project5")
998 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -0700999 self.init_repo("org/one-job-project")
1000 self.init_repo("org/nonvoting-project")
1001 self.init_repo("org/templated-project")
1002 self.init_repo("org/layered-project")
1003 self.init_repo("org/node-project")
1004 self.init_repo("org/conflict-project")
1005 self.init_repo("org/noop-project")
1006 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001007 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001008
1009 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001010 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1011 # see: https://github.com/jsocol/pystatsd/issues/61
1012 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001013 os.environ['STATSD_PORT'] = str(self.statsd.port)
1014 self.statsd.start()
1015 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001016 reload_module(statsd)
1017 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001018
1019 self.gearman_server = FakeGearmanServer()
1020
1021 self.config.set('gearman', 'port', str(self.gearman_server.port))
1022
1023 self.worker = FakeWorker('fake_worker', self)
1024 self.worker.addServer('127.0.0.1', self.gearman_server.port)
1025 self.gearman_server.worker = self.worker
1026
Joshua Hesketh352264b2015-08-11 23:42:08 +10001027 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1028 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1029 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001030
Joshua Hesketh352264b2015-08-11 23:42:08 +10001031 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001032
1033 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1034 FakeSwiftClientConnection))
1035 self.swift = zuul.lib.swift.Swift(self.config)
1036
Jan Hruban6b71aff2015-10-22 16:58:08 +02001037 self.event_queues = [
1038 self.sched.result_event_queue,
1039 self.sched.trigger_event_queue
1040 ]
1041
Joshua Hesketh352264b2015-08-11 23:42:08 +10001042 self.configure_connections()
1043 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001044
Clark Boylanb640e052014-04-03 16:41:46 -07001045 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001046 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001047 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001048 return FakeURLOpener(self.upstream_root, *args, **kw)
1049
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001050 old_urlopen = urllib.request.urlopen
1051 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001052
Joshua Hesketh352264b2015-08-11 23:42:08 +10001053 self.merge_server = zuul.merger.server.MergeServer(self.config,
1054 self.connections)
1055 self.merge_server.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001056
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001057 self.launcher = zuul.launcher.gearman.Gearman(self.config, self.sched,
1058 self.swift)
1059 self.merge_client = zuul.merger.client.MergeClient(
1060 self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001061
1062 self.sched.setLauncher(self.launcher)
1063 self.sched.setMerger(self.merge_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001064
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001065 self.webapp = zuul.webapp.WebApp(
1066 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001067 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001068
1069 self.sched.start()
1070 self.sched.reconfigure(self.config)
1071 self.sched.resume()
1072 self.webapp.start()
1073 self.rpc.start()
1074 self.launcher.gearman.waitForServer()
1075 self.registerJobs()
1076 self.builds = self.worker.running_builds
1077 self.history = self.worker.build_history
1078
1079 self.addCleanup(self.assertFinalState)
1080 self.addCleanup(self.shutdown)
1081
Joshua Hesketh352264b2015-08-11 23:42:08 +10001082 def configure_connections(self):
Joshua Heskethd78b4482015-09-14 16:56:34 -06001083 # TODO(jhesketh): This should come from lib.connections for better
1084 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001085 # Register connections from the config
1086 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001087
Joshua Hesketh352264b2015-08-11 23:42:08 +10001088 def FakeSMTPFactory(*args, **kw):
1089 args = [self.smtp_messages] + list(args)
1090 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001091
Joshua Hesketh352264b2015-08-11 23:42:08 +10001092 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001093
Joshua Hesketh352264b2015-08-11 23:42:08 +10001094 # Set a changes database so multiple FakeGerrit's can report back to
1095 # a virtual canonical database given by the configured hostname
1096 self.gerrit_changes_dbs = {}
1097 self.gerrit_queues_dbs = {}
1098 self.connections = {}
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001099
Joshua Hesketh352264b2015-08-11 23:42:08 +10001100 for section_name in self.config.sections():
1101 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1102 section_name, re.I)
1103 if not con_match:
1104 continue
1105 con_name = con_match.group(2)
1106 con_config = dict(self.config.items(section_name))
1107
1108 if 'driver' not in con_config:
1109 raise Exception("No driver specified for connection %s."
1110 % con_name)
1111
1112 con_driver = con_config['driver']
1113
1114 # TODO(jhesketh): load the required class automatically
1115 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001116 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1117 self.gerrit_changes_dbs[con_config['server']] = {}
1118 if con_config['server'] not in self.gerrit_queues_dbs.keys():
1119 self.gerrit_queues_dbs[con_config['server']] = \
1120 Queue.Queue()
1121 self.event_queues.append(
1122 self.gerrit_queues_dbs[con_config['server']])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001123 self.connections[con_name] = FakeGerritConnection(
1124 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001125 changes_db=self.gerrit_changes_dbs[con_config['server']],
1126 queues_db=self.gerrit_queues_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001127 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001128 )
Joshua Heskethacccffc2015-03-31 23:38:17 +11001129 setattr(self, 'fake_' + con_name, self.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001130 elif con_driver == 'smtp':
1131 self.connections[con_name] = \
1132 zuul.connection.smtp.SMTPConnection(con_name, con_config)
Joshua Heskethd78b4482015-09-14 16:56:34 -06001133 elif con_driver == 'sql':
1134 self.connections[con_name] = \
1135 zuul.connection.sql.SQLConnection(con_name, con_config)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001136 else:
1137 raise Exception("Unknown driver, %s, for connection %s"
1138 % (con_config['driver'], con_name))
1139
1140 # If the [gerrit] or [smtp] sections still exist, load them in as a
1141 # connection named 'gerrit' or 'smtp' respectfully
1142
1143 if 'gerrit' in self.config.sections():
1144 self.gerrit_changes_dbs['gerrit'] = {}
1145 self.gerrit_queues_dbs['gerrit'] = Queue.Queue()
Jan Hruban6b71aff2015-10-22 16:58:08 +02001146 self.event_queues.append(self.gerrit_queues_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001147 self.connections['gerrit'] = FakeGerritConnection(
1148 '_legacy_gerrit', dict(self.config.items('gerrit')),
1149 changes_db=self.gerrit_changes_dbs['gerrit'],
1150 queues_db=self.gerrit_queues_dbs['gerrit'])
1151
1152 if 'smtp' in self.config.sections():
1153 self.connections['smtp'] = \
1154 zuul.connection.smtp.SMTPConnection(
1155 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001156
Joshua Heskethacccffc2015-03-31 23:38:17 +11001157 def setup_config(self, config_file='zuul.conf'):
Clark Boylanb640e052014-04-03 16:41:46 -07001158 """Per test config object. Override to set different config."""
1159 self.config = ConfigParser.ConfigParser()
Joshua Heskethacccffc2015-03-31 23:38:17 +11001160 self.config.read(os.path.join(FIXTURE_DIR, config_file))
Clark Boylanb640e052014-04-03 16:41:46 -07001161
1162 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001163 # Make sure that git.Repo objects have been garbage collected.
1164 repos = []
1165 gc.collect()
1166 for obj in gc.get_objects():
1167 if isinstance(obj, git.Repo):
1168 repos.append(obj)
1169 self.assertEqual(len(repos), 0)
1170 self.assertEmptyQueues()
James E. Blair0577cd62015-02-07 11:42:12 -08001171 for pipeline in self.sched.layout.pipelines.values():
1172 if isinstance(pipeline.manager,
1173 zuul.scheduler.IndependentPipelineManager):
1174 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001175
1176 def shutdown(self):
1177 self.log.debug("Shutting down after tests")
1178 self.launcher.stop()
1179 self.merge_server.stop()
1180 self.merge_server.join()
1181 self.merge_client.stop()
1182 self.worker.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07001183 self.sched.stop()
1184 self.sched.join()
1185 self.statsd.stop()
1186 self.statsd.join()
1187 self.webapp.stop()
1188 self.webapp.join()
1189 self.rpc.stop()
1190 self.rpc.join()
1191 self.gearman_server.shutdown()
1192 threads = threading.enumerate()
1193 if len(threads) > 1:
1194 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001195
1196 def init_repo(self, project):
1197 parts = project.split('/')
1198 path = os.path.join(self.upstream_root, *parts[:-1])
1199 if not os.path.exists(path):
1200 os.makedirs(path)
1201 path = os.path.join(self.upstream_root, project)
1202 repo = git.Repo.init(path)
1203
1204 repo.config_writer().set_value('user', 'email', 'user@example.com')
1205 repo.config_writer().set_value('user', 'name', 'User Name')
1206 repo.config_writer().write()
1207
1208 fn = os.path.join(path, 'README')
1209 f = open(fn, 'w')
1210 f.write("test\n")
1211 f.close()
1212 repo.index.add([fn])
1213 repo.index.commit('initial commit')
1214 master = repo.create_head('master')
1215 repo.create_tag('init')
1216
James E. Blair97d902e2014-08-21 13:25:56 -07001217 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001218 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001219 repo.git.clean('-x', '-f', '-d')
1220
1221 self.create_branch(project, 'mp')
1222
1223 def create_branch(self, project, branch):
1224 path = os.path.join(self.upstream_root, project)
1225 repo = git.Repo.init(path)
1226 fn = os.path.join(path, 'README')
1227
1228 branch_head = repo.create_head(branch)
1229 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001230 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001231 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001232 f.close()
1233 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001234 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001235
James E. Blair97d902e2014-08-21 13:25:56 -07001236 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001237 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001238 repo.git.clean('-x', '-f', '-d')
1239
Sachi King9f16d522016-03-16 12:20:45 +11001240 def create_commit(self, project):
1241 path = os.path.join(self.upstream_root, project)
1242 repo = git.Repo(path)
1243 repo.head.reference = repo.heads['master']
1244 file_name = os.path.join(path, 'README')
1245 with open(file_name, 'a') as f:
1246 f.write('creating fake commit\n')
1247 repo.index.add([file_name])
1248 commit = repo.index.commit('Creating a fake commit')
1249 return commit.hexsha
1250
Clark Boylanb640e052014-04-03 16:41:46 -07001251 def ref_has_change(self, ref, change):
1252 path = os.path.join(self.git_root, change.project)
1253 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001254 try:
1255 for commit in repo.iter_commits(ref):
1256 if commit.message.strip() == ('%s-1' % change.subject):
1257 return True
1258 except GitCommandError:
1259 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001260 return False
1261
1262 def job_has_changes(self, *args):
1263 job = args[0]
1264 commits = args[1:]
1265 if isinstance(job, FakeBuild):
1266 parameters = job.parameters
1267 else:
1268 parameters = json.loads(job.arguments)
1269 project = parameters['ZUUL_PROJECT']
1270 path = os.path.join(self.git_root, project)
1271 repo = git.Repo(path)
1272 ref = parameters['ZUUL_REF']
1273 sha = parameters['ZUUL_COMMIT']
1274 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1275 repo_shas = [c.hexsha for c in repo.iter_commits(ref)]
1276 commit_messages = ['%s-1' % commit.subject for commit in commits]
1277 self.log.debug("Checking if job %s has changes; commit_messages %s;"
1278 " repo_messages %s; sha %s" % (job, commit_messages,
1279 repo_messages, sha))
1280 for msg in commit_messages:
1281 if msg not in repo_messages:
1282 self.log.debug(" messages do not match")
1283 return False
1284 if repo_shas[0] != sha:
1285 self.log.debug(" sha does not match")
1286 return False
1287 self.log.debug(" OK")
1288 return True
1289
1290 def registerJobs(self):
1291 count = 0
1292 for job in self.sched.layout.jobs.keys():
1293 self.worker.registerFunction('build:' + job)
1294 count += 1
1295 self.worker.registerFunction('stop:' + self.worker.worker_id)
1296 count += 1
1297
1298 while len(self.gearman_server.functions) < count:
1299 time.sleep(0)
1300
James E. Blairb8c16472015-05-05 14:55:26 -07001301 def orderedRelease(self):
1302 # Run one build at a time to ensure non-race order:
1303 while len(self.builds):
1304 self.release(self.builds[0])
1305 self.waitUntilSettled()
1306
Clark Boylanb640e052014-04-03 16:41:46 -07001307 def release(self, job):
1308 if isinstance(job, FakeBuild):
1309 job.release()
1310 else:
1311 job.waiting = False
1312 self.log.debug("Queued job %s released" % job.unique)
1313 self.gearman_server.wakeConnections()
1314
1315 def getParameter(self, job, name):
1316 if isinstance(job, FakeBuild):
1317 return job.parameters[name]
1318 else:
1319 parameters = json.loads(job.arguments)
1320 return parameters[name]
1321
1322 def resetGearmanServer(self):
1323 self.worker.setFunctions([])
1324 while True:
1325 done = True
1326 for connection in self.gearman_server.active_connections:
1327 if (connection.functions and
1328 connection.client_id not in ['Zuul RPC Listener',
1329 'Zuul Merger']):
1330 done = False
1331 if done:
1332 break
1333 time.sleep(0)
1334 self.gearman_server.functions = set()
1335 self.rpc.register()
1336 self.merge_server.register()
1337
1338 def haveAllBuildsReported(self):
1339 # See if Zuul is waiting on a meta job to complete
1340 if self.launcher.meta_jobs:
1341 return False
1342 # Find out if every build that the worker has completed has been
1343 # reported back to Zuul. If it hasn't then that means a Gearman
1344 # event is still in transit and the system is not stable.
1345 for build in self.worker.build_history:
1346 zbuild = self.launcher.builds.get(build.uuid)
1347 if not zbuild:
1348 # It has already been reported
1349 continue
1350 # It hasn't been reported yet.
1351 return False
1352 # Make sure that none of the worker connections are in GRAB_WAIT
1353 for connection in self.worker.active_connections:
1354 if connection.state == 'GRAB_WAIT':
1355 return False
1356 return True
1357
1358 def areAllBuildsWaiting(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001359 builds = self.launcher.builds.values()
1360 for build in builds:
1361 client_job = None
1362 for conn in self.launcher.gearman.active_connections:
1363 for j in conn.related_jobs.values():
1364 if j.unique == build.uuid:
1365 client_job = j
1366 break
1367 if not client_job:
1368 self.log.debug("%s is not known to the gearman client" %
1369 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001370 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001371 if not client_job.handle:
1372 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001373 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001374 server_job = self.gearman_server.jobs.get(client_job.handle)
1375 if not server_job:
1376 self.log.debug("%s is not known to the gearman server" %
1377 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001378 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001379 if not hasattr(server_job, 'waiting'):
1380 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001381 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001382 if server_job.waiting:
1383 continue
1384 worker_job = self.worker.gearman_jobs.get(server_job.unique)
1385 if worker_job:
James E. Blairf15139b2015-04-02 16:37:15 -07001386 if build.number is None:
1387 self.log.debug("%s has not reported start" % worker_job)
1388 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001389 if worker_job.build.isWaiting():
1390 continue
1391 else:
1392 self.log.debug("%s is running" % worker_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001393 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001394 else:
1395 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001396 return False
1397 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001398
Jan Hruban6b71aff2015-10-22 16:58:08 +02001399 def eventQueuesEmpty(self):
1400 for queue in self.event_queues:
1401 yield queue.empty()
1402
1403 def eventQueuesJoin(self):
1404 for queue in self.event_queues:
1405 queue.join()
1406
Clark Boylanb640e052014-04-03 16:41:46 -07001407 def waitUntilSettled(self):
1408 self.log.debug("Waiting until settled...")
1409 start = time.time()
1410 while True:
1411 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001412 self.log.debug("Queue status:")
1413 for queue in self.event_queues:
1414 self.log.debug(" %s: %s" % (queue, queue.empty()))
1415 self.log.debug("All builds waiting: %s" %
1416 (self.areAllBuildsWaiting(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001417 raise Exception("Timeout waiting for Zuul to settle")
1418 # Make sure no new events show up while we're checking
1419 self.worker.lock.acquire()
1420 # have all build states propogated to zuul?
1421 if self.haveAllBuildsReported():
1422 # Join ensures that the queue is empty _and_ events have been
1423 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001424 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001425 self.sched.run_handler_lock.acquire()
James E. Blairae1b2d12015-02-07 08:01:21 -08001426 if (not self.merge_client.build_sets and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001427 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001428 self.haveAllBuildsReported() and
1429 self.areAllBuildsWaiting()):
1430 self.sched.run_handler_lock.release()
1431 self.worker.lock.release()
1432 self.log.debug("...settled.")
1433 return
1434 self.sched.run_handler_lock.release()
1435 self.worker.lock.release()
1436 self.sched.wake_event.wait(0.1)
1437
1438 def countJobResults(self, jobs, result):
1439 jobs = filter(lambda x: x.result == result, jobs)
1440 return len(jobs)
1441
1442 def getJobFromHistory(self, name):
1443 history = self.worker.build_history
1444 for job in history:
1445 if job.name == name:
1446 return job
1447 raise Exception("Unable to find job %s in history" % name)
1448
1449 def assertEmptyQueues(self):
1450 # Make sure there are no orphaned jobs
1451 for pipeline in self.sched.layout.pipelines.values():
1452 for queue in pipeline.queues:
1453 if len(queue.queue) != 0:
Morgan Fainberg4c6a7742016-05-27 08:42:17 -07001454 print('pipeline %s queue %s contents %s' % (
1455 pipeline.name, queue.name, queue.queue))
Antoine Mussobd86a312014-01-08 14:51:33 +01001456 self.assertEqual(len(queue.queue), 0,
1457 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001458
1459 def assertReportedStat(self, key, value=None, kind=None):
1460 start = time.time()
1461 while time.time() < (start + 5):
1462 for stat in self.statsd.stats:
1463 pprint.pprint(self.statsd.stats)
1464 k, v = stat.split(':')
1465 if key == k:
1466 if value is None and kind is None:
1467 return
1468 elif value:
1469 if value == v:
1470 return
1471 elif kind:
1472 if v.endswith('|' + kind):
1473 return
1474 time.sleep(0.1)
1475
1476 pprint.pprint(self.statsd.stats)
1477 raise Exception("Key %s not found in reported stats" % key)
Joshua Heskethd78b4482015-09-14 16:56:34 -06001478
1479
1480class ZuulDBTestCase(ZuulTestCase):
1481 def setup_config(self, config_file='zuul-connections-same-gerrit.conf'):
1482 super(ZuulDBTestCase, self).setup_config(config_file)
1483 for section_name in self.config.sections():
1484 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1485 section_name, re.I)
1486 if not con_match:
1487 continue
1488
1489 if self.config.get(section_name, 'driver') == 'sql':
1490 f = MySQLSchemaFixture()
1491 self.useFixture(f)
1492 if (self.config.get(section_name, 'dburi') ==
1493 '$MYSQL_FIXTURE_DBURI$'):
1494 self.config.set(section_name, 'dburi', f.dburi)