blob: b343655cbad59a2c702d7eb714e87a9d789998c4 [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
Clint Byrum3343e3e2016-11-15 16:05:03 -080044from git.exc import NoSuchPathError
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. Blair34776ee2016-08-25 13:53:54 -0700544 def __init__(self, launch_server, job):
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.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700551 # TODOv3(jeblair): self.node is really "the image of the node
552 # assigned". We should rename it (self.node_image?) if we
553 # keep using it like this, or we may end up exposing more of
554 # the complexity around multi-node jobs here
555 # (self.nodes[0].image?)
556 self.node = None
557 if len(self.parameters.get('nodes')) == 1:
558 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700559 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700560 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700561 self.wait_condition = threading.Condition()
562 self.waiting = False
563 self.aborted = False
564 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700566 self.changes = None
567 if 'ZUUL_CHANGE_IDS' in self.parameters:
568 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700569
James E. Blair3158e282016-08-19 09:34:11 -0700570 def __repr__(self):
571 waiting = ''
572 if self.waiting:
573 waiting = ' [waiting]'
574 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
575
Clark Boylanb640e052014-04-03 16:41:46 -0700576 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700577 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700578 self.wait_condition.acquire()
579 self.wait_condition.notify()
580 self.waiting = False
581 self.log.debug("Build %s released" % self.unique)
582 self.wait_condition.release()
583
584 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700585 """Return whether this build is being held.
586
587 :returns: Whether the build is being held.
588 :rtype: bool
589 """
590
Clark Boylanb640e052014-04-03 16:41:46 -0700591 self.wait_condition.acquire()
592 if self.waiting:
593 ret = True
594 else:
595 ret = False
596 self.wait_condition.release()
597 return ret
598
599 def _wait(self):
600 self.wait_condition.acquire()
601 self.waiting = True
602 self.log.debug("Build %s waiting" % self.unique)
603 self.wait_condition.wait()
604 self.wait_condition.release()
605
606 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700607 self.log.debug('Running build %s' % self.unique)
608
James E. Blaire1767bc2016-08-02 10:00:27 -0700609 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700610 self.log.debug('Holding build %s' % self.unique)
611 self._wait()
612 self.log.debug("Build %s continuing" % self.unique)
613
Clark Boylanb640e052014-04-03 16:41:46 -0700614 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700615 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700616 result = 'FAILURE'
617 if self.aborted:
618 result = 'ABORTED'
619
620 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700621 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700622
James E. Blaire1767bc2016-08-02 10:00:27 -0700623 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700624
James E. Blaira5dba232016-08-08 15:53:24 -0700625 def shouldFail(self):
626 changes = self.launch_server.fail_tests.get(self.name, [])
627 for change in changes:
628 if self.hasChanges(change):
629 return True
630 return False
631
James E. Blaire7b99a02016-08-05 14:27:34 -0700632 def hasChanges(self, *changes):
633 """Return whether this build has certain changes in its git repos.
634
635 :arg FakeChange changes: One or more changes (varargs) that
636 are expected to be present (in order) in the git repository of
637 the active project.
638
639 :returns: Whether the build has the indicated changes.
640 :rtype: bool
641
642 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800643 for change in changes:
644 path = os.path.join(self.jobdir.git_root, change.project)
645 try:
646 repo = git.Repo(path)
647 except NoSuchPathError as e:
648 self.log.debug('%s' % e)
649 return False
650 ref = self.parameters['ZUUL_REF']
651 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
652 commit_message = '%s-1' % change.subject
653 self.log.debug("Checking if build %s has changes; commit_message "
654 "%s; repo_messages %s" % (self, commit_message,
655 repo_messages))
656 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700657 self.log.debug(" messages do not match")
658 return False
659 self.log.debug(" OK")
660 return True
661
Clark Boylanb640e052014-04-03 16:41:46 -0700662
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000663class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700664 """An Ansible launcher to be used in tests.
665
666 :ivar bool hold_jobs_in_build: If true, when jobs are launched
667 they will report that they have started but then pause until
668 released before reporting completion. This attribute may be
669 changed at any time and will take effect for subsequently
670 launched builds, but previously held builds will still need to
671 be explicitly released.
672
673 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800674 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700675 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800676 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700677 self.hold_jobs_in_build = False
678 self.lock = threading.Lock()
679 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700680 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700681 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700682 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800683
James E. Blaira5dba232016-08-08 15:53:24 -0700684 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700685 """Instruct the launcher to report matching builds as failures.
686
687 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700688 :arg Change change: The :py:class:`~tests.base.FakeChange`
689 instance which should cause the job to fail. This job
690 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700691
692 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700693 l = self.fail_tests.get(name, [])
694 l.append(change)
695 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800696
James E. Blair962220f2016-08-03 11:22:38 -0700697 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700698 """Release a held build.
699
700 :arg str regex: A regular expression which, if supplied, will
701 cause only builds with matching names to be released. If
702 not supplied, all builds will be released.
703
704 """
James E. Blair962220f2016-08-03 11:22:38 -0700705 builds = self.running_builds[:]
706 self.log.debug("Releasing build %s (%s)" % (regex,
707 len(self.running_builds)))
708 for build in builds:
709 if not regex or re.match(regex, build.name):
710 self.log.debug("Releasing build %s" %
711 (build.parameters['ZUUL_UUID']))
712 build.release()
713 else:
714 self.log.debug("Not releasing build %s" %
715 (build.parameters['ZUUL_UUID']))
716 self.log.debug("Done releasing builds %s (%s)" %
717 (regex, len(self.running_builds)))
718
James E. Blair17302972016-08-10 16:11:42 -0700719 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700720 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700721 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700722 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700723 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700724 super(RecordingLaunchServer, self).launchJob(job)
725
726 def stopJob(self, job):
727 self.log.debug("handle stop")
728 parameters = json.loads(job.arguments)
729 uuid = parameters['uuid']
730 for build in self.running_builds:
731 if build.unique == uuid:
732 build.aborted = True
733 build.release()
734 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700735
736 def runAnsible(self, jobdir, job):
737 build = self.job_builds[job.unique]
738 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700739
740 if self._run_ansible:
741 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
742 else:
743 result = build.run()
744
745 self.lock.acquire()
746 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700747 BuildHistory(name=build.name, result=result, changes=build.changes,
748 node=build.node, uuid=build.unique,
749 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700750 pipeline=build.parameters['ZUUL_PIPELINE'])
751 )
James E. Blairab7132b2016-08-05 12:36:22 -0700752 self.running_builds.remove(build)
753 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700754 self.lock.release()
755 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800756
757
Clark Boylanb640e052014-04-03 16:41:46 -0700758class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700759 """A Gearman server for use in tests.
760
761 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
762 added to the queue but will not be distributed to workers
763 until released. This attribute may be changed at any time and
764 will take effect for subsequently enqueued jobs, but
765 previously held jobs will still need to be explicitly
766 released.
767
768 """
769
Clark Boylanb640e052014-04-03 16:41:46 -0700770 def __init__(self):
771 self.hold_jobs_in_queue = False
772 super(FakeGearmanServer, self).__init__(0)
773
774 def getJobForConnection(self, connection, peek=False):
775 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
776 for job in queue:
777 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500778 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700779 job.waiting = self.hold_jobs_in_queue
780 else:
781 job.waiting = False
782 if job.waiting:
783 continue
784 if job.name in connection.functions:
785 if not peek:
786 queue.remove(job)
787 connection.related_jobs[job.handle] = job
788 job.worker_connection = connection
789 job.running = True
790 return job
791 return None
792
793 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700794 """Release a held job.
795
796 :arg str regex: A regular expression which, if supplied, will
797 cause only jobs with matching names to be released. If
798 not supplied, all jobs will be released.
799 """
Clark Boylanb640e052014-04-03 16:41:46 -0700800 released = False
801 qlen = (len(self.high_queue) + len(self.normal_queue) +
802 len(self.low_queue))
803 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
804 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500805 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700806 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500807 parameters = json.loads(job.arguments)
808 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700809 self.log.debug("releasing queued job %s" %
810 job.unique)
811 job.waiting = False
812 released = True
813 else:
814 self.log.debug("not releasing queued job %s" %
815 job.unique)
816 if released:
817 self.wakeConnections()
818 qlen = (len(self.high_queue) + len(self.normal_queue) +
819 len(self.low_queue))
820 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
821
822
823class FakeSMTP(object):
824 log = logging.getLogger('zuul.FakeSMTP')
825
826 def __init__(self, messages, server, port):
827 self.server = server
828 self.port = port
829 self.messages = messages
830
831 def sendmail(self, from_email, to_email, msg):
832 self.log.info("Sending email from %s, to %s, with msg %s" % (
833 from_email, to_email, msg))
834
835 headers = msg.split('\n\n', 1)[0]
836 body = msg.split('\n\n', 1)[1]
837
838 self.messages.append(dict(
839 from_email=from_email,
840 to_email=to_email,
841 msg=msg,
842 headers=headers,
843 body=body,
844 ))
845
846 return True
847
848 def quit(self):
849 return True
850
851
852class FakeSwiftClientConnection(swiftclient.client.Connection):
853 def post_account(self, headers):
854 # Do nothing
855 pass
856
857 def get_auth(self):
858 # Returns endpoint and (unused) auth token
859 endpoint = os.path.join('https://storage.example.org', 'V1',
860 'AUTH_account')
861 return endpoint, ''
862
863
Maru Newby3fe5f852015-01-13 04:22:14 +0000864class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700865 log = logging.getLogger("zuul.test")
866
867 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000868 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700869 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
870 try:
871 test_timeout = int(test_timeout)
872 except ValueError:
873 # If timeout value is invalid do not set a timeout.
874 test_timeout = 0
875 if test_timeout > 0:
876 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
877
878 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
879 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
880 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
881 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
882 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
883 os.environ.get('OS_STDERR_CAPTURE') == '1'):
884 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
885 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
886 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
887 os.environ.get('OS_LOG_CAPTURE') == '1'):
888 self.useFixture(fixtures.FakeLogger(
889 level=logging.DEBUG,
890 format='%(asctime)s %(name)-32s '
891 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000892
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700893 # NOTE(notmorgan): Extract logging overrides for specific libraries
894 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
895 # each. This is used to limit the output during test runs from
896 # libraries that zuul depends on such as gear.
897 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
898
899 if log_defaults_from_env:
900 for default in log_defaults_from_env.split(','):
901 try:
902 name, level_str = default.split('=', 1)
903 level = getattr(logging, level_str, logging.DEBUG)
904 self.useFixture(fixtures.FakeLogger(
905 name=name,
906 level=level,
907 format='%(asctime)s %(name)-32s '
908 '%(levelname)-8s %(message)s'))
909 except ValueError:
910 # NOTE(notmorgan): Invalid format of the log default,
911 # skip and don't try and apply a logger for the
912 # specified module
913 pass
914
Maru Newby3fe5f852015-01-13 04:22:14 +0000915
916class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700917 """A test case with a functioning Zuul.
918
919 The following class variables are used during test setup and can
920 be overidden by subclasses but are effectively read-only once a
921 test method starts running:
922
923 :cvar str config_file: This points to the main zuul config file
924 within the fixtures directory. Subclasses may override this
925 to obtain a different behavior.
926
927 :cvar str tenant_config_file: This is the tenant config file
928 (which specifies from what git repos the configuration should
929 be loaded). It defaults to the value specified in
930 `config_file` but can be overidden by subclasses to obtain a
931 different tenant/project layout while using the standard main
932 configuration.
933
934 The following are instance variables that are useful within test
935 methods:
936
937 :ivar FakeGerritConnection fake_<connection>:
938 A :py:class:`~tests.base.FakeGerritConnection` will be
939 instantiated for each connection present in the config file
940 and stored here. For instance, `fake_gerrit` will hold the
941 FakeGerritConnection object for a connection named `gerrit`.
942
943 :ivar FakeGearmanServer gearman_server: An instance of
944 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
945 server that all of the Zuul components in this test use to
946 communicate with each other.
947
948 :ivar RecordingLaunchServer launch_server: An instance of
949 :py:class:`~tests.base.RecordingLaunchServer` which is the
950 Ansible launch server used to run jobs for this test.
951
952 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
953 representing currently running builds. They are appended to
954 the list in the order they are launched, and removed from this
955 list upon completion.
956
957 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
958 objects representing completed builds. They are appended to
959 the list in the order they complete.
960
961 """
962
James E. Blair83005782015-12-11 14:46:03 -0800963 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700964 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700965
966 def _startMerger(self):
967 self.merge_server = zuul.merger.server.MergeServer(self.config,
968 self.connections)
969 self.merge_server.start()
970
Maru Newby3fe5f852015-01-13 04:22:14 +0000971 def setUp(self):
972 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700973 if USE_TEMPDIR:
974 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000975 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
976 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700977 else:
978 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700979 self.test_root = os.path.join(tmp_root, "zuul-test")
980 self.upstream_root = os.path.join(self.test_root, "upstream")
981 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700982 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700983
984 if os.path.exists(self.test_root):
985 shutil.rmtree(self.test_root)
986 os.makedirs(self.test_root)
987 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700988 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700989
990 # Make per test copy of Configuration.
991 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800992 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100993 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800994 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700995 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700996 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700997
998 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700999 # TODOv3(jeblair): remove these and replace with new git
1000 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001001 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001002 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001003 self.init_repo("org/project5")
1004 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001005 self.init_repo("org/one-job-project")
1006 self.init_repo("org/nonvoting-project")
1007 self.init_repo("org/templated-project")
1008 self.init_repo("org/layered-project")
1009 self.init_repo("org/node-project")
1010 self.init_repo("org/conflict-project")
1011 self.init_repo("org/noop-project")
1012 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001013 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001014
1015 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001016 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1017 # see: https://github.com/jsocol/pystatsd/issues/61
1018 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001019 os.environ['STATSD_PORT'] = str(self.statsd.port)
1020 self.statsd.start()
1021 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001022 reload_module(statsd)
1023 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001024
1025 self.gearman_server = FakeGearmanServer()
1026
1027 self.config.set('gearman', 'port', str(self.gearman_server.port))
1028
Joshua Hesketh352264b2015-08-11 23:42:08 +10001029 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1030 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1031 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001032
Joshua Hesketh352264b2015-08-11 23:42:08 +10001033 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001034
1035 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1036 FakeSwiftClientConnection))
1037 self.swift = zuul.lib.swift.Swift(self.config)
1038
Jan Hruban6b71aff2015-10-22 16:58:08 +02001039 self.event_queues = [
1040 self.sched.result_event_queue,
1041 self.sched.trigger_event_queue
1042 ]
1043
James E. Blairfef78942016-03-11 16:28:56 -08001044 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001045 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001046
Clark Boylanb640e052014-04-03 16:41:46 -07001047 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001048 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001049 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001050 return FakeURLOpener(self.upstream_root, *args, **kw)
1051
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001052 old_urlopen = urllib.request.urlopen
1053 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001054
James E. Blair3f876d52016-07-22 13:07:14 -07001055 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001056
James E. Blaire1767bc2016-08-02 10:00:27 -07001057 self.launch_server = RecordingLaunchServer(
1058 self.config, self.connections, _run_ansible=self.run_ansible)
1059 self.launch_server.start()
1060 self.history = self.launch_server.build_history
1061 self.builds = self.launch_server.running_builds
1062
1063 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001064 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001065 self.merge_client = zuul.merger.client.MergeClient(
1066 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001067 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001068
James E. Blaire1767bc2016-08-02 10:00:27 -07001069 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001070 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001071 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001072
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001073 self.webapp = zuul.webapp.WebApp(
1074 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001075 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001076
1077 self.sched.start()
1078 self.sched.reconfigure(self.config)
1079 self.sched.resume()
1080 self.webapp.start()
1081 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001082 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001083
1084 self.addCleanup(self.assertFinalState)
1085 self.addCleanup(self.shutdown)
1086
James E. Blairfef78942016-03-11 16:28:56 -08001087 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001088 # Register connections from the config
1089 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001090
Joshua Hesketh352264b2015-08-11 23:42:08 +10001091 def FakeSMTPFactory(*args, **kw):
1092 args = [self.smtp_messages] + list(args)
1093 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001094
Joshua Hesketh352264b2015-08-11 23:42:08 +10001095 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001096
Joshua Hesketh352264b2015-08-11 23:42:08 +10001097 # Set a changes database so multiple FakeGerrit's can report back to
1098 # a virtual canonical database given by the configured hostname
1099 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001100 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001101
Joshua Hesketh352264b2015-08-11 23:42:08 +10001102 for section_name in self.config.sections():
1103 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1104 section_name, re.I)
1105 if not con_match:
1106 continue
1107 con_name = con_match.group(2)
1108 con_config = dict(self.config.items(section_name))
1109
1110 if 'driver' not in con_config:
1111 raise Exception("No driver specified for connection %s."
1112 % con_name)
1113
1114 con_driver = con_config['driver']
1115
1116 # TODO(jhesketh): load the required class automatically
1117 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001118 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1119 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001120 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001121 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001122 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001123 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001124 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001125 self.event_queues.append(
1126 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001127 setattr(self, 'fake_' + con_name,
1128 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001129 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001130 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001131 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1132 else:
1133 raise Exception("Unknown driver, %s, for connection %s"
1134 % (con_config['driver'], con_name))
1135
1136 # If the [gerrit] or [smtp] sections still exist, load them in as a
1137 # connection named 'gerrit' or 'smtp' respectfully
1138
1139 if 'gerrit' in self.config.sections():
1140 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001141 self.event_queues.append(
1142 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001143 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001144 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001145 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001146
1147 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001148 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001149 zuul.connection.smtp.SMTPConnection(
1150 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001151
James E. Blair83005782015-12-11 14:46:03 -08001152 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001153 # This creates the per-test configuration object. It can be
1154 # overriden by subclasses, but should not need to be since it
1155 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001156 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001157 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001158 if hasattr(self, 'tenant_config_file'):
1159 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001160 git_path = os.path.join(
1161 os.path.dirname(
1162 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1163 'git')
1164 if os.path.exists(git_path):
1165 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001166 project = reponame.replace('_', '/')
1167 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001168 os.path.join(git_path, reponame))
1169
1170 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001171 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001172
1173 files = {}
1174 for (dirpath, dirnames, filenames) in os.walk(source_path):
1175 for filename in filenames:
1176 test_tree_filepath = os.path.join(dirpath, filename)
1177 common_path = os.path.commonprefix([test_tree_filepath,
1178 source_path])
1179 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1180 with open(test_tree_filepath, 'r') as f:
1181 content = f.read()
1182 files[relative_filepath] = content
1183 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001184 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001185
Clark Boylanb640e052014-04-03 16:41:46 -07001186 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001187 # Make sure that git.Repo objects have been garbage collected.
1188 repos = []
1189 gc.collect()
1190 for obj in gc.get_objects():
1191 if isinstance(obj, git.Repo):
1192 repos.append(obj)
1193 self.assertEqual(len(repos), 0)
1194 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001195 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001196 for tenant in self.sched.abide.tenants.values():
1197 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001198 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001199 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001200
1201 def shutdown(self):
1202 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001203 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001204 self.merge_server.stop()
1205 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001206 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001207 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001208 self.sched.stop()
1209 self.sched.join()
1210 self.statsd.stop()
1211 self.statsd.join()
1212 self.webapp.stop()
1213 self.webapp.join()
1214 self.rpc.stop()
1215 self.rpc.join()
1216 self.gearman_server.shutdown()
1217 threads = threading.enumerate()
1218 if len(threads) > 1:
1219 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001220
1221 def init_repo(self, project):
1222 parts = project.split('/')
1223 path = os.path.join(self.upstream_root, *parts[:-1])
1224 if not os.path.exists(path):
1225 os.makedirs(path)
1226 path = os.path.join(self.upstream_root, project)
1227 repo = git.Repo.init(path)
1228
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001229 with repo.config_writer() as config_writer:
1230 config_writer.set_value('user', 'email', 'user@example.com')
1231 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001232
Clark Boylanb640e052014-04-03 16:41:46 -07001233 repo.index.commit('initial commit')
1234 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001235
James E. Blair97d902e2014-08-21 13:25:56 -07001236 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001237 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001238 repo.git.clean('-x', '-f', '-d')
1239
James E. Blair97d902e2014-08-21 13:25:56 -07001240 def create_branch(self, project, branch):
1241 path = os.path.join(self.upstream_root, project)
1242 repo = git.Repo.init(path)
1243 fn = os.path.join(path, 'README')
1244
1245 branch_head = repo.create_head(branch)
1246 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001247 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001248 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001249 f.close()
1250 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001251 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001252
James E. Blair97d902e2014-08-21 13:25:56 -07001253 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001254 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001255 repo.git.clean('-x', '-f', '-d')
1256
Sachi King9f16d522016-03-16 12:20:45 +11001257 def create_commit(self, project):
1258 path = os.path.join(self.upstream_root, project)
1259 repo = git.Repo(path)
1260 repo.head.reference = repo.heads['master']
1261 file_name = os.path.join(path, 'README')
1262 with open(file_name, 'a') as f:
1263 f.write('creating fake commit\n')
1264 repo.index.add([file_name])
1265 commit = repo.index.commit('Creating a fake commit')
1266 return commit.hexsha
1267
James E. Blairb8c16472015-05-05 14:55:26 -07001268 def orderedRelease(self):
1269 # Run one build at a time to ensure non-race order:
1270 while len(self.builds):
1271 self.release(self.builds[0])
1272 self.waitUntilSettled()
1273
Clark Boylanb640e052014-04-03 16:41:46 -07001274 def release(self, job):
1275 if isinstance(job, FakeBuild):
1276 job.release()
1277 else:
1278 job.waiting = False
1279 self.log.debug("Queued job %s released" % job.unique)
1280 self.gearman_server.wakeConnections()
1281
1282 def getParameter(self, job, name):
1283 if isinstance(job, FakeBuild):
1284 return job.parameters[name]
1285 else:
1286 parameters = json.loads(job.arguments)
1287 return parameters[name]
1288
1289 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001290 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001291 while True:
1292 done = True
1293 for connection in self.gearman_server.active_connections:
1294 if (connection.functions and
1295 connection.client_id not in ['Zuul RPC Listener',
1296 'Zuul Merger']):
1297 done = False
1298 if done:
1299 break
1300 time.sleep(0)
1301 self.gearman_server.functions = set()
1302 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001303
1304 def haveAllBuildsReported(self):
1305 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001306 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001307 return False
1308 # Find out if every build that the worker has completed has been
1309 # reported back to Zuul. If it hasn't then that means a Gearman
1310 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001311 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001312 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001313 if not zbuild:
1314 # It has already been reported
1315 continue
1316 # It hasn't been reported yet.
1317 return False
1318 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001319 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001320 if connection.state == 'GRAB_WAIT':
1321 return False
1322 return True
1323
1324 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001325 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001326 for build in builds:
1327 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001328 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001329 for j in conn.related_jobs.values():
1330 if j.unique == build.uuid:
1331 client_job = j
1332 break
1333 if not client_job:
1334 self.log.debug("%s is not known to the gearman client" %
1335 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001336 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001337 if not client_job.handle:
1338 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001339 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001340 server_job = self.gearman_server.jobs.get(client_job.handle)
1341 if not server_job:
1342 self.log.debug("%s is not known to the gearman server" %
1343 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001345 if not hasattr(server_job, 'waiting'):
1346 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001348 if server_job.waiting:
1349 continue
James E. Blair17302972016-08-10 16:11:42 -07001350 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001351 self.log.debug("%s has not reported start" % build)
1352 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001353 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001354 if worker_build:
1355 if worker_build.isWaiting():
1356 continue
1357 else:
1358 self.log.debug("%s is running" % worker_build)
1359 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001360 else:
James E. Blair962220f2016-08-03 11:22:38 -07001361 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001362 return False
1363 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001364
Jan Hruban6b71aff2015-10-22 16:58:08 +02001365 def eventQueuesEmpty(self):
1366 for queue in self.event_queues:
1367 yield queue.empty()
1368
1369 def eventQueuesJoin(self):
1370 for queue in self.event_queues:
1371 queue.join()
1372
Clark Boylanb640e052014-04-03 16:41:46 -07001373 def waitUntilSettled(self):
1374 self.log.debug("Waiting until settled...")
1375 start = time.time()
1376 while True:
1377 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001378 self.log.debug("Queue status:")
1379 for queue in self.event_queues:
1380 self.log.debug(" %s: %s" % (queue, queue.empty()))
1381 self.log.debug("All builds waiting: %s" %
1382 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001383 self.log.debug("All builds reported: %s" %
1384 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001385 raise Exception("Timeout waiting for Zuul to settle")
1386 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001387
James E. Blaire1767bc2016-08-02 10:00:27 -07001388 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001389 # have all build states propogated to zuul?
1390 if self.haveAllBuildsReported():
1391 # Join ensures that the queue is empty _and_ events have been
1392 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001393 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001394 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001395 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001396 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001397 self.haveAllBuildsReported() and
1398 self.areAllBuildsWaiting()):
1399 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001400 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.log.debug("...settled.")
1402 return
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.sched.wake_event.wait(0.1)
1406
1407 def countJobResults(self, jobs, result):
1408 jobs = filter(lambda x: x.result == result, jobs)
1409 return len(jobs)
1410
James E. Blair96c6bf82016-01-15 16:20:40 -08001411 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001412 for job in self.history:
1413 if (job.name == name and
1414 (project is None or
1415 job.parameters['ZUUL_PROJECT'] == project)):
1416 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001417 raise Exception("Unable to find job %s in history" % name)
1418
1419 def assertEmptyQueues(self):
1420 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001421 for tenant in self.sched.abide.tenants.values():
1422 for pipeline in tenant.layout.pipelines.values():
1423 for queue in pipeline.queues:
1424 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001425 print('pipeline %s queue %s contents %s' % (
1426 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001427 self.assertEqual(len(queue.queue), 0,
1428 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001429
1430 def assertReportedStat(self, key, value=None, kind=None):
1431 start = time.time()
1432 while time.time() < (start + 5):
1433 for stat in self.statsd.stats:
1434 pprint.pprint(self.statsd.stats)
1435 k, v = stat.split(':')
1436 if key == k:
1437 if value is None and kind is None:
1438 return
1439 elif value:
1440 if value == v:
1441 return
1442 elif kind:
1443 if v.endswith('|' + kind):
1444 return
1445 time.sleep(0.1)
1446
1447 pprint.pprint(self.statsd.stats)
1448 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001449
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001450 def assertBuilds(self, builds):
1451 """Assert that the running builds are as described.
1452
1453 The list of running builds is examined and must match exactly
1454 the list of builds described by the input.
1455
1456 :arg list builds: A list of dictionaries. Each item in the
1457 list must match the corresponding build in the build
1458 history, and each element of the dictionary must match the
1459 corresponding attribute of the build.
1460
1461 """
James E. Blair3158e282016-08-19 09:34:11 -07001462 try:
1463 self.assertEqual(len(self.builds), len(builds))
1464 for i, d in enumerate(builds):
1465 for k, v in d.items():
1466 self.assertEqual(
1467 getattr(self.builds[i], k), v,
1468 "Element %i in builds does not match" % (i,))
1469 except Exception:
1470 for build in self.builds:
1471 self.log.error("Running build: %s" % build)
1472 else:
1473 self.log.error("No running builds")
1474 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001475
James E. Blairb536ecc2016-08-31 10:11:42 -07001476 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001477 """Assert that the completed builds are as described.
1478
1479 The list of completed builds is examined and must match
1480 exactly the list of builds described by the input.
1481
1482 :arg list history: A list of dictionaries. Each item in the
1483 list must match the corresponding build in the build
1484 history, and each element of the dictionary must match the
1485 corresponding attribute of the build.
1486
James E. Blairb536ecc2016-08-31 10:11:42 -07001487 :arg bool ordered: If true, the history must match the order
1488 supplied, if false, the builds are permitted to have
1489 arrived in any order.
1490
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001491 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001492 def matches(history_item, item):
1493 for k, v in item.items():
1494 if getattr(history_item, k) != v:
1495 return False
1496 return True
James E. Blair3158e282016-08-19 09:34:11 -07001497 try:
1498 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001499 if ordered:
1500 for i, d in enumerate(history):
1501 if not matches(self.history[i], d):
1502 raise Exception(
1503 "Element %i in history does not match" % (i,))
1504 else:
1505 unseen = self.history[:]
1506 for i, d in enumerate(history):
1507 found = False
1508 for unseen_item in unseen:
1509 if matches(unseen_item, d):
1510 found = True
1511 unseen.remove(unseen_item)
1512 break
1513 if not found:
1514 raise Exception("No match found for element %i "
1515 "in history" % (i,))
1516 if unseen:
1517 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001518 except Exception:
1519 for build in self.history:
1520 self.log.error("Completed build: %s" % build)
1521 else:
1522 self.log.error("No completed builds")
1523 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001524
James E. Blair59fdbac2015-12-07 17:08:06 -08001525 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001526 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1527
1528 def updateConfigLayout(self, path):
1529 root = os.path.join(self.test_root, "config")
1530 os.makedirs(root)
1531 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1532 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001533- tenant:
1534 name: openstack
1535 source:
1536 gerrit:
1537 config-repos:
1538 - %s
1539 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001540 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001541 self.config.set('zuul', 'tenant_config',
1542 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001543
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001544 def addCommitToRepo(self, project, message, files,
1545 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001546 path = os.path.join(self.upstream_root, project)
1547 repo = git.Repo(path)
1548 repo.head.reference = branch
1549 zuul.merger.merger.reset_repo_to_head(repo)
1550 for fn, content in files.items():
1551 fn = os.path.join(path, fn)
1552 with open(fn, 'w') as f:
1553 f.write(content)
1554 repo.index.add([fn])
1555 commit = repo.index.commit(message)
1556 repo.heads[branch].commit = commit
1557 repo.head.reference = branch
1558 repo.git.clean('-x', '-f', '-d')
1559 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001560 if tag:
1561 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001562
James E. Blair7fc8daa2016-08-08 15:37:15 -07001563 def addEvent(self, connection, event):
1564 """Inject a Fake (Gerrit) event.
1565
1566 This method accepts a JSON-encoded event and simulates Zuul
1567 having received it from Gerrit. It could (and should)
1568 eventually apply to any connection type, but is currently only
1569 used with Gerrit connections. The name of the connection is
1570 used to look up the corresponding server, and the event is
1571 simulated as having been received by all Zuul connections
1572 attached to that server. So if two Gerrit connections in Zuul
1573 are connected to the same Gerrit server, and you invoke this
1574 method specifying the name of one of them, the event will be
1575 received by both.
1576
1577 .. note::
1578
1579 "self.fake_gerrit.addEvent" calls should be migrated to
1580 this method.
1581
1582 :arg str connection: The name of the connection corresponding
1583 to the gerrit server.
1584 :arg str event: The JSON-encoded event.
1585
1586 """
1587 specified_conn = self.connections.connections[connection]
1588 for conn in self.connections.connections.values():
1589 if (isinstance(conn, specified_conn.__class__) and
1590 specified_conn.server == conn.server):
1591 conn.addEvent(event)
1592
James E. Blair3f876d52016-07-22 13:07:14 -07001593
1594class AnsibleZuulTestCase(ZuulTestCase):
1595 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001596 run_ansible = True