blob: 9acc0ef491da1b7f1eec73d1dd084f327972cf21 [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):
486 return ("<Completed build, result: %s name: %s #%s changes: %s>" %
487 (self.result, self.name, self.number, self.changes))
488
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. Blairab7132b2016-08-05 12:36:22 -0700544 def __init__(self, launch_server, job, number, 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
Clark Boylanb640e052014-04-03 16:41:46 -0700549 self.number = number
550 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):
594 data = {
595 'url': 'https://server/job/%s/%s/' % (self.name, self.number),
596 'name': self.name,
597 'number': self.number,
James E. Blaire1767bc2016-08-02 10:00:27 -0700598 'manager': self.launch_server.worker.worker_id,
Clark Boylanb640e052014-04-03 16:41:46 -0700599 'worker_name': 'My Worker',
600 'worker_hostname': 'localhost',
601 'worker_ips': ['127.0.0.1', '192.168.1.1'],
602 'worker_fqdn': 'zuul.example.org',
603 'worker_program': 'FakeBuilder',
604 'worker_version': 'v1.1',
605 'worker_extra': {'something': 'else'}
606 }
607
608 self.log.debug('Running build %s' % self.unique)
609
610 self.job.sendWorkData(json.dumps(data))
611 self.log.debug('Sent WorkData packet with %s' % json.dumps(data))
612 self.job.sendWorkStatus(0, 100)
613
James E. Blaire1767bc2016-08-02 10:00:27 -0700614 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700615 self.log.debug('Holding build %s' % self.unique)
616 self._wait()
617 self.log.debug("Build %s continuing" % self.unique)
618
Clark Boylanb640e052014-04-03 16:41:46 -0700619 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700620 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700621 result = 'FAILURE'
622 if self.aborted:
623 result = 'ABORTED'
624
625 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700626 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700627
James E. Blaire1767bc2016-08-02 10:00:27 -0700628 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700629
James E. Blaira5dba232016-08-08 15:53:24 -0700630 def shouldFail(self):
631 changes = self.launch_server.fail_tests.get(self.name, [])
632 for change in changes:
633 if self.hasChanges(change):
634 return True
635 return False
636
James E. Blaire7b99a02016-08-05 14:27:34 -0700637 def hasChanges(self, *changes):
638 """Return whether this build has certain changes in its git repos.
639
640 :arg FakeChange changes: One or more changes (varargs) that
641 are expected to be present (in order) in the git repository of
642 the active project.
643
644 :returns: Whether the build has the indicated changes.
645 :rtype: bool
646
647 """
James E. Blair962220f2016-08-03 11:22:38 -0700648 project = self.parameters['ZUUL_PROJECT']
649 path = os.path.join(self.jobdir.git_root, project)
650 repo = git.Repo(path)
651 ref = self.parameters['ZUUL_REF']
652 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700653 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700654 self.log.debug("Checking if build %s has changes; commit_messages %s;"
655 " repo_messages %s" % (self, commit_messages,
656 repo_messages))
657 for msg in commit_messages:
658 if msg not in repo_messages:
659 self.log.debug(" messages do not match")
660 return False
661 self.log.debug(" OK")
662 return True
663
Clark Boylanb640e052014-04-03 16:41:46 -0700664
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000665class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700666 """An Ansible launcher to be used in tests.
667
668 :ivar bool hold_jobs_in_build: If true, when jobs are launched
669 they will report that they have started but then pause until
670 released before reporting completion. This attribute may be
671 changed at any time and will take effect for subsequently
672 launched builds, but previously held builds will still need to
673 be explicitly released.
674
675 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800676 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700677 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800678 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700679 self.hold_jobs_in_build = False
680 self.lock = threading.Lock()
681 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700682 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 self._build_counter_lock = threading.Lock()
684 self.build_counter = 0
685 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700686 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800687
James E. Blaira5dba232016-08-08 15:53:24 -0700688 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700689 """Instruct the launcher to report matching builds as failures.
690
691 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700692 :arg Change change: The :py:class:`~tests.base.FakeChange`
693 instance which should cause the job to fail. This job
694 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700695
696 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700697 l = self.fail_tests.get(name, [])
698 l.append(change)
699 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800700
James E. Blair962220f2016-08-03 11:22:38 -0700701 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700702 """Release a held build.
703
704 :arg str regex: A regular expression which, if supplied, will
705 cause only builds with matching names to be released. If
706 not supplied, all builds will be released.
707
708 """
James E. Blair962220f2016-08-03 11:22:38 -0700709 builds = self.running_builds[:]
710 self.log.debug("Releasing build %s (%s)" % (regex,
711 len(self.running_builds)))
712 for build in builds:
713 if not regex or re.match(regex, build.name):
714 self.log.debug("Releasing build %s" %
715 (build.parameters['ZUUL_UUID']))
716 build.release()
717 else:
718 self.log.debug("Not releasing build %s" %
719 (build.parameters['ZUUL_UUID']))
720 self.log.debug("Done releasing builds %s (%s)" %
721 (regex, len(self.running_builds)))
722
James E. Blairab7132b2016-08-05 12:36:22 -0700723 def launch(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 with self._build_counter_lock:
725 self.build_counter += 1
726 build_counter = self.build_counter
727 node = None
James E. Blairab7132b2016-08-05 12:36:22 -0700728 build = FakeBuild(self, job, build_counter, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700729 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700730 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700731 self.job_builds[job.unique] = build
732 super(RecordingLaunchServer, self).launch(job)
733
734 def runAnsible(self, jobdir, job):
735 build = self.job_builds[job.unique]
736 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700737
738 if self._run_ansible:
739 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
740 else:
741 result = build.run()
742
743 self.lock.acquire()
744 self.build_history.append(
745 BuildHistory(name=build.name, number=build.number,
746 result=result, changes=build.changes, node=build.node,
747 uuid=build.unique, parameters=build.parameters,
748 pipeline=build.parameters['ZUUL_PIPELINE'])
749 )
James E. Blairab7132b2016-08-05 12:36:22 -0700750 self.running_builds.remove(build)
751 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700752 self.lock.release()
753 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800754
755
Clark Boylanb640e052014-04-03 16:41:46 -0700756class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700757 """A Gearman server for use in tests.
758
759 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
760 added to the queue but will not be distributed to workers
761 until released. This attribute may be changed at any time and
762 will take effect for subsequently enqueued jobs, but
763 previously held jobs will still need to be explicitly
764 released.
765
766 """
767
Clark Boylanb640e052014-04-03 16:41:46 -0700768 def __init__(self):
769 self.hold_jobs_in_queue = False
770 super(FakeGearmanServer, self).__init__(0)
771
772 def getJobForConnection(self, connection, peek=False):
773 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
774 for job in queue:
775 if not hasattr(job, 'waiting'):
776 if job.name.startswith('build:'):
777 job.waiting = self.hold_jobs_in_queue
778 else:
779 job.waiting = False
780 if job.waiting:
781 continue
782 if job.name in connection.functions:
783 if not peek:
784 queue.remove(job)
785 connection.related_jobs[job.handle] = job
786 job.worker_connection = connection
787 job.running = True
788 return job
789 return None
790
791 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700792 """Release a held job.
793
794 :arg str regex: A regular expression which, if supplied, will
795 cause only jobs with matching names to be released. If
796 not supplied, all jobs will be released.
797 """
Clark Boylanb640e052014-04-03 16:41:46 -0700798 released = False
799 qlen = (len(self.high_queue) + len(self.normal_queue) +
800 len(self.low_queue))
801 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
802 for job in self.getQueue():
803 cmd, name = job.name.split(':')
804 if cmd != 'build':
805 continue
806 if not regex or re.match(regex, name):
807 self.log.debug("releasing queued job %s" %
808 job.unique)
809 job.waiting = False
810 released = True
811 else:
812 self.log.debug("not releasing queued job %s" %
813 job.unique)
814 if released:
815 self.wakeConnections()
816 qlen = (len(self.high_queue) + len(self.normal_queue) +
817 len(self.low_queue))
818 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
819
820
821class FakeSMTP(object):
822 log = logging.getLogger('zuul.FakeSMTP')
823
824 def __init__(self, messages, server, port):
825 self.server = server
826 self.port = port
827 self.messages = messages
828
829 def sendmail(self, from_email, to_email, msg):
830 self.log.info("Sending email from %s, to %s, with msg %s" % (
831 from_email, to_email, msg))
832
833 headers = msg.split('\n\n', 1)[0]
834 body = msg.split('\n\n', 1)[1]
835
836 self.messages.append(dict(
837 from_email=from_email,
838 to_email=to_email,
839 msg=msg,
840 headers=headers,
841 body=body,
842 ))
843
844 return True
845
846 def quit(self):
847 return True
848
849
850class FakeSwiftClientConnection(swiftclient.client.Connection):
851 def post_account(self, headers):
852 # Do nothing
853 pass
854
855 def get_auth(self):
856 # Returns endpoint and (unused) auth token
857 endpoint = os.path.join('https://storage.example.org', 'V1',
858 'AUTH_account')
859 return endpoint, ''
860
861
Maru Newby3fe5f852015-01-13 04:22:14 +0000862class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700863 log = logging.getLogger("zuul.test")
864
865 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000866 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700867 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
868 try:
869 test_timeout = int(test_timeout)
870 except ValueError:
871 # If timeout value is invalid do not set a timeout.
872 test_timeout = 0
873 if test_timeout > 0:
874 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
875
876 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
877 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
878 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
879 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
880 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
881 os.environ.get('OS_STDERR_CAPTURE') == '1'):
882 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
883 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
884 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
885 os.environ.get('OS_LOG_CAPTURE') == '1'):
886 self.useFixture(fixtures.FakeLogger(
887 level=logging.DEBUG,
888 format='%(asctime)s %(name)-32s '
889 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000890
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700891 # NOTE(notmorgan): Extract logging overrides for specific libraries
892 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
893 # each. This is used to limit the output during test runs from
894 # libraries that zuul depends on such as gear.
895 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
896
897 if log_defaults_from_env:
898 for default in log_defaults_from_env.split(','):
899 try:
900 name, level_str = default.split('=', 1)
901 level = getattr(logging, level_str, logging.DEBUG)
902 self.useFixture(fixtures.FakeLogger(
903 name=name,
904 level=level,
905 format='%(asctime)s %(name)-32s '
906 '%(levelname)-8s %(message)s'))
907 except ValueError:
908 # NOTE(notmorgan): Invalid format of the log default,
909 # skip and don't try and apply a logger for the
910 # specified module
911 pass
912
Maru Newby3fe5f852015-01-13 04:22:14 +0000913
914class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700915 """A test case with a functioning Zuul.
916
917 The following class variables are used during test setup and can
918 be overidden by subclasses but are effectively read-only once a
919 test method starts running:
920
921 :cvar str config_file: This points to the main zuul config file
922 within the fixtures directory. Subclasses may override this
923 to obtain a different behavior.
924
925 :cvar str tenant_config_file: This is the tenant config file
926 (which specifies from what git repos the configuration should
927 be loaded). It defaults to the value specified in
928 `config_file` but can be overidden by subclasses to obtain a
929 different tenant/project layout while using the standard main
930 configuration.
931
932 The following are instance variables that are useful within test
933 methods:
934
935 :ivar FakeGerritConnection fake_<connection>:
936 A :py:class:`~tests.base.FakeGerritConnection` will be
937 instantiated for each connection present in the config file
938 and stored here. For instance, `fake_gerrit` will hold the
939 FakeGerritConnection object for a connection named `gerrit`.
940
941 :ivar FakeGearmanServer gearman_server: An instance of
942 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
943 server that all of the Zuul components in this test use to
944 communicate with each other.
945
946 :ivar RecordingLaunchServer launch_server: An instance of
947 :py:class:`~tests.base.RecordingLaunchServer` which is the
948 Ansible launch server used to run jobs for this test.
949
950 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
951 representing currently running builds. They are appended to
952 the list in the order they are launched, and removed from this
953 list upon completion.
954
955 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
956 objects representing completed builds. They are appended to
957 the list in the order they complete.
958
959 """
960
James E. Blair83005782015-12-11 14:46:03 -0800961 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700962 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700963
964 def _startMerger(self):
965 self.merge_server = zuul.merger.server.MergeServer(self.config,
966 self.connections)
967 self.merge_server.start()
968
Maru Newby3fe5f852015-01-13 04:22:14 +0000969 def setUp(self):
970 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700971 if USE_TEMPDIR:
972 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000973 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
974 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700975 else:
976 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700977 self.test_root = os.path.join(tmp_root, "zuul-test")
978 self.upstream_root = os.path.join(self.test_root, "upstream")
979 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700980 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700981
982 if os.path.exists(self.test_root):
983 shutil.rmtree(self.test_root)
984 os.makedirs(self.test_root)
985 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700986 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700987
988 # Make per test copy of Configuration.
989 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800990 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100991 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800992 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700994 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700995
996 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700997 # TODOv3(jeblair): remove these and replace with new git
998 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700999 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001000 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001001 self.init_repo("org/project5")
1002 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001003 self.init_repo("org/one-job-project")
1004 self.init_repo("org/nonvoting-project")
1005 self.init_repo("org/templated-project")
1006 self.init_repo("org/layered-project")
1007 self.init_repo("org/node-project")
1008 self.init_repo("org/conflict-project")
1009 self.init_repo("org/noop-project")
1010 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001011 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001012
1013 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001014 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1015 # see: https://github.com/jsocol/pystatsd/issues/61
1016 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001017 os.environ['STATSD_PORT'] = str(self.statsd.port)
1018 self.statsd.start()
1019 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001020 reload_module(statsd)
1021 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001022
1023 self.gearman_server = FakeGearmanServer()
1024
1025 self.config.set('gearman', 'port', str(self.gearman_server.port))
1026
Joshua Hesketh352264b2015-08-11 23:42:08 +10001027 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1028 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1029 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001030
Joshua Hesketh352264b2015-08-11 23:42:08 +10001031 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001032
1033 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1034 FakeSwiftClientConnection))
1035 self.swift = zuul.lib.swift.Swift(self.config)
1036
Jan Hruban6b71aff2015-10-22 16:58:08 +02001037 self.event_queues = [
1038 self.sched.result_event_queue,
1039 self.sched.trigger_event_queue
1040 ]
1041
James E. Blairfef78942016-03-11 16:28:56 -08001042 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001043 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001044
Clark Boylanb640e052014-04-03 16:41:46 -07001045 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001046 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001047 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001048 return FakeURLOpener(self.upstream_root, *args, **kw)
1049
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001050 old_urlopen = urllib.request.urlopen
1051 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001052
James E. Blair3f876d52016-07-22 13:07:14 -07001053 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001054
James E. Blaire1767bc2016-08-02 10:00:27 -07001055 self.launch_server = RecordingLaunchServer(
1056 self.config, self.connections, _run_ansible=self.run_ansible)
1057 self.launch_server.start()
1058 self.history = self.launch_server.build_history
1059 self.builds = self.launch_server.running_builds
1060
1061 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001062 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001063 self.merge_client = zuul.merger.client.MergeClient(
1064 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001065 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001066
James E. Blaire1767bc2016-08-02 10:00:27 -07001067 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001068 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001069 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001070
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001071 self.webapp = zuul.webapp.WebApp(
1072 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001073 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001074
1075 self.sched.start()
1076 self.sched.reconfigure(self.config)
1077 self.sched.resume()
1078 self.webapp.start()
1079 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001080 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001081
1082 self.addCleanup(self.assertFinalState)
1083 self.addCleanup(self.shutdown)
1084
James E. Blairfef78942016-03-11 16:28:56 -08001085 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001086 # Register connections from the config
1087 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001088
Joshua Hesketh352264b2015-08-11 23:42:08 +10001089 def FakeSMTPFactory(*args, **kw):
1090 args = [self.smtp_messages] + list(args)
1091 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001092
Joshua Hesketh352264b2015-08-11 23:42:08 +10001093 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001094
Joshua Hesketh352264b2015-08-11 23:42:08 +10001095 # Set a changes database so multiple FakeGerrit's can report back to
1096 # a virtual canonical database given by the configured hostname
1097 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001098 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001099
Joshua Hesketh352264b2015-08-11 23:42:08 +10001100 for section_name in self.config.sections():
1101 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1102 section_name, re.I)
1103 if not con_match:
1104 continue
1105 con_name = con_match.group(2)
1106 con_config = dict(self.config.items(section_name))
1107
1108 if 'driver' not in con_config:
1109 raise Exception("No driver specified for connection %s."
1110 % con_name)
1111
1112 con_driver = con_config['driver']
1113
1114 # TODO(jhesketh): load the required class automatically
1115 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001116 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1117 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001118 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001119 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001120 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001121 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001122 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001123 self.event_queues.append(
1124 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001125 setattr(self, 'fake_' + con_name,
1126 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001127 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001128 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001129 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1130 else:
1131 raise Exception("Unknown driver, %s, for connection %s"
1132 % (con_config['driver'], con_name))
1133
1134 # If the [gerrit] or [smtp] sections still exist, load them in as a
1135 # connection named 'gerrit' or 'smtp' respectfully
1136
1137 if 'gerrit' in self.config.sections():
1138 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001139 self.event_queues.append(
1140 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001141 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001142 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001143 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001144
1145 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001146 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001147 zuul.connection.smtp.SMTPConnection(
1148 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001149
James E. Blair83005782015-12-11 14:46:03 -08001150 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001151 # This creates the per-test configuration object. It can be
1152 # overriden by subclasses, but should not need to be since it
1153 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001155 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001156 if hasattr(self, 'tenant_config_file'):
1157 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001158 git_path = os.path.join(
1159 os.path.dirname(
1160 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1161 'git')
1162 if os.path.exists(git_path):
1163 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001164 project = reponame.replace('_', '/')
1165 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001166 os.path.join(git_path, reponame))
1167
1168 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001169 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001170
1171 files = {}
1172 for (dirpath, dirnames, filenames) in os.walk(source_path):
1173 for filename in filenames:
1174 test_tree_filepath = os.path.join(dirpath, filename)
1175 common_path = os.path.commonprefix([test_tree_filepath,
1176 source_path])
1177 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1178 with open(test_tree_filepath, 'r') as f:
1179 content = f.read()
1180 files[relative_filepath] = content
1181 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001182 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001183
Clark Boylanb640e052014-04-03 16:41:46 -07001184 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001185 # Make sure that git.Repo objects have been garbage collected.
1186 repos = []
1187 gc.collect()
1188 for obj in gc.get_objects():
1189 if isinstance(obj, git.Repo):
1190 repos.append(obj)
1191 self.assertEqual(len(repos), 0)
1192 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001193 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001194 for tenant in self.sched.abide.tenants.values():
1195 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001196 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001197 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001198
1199 def shutdown(self):
1200 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001201 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001202 self.merge_server.stop()
1203 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001204 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001205 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001206 self.sched.stop()
1207 self.sched.join()
1208 self.statsd.stop()
1209 self.statsd.join()
1210 self.webapp.stop()
1211 self.webapp.join()
1212 self.rpc.stop()
1213 self.rpc.join()
1214 self.gearman_server.shutdown()
1215 threads = threading.enumerate()
1216 if len(threads) > 1:
1217 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001218
1219 def init_repo(self, project):
1220 parts = project.split('/')
1221 path = os.path.join(self.upstream_root, *parts[:-1])
1222 if not os.path.exists(path):
1223 os.makedirs(path)
1224 path = os.path.join(self.upstream_root, project)
1225 repo = git.Repo.init(path)
1226
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001227 with repo.config_writer() as config_writer:
1228 config_writer.set_value('user', 'email', 'user@example.com')
1229 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001230
Clark Boylanb640e052014-04-03 16:41:46 -07001231 repo.index.commit('initial commit')
1232 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001233
James E. Blair97d902e2014-08-21 13:25:56 -07001234 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001235 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001236 repo.git.clean('-x', '-f', '-d')
1237
James E. Blair97d902e2014-08-21 13:25:56 -07001238 def create_branch(self, project, branch):
1239 path = os.path.join(self.upstream_root, project)
1240 repo = git.Repo.init(path)
1241 fn = os.path.join(path, 'README')
1242
1243 branch_head = repo.create_head(branch)
1244 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001245 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001246 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001247 f.close()
1248 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001249 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001250
James E. Blair97d902e2014-08-21 13:25:56 -07001251 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001252 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001253 repo.git.clean('-x', '-f', '-d')
1254
Sachi King9f16d522016-03-16 12:20:45 +11001255 def create_commit(self, project):
1256 path = os.path.join(self.upstream_root, project)
1257 repo = git.Repo(path)
1258 repo.head.reference = repo.heads['master']
1259 file_name = os.path.join(path, 'README')
1260 with open(file_name, 'a') as f:
1261 f.write('creating fake commit\n')
1262 repo.index.add([file_name])
1263 commit = repo.index.commit('Creating a fake commit')
1264 return commit.hexsha
1265
Clark Boylanb640e052014-04-03 16:41:46 -07001266 def ref_has_change(self, ref, change):
James E. Blaira5dba232016-08-08 15:53:24 -07001267 # TODOv3(jeblair): this should probably be removed in favor of
1268 # build.hasChanges
Clark Boylanb640e052014-04-03 16:41:46 -07001269 path = os.path.join(self.git_root, change.project)
1270 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001271 try:
1272 for commit in repo.iter_commits(ref):
1273 if commit.message.strip() == ('%s-1' % change.subject):
1274 return True
1275 except GitCommandError:
1276 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001277 return False
1278
James E. Blairb8c16472015-05-05 14:55:26 -07001279 def orderedRelease(self):
1280 # Run one build at a time to ensure non-race order:
1281 while len(self.builds):
1282 self.release(self.builds[0])
1283 self.waitUntilSettled()
1284
Clark Boylanb640e052014-04-03 16:41:46 -07001285 def release(self, job):
1286 if isinstance(job, FakeBuild):
1287 job.release()
1288 else:
1289 job.waiting = False
1290 self.log.debug("Queued job %s released" % job.unique)
1291 self.gearman_server.wakeConnections()
1292
1293 def getParameter(self, job, name):
1294 if isinstance(job, FakeBuild):
1295 return job.parameters[name]
1296 else:
1297 parameters = json.loads(job.arguments)
1298 return parameters[name]
1299
1300 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001301 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001302 while True:
1303 done = True
1304 for connection in self.gearman_server.active_connections:
1305 if (connection.functions and
1306 connection.client_id not in ['Zuul RPC Listener',
1307 'Zuul Merger']):
1308 done = False
1309 if done:
1310 break
1311 time.sleep(0)
1312 self.gearman_server.functions = set()
1313 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001314
1315 def haveAllBuildsReported(self):
1316 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001317 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001318 return False
1319 # Find out if every build that the worker has completed has been
1320 # reported back to Zuul. If it hasn't then that means a Gearman
1321 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001322 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001323 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001324 if not zbuild:
1325 # It has already been reported
1326 continue
1327 # It hasn't been reported yet.
1328 return False
1329 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001330 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001331 if connection.state == 'GRAB_WAIT':
1332 return False
1333 return True
1334
1335 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001336 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001337 for build in builds:
1338 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001339 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001340 for j in conn.related_jobs.values():
1341 if j.unique == build.uuid:
1342 client_job = j
1343 break
1344 if not client_job:
1345 self.log.debug("%s is not known to the gearman client" %
1346 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001348 if not client_job.handle:
1349 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001351 server_job = self.gearman_server.jobs.get(client_job.handle)
1352 if not server_job:
1353 self.log.debug("%s is not known to the gearman server" %
1354 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001355 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001356 if not hasattr(server_job, 'waiting'):
1357 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001358 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001359 if server_job.waiting:
1360 continue
James E. Blairbbda4702016-03-09 15:19:56 -08001361 if build.number is None:
1362 self.log.debug("%s has not reported start" % build)
1363 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001364 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001365 if worker_build:
1366 if worker_build.isWaiting():
1367 continue
1368 else:
1369 self.log.debug("%s is running" % worker_build)
1370 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001371 else:
James E. Blair962220f2016-08-03 11:22:38 -07001372 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001373 return False
1374 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001375
Jan Hruban6b71aff2015-10-22 16:58:08 +02001376 def eventQueuesEmpty(self):
1377 for queue in self.event_queues:
1378 yield queue.empty()
1379
1380 def eventQueuesJoin(self):
1381 for queue in self.event_queues:
1382 queue.join()
1383
Clark Boylanb640e052014-04-03 16:41:46 -07001384 def waitUntilSettled(self):
1385 self.log.debug("Waiting until settled...")
1386 start = time.time()
1387 while True:
1388 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001389 self.log.debug("Queue status:")
1390 for queue in self.event_queues:
1391 self.log.debug(" %s: %s" % (queue, queue.empty()))
1392 self.log.debug("All builds waiting: %s" %
1393 (self.areAllBuildsWaiting(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001394 raise Exception("Timeout waiting for Zuul to settle")
1395 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001396
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001398 # have all build states propogated to zuul?
1399 if self.haveAllBuildsReported():
1400 # Join ensures that the queue is empty _and_ events have been
1401 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001402 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001403 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001404 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001405 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001406 self.haveAllBuildsReported() and
1407 self.areAllBuildsWaiting()):
1408 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001409 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001410 self.log.debug("...settled.")
1411 return
1412 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001413 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001414 self.sched.wake_event.wait(0.1)
1415
1416 def countJobResults(self, jobs, result):
1417 jobs = filter(lambda x: x.result == result, jobs)
1418 return len(jobs)
1419
James E. Blair96c6bf82016-01-15 16:20:40 -08001420 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001421 for job in self.history:
1422 if (job.name == name and
1423 (project is None or
1424 job.parameters['ZUUL_PROJECT'] == project)):
1425 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001426 raise Exception("Unable to find job %s in history" % name)
1427
1428 def assertEmptyQueues(self):
1429 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001430 for tenant in self.sched.abide.tenants.values():
1431 for pipeline in tenant.layout.pipelines.values():
1432 for queue in pipeline.queues:
1433 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001434 print('pipeline %s queue %s contents %s' % (
1435 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001436 self.assertEqual(len(queue.queue), 0,
1437 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001438
1439 def assertReportedStat(self, key, value=None, kind=None):
1440 start = time.time()
1441 while time.time() < (start + 5):
1442 for stat in self.statsd.stats:
1443 pprint.pprint(self.statsd.stats)
1444 k, v = stat.split(':')
1445 if key == k:
1446 if value is None and kind is None:
1447 return
1448 elif value:
1449 if value == v:
1450 return
1451 elif kind:
1452 if v.endswith('|' + kind):
1453 return
1454 time.sleep(0.1)
1455
1456 pprint.pprint(self.statsd.stats)
1457 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001458
1459 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001460 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1461
1462 def updateConfigLayout(self, path):
1463 root = os.path.join(self.test_root, "config")
1464 os.makedirs(root)
1465 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1466 f.write("""
1467tenants:
1468 - name: openstack
1469 include:
1470 - %s
1471 """ % os.path.abspath(path))
1472 f.close()
1473 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001474
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001475 def addCommitToRepo(self, project, message, files,
1476 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001477 path = os.path.join(self.upstream_root, project)
1478 repo = git.Repo(path)
1479 repo.head.reference = branch
1480 zuul.merger.merger.reset_repo_to_head(repo)
1481 for fn, content in files.items():
1482 fn = os.path.join(path, fn)
1483 with open(fn, 'w') as f:
1484 f.write(content)
1485 repo.index.add([fn])
1486 commit = repo.index.commit(message)
1487 repo.heads[branch].commit = commit
1488 repo.head.reference = branch
1489 repo.git.clean('-x', '-f', '-d')
1490 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001491 if tag:
1492 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001493
James E. Blair7fc8daa2016-08-08 15:37:15 -07001494 def addEvent(self, connection, event):
1495 """Inject a Fake (Gerrit) event.
1496
1497 This method accepts a JSON-encoded event and simulates Zuul
1498 having received it from Gerrit. It could (and should)
1499 eventually apply to any connection type, but is currently only
1500 used with Gerrit connections. The name of the connection is
1501 used to look up the corresponding server, and the event is
1502 simulated as having been received by all Zuul connections
1503 attached to that server. So if two Gerrit connections in Zuul
1504 are connected to the same Gerrit server, and you invoke this
1505 method specifying the name of one of them, the event will be
1506 received by both.
1507
1508 .. note::
1509
1510 "self.fake_gerrit.addEvent" calls should be migrated to
1511 this method.
1512
1513 :arg str connection: The name of the connection corresponding
1514 to the gerrit server.
1515 :arg str event: The JSON-encoded event.
1516
1517 """
1518 specified_conn = self.connections.connections[connection]
1519 for conn in self.connections.connections.values():
1520 if (isinstance(conn, specified_conn.__class__) and
1521 specified_conn.server == conn.server):
1522 conn.addEvent(event)
1523
James E. Blair3f876d52016-07-22 13:07:14 -07001524
1525class AnsibleZuulTestCase(ZuulTestCase):
1526 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001527 run_ansible = True