blob: 9bbaf6292de17e3f5666e8a534e2b826e6a1efbe [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):
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,
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"},
263 "approvals": [{"type": "Code-Review",
264 "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
434 for cat in ['CRVW', 'VRFY', 'APRV']:
435 if cat in action:
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
438 if 'label' in action:
439 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000440 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000441
Clark Boylanb640e052014-04-03 16:41:46 -0700442 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000443
Clark Boylanb640e052014-04-03 16:41:46 -0700444 if 'submit' in action:
445 change.setMerged()
446 if message:
447 change.setReported()
448
449 def query(self, number):
450 change = self.changes.get(int(number))
451 if change:
452 return change.query()
453 return {}
454
James E. Blairc494d542014-08-06 09:23:52 -0700455 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700456 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700457 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800458 if query.startswith('change:'):
459 # Query a specific changeid
460 changeid = query[len('change:'):]
461 l = [change.query() for change in self.changes.values()
462 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700463 elif query.startswith('message:'):
464 # Query the content of a commit message
465 msg = query[len('message:'):].strip()
466 l = [change.query() for change in self.changes.values()
467 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800468 else:
469 # Query all open changes
470 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700471 return l
James E. Blairc494d542014-08-06 09:23:52 -0700472
Joshua Hesketh352264b2015-08-11 23:42:08 +1000473 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700474 pass
475
Joshua Hesketh352264b2015-08-11 23:42:08 +1000476 def getGitUrl(self, project):
477 return os.path.join(self.upstream_root, project.name)
478
Clark Boylanb640e052014-04-03 16:41:46 -0700479
480class BuildHistory(object):
481 def __init__(self, **kw):
482 self.__dict__.update(kw)
483
484 def __repr__(self):
485 return ("<Completed build, result: %s name: %s #%s changes: %s>" %
486 (self.result, self.name, self.number, self.changes))
487
488
489class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200490 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700491 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.url = url
493
494 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700495 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700496 path = res.path
497 project = '/'.join(path.split('/')[2:-2])
498 ret = '001e# service=git-upload-pack\n'
499 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
500 'multi_ack thin-pack side-band side-band-64k ofs-delta '
501 'shallow no-progress include-tag multi_ack_detailed no-done\n')
502 path = os.path.join(self.upstream_root, project)
503 repo = git.Repo(path)
504 for ref in repo.refs:
505 r = ref.object.hexsha + ' ' + ref.path + '\n'
506 ret += '%04x%s' % (len(r) + 4, r)
507 ret += '0000'
508 return ret
509
510
Clark Boylanb640e052014-04-03 16:41:46 -0700511class FakeStatsd(threading.Thread):
512 def __init__(self):
513 threading.Thread.__init__(self)
514 self.daemon = True
515 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
516 self.sock.bind(('', 0))
517 self.port = self.sock.getsockname()[1]
518 self.wake_read, self.wake_write = os.pipe()
519 self.stats = []
520
521 def run(self):
522 while True:
523 poll = select.poll()
524 poll.register(self.sock, select.POLLIN)
525 poll.register(self.wake_read, select.POLLIN)
526 ret = poll.poll()
527 for (fd, event) in ret:
528 if fd == self.sock.fileno():
529 data = self.sock.recvfrom(1024)
530 if not data:
531 return
532 self.stats.append(data[0])
533 if fd == self.wake_read:
534 return
535
536 def stop(self):
537 os.write(self.wake_write, '1\n')
538
539
James E. Blaire1767bc2016-08-02 10:00:27 -0700540class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700541 log = logging.getLogger("zuul.test")
542
James E. Blairab7132b2016-08-05 12:36:22 -0700543 def __init__(self, launch_server, job, number, node):
Clark Boylanb640e052014-04-03 16:41:46 -0700544 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700545 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700546 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700547 self.jobdir = None
Clark Boylanb640e052014-04-03 16:41:46 -0700548 self.number = number
549 self.node = node
550 self.parameters = json.loads(job.arguments)
551 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700552 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700553 self.wait_condition = threading.Condition()
554 self.waiting = False
555 self.aborted = False
556 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700557 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700558 self.changes = None
559 if 'ZUUL_CHANGE_IDS' in self.parameters:
560 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700561
562 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700563 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700564 self.wait_condition.acquire()
565 self.wait_condition.notify()
566 self.waiting = False
567 self.log.debug("Build %s released" % self.unique)
568 self.wait_condition.release()
569
570 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700571 """Return whether this build is being held.
572
573 :returns: Whether the build is being held.
574 :rtype: bool
575 """
576
Clark Boylanb640e052014-04-03 16:41:46 -0700577 self.wait_condition.acquire()
578 if self.waiting:
579 ret = True
580 else:
581 ret = False
582 self.wait_condition.release()
583 return ret
584
585 def _wait(self):
586 self.wait_condition.acquire()
587 self.waiting = True
588 self.log.debug("Build %s waiting" % self.unique)
589 self.wait_condition.wait()
590 self.wait_condition.release()
591
592 def run(self):
593 data = {
594 'url': 'https://server/job/%s/%s/' % (self.name, self.number),
595 'name': self.name,
596 'number': self.number,
James E. Blaire1767bc2016-08-02 10:00:27 -0700597 'manager': self.launch_server.worker.worker_id,
Clark Boylanb640e052014-04-03 16:41:46 -0700598 'worker_name': 'My Worker',
599 'worker_hostname': 'localhost',
600 'worker_ips': ['127.0.0.1', '192.168.1.1'],
601 'worker_fqdn': 'zuul.example.org',
602 'worker_program': 'FakeBuilder',
603 'worker_version': 'v1.1',
604 'worker_extra': {'something': 'else'}
605 }
606
607 self.log.debug('Running build %s' % self.unique)
608
609 self.job.sendWorkData(json.dumps(data))
610 self.log.debug('Sent WorkData packet with %s' % json.dumps(data))
611 self.job.sendWorkStatus(0, 100)
612
James E. Blaire1767bc2016-08-02 10:00:27 -0700613 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700614 self.log.debug('Holding build %s' % self.unique)
615 self._wait()
616 self.log.debug("Build %s continuing" % self.unique)
617
Clark Boylanb640e052014-04-03 16:41:46 -0700618 result = 'SUCCESS'
619 if (('ZUUL_REF' in self.parameters) and
James E. Blaire1767bc2016-08-02 10:00:27 -0700620 self.launch_server.shouldFailTest(self.name,
621 self.parameters['ZUUL_REF'])):
Clark Boylanb640e052014-04-03 16:41:46 -0700622 result = 'FAILURE'
623 if self.aborted:
624 result = 'ABORTED'
625
626 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700627 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700628
James E. Blaire1767bc2016-08-02 10:00:27 -0700629 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700630
James E. Blaire7b99a02016-08-05 14:27:34 -0700631 def hasChanges(self, *changes):
632 """Return whether this build has certain changes in its git repos.
633
634 :arg FakeChange changes: One or more changes (varargs) that
635 are expected to be present (in order) in the git repository of
636 the active project.
637
638 :returns: Whether the build has the indicated changes.
639 :rtype: bool
640
641 """
James E. Blair962220f2016-08-03 11:22:38 -0700642 project = self.parameters['ZUUL_PROJECT']
643 path = os.path.join(self.jobdir.git_root, project)
644 repo = git.Repo(path)
645 ref = self.parameters['ZUUL_REF']
646 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700647 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700648 self.log.debug("Checking if build %s has changes; commit_messages %s;"
649 " repo_messages %s" % (self, commit_messages,
650 repo_messages))
651 for msg in commit_messages:
652 if msg not in repo_messages:
653 self.log.debug(" messages do not match")
654 return False
655 self.log.debug(" OK")
656 return True
657
Clark Boylanb640e052014-04-03 16:41:46 -0700658
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000659class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700660 """An Ansible launcher to be used in tests.
661
662 :ivar bool hold_jobs_in_build: If true, when jobs are launched
663 they will report that they have started but then pause until
664 released before reporting completion. This attribute may be
665 changed at any time and will take effect for subsequently
666 launched builds, but previously held builds will still need to
667 be explicitly released.
668
669 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800670 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700671 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800672 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700673 self.hold_jobs_in_build = False
674 self.lock = threading.Lock()
675 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700676 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700677 self._build_counter_lock = threading.Lock()
678 self.build_counter = 0
679 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700680 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800681
James E. Blaire1767bc2016-08-02 10:00:27 -0700682 def addFailTest(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700683 """Instruct the launcher to report matching builds as failures.
684
685 :arg str name: The name of the job to fail.
686 :arg change: TODO: document
687
688 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700689 l = self.fail_tests.get(name, [])
690 l.append(change)
691 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800692
James E. Blaire1767bc2016-08-02 10:00:27 -0700693 def shouldFailTest(self, name, ref):
694 l = self.fail_tests.get(name, [])
695 for change in l:
696 if self.test.ref_has_change(ref, change):
697 return True
698 return False
James E. Blairf5dbd002015-12-23 15:26:17 -0800699
James E. Blair962220f2016-08-03 11:22:38 -0700700 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700701 """Release a held build.
702
703 :arg str regex: A regular expression which, if supplied, will
704 cause only builds with matching names to be released. If
705 not supplied, all builds will be released.
706
707 """
James E. Blair962220f2016-08-03 11:22:38 -0700708 builds = self.running_builds[:]
709 self.log.debug("Releasing build %s (%s)" % (regex,
710 len(self.running_builds)))
711 for build in builds:
712 if not regex or re.match(regex, build.name):
713 self.log.debug("Releasing build %s" %
714 (build.parameters['ZUUL_UUID']))
715 build.release()
716 else:
717 self.log.debug("Not releasing build %s" %
718 (build.parameters['ZUUL_UUID']))
719 self.log.debug("Done releasing builds %s (%s)" %
720 (regex, len(self.running_builds)))
721
James E. Blairab7132b2016-08-05 12:36:22 -0700722 def launch(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700723 with self._build_counter_lock:
724 self.build_counter += 1
725 build_counter = self.build_counter
726 node = None
James E. Blairab7132b2016-08-05 12:36:22 -0700727 build = FakeBuild(self, job, build_counter, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700728 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700729 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700730 self.job_builds[job.unique] = build
731 super(RecordingLaunchServer, self).launch(job)
732
733 def runAnsible(self, jobdir, job):
734 build = self.job_builds[job.unique]
735 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700736
737 if self._run_ansible:
738 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
739 else:
740 result = build.run()
741
742 self.lock.acquire()
743 self.build_history.append(
744 BuildHistory(name=build.name, number=build.number,
745 result=result, changes=build.changes, node=build.node,
746 uuid=build.unique, parameters=build.parameters,
747 pipeline=build.parameters['ZUUL_PIPELINE'])
748 )
James E. Blairab7132b2016-08-05 12:36:22 -0700749 self.running_builds.remove(build)
750 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700751 self.lock.release()
752 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800753
754
Clark Boylanb640e052014-04-03 16:41:46 -0700755class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700756 """A Gearman server for use in tests.
757
758 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
759 added to the queue but will not be distributed to workers
760 until released. This attribute may be changed at any time and
761 will take effect for subsequently enqueued jobs, but
762 previously held jobs will still need to be explicitly
763 released.
764
765 """
766
Clark Boylanb640e052014-04-03 16:41:46 -0700767 def __init__(self):
768 self.hold_jobs_in_queue = False
769 super(FakeGearmanServer, self).__init__(0)
770
771 def getJobForConnection(self, connection, peek=False):
772 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
773 for job in queue:
774 if not hasattr(job, 'waiting'):
775 if job.name.startswith('build:'):
776 job.waiting = self.hold_jobs_in_queue
777 else:
778 job.waiting = False
779 if job.waiting:
780 continue
781 if job.name in connection.functions:
782 if not peek:
783 queue.remove(job)
784 connection.related_jobs[job.handle] = job
785 job.worker_connection = connection
786 job.running = True
787 return job
788 return None
789
790 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700791 """Release a held job.
792
793 :arg str regex: A regular expression which, if supplied, will
794 cause only jobs with matching names to be released. If
795 not supplied, all jobs will be released.
796 """
Clark Boylanb640e052014-04-03 16:41:46 -0700797 released = False
798 qlen = (len(self.high_queue) + len(self.normal_queue) +
799 len(self.low_queue))
800 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
801 for job in self.getQueue():
802 cmd, name = job.name.split(':')
803 if cmd != 'build':
804 continue
805 if not regex or re.match(regex, name):
806 self.log.debug("releasing queued job %s" %
807 job.unique)
808 job.waiting = False
809 released = True
810 else:
811 self.log.debug("not releasing queued job %s" %
812 job.unique)
813 if released:
814 self.wakeConnections()
815 qlen = (len(self.high_queue) + len(self.normal_queue) +
816 len(self.low_queue))
817 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
818
819
820class FakeSMTP(object):
821 log = logging.getLogger('zuul.FakeSMTP')
822
823 def __init__(self, messages, server, port):
824 self.server = server
825 self.port = port
826 self.messages = messages
827
828 def sendmail(self, from_email, to_email, msg):
829 self.log.info("Sending email from %s, to %s, with msg %s" % (
830 from_email, to_email, msg))
831
832 headers = msg.split('\n\n', 1)[0]
833 body = msg.split('\n\n', 1)[1]
834
835 self.messages.append(dict(
836 from_email=from_email,
837 to_email=to_email,
838 msg=msg,
839 headers=headers,
840 body=body,
841 ))
842
843 return True
844
845 def quit(self):
846 return True
847
848
849class FakeSwiftClientConnection(swiftclient.client.Connection):
850 def post_account(self, headers):
851 # Do nothing
852 pass
853
854 def get_auth(self):
855 # Returns endpoint and (unused) auth token
856 endpoint = os.path.join('https://storage.example.org', 'V1',
857 'AUTH_account')
858 return endpoint, ''
859
860
Maru Newby3fe5f852015-01-13 04:22:14 +0000861class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700862 log = logging.getLogger("zuul.test")
863
864 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000865 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700866 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
867 try:
868 test_timeout = int(test_timeout)
869 except ValueError:
870 # If timeout value is invalid do not set a timeout.
871 test_timeout = 0
872 if test_timeout > 0:
873 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
874
875 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
876 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
877 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
878 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
879 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
880 os.environ.get('OS_STDERR_CAPTURE') == '1'):
881 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
882 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
883 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
884 os.environ.get('OS_LOG_CAPTURE') == '1'):
885 self.useFixture(fixtures.FakeLogger(
886 level=logging.DEBUG,
887 format='%(asctime)s %(name)-32s '
888 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000889
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700890 # NOTE(notmorgan): Extract logging overrides for specific libraries
891 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
892 # each. This is used to limit the output during test runs from
893 # libraries that zuul depends on such as gear.
894 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
895
896 if log_defaults_from_env:
897 for default in log_defaults_from_env.split(','):
898 try:
899 name, level_str = default.split('=', 1)
900 level = getattr(logging, level_str, logging.DEBUG)
901 self.useFixture(fixtures.FakeLogger(
902 name=name,
903 level=level,
904 format='%(asctime)s %(name)-32s '
905 '%(levelname)-8s %(message)s'))
906 except ValueError:
907 # NOTE(notmorgan): Invalid format of the log default,
908 # skip and don't try and apply a logger for the
909 # specified module
910 pass
911
Maru Newby3fe5f852015-01-13 04:22:14 +0000912
913class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700914 """A test case with a functioning Zuul.
915
916 The following class variables are used during test setup and can
917 be overidden by subclasses but are effectively read-only once a
918 test method starts running:
919
920 :cvar str config_file: This points to the main zuul config file
921 within the fixtures directory. Subclasses may override this
922 to obtain a different behavior.
923
924 :cvar str tenant_config_file: This is the tenant config file
925 (which specifies from what git repos the configuration should
926 be loaded). It defaults to the value specified in
927 `config_file` but can be overidden by subclasses to obtain a
928 different tenant/project layout while using the standard main
929 configuration.
930
931 The following are instance variables that are useful within test
932 methods:
933
934 :ivar FakeGerritConnection fake_<connection>:
935 A :py:class:`~tests.base.FakeGerritConnection` will be
936 instantiated for each connection present in the config file
937 and stored here. For instance, `fake_gerrit` will hold the
938 FakeGerritConnection object for a connection named `gerrit`.
939
940 :ivar FakeGearmanServer gearman_server: An instance of
941 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
942 server that all of the Zuul components in this test use to
943 communicate with each other.
944
945 :ivar RecordingLaunchServer launch_server: An instance of
946 :py:class:`~tests.base.RecordingLaunchServer` which is the
947 Ansible launch server used to run jobs for this test.
948
949 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
950 representing currently running builds. They are appended to
951 the list in the order they are launched, and removed from this
952 list upon completion.
953
954 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
955 objects representing completed builds. They are appended to
956 the list in the order they complete.
957
958 """
959
James E. Blair83005782015-12-11 14:46:03 -0800960 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700961 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700962
963 def _startMerger(self):
964 self.merge_server = zuul.merger.server.MergeServer(self.config,
965 self.connections)
966 self.merge_server.start()
967
Maru Newby3fe5f852015-01-13 04:22:14 +0000968 def setUp(self):
969 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700970 if USE_TEMPDIR:
971 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000972 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
973 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700974 else:
975 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700976 self.test_root = os.path.join(tmp_root, "zuul-test")
977 self.upstream_root = os.path.join(self.test_root, "upstream")
978 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700979 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700980
981 if os.path.exists(self.test_root):
982 shutil.rmtree(self.test_root)
983 os.makedirs(self.test_root)
984 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700985 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700986
987 # Make per test copy of Configuration.
988 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800989 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100990 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800991 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700992 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700993 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700994
995 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700996 # TODOv3(jeblair): remove these and replace with new git
997 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700998 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700999 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001000 self.init_repo("org/project5")
1001 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001002 self.init_repo("org/one-job-project")
1003 self.init_repo("org/nonvoting-project")
1004 self.init_repo("org/templated-project")
1005 self.init_repo("org/layered-project")
1006 self.init_repo("org/node-project")
1007 self.init_repo("org/conflict-project")
1008 self.init_repo("org/noop-project")
1009 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001010 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001011
1012 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001013 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1014 # see: https://github.com/jsocol/pystatsd/issues/61
1015 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001016 os.environ['STATSD_PORT'] = str(self.statsd.port)
1017 self.statsd.start()
1018 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001019 reload_module(statsd)
1020 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001021
1022 self.gearman_server = FakeGearmanServer()
1023
1024 self.config.set('gearman', 'port', str(self.gearman_server.port))
1025
Joshua Hesketh352264b2015-08-11 23:42:08 +10001026 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1027 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1028 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001029
Joshua Hesketh352264b2015-08-11 23:42:08 +10001030 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001031
1032 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1033 FakeSwiftClientConnection))
1034 self.swift = zuul.lib.swift.Swift(self.config)
1035
Jan Hruban6b71aff2015-10-22 16:58:08 +02001036 self.event_queues = [
1037 self.sched.result_event_queue,
1038 self.sched.trigger_event_queue
1039 ]
1040
James E. Blairfef78942016-03-11 16:28:56 -08001041 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001042 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001043
Clark Boylanb640e052014-04-03 16:41:46 -07001044 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001045 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001046 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001047 return FakeURLOpener(self.upstream_root, *args, **kw)
1048
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001049 old_urlopen = urllib.request.urlopen
1050 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001051
James E. Blair3f876d52016-07-22 13:07:14 -07001052 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001053
James E. Blaire1767bc2016-08-02 10:00:27 -07001054 self.launch_server = RecordingLaunchServer(
1055 self.config, self.connections, _run_ansible=self.run_ansible)
1056 self.launch_server.start()
1057 self.history = self.launch_server.build_history
1058 self.builds = self.launch_server.running_builds
1059
1060 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001061 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001062 self.merge_client = zuul.merger.client.MergeClient(
1063 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001064 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001067 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001068 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001069
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001070 self.webapp = zuul.webapp.WebApp(
1071 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001072 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001073
1074 self.sched.start()
1075 self.sched.reconfigure(self.config)
1076 self.sched.resume()
1077 self.webapp.start()
1078 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001079 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001080
1081 self.addCleanup(self.assertFinalState)
1082 self.addCleanup(self.shutdown)
1083
James E. Blairfef78942016-03-11 16:28:56 -08001084 def configure_connections(self):
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 = {}
James E. Blairfef78942016-03-11 16:28:56 -08001097 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001098
Joshua Hesketh352264b2015-08-11 23:42:08 +10001099 for section_name in self.config.sections():
1100 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1101 section_name, re.I)
1102 if not con_match:
1103 continue
1104 con_name = con_match.group(2)
1105 con_config = dict(self.config.items(section_name))
1106
1107 if 'driver' not in con_config:
1108 raise Exception("No driver specified for connection %s."
1109 % con_name)
1110
1111 con_driver = con_config['driver']
1112
1113 # TODO(jhesketh): load the required class automatically
1114 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001115 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1116 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001117 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001118 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001119 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001120 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001121 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001122 self.event_queues.append(
1123 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001124 setattr(self, 'fake_' + con_name,
1125 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001126 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001127 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001128 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1129 else:
1130 raise Exception("Unknown driver, %s, for connection %s"
1131 % (con_config['driver'], con_name))
1132
1133 # If the [gerrit] or [smtp] sections still exist, load them in as a
1134 # connection named 'gerrit' or 'smtp' respectfully
1135
1136 if 'gerrit' in self.config.sections():
1137 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001138 self.event_queues.append(
1139 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001140 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001141 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001142 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001143
1144 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001145 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001146 zuul.connection.smtp.SMTPConnection(
1147 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001148
James E. Blair83005782015-12-11 14:46:03 -08001149 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001150 # This creates the per-test configuration object. It can be
1151 # overriden by subclasses, but should not need to be since it
1152 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001154 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001155 if hasattr(self, 'tenant_config_file'):
1156 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001157 git_path = os.path.join(
1158 os.path.dirname(
1159 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1160 'git')
1161 if os.path.exists(git_path):
1162 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001163 project = reponame.replace('_', '/')
1164 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001165 os.path.join(git_path, reponame))
1166
1167 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001168 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001169
1170 files = {}
1171 for (dirpath, dirnames, filenames) in os.walk(source_path):
1172 for filename in filenames:
1173 test_tree_filepath = os.path.join(dirpath, filename)
1174 common_path = os.path.commonprefix([test_tree_filepath,
1175 source_path])
1176 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1177 with open(test_tree_filepath, 'r') as f:
1178 content = f.read()
1179 files[relative_filepath] = content
1180 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001181 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001182
Clark Boylanb640e052014-04-03 16:41:46 -07001183 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001184 # Make sure that git.Repo objects have been garbage collected.
1185 repos = []
1186 gc.collect()
1187 for obj in gc.get_objects():
1188 if isinstance(obj, git.Repo):
1189 repos.append(obj)
1190 self.assertEqual(len(repos), 0)
1191 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001192 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001193 for tenant in self.sched.abide.tenants.values():
1194 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001195 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001196 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001197
1198 def shutdown(self):
1199 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001200 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001201 self.merge_server.stop()
1202 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001203 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001204 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.sched.stop()
1206 self.sched.join()
1207 self.statsd.stop()
1208 self.statsd.join()
1209 self.webapp.stop()
1210 self.webapp.join()
1211 self.rpc.stop()
1212 self.rpc.join()
1213 self.gearman_server.shutdown()
1214 threads = threading.enumerate()
1215 if len(threads) > 1:
1216 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001217
1218 def init_repo(self, project):
1219 parts = project.split('/')
1220 path = os.path.join(self.upstream_root, *parts[:-1])
1221 if not os.path.exists(path):
1222 os.makedirs(path)
1223 path = os.path.join(self.upstream_root, project)
1224 repo = git.Repo.init(path)
1225
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001226 with repo.config_writer() as config_writer:
1227 config_writer.set_value('user', 'email', 'user@example.com')
1228 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001229
Clark Boylanb640e052014-04-03 16:41:46 -07001230 repo.index.commit('initial commit')
1231 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001232
James E. Blair97d902e2014-08-21 13:25:56 -07001233 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001234 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001235 repo.git.clean('-x', '-f', '-d')
1236
James E. Blair97d902e2014-08-21 13:25:56 -07001237 def create_branch(self, project, branch):
1238 path = os.path.join(self.upstream_root, project)
1239 repo = git.Repo.init(path)
1240 fn = os.path.join(path, 'README')
1241
1242 branch_head = repo.create_head(branch)
1243 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001244 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001245 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001246 f.close()
1247 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001248 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001249
James E. Blair97d902e2014-08-21 13:25:56 -07001250 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001251 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001252 repo.git.clean('-x', '-f', '-d')
1253
Sachi King9f16d522016-03-16 12:20:45 +11001254 def create_commit(self, project):
1255 path = os.path.join(self.upstream_root, project)
1256 repo = git.Repo(path)
1257 repo.head.reference = repo.heads['master']
1258 file_name = os.path.join(path, 'README')
1259 with open(file_name, 'a') as f:
1260 f.write('creating fake commit\n')
1261 repo.index.add([file_name])
1262 commit = repo.index.commit('Creating a fake commit')
1263 return commit.hexsha
1264
Clark Boylanb640e052014-04-03 16:41:46 -07001265 def ref_has_change(self, ref, change):
1266 path = os.path.join(self.git_root, change.project)
1267 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001268 try:
1269 for commit in repo.iter_commits(ref):
1270 if commit.message.strip() == ('%s-1' % change.subject):
1271 return True
1272 except GitCommandError:
1273 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001274 return False
1275
James E. Blairb8c16472015-05-05 14:55:26 -07001276 def orderedRelease(self):
1277 # Run one build at a time to ensure non-race order:
1278 while len(self.builds):
1279 self.release(self.builds[0])
1280 self.waitUntilSettled()
1281
Clark Boylanb640e052014-04-03 16:41:46 -07001282 def release(self, job):
1283 if isinstance(job, FakeBuild):
1284 job.release()
1285 else:
1286 job.waiting = False
1287 self.log.debug("Queued job %s released" % job.unique)
1288 self.gearman_server.wakeConnections()
1289
1290 def getParameter(self, job, name):
1291 if isinstance(job, FakeBuild):
1292 return job.parameters[name]
1293 else:
1294 parameters = json.loads(job.arguments)
1295 return parameters[name]
1296
1297 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001298 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001299 while True:
1300 done = True
1301 for connection in self.gearman_server.active_connections:
1302 if (connection.functions and
1303 connection.client_id not in ['Zuul RPC Listener',
1304 'Zuul Merger']):
1305 done = False
1306 if done:
1307 break
1308 time.sleep(0)
1309 self.gearman_server.functions = set()
1310 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001311
1312 def haveAllBuildsReported(self):
1313 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001314 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001315 return False
1316 # Find out if every build that the worker has completed has been
1317 # reported back to Zuul. If it hasn't then that means a Gearman
1318 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001319 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001320 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001321 if not zbuild:
1322 # It has already been reported
1323 continue
1324 # It hasn't been reported yet.
1325 return False
1326 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001327 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001328 if connection.state == 'GRAB_WAIT':
1329 return False
1330 return True
1331
1332 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001333 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001334 for build in builds:
1335 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001336 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001337 for j in conn.related_jobs.values():
1338 if j.unique == build.uuid:
1339 client_job = j
1340 break
1341 if not client_job:
1342 self.log.debug("%s is not known to the gearman client" %
1343 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001345 if not client_job.handle:
1346 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001348 server_job = self.gearman_server.jobs.get(client_job.handle)
1349 if not server_job:
1350 self.log.debug("%s is not known to the gearman server" %
1351 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001352 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001353 if not hasattr(server_job, 'waiting'):
1354 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001355 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001356 if server_job.waiting:
1357 continue
James E. Blairbbda4702016-03-09 15:19:56 -08001358 if build.number is None:
1359 self.log.debug("%s has not reported start" % build)
1360 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001361 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001362 if worker_build:
1363 if worker_build.isWaiting():
1364 continue
1365 else:
1366 self.log.debug("%s is running" % worker_build)
1367 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001368 else:
James E. Blair962220f2016-08-03 11:22:38 -07001369 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001370 return False
1371 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001372
Jan Hruban6b71aff2015-10-22 16:58:08 +02001373 def eventQueuesEmpty(self):
1374 for queue in self.event_queues:
1375 yield queue.empty()
1376
1377 def eventQueuesJoin(self):
1378 for queue in self.event_queues:
1379 queue.join()
1380
Clark Boylanb640e052014-04-03 16:41:46 -07001381 def waitUntilSettled(self):
1382 self.log.debug("Waiting until settled...")
1383 start = time.time()
1384 while True:
1385 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001386 self.log.debug("Queue status:")
1387 for queue in self.event_queues:
1388 self.log.debug(" %s: %s" % (queue, queue.empty()))
1389 self.log.debug("All builds waiting: %s" %
1390 (self.areAllBuildsWaiting(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001391 raise Exception("Timeout waiting for Zuul to settle")
1392 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001393
James E. Blaire1767bc2016-08-02 10:00:27 -07001394 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001395 # have all build states propogated to zuul?
1396 if self.haveAllBuildsReported():
1397 # Join ensures that the queue is empty _and_ events have been
1398 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001399 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001400 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001401 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001402 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001403 self.haveAllBuildsReported() and
1404 self.areAllBuildsWaiting()):
1405 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001406 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.log.debug("...settled.")
1408 return
1409 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001410 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001411 self.sched.wake_event.wait(0.1)
1412
1413 def countJobResults(self, jobs, result):
1414 jobs = filter(lambda x: x.result == result, jobs)
1415 return len(jobs)
1416
James E. Blair96c6bf82016-01-15 16:20:40 -08001417 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001418 for job in self.history:
1419 if (job.name == name and
1420 (project is None or
1421 job.parameters['ZUUL_PROJECT'] == project)):
1422 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001423 raise Exception("Unable to find job %s in history" % name)
1424
1425 def assertEmptyQueues(self):
1426 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001427 for tenant in self.sched.abide.tenants.values():
1428 for pipeline in tenant.layout.pipelines.values():
1429 for queue in pipeline.queues:
1430 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001431 print('pipeline %s queue %s contents %s' % (
1432 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001433 self.assertEqual(len(queue.queue), 0,
1434 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001435
1436 def assertReportedStat(self, key, value=None, kind=None):
1437 start = time.time()
1438 while time.time() < (start + 5):
1439 for stat in self.statsd.stats:
1440 pprint.pprint(self.statsd.stats)
1441 k, v = stat.split(':')
1442 if key == k:
1443 if value is None and kind is None:
1444 return
1445 elif value:
1446 if value == v:
1447 return
1448 elif kind:
1449 if v.endswith('|' + kind):
1450 return
1451 time.sleep(0.1)
1452
1453 pprint.pprint(self.statsd.stats)
1454 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001455
1456 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001457 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1458
1459 def updateConfigLayout(self, path):
1460 root = os.path.join(self.test_root, "config")
1461 os.makedirs(root)
1462 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1463 f.write("""
1464tenants:
1465 - name: openstack
1466 include:
1467 - %s
1468 """ % os.path.abspath(path))
1469 f.close()
1470 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001471
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001472 def addCommitToRepo(self, project, message, files,
1473 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001474 path = os.path.join(self.upstream_root, project)
1475 repo = git.Repo(path)
1476 repo.head.reference = branch
1477 zuul.merger.merger.reset_repo_to_head(repo)
1478 for fn, content in files.items():
1479 fn = os.path.join(path, fn)
1480 with open(fn, 'w') as f:
1481 f.write(content)
1482 repo.index.add([fn])
1483 commit = repo.index.commit(message)
1484 repo.heads[branch].commit = commit
1485 repo.head.reference = branch
1486 repo.git.clean('-x', '-f', '-d')
1487 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001488 if tag:
1489 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001490
James E. Blair7fc8daa2016-08-08 15:37:15 -07001491 def addEvent(self, connection, event):
1492 """Inject a Fake (Gerrit) event.
1493
1494 This method accepts a JSON-encoded event and simulates Zuul
1495 having received it from Gerrit. It could (and should)
1496 eventually apply to any connection type, but is currently only
1497 used with Gerrit connections. The name of the connection is
1498 used to look up the corresponding server, and the event is
1499 simulated as having been received by all Zuul connections
1500 attached to that server. So if two Gerrit connections in Zuul
1501 are connected to the same Gerrit server, and you invoke this
1502 method specifying the name of one of them, the event will be
1503 received by both.
1504
1505 .. note::
1506
1507 "self.fake_gerrit.addEvent" calls should be migrated to
1508 this method.
1509
1510 :arg str connection: The name of the connection corresponding
1511 to the gerrit server.
1512 :arg str event: The JSON-encoded event.
1513
1514 """
1515 specified_conn = self.connections.connections[connection]
1516 for conn in self.connections.connections.values():
1517 if (isinstance(conn, specified_conn.__class__) and
1518 specified_conn.server == conn.server):
1519 conn.addEvent(event)
1520
James E. Blair3f876d52016-07-22 13:07:14 -07001521
1522class AnsibleZuulTestCase(ZuulTestCase):
1523 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001524 run_ansible = True