blob: eb1f93350fb3b993d311d2c8aab4717729516b99 [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
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Mike Heald8225f522014-11-21 09:52:33 +000044from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
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):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 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
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 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:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 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"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700389 """A Fake Gerrit connection for use in tests.
390
391 This subclasses
392 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
393 ability for tests to add changes to the fake Gerrit it represents.
394 """
395
Joshua Hesketh352264b2015-08-11 23:42:08 +1000396 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700399 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 super(FakeGerritConnection, self).__init__(connection_name,
401 connection_config)
402
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700404 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
405 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000406 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700407 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200408 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700409
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700410 def addFakeChange(self, project, branch, subject, status='NEW',
411 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700412 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700413 self.change_number += 1
414 c = FakeChange(self, self.change_number, project, branch, subject,
415 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700416 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.changes[self.change_number] = c
418 return c
419
Clark Boylanb640e052014-04-03 16:41:46 -0700420 def review(self, project, changeid, message, action):
421 number, ps = changeid.split(',')
422 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000423
424 # Add the approval back onto the change (ie simulate what gerrit would
425 # do).
426 # Usually when zuul leaves a review it'll create a feedback loop where
427 # zuul's review enters another gerrit event (which is then picked up by
428 # zuul). However, we can't mimic this behaviour (by adding this
429 # approval event into the queue) as it stops jobs from checking what
430 # happens before this event is triggered. If a job needs to see what
431 # happens they can add their own verified event into the queue.
432 # Nevertheless, we can update change with the new review in gerrit.
433
James E. Blair8b5408c2016-08-08 15:37:46 -0700434 for cat in action.keys():
435 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439 if 'label' in action:
440 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000441 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442
Clark Boylanb640e052014-04-03 16:41:46 -0700443 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 if 'submit' in action:
446 change.setMerged()
447 if message:
448 change.setReported()
449
450 def query(self, number):
451 change = self.changes.get(int(number))
452 if change:
453 return change.query()
454 return {}
455
James E. Blairc494d542014-08-06 09:23:52 -0700456 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700457 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700458 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800459 if query.startswith('change:'):
460 # Query a specific changeid
461 changeid = query[len('change:'):]
462 l = [change.query() for change in self.changes.values()
463 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700464 elif query.startswith('message:'):
465 # Query the content of a commit message
466 msg = query[len('message:'):].strip()
467 l = [change.query() for change in self.changes.values()
468 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800469 else:
470 # Query all open changes
471 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700472 return l
James E. Blairc494d542014-08-06 09:23:52 -0700473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700475 pass
476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def getGitUrl(self, project):
478 return os.path.join(self.upstream_root, project.name)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700486 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
487 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. Blair17302972016-08-10 16:11:42 -0700544 def __init__(self, launch_server, job, node):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700546 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700548 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700549 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.node = node
551 self.parameters = json.loads(job.arguments)
552 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700553 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700554 self.wait_condition = threading.Condition()
555 self.waiting = False
556 self.aborted = False
557 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700558 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700559 self.changes = None
560 if 'ZUUL_CHANGE_IDS' in self.parameters:
561 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700562
563 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700564 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.wait_condition.acquire()
566 self.wait_condition.notify()
567 self.waiting = False
568 self.log.debug("Build %s released" % self.unique)
569 self.wait_condition.release()
570
571 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700572 """Return whether this build is being held.
573
574 :returns: Whether the build is being held.
575 :rtype: bool
576 """
577
Clark Boylanb640e052014-04-03 16:41:46 -0700578 self.wait_condition.acquire()
579 if self.waiting:
580 ret = True
581 else:
582 ret = False
583 self.wait_condition.release()
584 return ret
585
586 def _wait(self):
587 self.wait_condition.acquire()
588 self.waiting = True
589 self.log.debug("Build %s waiting" % self.unique)
590 self.wait_condition.wait()
591 self.wait_condition.release()
592
593 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700594 self.log.debug('Running build %s' % self.unique)
595
James E. Blaire1767bc2016-08-02 10:00:27 -0700596 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700597 self.log.debug('Holding build %s' % self.unique)
598 self._wait()
599 self.log.debug("Build %s continuing" % self.unique)
600
Clark Boylanb640e052014-04-03 16:41:46 -0700601 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700602 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700603 result = 'FAILURE'
604 if self.aborted:
605 result = 'ABORTED'
606
607 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700608 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700609
James E. Blaire1767bc2016-08-02 10:00:27 -0700610 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700611
James E. Blaira5dba232016-08-08 15:53:24 -0700612 def shouldFail(self):
613 changes = self.launch_server.fail_tests.get(self.name, [])
614 for change in changes:
615 if self.hasChanges(change):
616 return True
617 return False
618
James E. Blaire7b99a02016-08-05 14:27:34 -0700619 def hasChanges(self, *changes):
620 """Return whether this build has certain changes in its git repos.
621
622 :arg FakeChange changes: One or more changes (varargs) that
623 are expected to be present (in order) in the git repository of
624 the active project.
625
626 :returns: Whether the build has the indicated changes.
627 :rtype: bool
628
629 """
James E. Blair962220f2016-08-03 11:22:38 -0700630 project = self.parameters['ZUUL_PROJECT']
631 path = os.path.join(self.jobdir.git_root, project)
632 repo = git.Repo(path)
633 ref = self.parameters['ZUUL_REF']
634 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700635 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700636 self.log.debug("Checking if build %s has changes; commit_messages %s;"
637 " repo_messages %s" % (self, commit_messages,
638 repo_messages))
639 for msg in commit_messages:
640 if msg not in repo_messages:
641 self.log.debug(" messages do not match")
642 return False
643 self.log.debug(" OK")
644 return True
645
Clark Boylanb640e052014-04-03 16:41:46 -0700646
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000647class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700648 """An Ansible launcher to be used in tests.
649
650 :ivar bool hold_jobs_in_build: If true, when jobs are launched
651 they will report that they have started but then pause until
652 released before reporting completion. This attribute may be
653 changed at any time and will take effect for subsequently
654 launched builds, but previously held builds will still need to
655 be explicitly released.
656
657 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800658 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700659 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800660 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700661 self.hold_jobs_in_build = False
662 self.lock = threading.Lock()
663 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700664 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700665 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700666 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800667
James E. Blaira5dba232016-08-08 15:53:24 -0700668 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700669 """Instruct the launcher to report matching builds as failures.
670
671 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700672 :arg Change change: The :py:class:`~tests.base.FakeChange`
673 instance which should cause the job to fail. This job
674 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700675
676 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700677 l = self.fail_tests.get(name, [])
678 l.append(change)
679 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800680
James E. Blair962220f2016-08-03 11:22:38 -0700681 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700682 """Release a held build.
683
684 :arg str regex: A regular expression which, if supplied, will
685 cause only builds with matching names to be released. If
686 not supplied, all builds will be released.
687
688 """
James E. Blair962220f2016-08-03 11:22:38 -0700689 builds = self.running_builds[:]
690 self.log.debug("Releasing build %s (%s)" % (regex,
691 len(self.running_builds)))
692 for build in builds:
693 if not regex or re.match(regex, build.name):
694 self.log.debug("Releasing build %s" %
695 (build.parameters['ZUUL_UUID']))
696 build.release()
697 else:
698 self.log.debug("Not releasing build %s" %
699 (build.parameters['ZUUL_UUID']))
700 self.log.debug("Done releasing builds %s (%s)" %
701 (regex, len(self.running_builds)))
702
James E. Blair17302972016-08-10 16:11:42 -0700703 def launchJob(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700704 node = None
James E. Blair17302972016-08-10 16:11:42 -0700705 build = FakeBuild(self, job, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700706 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700707 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700708 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700709 super(RecordingLaunchServer, self).launchJob(job)
710
711 def stopJob(self, job):
712 self.log.debug("handle stop")
713 parameters = json.loads(job.arguments)
714 uuid = parameters['uuid']
715 for build in self.running_builds:
716 if build.unique == uuid:
717 build.aborted = True
718 build.release()
719 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700720
721 def runAnsible(self, jobdir, job):
722 build = self.job_builds[job.unique]
723 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700724
725 if self._run_ansible:
726 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
727 else:
728 result = build.run()
729
730 self.lock.acquire()
731 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700732 BuildHistory(name=build.name, result=result, changes=build.changes,
733 node=build.node, uuid=build.unique,
734 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700735 pipeline=build.parameters['ZUUL_PIPELINE'])
736 )
James E. Blairab7132b2016-08-05 12:36:22 -0700737 self.running_builds.remove(build)
738 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700739 self.lock.release()
740 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800741
742
Clark Boylanb640e052014-04-03 16:41:46 -0700743class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700744 """A Gearman server for use in tests.
745
746 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
747 added to the queue but will not be distributed to workers
748 until released. This attribute may be changed at any time and
749 will take effect for subsequently enqueued jobs, but
750 previously held jobs will still need to be explicitly
751 released.
752
753 """
754
Clark Boylanb640e052014-04-03 16:41:46 -0700755 def __init__(self):
756 self.hold_jobs_in_queue = False
757 super(FakeGearmanServer, self).__init__(0)
758
759 def getJobForConnection(self, connection, peek=False):
760 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
761 for job in queue:
762 if not hasattr(job, 'waiting'):
763 if job.name.startswith('build:'):
764 job.waiting = self.hold_jobs_in_queue
765 else:
766 job.waiting = False
767 if job.waiting:
768 continue
769 if job.name in connection.functions:
770 if not peek:
771 queue.remove(job)
772 connection.related_jobs[job.handle] = job
773 job.worker_connection = connection
774 job.running = True
775 return job
776 return None
777
778 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700779 """Release a held job.
780
781 :arg str regex: A regular expression which, if supplied, will
782 cause only jobs with matching names to be released. If
783 not supplied, all jobs will be released.
784 """
Clark Boylanb640e052014-04-03 16:41:46 -0700785 released = False
786 qlen = (len(self.high_queue) + len(self.normal_queue) +
787 len(self.low_queue))
788 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
789 for job in self.getQueue():
790 cmd, name = job.name.split(':')
791 if cmd != 'build':
792 continue
793 if not regex or re.match(regex, name):
794 self.log.debug("releasing queued job %s" %
795 job.unique)
796 job.waiting = False
797 released = True
798 else:
799 self.log.debug("not releasing queued job %s" %
800 job.unique)
801 if released:
802 self.wakeConnections()
803 qlen = (len(self.high_queue) + len(self.normal_queue) +
804 len(self.low_queue))
805 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
806
807
808class FakeSMTP(object):
809 log = logging.getLogger('zuul.FakeSMTP')
810
811 def __init__(self, messages, server, port):
812 self.server = server
813 self.port = port
814 self.messages = messages
815
816 def sendmail(self, from_email, to_email, msg):
817 self.log.info("Sending email from %s, to %s, with msg %s" % (
818 from_email, to_email, msg))
819
820 headers = msg.split('\n\n', 1)[0]
821 body = msg.split('\n\n', 1)[1]
822
823 self.messages.append(dict(
824 from_email=from_email,
825 to_email=to_email,
826 msg=msg,
827 headers=headers,
828 body=body,
829 ))
830
831 return True
832
833 def quit(self):
834 return True
835
836
837class FakeSwiftClientConnection(swiftclient.client.Connection):
838 def post_account(self, headers):
839 # Do nothing
840 pass
841
842 def get_auth(self):
843 # Returns endpoint and (unused) auth token
844 endpoint = os.path.join('https://storage.example.org', 'V1',
845 'AUTH_account')
846 return endpoint, ''
847
848
Maru Newby3fe5f852015-01-13 04:22:14 +0000849class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700850 log = logging.getLogger("zuul.test")
851
852 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000853 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700854 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
855 try:
856 test_timeout = int(test_timeout)
857 except ValueError:
858 # If timeout value is invalid do not set a timeout.
859 test_timeout = 0
860 if test_timeout > 0:
861 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
862
863 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
864 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
865 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
866 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
867 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
868 os.environ.get('OS_STDERR_CAPTURE') == '1'):
869 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
870 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
871 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
872 os.environ.get('OS_LOG_CAPTURE') == '1'):
873 self.useFixture(fixtures.FakeLogger(
874 level=logging.DEBUG,
875 format='%(asctime)s %(name)-32s '
876 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000877
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700878 # NOTE(notmorgan): Extract logging overrides for specific libraries
879 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
880 # each. This is used to limit the output during test runs from
881 # libraries that zuul depends on such as gear.
882 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
883
884 if log_defaults_from_env:
885 for default in log_defaults_from_env.split(','):
886 try:
887 name, level_str = default.split('=', 1)
888 level = getattr(logging, level_str, logging.DEBUG)
889 self.useFixture(fixtures.FakeLogger(
890 name=name,
891 level=level,
892 format='%(asctime)s %(name)-32s '
893 '%(levelname)-8s %(message)s'))
894 except ValueError:
895 # NOTE(notmorgan): Invalid format of the log default,
896 # skip and don't try and apply a logger for the
897 # specified module
898 pass
899
Maru Newby3fe5f852015-01-13 04:22:14 +0000900
901class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700902 """A test case with a functioning Zuul.
903
904 The following class variables are used during test setup and can
905 be overidden by subclasses but are effectively read-only once a
906 test method starts running:
907
908 :cvar str config_file: This points to the main zuul config file
909 within the fixtures directory. Subclasses may override this
910 to obtain a different behavior.
911
912 :cvar str tenant_config_file: This is the tenant config file
913 (which specifies from what git repos the configuration should
914 be loaded). It defaults to the value specified in
915 `config_file` but can be overidden by subclasses to obtain a
916 different tenant/project layout while using the standard main
917 configuration.
918
919 The following are instance variables that are useful within test
920 methods:
921
922 :ivar FakeGerritConnection fake_<connection>:
923 A :py:class:`~tests.base.FakeGerritConnection` will be
924 instantiated for each connection present in the config file
925 and stored here. For instance, `fake_gerrit` will hold the
926 FakeGerritConnection object for a connection named `gerrit`.
927
928 :ivar FakeGearmanServer gearman_server: An instance of
929 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
930 server that all of the Zuul components in this test use to
931 communicate with each other.
932
933 :ivar RecordingLaunchServer launch_server: An instance of
934 :py:class:`~tests.base.RecordingLaunchServer` which is the
935 Ansible launch server used to run jobs for this test.
936
937 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
938 representing currently running builds. They are appended to
939 the list in the order they are launched, and removed from this
940 list upon completion.
941
942 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
943 objects representing completed builds. They are appended to
944 the list in the order they complete.
945
946 """
947
James E. Blair83005782015-12-11 14:46:03 -0800948 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700949 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700950
951 def _startMerger(self):
952 self.merge_server = zuul.merger.server.MergeServer(self.config,
953 self.connections)
954 self.merge_server.start()
955
Maru Newby3fe5f852015-01-13 04:22:14 +0000956 def setUp(self):
957 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700958 if USE_TEMPDIR:
959 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000960 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
961 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700962 else:
963 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700964 self.test_root = os.path.join(tmp_root, "zuul-test")
965 self.upstream_root = os.path.join(self.test_root, "upstream")
966 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700967 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700968
969 if os.path.exists(self.test_root):
970 shutil.rmtree(self.test_root)
971 os.makedirs(self.test_root)
972 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700973 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700974
975 # Make per test copy of Configuration.
976 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800977 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100978 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800979 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700980 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700981 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700982
983 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700984 # TODOv3(jeblair): remove these and replace with new git
985 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700986 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700987 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -0700988 self.init_repo("org/project5")
989 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -0700990 self.init_repo("org/one-job-project")
991 self.init_repo("org/nonvoting-project")
992 self.init_repo("org/templated-project")
993 self.init_repo("org/layered-project")
994 self.init_repo("org/node-project")
995 self.init_repo("org/conflict-project")
996 self.init_repo("org/noop-project")
997 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +0000998 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -0700999
1000 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001001 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1002 # see: https://github.com/jsocol/pystatsd/issues/61
1003 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001004 os.environ['STATSD_PORT'] = str(self.statsd.port)
1005 self.statsd.start()
1006 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001007 reload_module(statsd)
1008 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001009
1010 self.gearman_server = FakeGearmanServer()
1011
1012 self.config.set('gearman', 'port', str(self.gearman_server.port))
1013
Joshua Hesketh352264b2015-08-11 23:42:08 +10001014 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1015 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1016 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001017
Joshua Hesketh352264b2015-08-11 23:42:08 +10001018 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001019
1020 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1021 FakeSwiftClientConnection))
1022 self.swift = zuul.lib.swift.Swift(self.config)
1023
Jan Hruban6b71aff2015-10-22 16:58:08 +02001024 self.event_queues = [
1025 self.sched.result_event_queue,
1026 self.sched.trigger_event_queue
1027 ]
1028
James E. Blairfef78942016-03-11 16:28:56 -08001029 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001030 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001031
Clark Boylanb640e052014-04-03 16:41:46 -07001032 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001033 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001034 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001035 return FakeURLOpener(self.upstream_root, *args, **kw)
1036
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001037 old_urlopen = urllib.request.urlopen
1038 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001039
James E. Blair3f876d52016-07-22 13:07:14 -07001040 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001041
James E. Blaire1767bc2016-08-02 10:00:27 -07001042 self.launch_server = RecordingLaunchServer(
1043 self.config, self.connections, _run_ansible=self.run_ansible)
1044 self.launch_server.start()
1045 self.history = self.launch_server.build_history
1046 self.builds = self.launch_server.running_builds
1047
1048 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001049 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001050 self.merge_client = zuul.merger.client.MergeClient(
1051 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001052 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001053
James E. Blaire1767bc2016-08-02 10:00:27 -07001054 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001055 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001056 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001057
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001058 self.webapp = zuul.webapp.WebApp(
1059 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001060 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001061
1062 self.sched.start()
1063 self.sched.reconfigure(self.config)
1064 self.sched.resume()
1065 self.webapp.start()
1066 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001067 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001068
1069 self.addCleanup(self.assertFinalState)
1070 self.addCleanup(self.shutdown)
1071
James E. Blairfef78942016-03-11 16:28:56 -08001072 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001073 # Register connections from the config
1074 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001075
Joshua Hesketh352264b2015-08-11 23:42:08 +10001076 def FakeSMTPFactory(*args, **kw):
1077 args = [self.smtp_messages] + list(args)
1078 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001079
Joshua Hesketh352264b2015-08-11 23:42:08 +10001080 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001081
Joshua Hesketh352264b2015-08-11 23:42:08 +10001082 # Set a changes database so multiple FakeGerrit's can report back to
1083 # a virtual canonical database given by the configured hostname
1084 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001085 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001086
Joshua Hesketh352264b2015-08-11 23:42:08 +10001087 for section_name in self.config.sections():
1088 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1089 section_name, re.I)
1090 if not con_match:
1091 continue
1092 con_name = con_match.group(2)
1093 con_config = dict(self.config.items(section_name))
1094
1095 if 'driver' not in con_config:
1096 raise Exception("No driver specified for connection %s."
1097 % con_name)
1098
1099 con_driver = con_config['driver']
1100
1101 # TODO(jhesketh): load the required class automatically
1102 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001103 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1104 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001105 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001106 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001107 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001108 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001109 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001110 self.event_queues.append(
1111 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001112 setattr(self, 'fake_' + con_name,
1113 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001114 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001115 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001116 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1117 else:
1118 raise Exception("Unknown driver, %s, for connection %s"
1119 % (con_config['driver'], con_name))
1120
1121 # If the [gerrit] or [smtp] sections still exist, load them in as a
1122 # connection named 'gerrit' or 'smtp' respectfully
1123
1124 if 'gerrit' in self.config.sections():
1125 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001126 self.event_queues.append(
1127 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001128 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001129 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001130 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001131
1132 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001133 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001134 zuul.connection.smtp.SMTPConnection(
1135 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001136
James E. Blair83005782015-12-11 14:46:03 -08001137 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001138 # This creates the per-test configuration object. It can be
1139 # overriden by subclasses, but should not need to be since it
1140 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001142 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001143 if hasattr(self, 'tenant_config_file'):
1144 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001145 git_path = os.path.join(
1146 os.path.dirname(
1147 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1148 'git')
1149 if os.path.exists(git_path):
1150 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001151 project = reponame.replace('_', '/')
1152 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001153 os.path.join(git_path, reponame))
1154
1155 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001156 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001157
1158 files = {}
1159 for (dirpath, dirnames, filenames) in os.walk(source_path):
1160 for filename in filenames:
1161 test_tree_filepath = os.path.join(dirpath, filename)
1162 common_path = os.path.commonprefix([test_tree_filepath,
1163 source_path])
1164 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1165 with open(test_tree_filepath, 'r') as f:
1166 content = f.read()
1167 files[relative_filepath] = content
1168 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001169 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001170
Clark Boylanb640e052014-04-03 16:41:46 -07001171 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001172 # Make sure that git.Repo objects have been garbage collected.
1173 repos = []
1174 gc.collect()
1175 for obj in gc.get_objects():
1176 if isinstance(obj, git.Repo):
1177 repos.append(obj)
1178 self.assertEqual(len(repos), 0)
1179 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001180 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001181 for tenant in self.sched.abide.tenants.values():
1182 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001183 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001184 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001185
1186 def shutdown(self):
1187 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001188 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001189 self.merge_server.stop()
1190 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001191 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001192 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001193 self.sched.stop()
1194 self.sched.join()
1195 self.statsd.stop()
1196 self.statsd.join()
1197 self.webapp.stop()
1198 self.webapp.join()
1199 self.rpc.stop()
1200 self.rpc.join()
1201 self.gearman_server.shutdown()
1202 threads = threading.enumerate()
1203 if len(threads) > 1:
1204 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001205
1206 def init_repo(self, project):
1207 parts = project.split('/')
1208 path = os.path.join(self.upstream_root, *parts[:-1])
1209 if not os.path.exists(path):
1210 os.makedirs(path)
1211 path = os.path.join(self.upstream_root, project)
1212 repo = git.Repo.init(path)
1213
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001214 with repo.config_writer() as config_writer:
1215 config_writer.set_value('user', 'email', 'user@example.com')
1216 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001217
Clark Boylanb640e052014-04-03 16:41:46 -07001218 repo.index.commit('initial commit')
1219 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001220
James E. Blair97d902e2014-08-21 13:25:56 -07001221 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001222 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001223 repo.git.clean('-x', '-f', '-d')
1224
James E. Blair97d902e2014-08-21 13:25:56 -07001225 def create_branch(self, project, branch):
1226 path = os.path.join(self.upstream_root, project)
1227 repo = git.Repo.init(path)
1228 fn = os.path.join(path, 'README')
1229
1230 branch_head = repo.create_head(branch)
1231 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001232 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001233 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001234 f.close()
1235 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001236 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001237
James E. Blair97d902e2014-08-21 13:25:56 -07001238 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001239 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001240 repo.git.clean('-x', '-f', '-d')
1241
Sachi King9f16d522016-03-16 12:20:45 +11001242 def create_commit(self, project):
1243 path = os.path.join(self.upstream_root, project)
1244 repo = git.Repo(path)
1245 repo.head.reference = repo.heads['master']
1246 file_name = os.path.join(path, 'README')
1247 with open(file_name, 'a') as f:
1248 f.write('creating fake commit\n')
1249 repo.index.add([file_name])
1250 commit = repo.index.commit('Creating a fake commit')
1251 return commit.hexsha
1252
Clark Boylanb640e052014-04-03 16:41:46 -07001253 def ref_has_change(self, ref, change):
James E. Blaira5dba232016-08-08 15:53:24 -07001254 # TODOv3(jeblair): this should probably be removed in favor of
1255 # build.hasChanges
Clark Boylanb640e052014-04-03 16:41:46 -07001256 path = os.path.join(self.git_root, change.project)
1257 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001258 try:
1259 for commit in repo.iter_commits(ref):
1260 if commit.message.strip() == ('%s-1' % change.subject):
1261 return True
1262 except GitCommandError:
1263 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001264 return False
1265
James E. Blairb8c16472015-05-05 14:55:26 -07001266 def orderedRelease(self):
1267 # Run one build at a time to ensure non-race order:
1268 while len(self.builds):
1269 self.release(self.builds[0])
1270 self.waitUntilSettled()
1271
Clark Boylanb640e052014-04-03 16:41:46 -07001272 def release(self, job):
1273 if isinstance(job, FakeBuild):
1274 job.release()
1275 else:
1276 job.waiting = False
1277 self.log.debug("Queued job %s released" % job.unique)
1278 self.gearman_server.wakeConnections()
1279
1280 def getParameter(self, job, name):
1281 if isinstance(job, FakeBuild):
1282 return job.parameters[name]
1283 else:
1284 parameters = json.loads(job.arguments)
1285 return parameters[name]
1286
1287 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001289 while True:
1290 done = True
1291 for connection in self.gearman_server.active_connections:
1292 if (connection.functions and
1293 connection.client_id not in ['Zuul RPC Listener',
1294 'Zuul Merger']):
1295 done = False
1296 if done:
1297 break
1298 time.sleep(0)
1299 self.gearman_server.functions = set()
1300 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001301
1302 def haveAllBuildsReported(self):
1303 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001305 return False
1306 # Find out if every build that the worker has completed has been
1307 # reported back to Zuul. If it hasn't then that means a Gearman
1308 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001309 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001311 if not zbuild:
1312 # It has already been reported
1313 continue
1314 # It hasn't been reported yet.
1315 return False
1316 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001317 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001318 if connection.state == 'GRAB_WAIT':
1319 return False
1320 return True
1321
1322 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001323 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001324 for build in builds:
1325 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001326 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001327 for j in conn.related_jobs.values():
1328 if j.unique == build.uuid:
1329 client_job = j
1330 break
1331 if not client_job:
1332 self.log.debug("%s is not known to the gearman client" %
1333 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001334 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001335 if not client_job.handle:
1336 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001337 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001338 server_job = self.gearman_server.jobs.get(client_job.handle)
1339 if not server_job:
1340 self.log.debug("%s is not known to the gearman server" %
1341 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001342 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001343 if not hasattr(server_job, 'waiting'):
1344 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001345 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001346 if server_job.waiting:
1347 continue
James E. Blair17302972016-08-10 16:11:42 -07001348 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001349 self.log.debug("%s has not reported start" % build)
1350 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001351 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001352 if worker_build:
1353 if worker_build.isWaiting():
1354 continue
1355 else:
1356 self.log.debug("%s is running" % worker_build)
1357 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001358 else:
James E. Blair962220f2016-08-03 11:22:38 -07001359 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001360 return False
1361 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001362
Jan Hruban6b71aff2015-10-22 16:58:08 +02001363 def eventQueuesEmpty(self):
1364 for queue in self.event_queues:
1365 yield queue.empty()
1366
1367 def eventQueuesJoin(self):
1368 for queue in self.event_queues:
1369 queue.join()
1370
Clark Boylanb640e052014-04-03 16:41:46 -07001371 def waitUntilSettled(self):
1372 self.log.debug("Waiting until settled...")
1373 start = time.time()
1374 while True:
1375 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001376 self.log.debug("Queue status:")
1377 for queue in self.event_queues:
1378 self.log.debug(" %s: %s" % (queue, queue.empty()))
1379 self.log.debug("All builds waiting: %s" %
1380 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001381 self.log.debug("All builds reported: %s" %
1382 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001383 raise Exception("Timeout waiting for Zuul to settle")
1384 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001385
James E. Blaire1767bc2016-08-02 10:00:27 -07001386 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001387 # have all build states propogated to zuul?
1388 if self.haveAllBuildsReported():
1389 # Join ensures that the queue is empty _and_ events have been
1390 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001391 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001392 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001393 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001394 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001395 self.haveAllBuildsReported() and
1396 self.areAllBuildsWaiting()):
1397 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001398 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001399 self.log.debug("...settled.")
1400 return
1401 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001402 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001403 self.sched.wake_event.wait(0.1)
1404
1405 def countJobResults(self, jobs, result):
1406 jobs = filter(lambda x: x.result == result, jobs)
1407 return len(jobs)
1408
James E. Blair96c6bf82016-01-15 16:20:40 -08001409 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001410 for job in self.history:
1411 if (job.name == name and
1412 (project is None or
1413 job.parameters['ZUUL_PROJECT'] == project)):
1414 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001415 raise Exception("Unable to find job %s in history" % name)
1416
1417 def assertEmptyQueues(self):
1418 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001419 for tenant in self.sched.abide.tenants.values():
1420 for pipeline in tenant.layout.pipelines.values():
1421 for queue in pipeline.queues:
1422 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001423 print('pipeline %s queue %s contents %s' % (
1424 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001425 self.assertEqual(len(queue.queue), 0,
1426 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001427
1428 def assertReportedStat(self, key, value=None, kind=None):
1429 start = time.time()
1430 while time.time() < (start + 5):
1431 for stat in self.statsd.stats:
1432 pprint.pprint(self.statsd.stats)
1433 k, v = stat.split(':')
1434 if key == k:
1435 if value is None and kind is None:
1436 return
1437 elif value:
1438 if value == v:
1439 return
1440 elif kind:
1441 if v.endswith('|' + kind):
1442 return
1443 time.sleep(0.1)
1444
1445 pprint.pprint(self.statsd.stats)
1446 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001447
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001448 def assertBuilds(self, builds):
1449 """Assert that the running builds are as described.
1450
1451 The list of running builds is examined and must match exactly
1452 the list of builds described by the input.
1453
1454 :arg list builds: A list of dictionaries. Each item in the
1455 list must match the corresponding build in the build
1456 history, and each element of the dictionary must match the
1457 corresponding attribute of the build.
1458
1459 """
1460 self.assertEqual(len(self.builds), len(builds))
1461 for i, d in enumerate(builds):
1462 for k, v in d.items():
1463 self.assertEqual(getattr(self.builds[i], k), v,
1464 "Element %i in builds does not match" % (i,))
1465
1466 def assertHistory(self, history):
1467 """Assert that the completed builds are as described.
1468
1469 The list of completed builds is examined and must match
1470 exactly the list of builds described by the input.
1471
1472 :arg list history: A list of dictionaries. Each item in the
1473 list must match the corresponding build in the build
1474 history, and each element of the dictionary must match the
1475 corresponding attribute of the build.
1476
1477 """
1478 self.assertEqual(len(self.history), len(history))
1479 for i, d in enumerate(history):
1480 for k, v in d.items():
1481 self.assertEqual(getattr(self.history[i], k), v,
1482 "Element %i in history does not match" % (i,))
1483
James E. Blair59fdbac2015-12-07 17:08:06 -08001484 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001485 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1486
1487 def updateConfigLayout(self, path):
1488 root = os.path.join(self.test_root, "config")
1489 os.makedirs(root)
1490 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1491 f.write("""
1492tenants:
1493 - name: openstack
1494 include:
1495 - %s
1496 """ % os.path.abspath(path))
1497 f.close()
1498 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001499
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001500 def addCommitToRepo(self, project, message, files,
1501 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001502 path = os.path.join(self.upstream_root, project)
1503 repo = git.Repo(path)
1504 repo.head.reference = branch
1505 zuul.merger.merger.reset_repo_to_head(repo)
1506 for fn, content in files.items():
1507 fn = os.path.join(path, fn)
1508 with open(fn, 'w') as f:
1509 f.write(content)
1510 repo.index.add([fn])
1511 commit = repo.index.commit(message)
1512 repo.heads[branch].commit = commit
1513 repo.head.reference = branch
1514 repo.git.clean('-x', '-f', '-d')
1515 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001516 if tag:
1517 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001518
James E. Blair7fc8daa2016-08-08 15:37:15 -07001519 def addEvent(self, connection, event):
1520 """Inject a Fake (Gerrit) event.
1521
1522 This method accepts a JSON-encoded event and simulates Zuul
1523 having received it from Gerrit. It could (and should)
1524 eventually apply to any connection type, but is currently only
1525 used with Gerrit connections. The name of the connection is
1526 used to look up the corresponding server, and the event is
1527 simulated as having been received by all Zuul connections
1528 attached to that server. So if two Gerrit connections in Zuul
1529 are connected to the same Gerrit server, and you invoke this
1530 method specifying the name of one of them, the event will be
1531 received by both.
1532
1533 .. note::
1534
1535 "self.fake_gerrit.addEvent" calls should be migrated to
1536 this method.
1537
1538 :arg str connection: The name of the connection corresponding
1539 to the gerrit server.
1540 :arg str event: The JSON-encoded event.
1541
1542 """
1543 specified_conn = self.connections.connections[connection]
1544 for conn in self.connections.connections.values():
1545 if (isinstance(conn, specified_conn.__class__) and
1546 specified_conn.server == conn.server):
1547 conn.addEvent(event)
1548
James E. Blair3f876d52016-07-22 13:07:14 -07001549
1550class AnsibleZuulTestCase(ZuulTestCase):
1551 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001552 run_ansible = True