blob: 552bdd6d6898f1fa1886007364296975f1855376 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Mike Heald8225f522014-11-21 09:52:33 +000044from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700389 """A Fake Gerrit connection for use in tests.
390
391 This subclasses
392 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
393 ability for tests to add changes to the fake Gerrit it represents.
394 """
395
Joshua Hesketh352264b2015-08-11 23:42:08 +1000396 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700399 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 super(FakeGerritConnection, self).__init__(connection_name,
401 connection_config)
402
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700404 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
405 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000406 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700407 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200408 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700409
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700410 def addFakeChange(self, project, branch, subject, status='NEW',
411 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700412 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700413 self.change_number += 1
414 c = FakeChange(self, self.change_number, project, branch, subject,
415 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700416 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.changes[self.change_number] = c
418 return c
419
Clark Boylanb640e052014-04-03 16:41:46 -0700420 def review(self, project, changeid, message, action):
421 number, ps = changeid.split(',')
422 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000423
424 # Add the approval back onto the change (ie simulate what gerrit would
425 # do).
426 # Usually when zuul leaves a review it'll create a feedback loop where
427 # zuul's review enters another gerrit event (which is then picked up by
428 # zuul). However, we can't mimic this behaviour (by adding this
429 # approval event into the queue) as it stops jobs from checking what
430 # happens before this event is triggered. If a job needs to see what
431 # happens they can add their own verified event into the queue.
432 # Nevertheless, we can update change with the new review in gerrit.
433
James E. Blair8b5408c2016-08-08 15:37:46 -0700434 for cat in action.keys():
435 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439 if 'label' in action:
440 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000441 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442
Clark Boylanb640e052014-04-03 16:41:46 -0700443 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 if 'submit' in action:
446 change.setMerged()
447 if message:
448 change.setReported()
449
450 def query(self, number):
451 change = self.changes.get(int(number))
452 if change:
453 return change.query()
454 return {}
455
James E. Blairc494d542014-08-06 09:23:52 -0700456 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700457 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700458 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800459 if query.startswith('change:'):
460 # Query a specific changeid
461 changeid = query[len('change:'):]
462 l = [change.query() for change in self.changes.values()
463 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700464 elif query.startswith('message:'):
465 # Query the content of a commit message
466 msg = query[len('message:'):].strip()
467 l = [change.query() for change in self.changes.values()
468 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800469 else:
470 # Query all open changes
471 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700472 return l
James E. Blairc494d542014-08-06 09:23:52 -0700473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700475 pass
476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def getGitUrl(self, project):
478 return os.path.join(self.upstream_root, project.name)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700486 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
487 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. Blair17302972016-08-10 16:11:42 -0700544 def __init__(self, launch_server, job, node):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700546 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700548 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700549 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.node = node
551 self.parameters = json.loads(job.arguments)
552 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700553 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700554 self.wait_condition = threading.Condition()
555 self.waiting = False
556 self.aborted = False
557 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700558 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700559 self.changes = None
560 if 'ZUUL_CHANGE_IDS' in self.parameters:
561 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700562
James E. Blair3158e282016-08-19 09:34:11 -0700563 def __repr__(self):
564 waiting = ''
565 if self.waiting:
566 waiting = ' [waiting]'
567 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
568
Clark Boylanb640e052014-04-03 16:41:46 -0700569 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700570 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700571 self.wait_condition.acquire()
572 self.wait_condition.notify()
573 self.waiting = False
574 self.log.debug("Build %s released" % self.unique)
575 self.wait_condition.release()
576
577 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700578 """Return whether this build is being held.
579
580 :returns: Whether the build is being held.
581 :rtype: bool
582 """
583
Clark Boylanb640e052014-04-03 16:41:46 -0700584 self.wait_condition.acquire()
585 if self.waiting:
586 ret = True
587 else:
588 ret = False
589 self.wait_condition.release()
590 return ret
591
592 def _wait(self):
593 self.wait_condition.acquire()
594 self.waiting = True
595 self.log.debug("Build %s waiting" % self.unique)
596 self.wait_condition.wait()
597 self.wait_condition.release()
598
599 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700600 self.log.debug('Running build %s' % self.unique)
601
James E. Blaire1767bc2016-08-02 10:00:27 -0700602 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700603 self.log.debug('Holding build %s' % self.unique)
604 self._wait()
605 self.log.debug("Build %s continuing" % self.unique)
606
Clark Boylanb640e052014-04-03 16:41:46 -0700607 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700608 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700609 result = 'FAILURE'
610 if self.aborted:
611 result = 'ABORTED'
612
613 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700614 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700615
James E. Blaire1767bc2016-08-02 10:00:27 -0700616 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700617
James E. Blaira5dba232016-08-08 15:53:24 -0700618 def shouldFail(self):
619 changes = self.launch_server.fail_tests.get(self.name, [])
620 for change in changes:
621 if self.hasChanges(change):
622 return True
623 return False
624
James E. Blaire7b99a02016-08-05 14:27:34 -0700625 def hasChanges(self, *changes):
626 """Return whether this build has certain changes in its git repos.
627
628 :arg FakeChange changes: One or more changes (varargs) that
629 are expected to be present (in order) in the git repository of
630 the active project.
631
632 :returns: Whether the build has the indicated changes.
633 :rtype: bool
634
635 """
James E. Blair962220f2016-08-03 11:22:38 -0700636 project = self.parameters['ZUUL_PROJECT']
637 path = os.path.join(self.jobdir.git_root, project)
638 repo = git.Repo(path)
639 ref = self.parameters['ZUUL_REF']
640 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700641 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700642 self.log.debug("Checking if build %s has changes; commit_messages %s;"
643 " repo_messages %s" % (self, commit_messages,
644 repo_messages))
645 for msg in commit_messages:
646 if msg not in repo_messages:
647 self.log.debug(" messages do not match")
648 return False
649 self.log.debug(" OK")
650 return True
651
Clark Boylanb640e052014-04-03 16:41:46 -0700652
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000653class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700654 """An Ansible launcher to be used in tests.
655
656 :ivar bool hold_jobs_in_build: If true, when jobs are launched
657 they will report that they have started but then pause until
658 released before reporting completion. This attribute may be
659 changed at any time and will take effect for subsequently
660 launched builds, but previously held builds will still need to
661 be explicitly released.
662
663 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800664 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700665 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800666 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700667 self.hold_jobs_in_build = False
668 self.lock = threading.Lock()
669 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700670 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700671 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700672 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800673
James E. Blaira5dba232016-08-08 15:53:24 -0700674 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700675 """Instruct the launcher to report matching builds as failures.
676
677 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700678 :arg Change change: The :py:class:`~tests.base.FakeChange`
679 instance which should cause the job to fail. This job
680 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700681
682 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 l = self.fail_tests.get(name, [])
684 l.append(change)
685 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800686
James E. Blair962220f2016-08-03 11:22:38 -0700687 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700688 """Release a held build.
689
690 :arg str regex: A regular expression which, if supplied, will
691 cause only builds with matching names to be released. If
692 not supplied, all builds will be released.
693
694 """
James E. Blair962220f2016-08-03 11:22:38 -0700695 builds = self.running_builds[:]
696 self.log.debug("Releasing build %s (%s)" % (regex,
697 len(self.running_builds)))
698 for build in builds:
699 if not regex or re.match(regex, build.name):
700 self.log.debug("Releasing build %s" %
701 (build.parameters['ZUUL_UUID']))
702 build.release()
703 else:
704 self.log.debug("Not releasing build %s" %
705 (build.parameters['ZUUL_UUID']))
706 self.log.debug("Done releasing builds %s (%s)" %
707 (regex, len(self.running_builds)))
708
James E. Blair17302972016-08-10 16:11:42 -0700709 def launchJob(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700710 node = None
James E. Blair17302972016-08-10 16:11:42 -0700711 build = FakeBuild(self, job, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700712 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700713 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700714 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700715 super(RecordingLaunchServer, self).launchJob(job)
716
717 def stopJob(self, job):
718 self.log.debug("handle stop")
719 parameters = json.loads(job.arguments)
720 uuid = parameters['uuid']
721 for build in self.running_builds:
722 if build.unique == uuid:
723 build.aborted = True
724 build.release()
725 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700726
727 def runAnsible(self, jobdir, job):
728 build = self.job_builds[job.unique]
729 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700730
731 if self._run_ansible:
732 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
733 else:
734 result = build.run()
735
736 self.lock.acquire()
737 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700738 BuildHistory(name=build.name, result=result, changes=build.changes,
739 node=build.node, uuid=build.unique,
740 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700741 pipeline=build.parameters['ZUUL_PIPELINE'])
742 )
James E. Blairab7132b2016-08-05 12:36:22 -0700743 self.running_builds.remove(build)
744 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700745 self.lock.release()
746 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800747
748
Clark Boylanb640e052014-04-03 16:41:46 -0700749class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700750 """A Gearman server for use in tests.
751
752 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
753 added to the queue but will not be distributed to workers
754 until released. This attribute may be changed at any time and
755 will take effect for subsequently enqueued jobs, but
756 previously held jobs will still need to be explicitly
757 released.
758
759 """
760
Clark Boylanb640e052014-04-03 16:41:46 -0700761 def __init__(self):
762 self.hold_jobs_in_queue = False
763 super(FakeGearmanServer, self).__init__(0)
764
765 def getJobForConnection(self, connection, peek=False):
766 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
767 for job in queue:
768 if not hasattr(job, 'waiting'):
769 if job.name.startswith('build:'):
770 job.waiting = self.hold_jobs_in_queue
771 else:
772 job.waiting = False
773 if job.waiting:
774 continue
775 if job.name in connection.functions:
776 if not peek:
777 queue.remove(job)
778 connection.related_jobs[job.handle] = job
779 job.worker_connection = connection
780 job.running = True
781 return job
782 return None
783
784 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700785 """Release a held job.
786
787 :arg str regex: A regular expression which, if supplied, will
788 cause only jobs with matching names to be released. If
789 not supplied, all jobs will be released.
790 """
Clark Boylanb640e052014-04-03 16:41:46 -0700791 released = False
792 qlen = (len(self.high_queue) + len(self.normal_queue) +
793 len(self.low_queue))
794 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
795 for job in self.getQueue():
796 cmd, name = job.name.split(':')
797 if cmd != 'build':
798 continue
799 if not regex or re.match(regex, name):
800 self.log.debug("releasing queued job %s" %
801 job.unique)
802 job.waiting = False
803 released = True
804 else:
805 self.log.debug("not releasing queued job %s" %
806 job.unique)
807 if released:
808 self.wakeConnections()
809 qlen = (len(self.high_queue) + len(self.normal_queue) +
810 len(self.low_queue))
811 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
812
813
814class FakeSMTP(object):
815 log = logging.getLogger('zuul.FakeSMTP')
816
817 def __init__(self, messages, server, port):
818 self.server = server
819 self.port = port
820 self.messages = messages
821
822 def sendmail(self, from_email, to_email, msg):
823 self.log.info("Sending email from %s, to %s, with msg %s" % (
824 from_email, to_email, msg))
825
826 headers = msg.split('\n\n', 1)[0]
827 body = msg.split('\n\n', 1)[1]
828
829 self.messages.append(dict(
830 from_email=from_email,
831 to_email=to_email,
832 msg=msg,
833 headers=headers,
834 body=body,
835 ))
836
837 return True
838
839 def quit(self):
840 return True
841
842
843class FakeSwiftClientConnection(swiftclient.client.Connection):
844 def post_account(self, headers):
845 # Do nothing
846 pass
847
848 def get_auth(self):
849 # Returns endpoint and (unused) auth token
850 endpoint = os.path.join('https://storage.example.org', 'V1',
851 'AUTH_account')
852 return endpoint, ''
853
854
Maru Newby3fe5f852015-01-13 04:22:14 +0000855class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700856 log = logging.getLogger("zuul.test")
857
858 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000859 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700860 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
861 try:
862 test_timeout = int(test_timeout)
863 except ValueError:
864 # If timeout value is invalid do not set a timeout.
865 test_timeout = 0
866 if test_timeout > 0:
867 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
868
869 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
870 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
871 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
872 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
873 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
874 os.environ.get('OS_STDERR_CAPTURE') == '1'):
875 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
876 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
877 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
878 os.environ.get('OS_LOG_CAPTURE') == '1'):
879 self.useFixture(fixtures.FakeLogger(
880 level=logging.DEBUG,
881 format='%(asctime)s %(name)-32s '
882 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000883
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700884 # NOTE(notmorgan): Extract logging overrides for specific libraries
885 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
886 # each. This is used to limit the output during test runs from
887 # libraries that zuul depends on such as gear.
888 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
889
890 if log_defaults_from_env:
891 for default in log_defaults_from_env.split(','):
892 try:
893 name, level_str = default.split('=', 1)
894 level = getattr(logging, level_str, logging.DEBUG)
895 self.useFixture(fixtures.FakeLogger(
896 name=name,
897 level=level,
898 format='%(asctime)s %(name)-32s '
899 '%(levelname)-8s %(message)s'))
900 except ValueError:
901 # NOTE(notmorgan): Invalid format of the log default,
902 # skip and don't try and apply a logger for the
903 # specified module
904 pass
905
Maru Newby3fe5f852015-01-13 04:22:14 +0000906
907class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700908 """A test case with a functioning Zuul.
909
910 The following class variables are used during test setup and can
911 be overidden by subclasses but are effectively read-only once a
912 test method starts running:
913
914 :cvar str config_file: This points to the main zuul config file
915 within the fixtures directory. Subclasses may override this
916 to obtain a different behavior.
917
918 :cvar str tenant_config_file: This is the tenant config file
919 (which specifies from what git repos the configuration should
920 be loaded). It defaults to the value specified in
921 `config_file` but can be overidden by subclasses to obtain a
922 different tenant/project layout while using the standard main
923 configuration.
924
925 The following are instance variables that are useful within test
926 methods:
927
928 :ivar FakeGerritConnection fake_<connection>:
929 A :py:class:`~tests.base.FakeGerritConnection` will be
930 instantiated for each connection present in the config file
931 and stored here. For instance, `fake_gerrit` will hold the
932 FakeGerritConnection object for a connection named `gerrit`.
933
934 :ivar FakeGearmanServer gearman_server: An instance of
935 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
936 server that all of the Zuul components in this test use to
937 communicate with each other.
938
939 :ivar RecordingLaunchServer launch_server: An instance of
940 :py:class:`~tests.base.RecordingLaunchServer` which is the
941 Ansible launch server used to run jobs for this test.
942
943 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
944 representing currently running builds. They are appended to
945 the list in the order they are launched, and removed from this
946 list upon completion.
947
948 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
949 objects representing completed builds. They are appended to
950 the list in the order they complete.
951
952 """
953
James E. Blair83005782015-12-11 14:46:03 -0800954 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700955 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700956
957 def _startMerger(self):
958 self.merge_server = zuul.merger.server.MergeServer(self.config,
959 self.connections)
960 self.merge_server.start()
961
Maru Newby3fe5f852015-01-13 04:22:14 +0000962 def setUp(self):
963 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700964 if USE_TEMPDIR:
965 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000966 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
967 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700968 else:
969 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700970 self.test_root = os.path.join(tmp_root, "zuul-test")
971 self.upstream_root = os.path.join(self.test_root, "upstream")
972 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700973 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700974
975 if os.path.exists(self.test_root):
976 shutil.rmtree(self.test_root)
977 os.makedirs(self.test_root)
978 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700979 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700980
981 # Make per test copy of Configuration.
982 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800983 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100984 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800985 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700986 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700987 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700988
989 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700990 # TODOv3(jeblair): remove these and replace with new git
991 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700992 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700993 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -0700994 self.init_repo("org/project5")
995 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -0700996 self.init_repo("org/one-job-project")
997 self.init_repo("org/nonvoting-project")
998 self.init_repo("org/templated-project")
999 self.init_repo("org/layered-project")
1000 self.init_repo("org/node-project")
1001 self.init_repo("org/conflict-project")
1002 self.init_repo("org/noop-project")
1003 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001004 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001005
1006 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001007 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1008 # see: https://github.com/jsocol/pystatsd/issues/61
1009 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001010 os.environ['STATSD_PORT'] = str(self.statsd.port)
1011 self.statsd.start()
1012 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001013 reload_module(statsd)
1014 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001015
1016 self.gearman_server = FakeGearmanServer()
1017
1018 self.config.set('gearman', 'port', str(self.gearman_server.port))
1019
Joshua Hesketh352264b2015-08-11 23:42:08 +10001020 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1021 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1022 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001023
Joshua Hesketh352264b2015-08-11 23:42:08 +10001024 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001025
1026 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1027 FakeSwiftClientConnection))
1028 self.swift = zuul.lib.swift.Swift(self.config)
1029
Jan Hruban6b71aff2015-10-22 16:58:08 +02001030 self.event_queues = [
1031 self.sched.result_event_queue,
1032 self.sched.trigger_event_queue
1033 ]
1034
James E. Blairfef78942016-03-11 16:28:56 -08001035 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001036 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001037
Clark Boylanb640e052014-04-03 16:41:46 -07001038 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001039 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001040 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001041 return FakeURLOpener(self.upstream_root, *args, **kw)
1042
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001043 old_urlopen = urllib.request.urlopen
1044 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001045
James E. Blair3f876d52016-07-22 13:07:14 -07001046 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001047
James E. Blaire1767bc2016-08-02 10:00:27 -07001048 self.launch_server = RecordingLaunchServer(
1049 self.config, self.connections, _run_ansible=self.run_ansible)
1050 self.launch_server.start()
1051 self.history = self.launch_server.build_history
1052 self.builds = self.launch_server.running_builds
1053
1054 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001055 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001056 self.merge_client = zuul.merger.client.MergeClient(
1057 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001058 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001059
James E. Blaire1767bc2016-08-02 10:00:27 -07001060 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001061 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001062 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001063
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001064 self.webapp = zuul.webapp.WebApp(
1065 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001066 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001067
1068 self.sched.start()
1069 self.sched.reconfigure(self.config)
1070 self.sched.resume()
1071 self.webapp.start()
1072 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001073 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001074
1075 self.addCleanup(self.assertFinalState)
1076 self.addCleanup(self.shutdown)
1077
James E. Blairfef78942016-03-11 16:28:56 -08001078 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001079 # Register connections from the config
1080 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001081
Joshua Hesketh352264b2015-08-11 23:42:08 +10001082 def FakeSMTPFactory(*args, **kw):
1083 args = [self.smtp_messages] + list(args)
1084 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001085
Joshua Hesketh352264b2015-08-11 23:42:08 +10001086 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001087
Joshua Hesketh352264b2015-08-11 23:42:08 +10001088 # Set a changes database so multiple FakeGerrit's can report back to
1089 # a virtual canonical database given by the configured hostname
1090 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001091 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001092
Joshua Hesketh352264b2015-08-11 23:42:08 +10001093 for section_name in self.config.sections():
1094 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1095 section_name, re.I)
1096 if not con_match:
1097 continue
1098 con_name = con_match.group(2)
1099 con_config = dict(self.config.items(section_name))
1100
1101 if 'driver' not in con_config:
1102 raise Exception("No driver specified for connection %s."
1103 % con_name)
1104
1105 con_driver = con_config['driver']
1106
1107 # TODO(jhesketh): load the required class automatically
1108 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001109 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1110 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001111 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001112 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001113 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001114 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001115 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001116 self.event_queues.append(
1117 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001118 setattr(self, 'fake_' + con_name,
1119 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001120 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001121 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001122 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1123 else:
1124 raise Exception("Unknown driver, %s, for connection %s"
1125 % (con_config['driver'], con_name))
1126
1127 # If the [gerrit] or [smtp] sections still exist, load them in as a
1128 # connection named 'gerrit' or 'smtp' respectfully
1129
1130 if 'gerrit' in self.config.sections():
1131 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001132 self.event_queues.append(
1133 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001134 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001135 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001136 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001137
1138 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001139 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001140 zuul.connection.smtp.SMTPConnection(
1141 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001142
James E. Blair83005782015-12-11 14:46:03 -08001143 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001144 # This creates the per-test configuration object. It can be
1145 # overriden by subclasses, but should not need to be since it
1146 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001147 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001148 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001149 if hasattr(self, 'tenant_config_file'):
1150 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001151 git_path = os.path.join(
1152 os.path.dirname(
1153 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1154 'git')
1155 if os.path.exists(git_path):
1156 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001157 project = reponame.replace('_', '/')
1158 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001159 os.path.join(git_path, reponame))
1160
1161 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001162 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001163
1164 files = {}
1165 for (dirpath, dirnames, filenames) in os.walk(source_path):
1166 for filename in filenames:
1167 test_tree_filepath = os.path.join(dirpath, filename)
1168 common_path = os.path.commonprefix([test_tree_filepath,
1169 source_path])
1170 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1171 with open(test_tree_filepath, 'r') as f:
1172 content = f.read()
1173 files[relative_filepath] = content
1174 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001175 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001176
Clark Boylanb640e052014-04-03 16:41:46 -07001177 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001178 # Make sure that git.Repo objects have been garbage collected.
1179 repos = []
1180 gc.collect()
1181 for obj in gc.get_objects():
1182 if isinstance(obj, git.Repo):
1183 repos.append(obj)
1184 self.assertEqual(len(repos), 0)
1185 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001186 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001187 for tenant in self.sched.abide.tenants.values():
1188 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001189 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001190 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001191
1192 def shutdown(self):
1193 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001194 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001195 self.merge_server.stop()
1196 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001197 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001198 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001199 self.sched.stop()
1200 self.sched.join()
1201 self.statsd.stop()
1202 self.statsd.join()
1203 self.webapp.stop()
1204 self.webapp.join()
1205 self.rpc.stop()
1206 self.rpc.join()
1207 self.gearman_server.shutdown()
1208 threads = threading.enumerate()
1209 if len(threads) > 1:
1210 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001211
1212 def init_repo(self, project):
1213 parts = project.split('/')
1214 path = os.path.join(self.upstream_root, *parts[:-1])
1215 if not os.path.exists(path):
1216 os.makedirs(path)
1217 path = os.path.join(self.upstream_root, project)
1218 repo = git.Repo.init(path)
1219
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001220 with repo.config_writer() as config_writer:
1221 config_writer.set_value('user', 'email', 'user@example.com')
1222 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001223
Clark Boylanb640e052014-04-03 16:41:46 -07001224 repo.index.commit('initial commit')
1225 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001226
James E. Blair97d902e2014-08-21 13:25:56 -07001227 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001228 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001229 repo.git.clean('-x', '-f', '-d')
1230
James E. Blair97d902e2014-08-21 13:25:56 -07001231 def create_branch(self, project, branch):
1232 path = os.path.join(self.upstream_root, project)
1233 repo = git.Repo.init(path)
1234 fn = os.path.join(path, 'README')
1235
1236 branch_head = repo.create_head(branch)
1237 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001238 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001239 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001240 f.close()
1241 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001242 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001243
James E. Blair97d902e2014-08-21 13:25:56 -07001244 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001245 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001246 repo.git.clean('-x', '-f', '-d')
1247
Sachi King9f16d522016-03-16 12:20:45 +11001248 def create_commit(self, project):
1249 path = os.path.join(self.upstream_root, project)
1250 repo = git.Repo(path)
1251 repo.head.reference = repo.heads['master']
1252 file_name = os.path.join(path, 'README')
1253 with open(file_name, 'a') as f:
1254 f.write('creating fake commit\n')
1255 repo.index.add([file_name])
1256 commit = repo.index.commit('Creating a fake commit')
1257 return commit.hexsha
1258
Clark Boylanb640e052014-04-03 16:41:46 -07001259 def ref_has_change(self, ref, change):
James E. Blaira5dba232016-08-08 15:53:24 -07001260 # TODOv3(jeblair): this should probably be removed in favor of
1261 # build.hasChanges
Clark Boylanb640e052014-04-03 16:41:46 -07001262 path = os.path.join(self.git_root, change.project)
1263 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001264 try:
1265 for commit in repo.iter_commits(ref):
1266 if commit.message.strip() == ('%s-1' % change.subject):
1267 return True
1268 except GitCommandError:
1269 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001270 return False
1271
James E. Blairb8c16472015-05-05 14:55:26 -07001272 def orderedRelease(self):
1273 # Run one build at a time to ensure non-race order:
1274 while len(self.builds):
1275 self.release(self.builds[0])
1276 self.waitUntilSettled()
1277
Clark Boylanb640e052014-04-03 16:41:46 -07001278 def release(self, job):
1279 if isinstance(job, FakeBuild):
1280 job.release()
1281 else:
1282 job.waiting = False
1283 self.log.debug("Queued job %s released" % job.unique)
1284 self.gearman_server.wakeConnections()
1285
1286 def getParameter(self, job, name):
1287 if isinstance(job, FakeBuild):
1288 return job.parameters[name]
1289 else:
1290 parameters = json.loads(job.arguments)
1291 return parameters[name]
1292
1293 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001294 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001295 while True:
1296 done = True
1297 for connection in self.gearman_server.active_connections:
1298 if (connection.functions and
1299 connection.client_id not in ['Zuul RPC Listener',
1300 'Zuul Merger']):
1301 done = False
1302 if done:
1303 break
1304 time.sleep(0)
1305 self.gearman_server.functions = set()
1306 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001307
1308 def haveAllBuildsReported(self):
1309 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001311 return False
1312 # Find out if every build that the worker has completed has been
1313 # reported back to Zuul. If it hasn't then that means a Gearman
1314 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001315 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001316 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001317 if not zbuild:
1318 # It has already been reported
1319 continue
1320 # It hasn't been reported yet.
1321 return False
1322 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001323 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001324 if connection.state == 'GRAB_WAIT':
1325 return False
1326 return True
1327
1328 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001329 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001330 for build in builds:
1331 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001332 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001333 for j in conn.related_jobs.values():
1334 if j.unique == build.uuid:
1335 client_job = j
1336 break
1337 if not client_job:
1338 self.log.debug("%s is not known to the gearman client" %
1339 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001340 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001341 if not client_job.handle:
1342 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001343 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001344 server_job = self.gearman_server.jobs.get(client_job.handle)
1345 if not server_job:
1346 self.log.debug("%s is not known to the gearman server" %
1347 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001348 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001349 if not hasattr(server_job, 'waiting'):
1350 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001351 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001352 if server_job.waiting:
1353 continue
James E. Blair17302972016-08-10 16:11:42 -07001354 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001355 self.log.debug("%s has not reported start" % build)
1356 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001357 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001358 if worker_build:
1359 if worker_build.isWaiting():
1360 continue
1361 else:
1362 self.log.debug("%s is running" % worker_build)
1363 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001364 else:
James E. Blair962220f2016-08-03 11:22:38 -07001365 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001366 return False
1367 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001368
Jan Hruban6b71aff2015-10-22 16:58:08 +02001369 def eventQueuesEmpty(self):
1370 for queue in self.event_queues:
1371 yield queue.empty()
1372
1373 def eventQueuesJoin(self):
1374 for queue in self.event_queues:
1375 queue.join()
1376
Clark Boylanb640e052014-04-03 16:41:46 -07001377 def waitUntilSettled(self):
1378 self.log.debug("Waiting until settled...")
1379 start = time.time()
1380 while True:
1381 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001382 self.log.debug("Queue status:")
1383 for queue in self.event_queues:
1384 self.log.debug(" %s: %s" % (queue, queue.empty()))
1385 self.log.debug("All builds waiting: %s" %
1386 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001387 self.log.debug("All builds reported: %s" %
1388 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001389 raise Exception("Timeout waiting for Zuul to settle")
1390 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001391
James E. Blaire1767bc2016-08-02 10:00:27 -07001392 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001393 # have all build states propogated to zuul?
1394 if self.haveAllBuildsReported():
1395 # Join ensures that the queue is empty _and_ events have been
1396 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001397 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001399 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001400 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.haveAllBuildsReported() and
1402 self.areAllBuildsWaiting()):
1403 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001404 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001405 self.log.debug("...settled.")
1406 return
1407 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001408 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001409 self.sched.wake_event.wait(0.1)
1410
1411 def countJobResults(self, jobs, result):
1412 jobs = filter(lambda x: x.result == result, jobs)
1413 return len(jobs)
1414
James E. Blair96c6bf82016-01-15 16:20:40 -08001415 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001416 for job in self.history:
1417 if (job.name == name and
1418 (project is None or
1419 job.parameters['ZUUL_PROJECT'] == project)):
1420 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001421 raise Exception("Unable to find job %s in history" % name)
1422
1423 def assertEmptyQueues(self):
1424 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001425 for tenant in self.sched.abide.tenants.values():
1426 for pipeline in tenant.layout.pipelines.values():
1427 for queue in pipeline.queues:
1428 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001429 print('pipeline %s queue %s contents %s' % (
1430 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001431 self.assertEqual(len(queue.queue), 0,
1432 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001433
1434 def assertReportedStat(self, key, value=None, kind=None):
1435 start = time.time()
1436 while time.time() < (start + 5):
1437 for stat in self.statsd.stats:
1438 pprint.pprint(self.statsd.stats)
1439 k, v = stat.split(':')
1440 if key == k:
1441 if value is None and kind is None:
1442 return
1443 elif value:
1444 if value == v:
1445 return
1446 elif kind:
1447 if v.endswith('|' + kind):
1448 return
1449 time.sleep(0.1)
1450
1451 pprint.pprint(self.statsd.stats)
1452 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001453
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001454 def assertBuilds(self, builds):
1455 """Assert that the running builds are as described.
1456
1457 The list of running builds is examined and must match exactly
1458 the list of builds described by the input.
1459
1460 :arg list builds: A list of dictionaries. Each item in the
1461 list must match the corresponding build in the build
1462 history, and each element of the dictionary must match the
1463 corresponding attribute of the build.
1464
1465 """
James E. Blair3158e282016-08-19 09:34:11 -07001466 try:
1467 self.assertEqual(len(self.builds), len(builds))
1468 for i, d in enumerate(builds):
1469 for k, v in d.items():
1470 self.assertEqual(
1471 getattr(self.builds[i], k), v,
1472 "Element %i in builds does not match" % (i,))
1473 except Exception:
1474 for build in self.builds:
1475 self.log.error("Running build: %s" % build)
1476 else:
1477 self.log.error("No running builds")
1478 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001479
1480 def assertHistory(self, history):
1481 """Assert that the completed builds are as described.
1482
1483 The list of completed builds is examined and must match
1484 exactly the list of builds described by the input.
1485
1486 :arg list history: A list of dictionaries. Each item in the
1487 list must match the corresponding build in the build
1488 history, and each element of the dictionary must match the
1489 corresponding attribute of the build.
1490
1491 """
James E. Blair3158e282016-08-19 09:34:11 -07001492 try:
1493 self.assertEqual(len(self.history), len(history))
1494 for i, d in enumerate(history):
1495 for k, v in d.items():
1496 self.assertEqual(
1497 getattr(self.history[i], k), v,
1498 "Element %i in history does not match" % (i,))
1499 except Exception:
1500 for build in self.history:
1501 self.log.error("Completed build: %s" % build)
1502 else:
1503 self.log.error("No completed builds")
1504 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001505
James E. Blair59fdbac2015-12-07 17:08:06 -08001506 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001507 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1508
1509 def updateConfigLayout(self, path):
1510 root = os.path.join(self.test_root, "config")
1511 os.makedirs(root)
1512 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1513 f.write("""
1514tenants:
1515 - name: openstack
1516 include:
1517 - %s
1518 """ % os.path.abspath(path))
1519 f.close()
1520 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001521
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001522 def addCommitToRepo(self, project, message, files,
1523 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001524 path = os.path.join(self.upstream_root, project)
1525 repo = git.Repo(path)
1526 repo.head.reference = branch
1527 zuul.merger.merger.reset_repo_to_head(repo)
1528 for fn, content in files.items():
1529 fn = os.path.join(path, fn)
1530 with open(fn, 'w') as f:
1531 f.write(content)
1532 repo.index.add([fn])
1533 commit = repo.index.commit(message)
1534 repo.heads[branch].commit = commit
1535 repo.head.reference = branch
1536 repo.git.clean('-x', '-f', '-d')
1537 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001538 if tag:
1539 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001540
James E. Blair7fc8daa2016-08-08 15:37:15 -07001541 def addEvent(self, connection, event):
1542 """Inject a Fake (Gerrit) event.
1543
1544 This method accepts a JSON-encoded event and simulates Zuul
1545 having received it from Gerrit. It could (and should)
1546 eventually apply to any connection type, but is currently only
1547 used with Gerrit connections. The name of the connection is
1548 used to look up the corresponding server, and the event is
1549 simulated as having been received by all Zuul connections
1550 attached to that server. So if two Gerrit connections in Zuul
1551 are connected to the same Gerrit server, and you invoke this
1552 method specifying the name of one of them, the event will be
1553 received by both.
1554
1555 .. note::
1556
1557 "self.fake_gerrit.addEvent" calls should be migrated to
1558 this method.
1559
1560 :arg str connection: The name of the connection corresponding
1561 to the gerrit server.
1562 :arg str event: The JSON-encoded event.
1563
1564 """
1565 specified_conn = self.connections.connections[connection]
1566 for conn in self.connections.connections.values():
1567 if (isinstance(conn, specified_conn.__class__) and
1568 specified_conn.server == conn.server):
1569 conn.addEvent(event)
1570
James E. Blair3f876d52016-07-22 13:07:14 -07001571
1572class AnsibleZuulTestCase(ZuulTestCase):
1573 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001574 run_ansible = True