blob: af239adf1e0a7be92262fc1c9ff4474e800e9a9c [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
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800480 def _getGitwebUrl(self, project, sha=None):
481 return self.getGitwebUrl(project, sha)
482
Clark Boylanb640e052014-04-03 16:41:46 -0700483
484class BuildHistory(object):
485 def __init__(self, **kw):
486 self.__dict__.update(kw)
487
488 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700489 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
490 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700491
492
493class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200494 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700495 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700496 self.url = url
497
498 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700499 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700500 path = res.path
501 project = '/'.join(path.split('/')[2:-2])
502 ret = '001e# service=git-upload-pack\n'
503 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
504 'multi_ack thin-pack side-band side-band-64k ofs-delta '
505 'shallow no-progress include-tag multi_ack_detailed no-done\n')
506 path = os.path.join(self.upstream_root, project)
507 repo = git.Repo(path)
508 for ref in repo.refs:
509 r = ref.object.hexsha + ' ' + ref.path + '\n'
510 ret += '%04x%s' % (len(r) + 4, r)
511 ret += '0000'
512 return ret
513
514
Clark Boylanb640e052014-04-03 16:41:46 -0700515class FakeStatsd(threading.Thread):
516 def __init__(self):
517 threading.Thread.__init__(self)
518 self.daemon = True
519 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
520 self.sock.bind(('', 0))
521 self.port = self.sock.getsockname()[1]
522 self.wake_read, self.wake_write = os.pipe()
523 self.stats = []
524
525 def run(self):
526 while True:
527 poll = select.poll()
528 poll.register(self.sock, select.POLLIN)
529 poll.register(self.wake_read, select.POLLIN)
530 ret = poll.poll()
531 for (fd, event) in ret:
532 if fd == self.sock.fileno():
533 data = self.sock.recvfrom(1024)
534 if not data:
535 return
536 self.stats.append(data[0])
537 if fd == self.wake_read:
538 return
539
540 def stop(self):
541 os.write(self.wake_write, '1\n')
542
543
James E. Blaire1767bc2016-08-02 10:00:27 -0700544class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 log = logging.getLogger("zuul.test")
546
James E. Blair34776ee2016-08-25 13:53:54 -0700547 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700548 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700549 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700551 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700552 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700553 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700554 # TODOv3(jeblair): self.node is really "the image of the node
555 # assigned". We should rename it (self.node_image?) if we
556 # keep using it like this, or we may end up exposing more of
557 # the complexity around multi-node jobs here
558 # (self.nodes[0].image?)
559 self.node = None
560 if len(self.parameters.get('nodes')) == 1:
561 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700562 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700563 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700564 self.wait_condition = threading.Condition()
565 self.waiting = False
566 self.aborted = False
567 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700568 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700569 self.changes = None
570 if 'ZUUL_CHANGE_IDS' in self.parameters:
571 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700572
James E. Blair3158e282016-08-19 09:34:11 -0700573 def __repr__(self):
574 waiting = ''
575 if self.waiting:
576 waiting = ' [waiting]'
577 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
578
Clark Boylanb640e052014-04-03 16:41:46 -0700579 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700580 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700581 self.wait_condition.acquire()
582 self.wait_condition.notify()
583 self.waiting = False
584 self.log.debug("Build %s released" % self.unique)
585 self.wait_condition.release()
586
587 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700588 """Return whether this build is being held.
589
590 :returns: Whether the build is being held.
591 :rtype: bool
592 """
593
Clark Boylanb640e052014-04-03 16:41:46 -0700594 self.wait_condition.acquire()
595 if self.waiting:
596 ret = True
597 else:
598 ret = False
599 self.wait_condition.release()
600 return ret
601
602 def _wait(self):
603 self.wait_condition.acquire()
604 self.waiting = True
605 self.log.debug("Build %s waiting" % self.unique)
606 self.wait_condition.wait()
607 self.wait_condition.release()
608
609 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700610 self.log.debug('Running build %s' % self.unique)
611
James E. Blaire1767bc2016-08-02 10:00:27 -0700612 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700613 self.log.debug('Holding build %s' % self.unique)
614 self._wait()
615 self.log.debug("Build %s continuing" % self.unique)
616
Clark Boylanb640e052014-04-03 16:41:46 -0700617 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700618 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700619 result = 'FAILURE'
620 if self.aborted:
621 result = 'ABORTED'
622
623 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700624 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700625
James E. Blaire1767bc2016-08-02 10:00:27 -0700626 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700627
James E. Blaira5dba232016-08-08 15:53:24 -0700628 def shouldFail(self):
629 changes = self.launch_server.fail_tests.get(self.name, [])
630 for change in changes:
631 if self.hasChanges(change):
632 return True
633 return False
634
James E. Blaire7b99a02016-08-05 14:27:34 -0700635 def hasChanges(self, *changes):
636 """Return whether this build has certain changes in its git repos.
637
638 :arg FakeChange changes: One or more changes (varargs) that
639 are expected to be present (in order) in the git repository of
640 the active project.
641
642 :returns: Whether the build has the indicated changes.
643 :rtype: bool
644
645 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800646 for change in changes:
647 path = os.path.join(self.jobdir.git_root, change.project)
648 try:
649 repo = git.Repo(path)
650 except NoSuchPathError as e:
651 self.log.debug('%s' % e)
652 return False
653 ref = self.parameters['ZUUL_REF']
654 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
655 commit_message = '%s-1' % change.subject
656 self.log.debug("Checking if build %s has changes; commit_message "
657 "%s; repo_messages %s" % (self, commit_message,
658 repo_messages))
659 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700660 self.log.debug(" messages do not match")
661 return False
662 self.log.debug(" OK")
663 return True
664
Clark Boylanb640e052014-04-03 16:41:46 -0700665
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000666class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700667 """An Ansible launcher to be used in tests.
668
669 :ivar bool hold_jobs_in_build: If true, when jobs are launched
670 they will report that they have started but then pause until
671 released before reporting completion. This attribute may be
672 changed at any time and will take effect for subsequently
673 launched builds, but previously held builds will still need to
674 be explicitly released.
675
676 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800677 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800679 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700680 self.hold_jobs_in_build = False
681 self.lock = threading.Lock()
682 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700683 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700685 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800686
James E. Blaira5dba232016-08-08 15:53:24 -0700687 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700688 """Instruct the launcher to report matching builds as failures.
689
690 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700691 :arg Change change: The :py:class:`~tests.base.FakeChange`
692 instance which should cause the job to fail. This job
693 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700694
695 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700696 l = self.fail_tests.get(name, [])
697 l.append(change)
698 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800699
James E. Blair962220f2016-08-03 11:22:38 -0700700 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700701 """Release a held build.
702
703 :arg str regex: A regular expression which, if supplied, will
704 cause only builds with matching names to be released. If
705 not supplied, all builds will be released.
706
707 """
James E. Blair962220f2016-08-03 11:22:38 -0700708 builds = self.running_builds[:]
709 self.log.debug("Releasing build %s (%s)" % (regex,
710 len(self.running_builds)))
711 for build in builds:
712 if not regex or re.match(regex, build.name):
713 self.log.debug("Releasing build %s" %
714 (build.parameters['ZUUL_UUID']))
715 build.release()
716 else:
717 self.log.debug("Not releasing build %s" %
718 (build.parameters['ZUUL_UUID']))
719 self.log.debug("Done releasing builds %s (%s)" %
720 (regex, len(self.running_builds)))
721
James E. Blair17302972016-08-10 16:11:42 -0700722 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700723 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700726 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700727 super(RecordingLaunchServer, self).launchJob(job)
728
729 def stopJob(self, job):
730 self.log.debug("handle stop")
731 parameters = json.loads(job.arguments)
732 uuid = parameters['uuid']
733 for build in self.running_builds:
734 if build.unique == uuid:
735 build.aborted = True
736 build.release()
737 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700738
739 def runAnsible(self, jobdir, job):
740 build = self.job_builds[job.unique]
741 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700742
743 if self._run_ansible:
744 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
745 else:
746 result = build.run()
747
748 self.lock.acquire()
749 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700750 BuildHistory(name=build.name, result=result, changes=build.changes,
751 node=build.node, uuid=build.unique,
752 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 pipeline=build.parameters['ZUUL_PIPELINE'])
754 )
James E. Blairab7132b2016-08-05 12:36:22 -0700755 self.running_builds.remove(build)
756 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700757 self.lock.release()
758 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800759
760
Clark Boylanb640e052014-04-03 16:41:46 -0700761class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700762 """A Gearman server for use in tests.
763
764 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
765 added to the queue but will not be distributed to workers
766 until released. This attribute may be changed at any time and
767 will take effect for subsequently enqueued jobs, but
768 previously held jobs will still need to be explicitly
769 released.
770
771 """
772
Clark Boylanb640e052014-04-03 16:41:46 -0700773 def __init__(self):
774 self.hold_jobs_in_queue = False
775 super(FakeGearmanServer, self).__init__(0)
776
777 def getJobForConnection(self, connection, peek=False):
778 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
779 for job in queue:
780 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500781 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700782 job.waiting = self.hold_jobs_in_queue
783 else:
784 job.waiting = False
785 if job.waiting:
786 continue
787 if job.name in connection.functions:
788 if not peek:
789 queue.remove(job)
790 connection.related_jobs[job.handle] = job
791 job.worker_connection = connection
792 job.running = True
793 return job
794 return None
795
796 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700797 """Release a held job.
798
799 :arg str regex: A regular expression which, if supplied, will
800 cause only jobs with matching names to be released. If
801 not supplied, all jobs will be released.
802 """
Clark Boylanb640e052014-04-03 16:41:46 -0700803 released = False
804 qlen = (len(self.high_queue) + len(self.normal_queue) +
805 len(self.low_queue))
806 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
807 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500808 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700809 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500810 parameters = json.loads(job.arguments)
811 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700812 self.log.debug("releasing queued job %s" %
813 job.unique)
814 job.waiting = False
815 released = True
816 else:
817 self.log.debug("not releasing queued job %s" %
818 job.unique)
819 if released:
820 self.wakeConnections()
821 qlen = (len(self.high_queue) + len(self.normal_queue) +
822 len(self.low_queue))
823 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
824
825
826class FakeSMTP(object):
827 log = logging.getLogger('zuul.FakeSMTP')
828
829 def __init__(self, messages, server, port):
830 self.server = server
831 self.port = port
832 self.messages = messages
833
834 def sendmail(self, from_email, to_email, msg):
835 self.log.info("Sending email from %s, to %s, with msg %s" % (
836 from_email, to_email, msg))
837
838 headers = msg.split('\n\n', 1)[0]
839 body = msg.split('\n\n', 1)[1]
840
841 self.messages.append(dict(
842 from_email=from_email,
843 to_email=to_email,
844 msg=msg,
845 headers=headers,
846 body=body,
847 ))
848
849 return True
850
851 def quit(self):
852 return True
853
854
855class FakeSwiftClientConnection(swiftclient.client.Connection):
856 def post_account(self, headers):
857 # Do nothing
858 pass
859
860 def get_auth(self):
861 # Returns endpoint and (unused) auth token
862 endpoint = os.path.join('https://storage.example.org', 'V1',
863 'AUTH_account')
864 return endpoint, ''
865
866
Maru Newby3fe5f852015-01-13 04:22:14 +0000867class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700868 log = logging.getLogger("zuul.test")
869
870 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000871 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700872 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
873 try:
874 test_timeout = int(test_timeout)
875 except ValueError:
876 # If timeout value is invalid do not set a timeout.
877 test_timeout = 0
878 if test_timeout > 0:
879 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
880
881 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
882 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
883 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
884 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
885 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
886 os.environ.get('OS_STDERR_CAPTURE') == '1'):
887 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
888 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
889 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
890 os.environ.get('OS_LOG_CAPTURE') == '1'):
891 self.useFixture(fixtures.FakeLogger(
892 level=logging.DEBUG,
893 format='%(asctime)s %(name)-32s '
894 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000895
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700896 # NOTE(notmorgan): Extract logging overrides for specific libraries
897 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
898 # each. This is used to limit the output during test runs from
899 # libraries that zuul depends on such as gear.
900 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
901
902 if log_defaults_from_env:
903 for default in log_defaults_from_env.split(','):
904 try:
905 name, level_str = default.split('=', 1)
906 level = getattr(logging, level_str, logging.DEBUG)
907 self.useFixture(fixtures.FakeLogger(
908 name=name,
909 level=level,
910 format='%(asctime)s %(name)-32s '
911 '%(levelname)-8s %(message)s'))
912 except ValueError:
913 # NOTE(notmorgan): Invalid format of the log default,
914 # skip and don't try and apply a logger for the
915 # specified module
916 pass
917
Maru Newby3fe5f852015-01-13 04:22:14 +0000918
919class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700920 """A test case with a functioning Zuul.
921
922 The following class variables are used during test setup and can
923 be overidden by subclasses but are effectively read-only once a
924 test method starts running:
925
926 :cvar str config_file: This points to the main zuul config file
927 within the fixtures directory. Subclasses may override this
928 to obtain a different behavior.
929
930 :cvar str tenant_config_file: This is the tenant config file
931 (which specifies from what git repos the configuration should
932 be loaded). It defaults to the value specified in
933 `config_file` but can be overidden by subclasses to obtain a
934 different tenant/project layout while using the standard main
935 configuration.
936
937 The following are instance variables that are useful within test
938 methods:
939
940 :ivar FakeGerritConnection fake_<connection>:
941 A :py:class:`~tests.base.FakeGerritConnection` will be
942 instantiated for each connection present in the config file
943 and stored here. For instance, `fake_gerrit` will hold the
944 FakeGerritConnection object for a connection named `gerrit`.
945
946 :ivar FakeGearmanServer gearman_server: An instance of
947 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
948 server that all of the Zuul components in this test use to
949 communicate with each other.
950
951 :ivar RecordingLaunchServer launch_server: An instance of
952 :py:class:`~tests.base.RecordingLaunchServer` which is the
953 Ansible launch server used to run jobs for this test.
954
955 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
956 representing currently running builds. They are appended to
957 the list in the order they are launched, and removed from this
958 list upon completion.
959
960 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
961 objects representing completed builds. They are appended to
962 the list in the order they complete.
963
964 """
965
James E. Blair83005782015-12-11 14:46:03 -0800966 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700967 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700968
969 def _startMerger(self):
970 self.merge_server = zuul.merger.server.MergeServer(self.config,
971 self.connections)
972 self.merge_server.start()
973
Maru Newby3fe5f852015-01-13 04:22:14 +0000974 def setUp(self):
975 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700976 if USE_TEMPDIR:
977 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000978 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
979 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700980 else:
981 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700982 self.test_root = os.path.join(tmp_root, "zuul-test")
983 self.upstream_root = os.path.join(self.test_root, "upstream")
984 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700985 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700986
987 if os.path.exists(self.test_root):
988 shutil.rmtree(self.test_root)
989 os.makedirs(self.test_root)
990 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700991 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700992
993 # Make per test copy of Configuration.
994 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800995 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100996 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800997 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700998 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700999 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001000
1001 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001002 # TODOv3(jeblair): remove these and replace with new git
1003 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001004 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001005 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001006 self.init_repo("org/project5")
1007 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001008 self.init_repo("org/one-job-project")
1009 self.init_repo("org/nonvoting-project")
1010 self.init_repo("org/templated-project")
1011 self.init_repo("org/layered-project")
1012 self.init_repo("org/node-project")
1013 self.init_repo("org/conflict-project")
1014 self.init_repo("org/noop-project")
1015 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001016 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001017
1018 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001019 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1020 # see: https://github.com/jsocol/pystatsd/issues/61
1021 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001022 os.environ['STATSD_PORT'] = str(self.statsd.port)
1023 self.statsd.start()
1024 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001025 reload_module(statsd)
1026 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001027
1028 self.gearman_server = FakeGearmanServer()
1029
1030 self.config.set('gearman', 'port', str(self.gearman_server.port))
1031
Joshua Hesketh352264b2015-08-11 23:42:08 +10001032 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1033 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1034 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001035
Joshua Hesketh352264b2015-08-11 23:42:08 +10001036 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001037
1038 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1039 FakeSwiftClientConnection))
1040 self.swift = zuul.lib.swift.Swift(self.config)
1041
Jan Hruban6b71aff2015-10-22 16:58:08 +02001042 self.event_queues = [
1043 self.sched.result_event_queue,
1044 self.sched.trigger_event_queue
1045 ]
1046
James E. Blairfef78942016-03-11 16:28:56 -08001047 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001048 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001049
Clark Boylanb640e052014-04-03 16:41:46 -07001050 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001051 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001052 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001053 return FakeURLOpener(self.upstream_root, *args, **kw)
1054
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001055 old_urlopen = urllib.request.urlopen
1056 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001057
James E. Blair3f876d52016-07-22 13:07:14 -07001058 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001059
James E. Blaire1767bc2016-08-02 10:00:27 -07001060 self.launch_server = RecordingLaunchServer(
1061 self.config, self.connections, _run_ansible=self.run_ansible)
1062 self.launch_server.start()
1063 self.history = self.launch_server.build_history
1064 self.builds = self.launch_server.running_builds
1065
1066 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001067 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001068 self.merge_client = zuul.merger.client.MergeClient(
1069 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001070 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001071
James E. Blaire1767bc2016-08-02 10:00:27 -07001072 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001073 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001074 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001075
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001076 self.webapp = zuul.webapp.WebApp(
1077 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001078 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001079
1080 self.sched.start()
1081 self.sched.reconfigure(self.config)
1082 self.sched.resume()
1083 self.webapp.start()
1084 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001085 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001086
1087 self.addCleanup(self.assertFinalState)
1088 self.addCleanup(self.shutdown)
1089
James E. Blairfef78942016-03-11 16:28:56 -08001090 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001091 # Register connections from the config
1092 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001093
Joshua Hesketh352264b2015-08-11 23:42:08 +10001094 def FakeSMTPFactory(*args, **kw):
1095 args = [self.smtp_messages] + list(args)
1096 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001097
Joshua Hesketh352264b2015-08-11 23:42:08 +10001098 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001099
Joshua Hesketh352264b2015-08-11 23:42:08 +10001100 # Set a changes database so multiple FakeGerrit's can report back to
1101 # a virtual canonical database given by the configured hostname
1102 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001103 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001104
Joshua Hesketh352264b2015-08-11 23:42:08 +10001105 for section_name in self.config.sections():
1106 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1107 section_name, re.I)
1108 if not con_match:
1109 continue
1110 con_name = con_match.group(2)
1111 con_config = dict(self.config.items(section_name))
1112
1113 if 'driver' not in con_config:
1114 raise Exception("No driver specified for connection %s."
1115 % con_name)
1116
1117 con_driver = con_config['driver']
1118
1119 # TODO(jhesketh): load the required class automatically
1120 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001121 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1122 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001123 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001124 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001125 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001126 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001127 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001128 self.event_queues.append(
1129 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001130 setattr(self, 'fake_' + con_name,
1131 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001132 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001133 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001134 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1135 else:
1136 raise Exception("Unknown driver, %s, for connection %s"
1137 % (con_config['driver'], con_name))
1138
1139 # If the [gerrit] or [smtp] sections still exist, load them in as a
1140 # connection named 'gerrit' or 'smtp' respectfully
1141
1142 if 'gerrit' in self.config.sections():
1143 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001144 self.event_queues.append(
1145 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001146 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001147 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001148 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001149
1150 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001151 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001152 zuul.connection.smtp.SMTPConnection(
1153 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001154
James E. Blair83005782015-12-11 14:46:03 -08001155 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001156 # This creates the per-test configuration object. It can be
1157 # overriden by subclasses, but should not need to be since it
1158 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001159 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001160 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001161 if hasattr(self, 'tenant_config_file'):
1162 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001163 git_path = os.path.join(
1164 os.path.dirname(
1165 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1166 'git')
1167 if os.path.exists(git_path):
1168 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001169 project = reponame.replace('_', '/')
1170 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001171 os.path.join(git_path, reponame))
1172
1173 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001174 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001175
1176 files = {}
1177 for (dirpath, dirnames, filenames) in os.walk(source_path):
1178 for filename in filenames:
1179 test_tree_filepath = os.path.join(dirpath, filename)
1180 common_path = os.path.commonprefix([test_tree_filepath,
1181 source_path])
1182 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1183 with open(test_tree_filepath, 'r') as f:
1184 content = f.read()
1185 files[relative_filepath] = content
1186 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001187 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001188
Clark Boylanb640e052014-04-03 16:41:46 -07001189 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001190 # Make sure that git.Repo objects have been garbage collected.
1191 repos = []
1192 gc.collect()
1193 for obj in gc.get_objects():
1194 if isinstance(obj, git.Repo):
1195 repos.append(obj)
1196 self.assertEqual(len(repos), 0)
1197 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001198 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001199 for tenant in self.sched.abide.tenants.values():
1200 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001201 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001202 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001203
1204 def shutdown(self):
1205 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001206 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001207 self.merge_server.stop()
1208 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001209 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001210 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001211 self.sched.stop()
1212 self.sched.join()
1213 self.statsd.stop()
1214 self.statsd.join()
1215 self.webapp.stop()
1216 self.webapp.join()
1217 self.rpc.stop()
1218 self.rpc.join()
1219 self.gearman_server.shutdown()
1220 threads = threading.enumerate()
1221 if len(threads) > 1:
1222 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001223
1224 def init_repo(self, project):
1225 parts = project.split('/')
1226 path = os.path.join(self.upstream_root, *parts[:-1])
1227 if not os.path.exists(path):
1228 os.makedirs(path)
1229 path = os.path.join(self.upstream_root, project)
1230 repo = git.Repo.init(path)
1231
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001232 with repo.config_writer() as config_writer:
1233 config_writer.set_value('user', 'email', 'user@example.com')
1234 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001235
Clark Boylanb640e052014-04-03 16:41:46 -07001236 repo.index.commit('initial commit')
1237 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001238
James E. Blair97d902e2014-08-21 13:25:56 -07001239 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001240 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001241 repo.git.clean('-x', '-f', '-d')
1242
James E. Blair97d902e2014-08-21 13:25:56 -07001243 def create_branch(self, project, branch):
1244 path = os.path.join(self.upstream_root, project)
1245 repo = git.Repo.init(path)
1246 fn = os.path.join(path, 'README')
1247
1248 branch_head = repo.create_head(branch)
1249 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001250 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001251 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001252 f.close()
1253 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001254 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001255
James E. Blair97d902e2014-08-21 13:25:56 -07001256 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001257 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001258 repo.git.clean('-x', '-f', '-d')
1259
Sachi King9f16d522016-03-16 12:20:45 +11001260 def create_commit(self, project):
1261 path = os.path.join(self.upstream_root, project)
1262 repo = git.Repo(path)
1263 repo.head.reference = repo.heads['master']
1264 file_name = os.path.join(path, 'README')
1265 with open(file_name, 'a') as f:
1266 f.write('creating fake commit\n')
1267 repo.index.add([file_name])
1268 commit = repo.index.commit('Creating a fake commit')
1269 return commit.hexsha
1270
James E. Blairb8c16472015-05-05 14:55:26 -07001271 def orderedRelease(self):
1272 # Run one build at a time to ensure non-race order:
1273 while len(self.builds):
1274 self.release(self.builds[0])
1275 self.waitUntilSettled()
1276
Clark Boylanb640e052014-04-03 16:41:46 -07001277 def release(self, job):
1278 if isinstance(job, FakeBuild):
1279 job.release()
1280 else:
1281 job.waiting = False
1282 self.log.debug("Queued job %s released" % job.unique)
1283 self.gearman_server.wakeConnections()
1284
1285 def getParameter(self, job, name):
1286 if isinstance(job, FakeBuild):
1287 return job.parameters[name]
1288 else:
1289 parameters = json.loads(job.arguments)
1290 return parameters[name]
1291
1292 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001293 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001294 while True:
1295 done = True
1296 for connection in self.gearman_server.active_connections:
1297 if (connection.functions and
1298 connection.client_id not in ['Zuul RPC Listener',
1299 'Zuul Merger']):
1300 done = False
1301 if done:
1302 break
1303 time.sleep(0)
1304 self.gearman_server.functions = set()
1305 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001306
1307 def haveAllBuildsReported(self):
1308 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001309 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001310 return False
1311 # Find out if every build that the worker has completed has been
1312 # reported back to Zuul. If it hasn't then that means a Gearman
1313 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001314 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001315 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001316 if not zbuild:
1317 # It has already been reported
1318 continue
1319 # It hasn't been reported yet.
1320 return False
1321 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001323 if connection.state == 'GRAB_WAIT':
1324 return False
1325 return True
1326
1327 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001328 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001329 for build in builds:
1330 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001331 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001332 for j in conn.related_jobs.values():
1333 if j.unique == build.uuid:
1334 client_job = j
1335 break
1336 if not client_job:
1337 self.log.debug("%s is not known to the gearman client" %
1338 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001339 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001340 if not client_job.handle:
1341 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001342 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001343 server_job = self.gearman_server.jobs.get(client_job.handle)
1344 if not server_job:
1345 self.log.debug("%s is not known to the gearman server" %
1346 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001348 if not hasattr(server_job, 'waiting'):
1349 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001351 if server_job.waiting:
1352 continue
James E. Blair17302972016-08-10 16:11:42 -07001353 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001354 self.log.debug("%s has not reported start" % build)
1355 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001356 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001357 if worker_build:
1358 if worker_build.isWaiting():
1359 continue
1360 else:
1361 self.log.debug("%s is running" % worker_build)
1362 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001363 else:
James E. Blair962220f2016-08-03 11:22:38 -07001364 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001365 return False
1366 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001367
Jan Hruban6b71aff2015-10-22 16:58:08 +02001368 def eventQueuesEmpty(self):
1369 for queue in self.event_queues:
1370 yield queue.empty()
1371
1372 def eventQueuesJoin(self):
1373 for queue in self.event_queues:
1374 queue.join()
1375
Clark Boylanb640e052014-04-03 16:41:46 -07001376 def waitUntilSettled(self):
1377 self.log.debug("Waiting until settled...")
1378 start = time.time()
1379 while True:
1380 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001381 self.log.debug("Queue status:")
1382 for queue in self.event_queues:
1383 self.log.debug(" %s: %s" % (queue, queue.empty()))
1384 self.log.debug("All builds waiting: %s" %
1385 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001386 self.log.debug("All builds reported: %s" %
1387 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001388 raise Exception("Timeout waiting for Zuul to settle")
1389 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001390
James E. Blaire1767bc2016-08-02 10:00:27 -07001391 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001392 # have all build states propogated to zuul?
1393 if self.haveAllBuildsReported():
1394 # Join ensures that the queue is empty _and_ events have been
1395 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001396 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001397 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001398 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001399 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001400 self.haveAllBuildsReported() and
1401 self.areAllBuildsWaiting()):
1402 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001403 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001404 self.log.debug("...settled.")
1405 return
1406 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001407 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.sched.wake_event.wait(0.1)
1409
1410 def countJobResults(self, jobs, result):
1411 jobs = filter(lambda x: x.result == result, jobs)
1412 return len(jobs)
1413
James E. Blair96c6bf82016-01-15 16:20:40 -08001414 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001415 for job in self.history:
1416 if (job.name == name and
1417 (project is None or
1418 job.parameters['ZUUL_PROJECT'] == project)):
1419 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001420 raise Exception("Unable to find job %s in history" % name)
1421
1422 def assertEmptyQueues(self):
1423 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001424 for tenant in self.sched.abide.tenants.values():
1425 for pipeline in tenant.layout.pipelines.values():
1426 for queue in pipeline.queues:
1427 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001428 print('pipeline %s queue %s contents %s' % (
1429 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001430 self.assertEqual(len(queue.queue), 0,
1431 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001432
1433 def assertReportedStat(self, key, value=None, kind=None):
1434 start = time.time()
1435 while time.time() < (start + 5):
1436 for stat in self.statsd.stats:
1437 pprint.pprint(self.statsd.stats)
1438 k, v = stat.split(':')
1439 if key == k:
1440 if value is None and kind is None:
1441 return
1442 elif value:
1443 if value == v:
1444 return
1445 elif kind:
1446 if v.endswith('|' + kind):
1447 return
1448 time.sleep(0.1)
1449
1450 pprint.pprint(self.statsd.stats)
1451 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001452
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001453 def assertBuilds(self, builds):
1454 """Assert that the running builds are as described.
1455
1456 The list of running builds is examined and must match exactly
1457 the list of builds described by the input.
1458
1459 :arg list builds: A list of dictionaries. Each item in the
1460 list must match the corresponding build in the build
1461 history, and each element of the dictionary must match the
1462 corresponding attribute of the build.
1463
1464 """
James E. Blair3158e282016-08-19 09:34:11 -07001465 try:
1466 self.assertEqual(len(self.builds), len(builds))
1467 for i, d in enumerate(builds):
1468 for k, v in d.items():
1469 self.assertEqual(
1470 getattr(self.builds[i], k), v,
1471 "Element %i in builds does not match" % (i,))
1472 except Exception:
1473 for build in self.builds:
1474 self.log.error("Running build: %s" % build)
1475 else:
1476 self.log.error("No running builds")
1477 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001478
James E. Blairb536ecc2016-08-31 10:11:42 -07001479 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001480 """Assert that the completed builds are as described.
1481
1482 The list of completed builds is examined and must match
1483 exactly the list of builds described by the input.
1484
1485 :arg list history: A list of dictionaries. Each item in the
1486 list must match the corresponding build in the build
1487 history, and each element of the dictionary must match the
1488 corresponding attribute of the build.
1489
James E. Blairb536ecc2016-08-31 10:11:42 -07001490 :arg bool ordered: If true, the history must match the order
1491 supplied, if false, the builds are permitted to have
1492 arrived in any order.
1493
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001494 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001495 def matches(history_item, item):
1496 for k, v in item.items():
1497 if getattr(history_item, k) != v:
1498 return False
1499 return True
James E. Blair3158e282016-08-19 09:34:11 -07001500 try:
1501 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001502 if ordered:
1503 for i, d in enumerate(history):
1504 if not matches(self.history[i], d):
1505 raise Exception(
1506 "Element %i in history does not match" % (i,))
1507 else:
1508 unseen = self.history[:]
1509 for i, d in enumerate(history):
1510 found = False
1511 for unseen_item in unseen:
1512 if matches(unseen_item, d):
1513 found = True
1514 unseen.remove(unseen_item)
1515 break
1516 if not found:
1517 raise Exception("No match found for element %i "
1518 "in history" % (i,))
1519 if unseen:
1520 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001521 except Exception:
1522 for build in self.history:
1523 self.log.error("Completed build: %s" % build)
1524 else:
1525 self.log.error("No completed builds")
1526 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001527
James E. Blair59fdbac2015-12-07 17:08:06 -08001528 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001529 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1530
1531 def updateConfigLayout(self, path):
1532 root = os.path.join(self.test_root, "config")
1533 os.makedirs(root)
1534 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1535 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001536- tenant:
1537 name: openstack
1538 source:
1539 gerrit:
1540 config-repos:
1541 - %s
1542 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001543 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001544 self.config.set('zuul', 'tenant_config',
1545 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001546
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001547 def addCommitToRepo(self, project, message, files,
1548 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001549 path = os.path.join(self.upstream_root, project)
1550 repo = git.Repo(path)
1551 repo.head.reference = branch
1552 zuul.merger.merger.reset_repo_to_head(repo)
1553 for fn, content in files.items():
1554 fn = os.path.join(path, fn)
1555 with open(fn, 'w') as f:
1556 f.write(content)
1557 repo.index.add([fn])
1558 commit = repo.index.commit(message)
1559 repo.heads[branch].commit = commit
1560 repo.head.reference = branch
1561 repo.git.clean('-x', '-f', '-d')
1562 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001563 if tag:
1564 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001565
James E. Blair7fc8daa2016-08-08 15:37:15 -07001566 def addEvent(self, connection, event):
1567 """Inject a Fake (Gerrit) event.
1568
1569 This method accepts a JSON-encoded event and simulates Zuul
1570 having received it from Gerrit. It could (and should)
1571 eventually apply to any connection type, but is currently only
1572 used with Gerrit connections. The name of the connection is
1573 used to look up the corresponding server, and the event is
1574 simulated as having been received by all Zuul connections
1575 attached to that server. So if two Gerrit connections in Zuul
1576 are connected to the same Gerrit server, and you invoke this
1577 method specifying the name of one of them, the event will be
1578 received by both.
1579
1580 .. note::
1581
1582 "self.fake_gerrit.addEvent" calls should be migrated to
1583 this method.
1584
1585 :arg str connection: The name of the connection corresponding
1586 to the gerrit server.
1587 :arg str event: The JSON-encoded event.
1588
1589 """
1590 specified_conn = self.connections.connections[connection]
1591 for conn in self.connections.connections.values():
1592 if (isinstance(conn, specified_conn.__class__) and
1593 specified_conn.server == conn.server):
1594 conn.addEvent(event)
1595
James E. Blair3f876d52016-07-22 13:07:14 -07001596
1597class AnsibleZuulTestCase(ZuulTestCase):
1598 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001599 run_ansible = True