blob: 073b8ad993dea8b10a02154ccd385de5a7b48540 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Mike Heald8225f522014-11-21 09:52:33 +000044from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700389 """A Fake Gerrit connection for use in tests.
390
391 This subclasses
392 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
393 ability for tests to add changes to the fake Gerrit it represents.
394 """
395
Joshua Hesketh352264b2015-08-11 23:42:08 +1000396 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700399 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 super(FakeGerritConnection, self).__init__(connection_name,
401 connection_config)
402
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700404 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
405 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000406 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700407 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200408 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700409
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700410 def addFakeChange(self, project, branch, subject, status='NEW',
411 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700412 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700413 self.change_number += 1
414 c = FakeChange(self, self.change_number, project, branch, subject,
415 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700416 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.changes[self.change_number] = c
418 return c
419
Clark Boylanb640e052014-04-03 16:41:46 -0700420 def review(self, project, changeid, message, action):
421 number, ps = changeid.split(',')
422 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000423
424 # Add the approval back onto the change (ie simulate what gerrit would
425 # do).
426 # Usually when zuul leaves a review it'll create a feedback loop where
427 # zuul's review enters another gerrit event (which is then picked up by
428 # zuul). However, we can't mimic this behaviour (by adding this
429 # approval event into the queue) as it stops jobs from checking what
430 # happens before this event is triggered. If a job needs to see what
431 # happens they can add their own verified event into the queue.
432 # Nevertheless, we can update change with the new review in gerrit.
433
James E. Blair8b5408c2016-08-08 15:37:46 -0700434 for cat in action.keys():
435 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439 if 'label' in action:
440 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000441 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442
Clark Boylanb640e052014-04-03 16:41:46 -0700443 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 if 'submit' in action:
446 change.setMerged()
447 if message:
448 change.setReported()
449
450 def query(self, number):
451 change = self.changes.get(int(number))
452 if change:
453 return change.query()
454 return {}
455
James E. Blairc494d542014-08-06 09:23:52 -0700456 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700457 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700458 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800459 if query.startswith('change:'):
460 # Query a specific changeid
461 changeid = query[len('change:'):]
462 l = [change.query() for change in self.changes.values()
463 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700464 elif query.startswith('message:'):
465 # Query the content of a commit message
466 msg = query[len('message:'):].strip()
467 l = [change.query() for change in self.changes.values()
468 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800469 else:
470 # Query all open changes
471 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700472 return l
James E. Blairc494d542014-08-06 09:23:52 -0700473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700475 pass
476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def getGitUrl(self, project):
478 return os.path.join(self.upstream_root, project.name)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700486 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
487 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. 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 """
James E. Blair962220f2016-08-03 11:22:38 -0700643 project = self.parameters['ZUUL_PROJECT']
644 path = os.path.join(self.jobdir.git_root, project)
645 repo = git.Repo(path)
646 ref = self.parameters['ZUUL_REF']
647 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700648 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700649 self.log.debug("Checking if build %s has changes; commit_messages %s;"
650 " repo_messages %s" % (self, commit_messages,
651 repo_messages))
652 for msg in commit_messages:
653 if msg not in repo_messages:
654 self.log.debug(" messages do not match")
655 return False
656 self.log.debug(" OK")
657 return True
658
Clark Boylanb640e052014-04-03 16:41:46 -0700659
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000660class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700661 """An Ansible launcher to be used in tests.
662
663 :ivar bool hold_jobs_in_build: If true, when jobs are launched
664 they will report that they have started but then pause until
665 released before reporting completion. This attribute may be
666 changed at any time and will take effect for subsequently
667 launched builds, but previously held builds will still need to
668 be explicitly released.
669
670 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800671 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700672 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800673 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700674 self.hold_jobs_in_build = False
675 self.lock = threading.Lock()
676 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700677 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700679 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800680
James E. Blaira5dba232016-08-08 15:53:24 -0700681 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700682 """Instruct the launcher to report matching builds as failures.
683
684 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700685 :arg Change change: The :py:class:`~tests.base.FakeChange`
686 instance which should cause the job to fail. This job
687 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700688
689 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700690 l = self.fail_tests.get(name, [])
691 l.append(change)
692 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800693
James E. Blair962220f2016-08-03 11:22:38 -0700694 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700695 """Release a held build.
696
697 :arg str regex: A regular expression which, if supplied, will
698 cause only builds with matching names to be released. If
699 not supplied, all builds will be released.
700
701 """
James E. Blair962220f2016-08-03 11:22:38 -0700702 builds = self.running_builds[:]
703 self.log.debug("Releasing build %s (%s)" % (regex,
704 len(self.running_builds)))
705 for build in builds:
706 if not regex or re.match(regex, build.name):
707 self.log.debug("Releasing build %s" %
708 (build.parameters['ZUUL_UUID']))
709 build.release()
710 else:
711 self.log.debug("Not releasing build %s" %
712 (build.parameters['ZUUL_UUID']))
713 self.log.debug("Done releasing builds %s (%s)" %
714 (regex, len(self.running_builds)))
715
James E. Blair17302972016-08-10 16:11:42 -0700716 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700717 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700718 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700719 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700720 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700721 super(RecordingLaunchServer, self).launchJob(job)
722
723 def stopJob(self, job):
724 self.log.debug("handle stop")
725 parameters = json.loads(job.arguments)
726 uuid = parameters['uuid']
727 for build in self.running_builds:
728 if build.unique == uuid:
729 build.aborted = True
730 build.release()
731 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700732
733 def runAnsible(self, jobdir, job):
734 build = self.job_builds[job.unique]
735 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700736
737 if self._run_ansible:
738 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
739 else:
740 result = build.run()
741
742 self.lock.acquire()
743 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700744 BuildHistory(name=build.name, result=result, changes=build.changes,
745 node=build.node, uuid=build.unique,
746 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700747 pipeline=build.parameters['ZUUL_PIPELINE'])
748 )
James E. Blairab7132b2016-08-05 12:36:22 -0700749 self.running_builds.remove(build)
750 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700751 self.lock.release()
752 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800753
754
Clark Boylanb640e052014-04-03 16:41:46 -0700755class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700756 """A Gearman server for use in tests.
757
758 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
759 added to the queue but will not be distributed to workers
760 until released. This attribute may be changed at any time and
761 will take effect for subsequently enqueued jobs, but
762 previously held jobs will still need to be explicitly
763 released.
764
765 """
766
Clark Boylanb640e052014-04-03 16:41:46 -0700767 def __init__(self):
768 self.hold_jobs_in_queue = False
769 super(FakeGearmanServer, self).__init__(0)
770
771 def getJobForConnection(self, connection, peek=False):
772 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
773 for job in queue:
774 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500775 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700776 job.waiting = self.hold_jobs_in_queue
777 else:
778 job.waiting = False
779 if job.waiting:
780 continue
781 if job.name in connection.functions:
782 if not peek:
783 queue.remove(job)
784 connection.related_jobs[job.handle] = job
785 job.worker_connection = connection
786 job.running = True
787 return job
788 return None
789
790 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700791 """Release a held job.
792
793 :arg str regex: A regular expression which, if supplied, will
794 cause only jobs with matching names to be released. If
795 not supplied, all jobs will be released.
796 """
Clark Boylanb640e052014-04-03 16:41:46 -0700797 released = False
798 qlen = (len(self.high_queue) + len(self.normal_queue) +
799 len(self.low_queue))
800 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
801 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500802 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700803 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500804 parameters = json.loads(job.arguments)
805 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700806 self.log.debug("releasing queued job %s" %
807 job.unique)
808 job.waiting = False
809 released = True
810 else:
811 self.log.debug("not releasing queued job %s" %
812 job.unique)
813 if released:
814 self.wakeConnections()
815 qlen = (len(self.high_queue) + len(self.normal_queue) +
816 len(self.low_queue))
817 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
818
819
820class FakeSMTP(object):
821 log = logging.getLogger('zuul.FakeSMTP')
822
823 def __init__(self, messages, server, port):
824 self.server = server
825 self.port = port
826 self.messages = messages
827
828 def sendmail(self, from_email, to_email, msg):
829 self.log.info("Sending email from %s, to %s, with msg %s" % (
830 from_email, to_email, msg))
831
832 headers = msg.split('\n\n', 1)[0]
833 body = msg.split('\n\n', 1)[1]
834
835 self.messages.append(dict(
836 from_email=from_email,
837 to_email=to_email,
838 msg=msg,
839 headers=headers,
840 body=body,
841 ))
842
843 return True
844
845 def quit(self):
846 return True
847
848
849class FakeSwiftClientConnection(swiftclient.client.Connection):
850 def post_account(self, headers):
851 # Do nothing
852 pass
853
854 def get_auth(self):
855 # Returns endpoint and (unused) auth token
856 endpoint = os.path.join('https://storage.example.org', 'V1',
857 'AUTH_account')
858 return endpoint, ''
859
860
Maru Newby3fe5f852015-01-13 04:22:14 +0000861class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700862 log = logging.getLogger("zuul.test")
863
864 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000865 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700866 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
867 try:
868 test_timeout = int(test_timeout)
869 except ValueError:
870 # If timeout value is invalid do not set a timeout.
871 test_timeout = 0
872 if test_timeout > 0:
873 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
874
875 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
876 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
877 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
878 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
879 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
880 os.environ.get('OS_STDERR_CAPTURE') == '1'):
881 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
882 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
883 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
884 os.environ.get('OS_LOG_CAPTURE') == '1'):
885 self.useFixture(fixtures.FakeLogger(
886 level=logging.DEBUG,
887 format='%(asctime)s %(name)-32s '
888 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000889
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700890 # NOTE(notmorgan): Extract logging overrides for specific libraries
891 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
892 # each. This is used to limit the output during test runs from
893 # libraries that zuul depends on such as gear.
894 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
895
896 if log_defaults_from_env:
897 for default in log_defaults_from_env.split(','):
898 try:
899 name, level_str = default.split('=', 1)
900 level = getattr(logging, level_str, logging.DEBUG)
901 self.useFixture(fixtures.FakeLogger(
902 name=name,
903 level=level,
904 format='%(asctime)s %(name)-32s '
905 '%(levelname)-8s %(message)s'))
906 except ValueError:
907 # NOTE(notmorgan): Invalid format of the log default,
908 # skip and don't try and apply a logger for the
909 # specified module
910 pass
911
Maru Newby3fe5f852015-01-13 04:22:14 +0000912
913class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700914 """A test case with a functioning Zuul.
915
916 The following class variables are used during test setup and can
917 be overidden by subclasses but are effectively read-only once a
918 test method starts running:
919
920 :cvar str config_file: This points to the main zuul config file
921 within the fixtures directory. Subclasses may override this
922 to obtain a different behavior.
923
924 :cvar str tenant_config_file: This is the tenant config file
925 (which specifies from what git repos the configuration should
926 be loaded). It defaults to the value specified in
927 `config_file` but can be overidden by subclasses to obtain a
928 different tenant/project layout while using the standard main
929 configuration.
930
931 The following are instance variables that are useful within test
932 methods:
933
934 :ivar FakeGerritConnection fake_<connection>:
935 A :py:class:`~tests.base.FakeGerritConnection` will be
936 instantiated for each connection present in the config file
937 and stored here. For instance, `fake_gerrit` will hold the
938 FakeGerritConnection object for a connection named `gerrit`.
939
940 :ivar FakeGearmanServer gearman_server: An instance of
941 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
942 server that all of the Zuul components in this test use to
943 communicate with each other.
944
945 :ivar RecordingLaunchServer launch_server: An instance of
946 :py:class:`~tests.base.RecordingLaunchServer` which is the
947 Ansible launch server used to run jobs for this test.
948
949 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
950 representing currently running builds. They are appended to
951 the list in the order they are launched, and removed from this
952 list upon completion.
953
954 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
955 objects representing completed builds. They are appended to
956 the list in the order they complete.
957
958 """
959
James E. Blair83005782015-12-11 14:46:03 -0800960 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700961 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700962
963 def _startMerger(self):
964 self.merge_server = zuul.merger.server.MergeServer(self.config,
965 self.connections)
966 self.merge_server.start()
967
Maru Newby3fe5f852015-01-13 04:22:14 +0000968 def setUp(self):
969 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700970 if USE_TEMPDIR:
971 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000972 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
973 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700974 else:
975 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700976 self.test_root = os.path.join(tmp_root, "zuul-test")
977 self.upstream_root = os.path.join(self.test_root, "upstream")
978 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700979 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700980
981 if os.path.exists(self.test_root):
982 shutil.rmtree(self.test_root)
983 os.makedirs(self.test_root)
984 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700985 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700986
987 # Make per test copy of Configuration.
988 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800989 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100990 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800991 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700992 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700993 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700994
995 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700996 # TODOv3(jeblair): remove these and replace with new git
997 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700998 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700999 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001000 self.init_repo("org/project5")
1001 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001002 self.init_repo("org/one-job-project")
1003 self.init_repo("org/nonvoting-project")
1004 self.init_repo("org/templated-project")
1005 self.init_repo("org/layered-project")
1006 self.init_repo("org/node-project")
1007 self.init_repo("org/conflict-project")
1008 self.init_repo("org/noop-project")
1009 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001010 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001011
1012 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001013 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1014 # see: https://github.com/jsocol/pystatsd/issues/61
1015 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001016 os.environ['STATSD_PORT'] = str(self.statsd.port)
1017 self.statsd.start()
1018 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001019 reload_module(statsd)
1020 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001021
1022 self.gearman_server = FakeGearmanServer()
1023
1024 self.config.set('gearman', 'port', str(self.gearman_server.port))
1025
Joshua Hesketh352264b2015-08-11 23:42:08 +10001026 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1027 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1028 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001029
Joshua Hesketh352264b2015-08-11 23:42:08 +10001030 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001031
1032 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1033 FakeSwiftClientConnection))
1034 self.swift = zuul.lib.swift.Swift(self.config)
1035
Jan Hruban6b71aff2015-10-22 16:58:08 +02001036 self.event_queues = [
1037 self.sched.result_event_queue,
1038 self.sched.trigger_event_queue
1039 ]
1040
James E. Blairfef78942016-03-11 16:28:56 -08001041 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001042 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001043
Clark Boylanb640e052014-04-03 16:41:46 -07001044 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001045 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001046 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001047 return FakeURLOpener(self.upstream_root, *args, **kw)
1048
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001049 old_urlopen = urllib.request.urlopen
1050 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001051
James E. Blair3f876d52016-07-22 13:07:14 -07001052 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001053
James E. Blaire1767bc2016-08-02 10:00:27 -07001054 self.launch_server = RecordingLaunchServer(
1055 self.config, self.connections, _run_ansible=self.run_ansible)
1056 self.launch_server.start()
1057 self.history = self.launch_server.build_history
1058 self.builds = self.launch_server.running_builds
1059
1060 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001061 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001062 self.merge_client = zuul.merger.client.MergeClient(
1063 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001064 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001067 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001068 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001069
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001070 self.webapp = zuul.webapp.WebApp(
1071 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001072 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001073
1074 self.sched.start()
1075 self.sched.reconfigure(self.config)
1076 self.sched.resume()
1077 self.webapp.start()
1078 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001079 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001080
1081 self.addCleanup(self.assertFinalState)
1082 self.addCleanup(self.shutdown)
1083
James E. Blairfef78942016-03-11 16:28:56 -08001084 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001085 # Register connections from the config
1086 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001087
Joshua Hesketh352264b2015-08-11 23:42:08 +10001088 def FakeSMTPFactory(*args, **kw):
1089 args = [self.smtp_messages] + list(args)
1090 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001091
Joshua Hesketh352264b2015-08-11 23:42:08 +10001092 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001093
Joshua Hesketh352264b2015-08-11 23:42:08 +10001094 # Set a changes database so multiple FakeGerrit's can report back to
1095 # a virtual canonical database given by the configured hostname
1096 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001097 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001098
Joshua Hesketh352264b2015-08-11 23:42:08 +10001099 for section_name in self.config.sections():
1100 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1101 section_name, re.I)
1102 if not con_match:
1103 continue
1104 con_name = con_match.group(2)
1105 con_config = dict(self.config.items(section_name))
1106
1107 if 'driver' not in con_config:
1108 raise Exception("No driver specified for connection %s."
1109 % con_name)
1110
1111 con_driver = con_config['driver']
1112
1113 # TODO(jhesketh): load the required class automatically
1114 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001115 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1116 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001117 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001118 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001119 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001120 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001121 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001122 self.event_queues.append(
1123 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001124 setattr(self, 'fake_' + con_name,
1125 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001126 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001127 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001128 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1129 else:
1130 raise Exception("Unknown driver, %s, for connection %s"
1131 % (con_config['driver'], con_name))
1132
1133 # If the [gerrit] or [smtp] sections still exist, load them in as a
1134 # connection named 'gerrit' or 'smtp' respectfully
1135
1136 if 'gerrit' in self.config.sections():
1137 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001138 self.event_queues.append(
1139 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001140 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001141 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001142 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001143
1144 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001145 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001146 zuul.connection.smtp.SMTPConnection(
1147 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001148
James E. Blair83005782015-12-11 14:46:03 -08001149 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001150 # This creates the per-test configuration object. It can be
1151 # overriden by subclasses, but should not need to be since it
1152 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001154 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001155 if hasattr(self, 'tenant_config_file'):
1156 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001157 git_path = os.path.join(
1158 os.path.dirname(
1159 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1160 'git')
1161 if os.path.exists(git_path):
1162 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001163 project = reponame.replace('_', '/')
1164 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001165 os.path.join(git_path, reponame))
1166
1167 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001168 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001169
1170 files = {}
1171 for (dirpath, dirnames, filenames) in os.walk(source_path):
1172 for filename in filenames:
1173 test_tree_filepath = os.path.join(dirpath, filename)
1174 common_path = os.path.commonprefix([test_tree_filepath,
1175 source_path])
1176 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1177 with open(test_tree_filepath, 'r') as f:
1178 content = f.read()
1179 files[relative_filepath] = content
1180 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001181 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001182
Clark Boylanb640e052014-04-03 16:41:46 -07001183 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001184 # Make sure that git.Repo objects have been garbage collected.
1185 repos = []
1186 gc.collect()
1187 for obj in gc.get_objects():
1188 if isinstance(obj, git.Repo):
1189 repos.append(obj)
1190 self.assertEqual(len(repos), 0)
1191 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001192 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001193 for tenant in self.sched.abide.tenants.values():
1194 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001195 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001196 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001197
1198 def shutdown(self):
1199 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001200 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001201 self.merge_server.stop()
1202 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001203 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001204 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.sched.stop()
1206 self.sched.join()
1207 self.statsd.stop()
1208 self.statsd.join()
1209 self.webapp.stop()
1210 self.webapp.join()
1211 self.rpc.stop()
1212 self.rpc.join()
1213 self.gearman_server.shutdown()
1214 threads = threading.enumerate()
1215 if len(threads) > 1:
1216 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001217
1218 def init_repo(self, project):
1219 parts = project.split('/')
1220 path = os.path.join(self.upstream_root, *parts[:-1])
1221 if not os.path.exists(path):
1222 os.makedirs(path)
1223 path = os.path.join(self.upstream_root, project)
1224 repo = git.Repo.init(path)
1225
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001226 with repo.config_writer() as config_writer:
1227 config_writer.set_value('user', 'email', 'user@example.com')
1228 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001229
Clark Boylanb640e052014-04-03 16:41:46 -07001230 repo.index.commit('initial commit')
1231 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001232
James E. Blair97d902e2014-08-21 13:25:56 -07001233 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001234 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001235 repo.git.clean('-x', '-f', '-d')
1236
James E. Blair97d902e2014-08-21 13:25:56 -07001237 def create_branch(self, project, branch):
1238 path = os.path.join(self.upstream_root, project)
1239 repo = git.Repo.init(path)
1240 fn = os.path.join(path, 'README')
1241
1242 branch_head = repo.create_head(branch)
1243 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001244 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001245 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001246 f.close()
1247 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001248 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001249
James E. Blair97d902e2014-08-21 13:25:56 -07001250 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001251 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001252 repo.git.clean('-x', '-f', '-d')
1253
Sachi King9f16d522016-03-16 12:20:45 +11001254 def create_commit(self, project):
1255 path = os.path.join(self.upstream_root, project)
1256 repo = git.Repo(path)
1257 repo.head.reference = repo.heads['master']
1258 file_name = os.path.join(path, 'README')
1259 with open(file_name, 'a') as f:
1260 f.write('creating fake commit\n')
1261 repo.index.add([file_name])
1262 commit = repo.index.commit('Creating a fake commit')
1263 return commit.hexsha
1264
Clark Boylanb640e052014-04-03 16:41:46 -07001265 def ref_has_change(self, ref, change):
James E. Blaira5dba232016-08-08 15:53:24 -07001266 # TODOv3(jeblair): this should probably be removed in favor of
1267 # build.hasChanges
Clark Boylanb640e052014-04-03 16:41:46 -07001268 path = os.path.join(self.git_root, change.project)
1269 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001270 try:
1271 for commit in repo.iter_commits(ref):
1272 if commit.message.strip() == ('%s-1' % change.subject):
1273 return True
1274 except GitCommandError:
1275 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001276 return False
1277
James E. Blairb8c16472015-05-05 14:55:26 -07001278 def orderedRelease(self):
1279 # Run one build at a time to ensure non-race order:
1280 while len(self.builds):
1281 self.release(self.builds[0])
1282 self.waitUntilSettled()
1283
Clark Boylanb640e052014-04-03 16:41:46 -07001284 def release(self, job):
1285 if isinstance(job, FakeBuild):
1286 job.release()
1287 else:
1288 job.waiting = False
1289 self.log.debug("Queued job %s released" % job.unique)
1290 self.gearman_server.wakeConnections()
1291
1292 def getParameter(self, job, name):
1293 if isinstance(job, FakeBuild):
1294 return job.parameters[name]
1295 else:
1296 parameters = json.loads(job.arguments)
1297 return parameters[name]
1298
1299 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001300 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001301 while True:
1302 done = True
1303 for connection in self.gearman_server.active_connections:
1304 if (connection.functions and
1305 connection.client_id not in ['Zuul RPC Listener',
1306 'Zuul Merger']):
1307 done = False
1308 if done:
1309 break
1310 time.sleep(0)
1311 self.gearman_server.functions = set()
1312 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001313
1314 def haveAllBuildsReported(self):
1315 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001316 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001317 return False
1318 # Find out if every build that the worker has completed has been
1319 # reported back to Zuul. If it hasn't then that means a Gearman
1320 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001321 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001323 if not zbuild:
1324 # It has already been reported
1325 continue
1326 # It hasn't been reported yet.
1327 return False
1328 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001329 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001330 if connection.state == 'GRAB_WAIT':
1331 return False
1332 return True
1333
1334 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001335 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001336 for build in builds:
1337 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001338 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001339 for j in conn.related_jobs.values():
1340 if j.unique == build.uuid:
1341 client_job = j
1342 break
1343 if not client_job:
1344 self.log.debug("%s is not known to the gearman client" %
1345 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001346 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001347 if not client_job.handle:
1348 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001349 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001350 server_job = self.gearman_server.jobs.get(client_job.handle)
1351 if not server_job:
1352 self.log.debug("%s is not known to the gearman server" %
1353 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001354 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001355 if not hasattr(server_job, 'waiting'):
1356 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001357 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001358 if server_job.waiting:
1359 continue
James E. Blair17302972016-08-10 16:11:42 -07001360 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001361 self.log.debug("%s has not reported start" % build)
1362 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001363 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001364 if worker_build:
1365 if worker_build.isWaiting():
1366 continue
1367 else:
1368 self.log.debug("%s is running" % worker_build)
1369 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001370 else:
James E. Blair962220f2016-08-03 11:22:38 -07001371 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001372 return False
1373 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001374
Jan Hruban6b71aff2015-10-22 16:58:08 +02001375 def eventQueuesEmpty(self):
1376 for queue in self.event_queues:
1377 yield queue.empty()
1378
1379 def eventQueuesJoin(self):
1380 for queue in self.event_queues:
1381 queue.join()
1382
Clark Boylanb640e052014-04-03 16:41:46 -07001383 def waitUntilSettled(self):
1384 self.log.debug("Waiting until settled...")
1385 start = time.time()
1386 while True:
1387 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001388 self.log.debug("Queue status:")
1389 for queue in self.event_queues:
1390 self.log.debug(" %s: %s" % (queue, queue.empty()))
1391 self.log.debug("All builds waiting: %s" %
1392 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001393 self.log.debug("All builds reported: %s" %
1394 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001395 raise Exception("Timeout waiting for Zuul to settle")
1396 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001397
James E. Blaire1767bc2016-08-02 10:00:27 -07001398 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001399 # have all build states propogated to zuul?
1400 if self.haveAllBuildsReported():
1401 # Join ensures that the queue is empty _and_ events have been
1402 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001403 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001404 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001405 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001406 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.haveAllBuildsReported() and
1408 self.areAllBuildsWaiting()):
1409 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001410 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001411 self.log.debug("...settled.")
1412 return
1413 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001414 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001415 self.sched.wake_event.wait(0.1)
1416
1417 def countJobResults(self, jobs, result):
1418 jobs = filter(lambda x: x.result == result, jobs)
1419 return len(jobs)
1420
James E. Blair96c6bf82016-01-15 16:20:40 -08001421 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001422 for job in self.history:
1423 if (job.name == name and
1424 (project is None or
1425 job.parameters['ZUUL_PROJECT'] == project)):
1426 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001427 raise Exception("Unable to find job %s in history" % name)
1428
1429 def assertEmptyQueues(self):
1430 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001431 for tenant in self.sched.abide.tenants.values():
1432 for pipeline in tenant.layout.pipelines.values():
1433 for queue in pipeline.queues:
1434 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001435 print('pipeline %s queue %s contents %s' % (
1436 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001437 self.assertEqual(len(queue.queue), 0,
1438 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001439
1440 def assertReportedStat(self, key, value=None, kind=None):
1441 start = time.time()
1442 while time.time() < (start + 5):
1443 for stat in self.statsd.stats:
1444 pprint.pprint(self.statsd.stats)
1445 k, v = stat.split(':')
1446 if key == k:
1447 if value is None and kind is None:
1448 return
1449 elif value:
1450 if value == v:
1451 return
1452 elif kind:
1453 if v.endswith('|' + kind):
1454 return
1455 time.sleep(0.1)
1456
1457 pprint.pprint(self.statsd.stats)
1458 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001459
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001460 def assertBuilds(self, builds):
1461 """Assert that the running builds are as described.
1462
1463 The list of running builds is examined and must match exactly
1464 the list of builds described by the input.
1465
1466 :arg list builds: A list of dictionaries. Each item in the
1467 list must match the corresponding build in the build
1468 history, and each element of the dictionary must match the
1469 corresponding attribute of the build.
1470
1471 """
James E. Blair3158e282016-08-19 09:34:11 -07001472 try:
1473 self.assertEqual(len(self.builds), len(builds))
1474 for i, d in enumerate(builds):
1475 for k, v in d.items():
1476 self.assertEqual(
1477 getattr(self.builds[i], k), v,
1478 "Element %i in builds does not match" % (i,))
1479 except Exception:
1480 for build in self.builds:
1481 self.log.error("Running build: %s" % build)
1482 else:
1483 self.log.error("No running builds")
1484 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001485
James E. Blairb536ecc2016-08-31 10:11:42 -07001486 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001487 """Assert that the completed builds are as described.
1488
1489 The list of completed builds is examined and must match
1490 exactly the list of builds described by the input.
1491
1492 :arg list history: A list of dictionaries. Each item in the
1493 list must match the corresponding build in the build
1494 history, and each element of the dictionary must match the
1495 corresponding attribute of the build.
1496
James E. Blairb536ecc2016-08-31 10:11:42 -07001497 :arg bool ordered: If true, the history must match the order
1498 supplied, if false, the builds are permitted to have
1499 arrived in any order.
1500
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001501 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001502 def matches(history_item, item):
1503 for k, v in item.items():
1504 if getattr(history_item, k) != v:
1505 return False
1506 return True
James E. Blair3158e282016-08-19 09:34:11 -07001507 try:
1508 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001509 if ordered:
1510 for i, d in enumerate(history):
1511 if not matches(self.history[i], d):
1512 raise Exception(
1513 "Element %i in history does not match" % (i,))
1514 else:
1515 unseen = self.history[:]
1516 for i, d in enumerate(history):
1517 found = False
1518 for unseen_item in unseen:
1519 if matches(unseen_item, d):
1520 found = True
1521 unseen.remove(unseen_item)
1522 break
1523 if not found:
1524 raise Exception("No match found for element %i "
1525 "in history" % (i,))
1526 if unseen:
1527 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001528 except Exception:
1529 for build in self.history:
1530 self.log.error("Completed build: %s" % build)
1531 else:
1532 self.log.error("No completed builds")
1533 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001534
James E. Blair59fdbac2015-12-07 17:08:06 -08001535 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001536 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1537
1538 def updateConfigLayout(self, path):
1539 root = os.path.join(self.test_root, "config")
1540 os.makedirs(root)
1541 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1542 f.write("""
1543tenants:
1544 - name: openstack
1545 include:
1546 - %s
1547 """ % os.path.abspath(path))
1548 f.close()
1549 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001550
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001551 def addCommitToRepo(self, project, message, files,
1552 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001553 path = os.path.join(self.upstream_root, project)
1554 repo = git.Repo(path)
1555 repo.head.reference = branch
1556 zuul.merger.merger.reset_repo_to_head(repo)
1557 for fn, content in files.items():
1558 fn = os.path.join(path, fn)
1559 with open(fn, 'w') as f:
1560 f.write(content)
1561 repo.index.add([fn])
1562 commit = repo.index.commit(message)
1563 repo.heads[branch].commit = commit
1564 repo.head.reference = branch
1565 repo.git.clean('-x', '-f', '-d')
1566 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001567 if tag:
1568 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001569
James E. Blair7fc8daa2016-08-08 15:37:15 -07001570 def addEvent(self, connection, event):
1571 """Inject a Fake (Gerrit) event.
1572
1573 This method accepts a JSON-encoded event and simulates Zuul
1574 having received it from Gerrit. It could (and should)
1575 eventually apply to any connection type, but is currently only
1576 used with Gerrit connections. The name of the connection is
1577 used to look up the corresponding server, and the event is
1578 simulated as having been received by all Zuul connections
1579 attached to that server. So if two Gerrit connections in Zuul
1580 are connected to the same Gerrit server, and you invoke this
1581 method specifying the name of one of them, the event will be
1582 received by both.
1583
1584 .. note::
1585
1586 "self.fake_gerrit.addEvent" calls should be migrated to
1587 this method.
1588
1589 :arg str connection: The name of the connection corresponding
1590 to the gerrit server.
1591 :arg str event: The JSON-encoded event.
1592
1593 """
1594 specified_conn = self.connections.connections[connection]
1595 for conn in self.connections.connections.values():
1596 if (isinstance(conn, specified_conn.__class__) and
1597 specified_conn.server == conn.server):
1598 conn.addEvent(event)
1599
James E. Blair3f876d52016-07-22 13:07:14 -07001600
1601class AnsibleZuulTestCase(ZuulTestCase):
1602 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001603 run_ansible = True