blob: 02953f524c547516de95cbed859815ec1bdf50a2 [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
Paul Belanger71d98172016-11-08 10:56:31 -0500567 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700568 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700569 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700570 self.changes = None
571 if 'ZUUL_CHANGE_IDS' in self.parameters:
572 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700573
James E. Blair3158e282016-08-19 09:34:11 -0700574 def __repr__(self):
575 waiting = ''
576 if self.waiting:
577 waiting = ' [waiting]'
578 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
579
Clark Boylanb640e052014-04-03 16:41:46 -0700580 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700581 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700582 self.wait_condition.acquire()
583 self.wait_condition.notify()
584 self.waiting = False
585 self.log.debug("Build %s released" % self.unique)
586 self.wait_condition.release()
587
588 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700589 """Return whether this build is being held.
590
591 :returns: Whether the build is being held.
592 :rtype: bool
593 """
594
Clark Boylanb640e052014-04-03 16:41:46 -0700595 self.wait_condition.acquire()
596 if self.waiting:
597 ret = True
598 else:
599 ret = False
600 self.wait_condition.release()
601 return ret
602
603 def _wait(self):
604 self.wait_condition.acquire()
605 self.waiting = True
606 self.log.debug("Build %s waiting" % self.unique)
607 self.wait_condition.wait()
608 self.wait_condition.release()
609
610 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700611 self.log.debug('Running build %s' % self.unique)
612
James E. Blaire1767bc2016-08-02 10:00:27 -0700613 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700614 self.log.debug('Holding build %s' % self.unique)
615 self._wait()
616 self.log.debug("Build %s continuing" % self.unique)
617
Clark Boylanb640e052014-04-03 16:41:46 -0700618 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700619 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700620 result = 'FAILURE'
621 if self.aborted:
622 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500623 if self.requeue:
624 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700625
626 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700627 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700628
James E. Blaire1767bc2016-08-02 10:00:27 -0700629 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700630
James E. Blaira5dba232016-08-08 15:53:24 -0700631 def shouldFail(self):
632 changes = self.launch_server.fail_tests.get(self.name, [])
633 for change in changes:
634 if self.hasChanges(change):
635 return True
636 return False
637
James E. Blaire7b99a02016-08-05 14:27:34 -0700638 def hasChanges(self, *changes):
639 """Return whether this build has certain changes in its git repos.
640
641 :arg FakeChange changes: One or more changes (varargs) that
642 are expected to be present (in order) in the git repository of
643 the active project.
644
645 :returns: Whether the build has the indicated changes.
646 :rtype: bool
647
648 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800649 for change in changes:
650 path = os.path.join(self.jobdir.git_root, change.project)
651 try:
652 repo = git.Repo(path)
653 except NoSuchPathError as e:
654 self.log.debug('%s' % e)
655 return False
656 ref = self.parameters['ZUUL_REF']
657 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
658 commit_message = '%s-1' % change.subject
659 self.log.debug("Checking if build %s has changes; commit_message "
660 "%s; repo_messages %s" % (self, commit_message,
661 repo_messages))
662 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700663 self.log.debug(" messages do not match")
664 return False
665 self.log.debug(" OK")
666 return True
667
Clark Boylanb640e052014-04-03 16:41:46 -0700668
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000669class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700670 """An Ansible launcher to be used in tests.
671
672 :ivar bool hold_jobs_in_build: If true, when jobs are launched
673 they will report that they have started but then pause until
674 released before reporting completion. This attribute may be
675 changed at any time and will take effect for subsequently
676 launched builds, but previously held builds will still need to
677 be explicitly released.
678
679 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800680 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700681 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800682 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 self.hold_jobs_in_build = False
684 self.lock = threading.Lock()
685 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700686 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700687 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700688 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800689
James E. Blaira5dba232016-08-08 15:53:24 -0700690 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700691 """Instruct the launcher to report matching builds as failures.
692
693 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700694 :arg Change change: The :py:class:`~tests.base.FakeChange`
695 instance which should cause the job to fail. This job
696 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700697
698 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700699 l = self.fail_tests.get(name, [])
700 l.append(change)
701 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800702
James E. Blair962220f2016-08-03 11:22:38 -0700703 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700704 """Release a held build.
705
706 :arg str regex: A regular expression which, if supplied, will
707 cause only builds with matching names to be released. If
708 not supplied, all builds will be released.
709
710 """
James E. Blair962220f2016-08-03 11:22:38 -0700711 builds = self.running_builds[:]
712 self.log.debug("Releasing build %s (%s)" % (regex,
713 len(self.running_builds)))
714 for build in builds:
715 if not regex or re.match(regex, build.name):
716 self.log.debug("Releasing build %s" %
717 (build.parameters['ZUUL_UUID']))
718 build.release()
719 else:
720 self.log.debug("Not releasing build %s" %
721 (build.parameters['ZUUL_UUID']))
722 self.log.debug("Done releasing builds %s (%s)" %
723 (regex, len(self.running_builds)))
724
James E. Blair17302972016-08-10 16:11:42 -0700725 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700726 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700727 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700728 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700729 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700730 super(RecordingLaunchServer, self).launchJob(job)
731
732 def stopJob(self, job):
733 self.log.debug("handle stop")
734 parameters = json.loads(job.arguments)
735 uuid = parameters['uuid']
736 for build in self.running_builds:
737 if build.unique == uuid:
738 build.aborted = True
739 build.release()
740 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700741
742 def runAnsible(self, jobdir, job):
743 build = self.job_builds[job.unique]
744 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700745
746 if self._run_ansible:
747 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
748 else:
749 result = build.run()
750
751 self.lock.acquire()
752 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700753 BuildHistory(name=build.name, result=result, changes=build.changes,
754 node=build.node, uuid=build.unique,
755 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700756 pipeline=build.parameters['ZUUL_PIPELINE'])
757 )
James E. Blairab7132b2016-08-05 12:36:22 -0700758 self.running_builds.remove(build)
759 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700760 self.lock.release()
761 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800762
763
Clark Boylanb640e052014-04-03 16:41:46 -0700764class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700765 """A Gearman server for use in tests.
766
767 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
768 added to the queue but will not be distributed to workers
769 until released. This attribute may be changed at any time and
770 will take effect for subsequently enqueued jobs, but
771 previously held jobs will still need to be explicitly
772 released.
773
774 """
775
Clark Boylanb640e052014-04-03 16:41:46 -0700776 def __init__(self):
777 self.hold_jobs_in_queue = False
778 super(FakeGearmanServer, self).__init__(0)
779
780 def getJobForConnection(self, connection, peek=False):
781 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
782 for job in queue:
783 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500784 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700785 job.waiting = self.hold_jobs_in_queue
786 else:
787 job.waiting = False
788 if job.waiting:
789 continue
790 if job.name in connection.functions:
791 if not peek:
792 queue.remove(job)
793 connection.related_jobs[job.handle] = job
794 job.worker_connection = connection
795 job.running = True
796 return job
797 return None
798
799 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700800 """Release a held job.
801
802 :arg str regex: A regular expression which, if supplied, will
803 cause only jobs with matching names to be released. If
804 not supplied, all jobs will be released.
805 """
Clark Boylanb640e052014-04-03 16:41:46 -0700806 released = False
807 qlen = (len(self.high_queue) + len(self.normal_queue) +
808 len(self.low_queue))
809 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
810 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500811 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700812 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500813 parameters = json.loads(job.arguments)
814 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700815 self.log.debug("releasing queued job %s" %
816 job.unique)
817 job.waiting = False
818 released = True
819 else:
820 self.log.debug("not releasing queued job %s" %
821 job.unique)
822 if released:
823 self.wakeConnections()
824 qlen = (len(self.high_queue) + len(self.normal_queue) +
825 len(self.low_queue))
826 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
827
828
829class FakeSMTP(object):
830 log = logging.getLogger('zuul.FakeSMTP')
831
832 def __init__(self, messages, server, port):
833 self.server = server
834 self.port = port
835 self.messages = messages
836
837 def sendmail(self, from_email, to_email, msg):
838 self.log.info("Sending email from %s, to %s, with msg %s" % (
839 from_email, to_email, msg))
840
841 headers = msg.split('\n\n', 1)[0]
842 body = msg.split('\n\n', 1)[1]
843
844 self.messages.append(dict(
845 from_email=from_email,
846 to_email=to_email,
847 msg=msg,
848 headers=headers,
849 body=body,
850 ))
851
852 return True
853
854 def quit(self):
855 return True
856
857
858class FakeSwiftClientConnection(swiftclient.client.Connection):
859 def post_account(self, headers):
860 # Do nothing
861 pass
862
863 def get_auth(self):
864 # Returns endpoint and (unused) auth token
865 endpoint = os.path.join('https://storage.example.org', 'V1',
866 'AUTH_account')
867 return endpoint, ''
868
869
Maru Newby3fe5f852015-01-13 04:22:14 +0000870class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700871 log = logging.getLogger("zuul.test")
872
873 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000874 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700875 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
876 try:
877 test_timeout = int(test_timeout)
878 except ValueError:
879 # If timeout value is invalid do not set a timeout.
880 test_timeout = 0
881 if test_timeout > 0:
882 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
883
884 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
885 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
886 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
887 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
888 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
889 os.environ.get('OS_STDERR_CAPTURE') == '1'):
890 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
891 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
892 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
893 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -0700894 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100895 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
896 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -0700897 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
898 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100899 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
900 log_level = logging.WARNING
901 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
902 log_level = logging.ERROR
903 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
904 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -0700905 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100906 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -0700907 format='%(asctime)s %(name)-32s '
908 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000909
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700910 # NOTE(notmorgan): Extract logging overrides for specific libraries
911 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
912 # each. This is used to limit the output during test runs from
913 # libraries that zuul depends on such as gear.
914 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
915
916 if log_defaults_from_env:
917 for default in log_defaults_from_env.split(','):
918 try:
919 name, level_str = default.split('=', 1)
920 level = getattr(logging, level_str, logging.DEBUG)
921 self.useFixture(fixtures.FakeLogger(
922 name=name,
923 level=level,
924 format='%(asctime)s %(name)-32s '
925 '%(levelname)-8s %(message)s'))
926 except ValueError:
927 # NOTE(notmorgan): Invalid format of the log default,
928 # skip and don't try and apply a logger for the
929 # specified module
930 pass
931
Maru Newby3fe5f852015-01-13 04:22:14 +0000932
933class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700934 """A test case with a functioning Zuul.
935
936 The following class variables are used during test setup and can
937 be overidden by subclasses but are effectively read-only once a
938 test method starts running:
939
940 :cvar str config_file: This points to the main zuul config file
941 within the fixtures directory. Subclasses may override this
942 to obtain a different behavior.
943
944 :cvar str tenant_config_file: This is the tenant config file
945 (which specifies from what git repos the configuration should
946 be loaded). It defaults to the value specified in
947 `config_file` but can be overidden by subclasses to obtain a
948 different tenant/project layout while using the standard main
949 configuration.
950
951 The following are instance variables that are useful within test
952 methods:
953
954 :ivar FakeGerritConnection fake_<connection>:
955 A :py:class:`~tests.base.FakeGerritConnection` will be
956 instantiated for each connection present in the config file
957 and stored here. For instance, `fake_gerrit` will hold the
958 FakeGerritConnection object for a connection named `gerrit`.
959
960 :ivar FakeGearmanServer gearman_server: An instance of
961 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
962 server that all of the Zuul components in this test use to
963 communicate with each other.
964
965 :ivar RecordingLaunchServer launch_server: An instance of
966 :py:class:`~tests.base.RecordingLaunchServer` which is the
967 Ansible launch server used to run jobs for this test.
968
969 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
970 representing currently running builds. They are appended to
971 the list in the order they are launched, and removed from this
972 list upon completion.
973
974 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
975 objects representing completed builds. They are appended to
976 the list in the order they complete.
977
978 """
979
James E. Blair83005782015-12-11 14:46:03 -0800980 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700981 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700982
983 def _startMerger(self):
984 self.merge_server = zuul.merger.server.MergeServer(self.config,
985 self.connections)
986 self.merge_server.start()
987
Maru Newby3fe5f852015-01-13 04:22:14 +0000988 def setUp(self):
989 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700990 if USE_TEMPDIR:
991 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000992 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
993 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700994 else:
995 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700996 self.test_root = os.path.join(tmp_root, "zuul-test")
997 self.upstream_root = os.path.join(self.test_root, "upstream")
998 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700999 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001000
1001 if os.path.exists(self.test_root):
1002 shutil.rmtree(self.test_root)
1003 os.makedirs(self.test_root)
1004 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001005 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001006
1007 # Make per test copy of Configuration.
1008 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001009 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001010 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001011 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001012 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001013 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001014
1015 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001016 # TODOv3(jeblair): remove these and replace with new git
1017 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001018 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001019 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001020 self.init_repo("org/project5")
1021 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001022 self.init_repo("org/one-job-project")
1023 self.init_repo("org/nonvoting-project")
1024 self.init_repo("org/templated-project")
1025 self.init_repo("org/layered-project")
1026 self.init_repo("org/node-project")
1027 self.init_repo("org/conflict-project")
1028 self.init_repo("org/noop-project")
1029 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001030 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001031
1032 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001033 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1034 # see: https://github.com/jsocol/pystatsd/issues/61
1035 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001036 os.environ['STATSD_PORT'] = str(self.statsd.port)
1037 self.statsd.start()
1038 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001039 reload_module(statsd)
1040 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001041
1042 self.gearman_server = FakeGearmanServer()
1043
1044 self.config.set('gearman', 'port', str(self.gearman_server.port))
1045
Joshua Hesketh352264b2015-08-11 23:42:08 +10001046 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1047 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1048 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001049
Joshua Hesketh352264b2015-08-11 23:42:08 +10001050 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001051
1052 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1053 FakeSwiftClientConnection))
1054 self.swift = zuul.lib.swift.Swift(self.config)
1055
Jan Hruban6b71aff2015-10-22 16:58:08 +02001056 self.event_queues = [
1057 self.sched.result_event_queue,
1058 self.sched.trigger_event_queue
1059 ]
1060
James E. Blairfef78942016-03-11 16:28:56 -08001061 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001062 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001063
Clark Boylanb640e052014-04-03 16:41:46 -07001064 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001065 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001066 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001067 return FakeURLOpener(self.upstream_root, *args, **kw)
1068
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001069 old_urlopen = urllib.request.urlopen
1070 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001071
James E. Blair3f876d52016-07-22 13:07:14 -07001072 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001073
James E. Blaire1767bc2016-08-02 10:00:27 -07001074 self.launch_server = RecordingLaunchServer(
1075 self.config, self.connections, _run_ansible=self.run_ansible)
1076 self.launch_server.start()
1077 self.history = self.launch_server.build_history
1078 self.builds = self.launch_server.running_builds
1079
1080 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001081 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001082 self.merge_client = zuul.merger.client.MergeClient(
1083 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001084 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001085
James E. Blaire1767bc2016-08-02 10:00:27 -07001086 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001087 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001088 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001089
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001090 self.webapp = zuul.webapp.WebApp(
1091 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001092 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001093
1094 self.sched.start()
1095 self.sched.reconfigure(self.config)
1096 self.sched.resume()
1097 self.webapp.start()
1098 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001099 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001100
1101 self.addCleanup(self.assertFinalState)
1102 self.addCleanup(self.shutdown)
1103
James E. Blairfef78942016-03-11 16:28:56 -08001104 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001105 # Register connections from the config
1106 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001107
Joshua Hesketh352264b2015-08-11 23:42:08 +10001108 def FakeSMTPFactory(*args, **kw):
1109 args = [self.smtp_messages] + list(args)
1110 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001111
Joshua Hesketh352264b2015-08-11 23:42:08 +10001112 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001113
Joshua Hesketh352264b2015-08-11 23:42:08 +10001114 # Set a changes database so multiple FakeGerrit's can report back to
1115 # a virtual canonical database given by the configured hostname
1116 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001117 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001118
Joshua Hesketh352264b2015-08-11 23:42:08 +10001119 for section_name in self.config.sections():
1120 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1121 section_name, re.I)
1122 if not con_match:
1123 continue
1124 con_name = con_match.group(2)
1125 con_config = dict(self.config.items(section_name))
1126
1127 if 'driver' not in con_config:
1128 raise Exception("No driver specified for connection %s."
1129 % con_name)
1130
1131 con_driver = con_config['driver']
1132
1133 # TODO(jhesketh): load the required class automatically
1134 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001135 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1136 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001137 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001138 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001139 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001140 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001141 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001142 self.event_queues.append(
1143 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001144 setattr(self, 'fake_' + con_name,
1145 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001146 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001147 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001148 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1149 else:
1150 raise Exception("Unknown driver, %s, for connection %s"
1151 % (con_config['driver'], con_name))
1152
1153 # If the [gerrit] or [smtp] sections still exist, load them in as a
1154 # connection named 'gerrit' or 'smtp' respectfully
1155
1156 if 'gerrit' in self.config.sections():
1157 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001158 self.event_queues.append(
1159 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001160 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001161 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001162 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001163
1164 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001165 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001166 zuul.connection.smtp.SMTPConnection(
1167 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001168
James E. Blair83005782015-12-11 14:46:03 -08001169 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001170 # This creates the per-test configuration object. It can be
1171 # overriden by subclasses, but should not need to be since it
1172 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001174 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001175 if hasattr(self, 'tenant_config_file'):
1176 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001177 git_path = os.path.join(
1178 os.path.dirname(
1179 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1180 'git')
1181 if os.path.exists(git_path):
1182 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001183 project = reponame.replace('_', '/')
1184 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001185 os.path.join(git_path, reponame))
1186
1187 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001188 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001189
1190 files = {}
1191 for (dirpath, dirnames, filenames) in os.walk(source_path):
1192 for filename in filenames:
1193 test_tree_filepath = os.path.join(dirpath, filename)
1194 common_path = os.path.commonprefix([test_tree_filepath,
1195 source_path])
1196 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1197 with open(test_tree_filepath, 'r') as f:
1198 content = f.read()
1199 files[relative_filepath] = content
1200 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001201 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001202
Clark Boylanb640e052014-04-03 16:41:46 -07001203 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001204 # Make sure that git.Repo objects have been garbage collected.
1205 repos = []
1206 gc.collect()
1207 for obj in gc.get_objects():
1208 if isinstance(obj, git.Repo):
1209 repos.append(obj)
1210 self.assertEqual(len(repos), 0)
1211 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001212 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001213 for tenant in self.sched.abide.tenants.values():
1214 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001215 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001216 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001217
1218 def shutdown(self):
1219 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001220 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001221 self.merge_server.stop()
1222 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001223 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001224 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001225 self.sched.stop()
1226 self.sched.join()
1227 self.statsd.stop()
1228 self.statsd.join()
1229 self.webapp.stop()
1230 self.webapp.join()
1231 self.rpc.stop()
1232 self.rpc.join()
1233 self.gearman_server.shutdown()
1234 threads = threading.enumerate()
1235 if len(threads) > 1:
1236 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001237
1238 def init_repo(self, project):
1239 parts = project.split('/')
1240 path = os.path.join(self.upstream_root, *parts[:-1])
1241 if not os.path.exists(path):
1242 os.makedirs(path)
1243 path = os.path.join(self.upstream_root, project)
1244 repo = git.Repo.init(path)
1245
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001246 with repo.config_writer() as config_writer:
1247 config_writer.set_value('user', 'email', 'user@example.com')
1248 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001249
Clark Boylanb640e052014-04-03 16:41:46 -07001250 repo.index.commit('initial commit')
1251 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001252
James E. Blair97d902e2014-08-21 13:25:56 -07001253 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001254 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001255 repo.git.clean('-x', '-f', '-d')
1256
James E. Blair97d902e2014-08-21 13:25:56 -07001257 def create_branch(self, project, branch):
1258 path = os.path.join(self.upstream_root, project)
1259 repo = git.Repo.init(path)
1260 fn = os.path.join(path, 'README')
1261
1262 branch_head = repo.create_head(branch)
1263 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001264 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001265 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001266 f.close()
1267 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001268 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001269
James E. Blair97d902e2014-08-21 13:25:56 -07001270 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001271 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001272 repo.git.clean('-x', '-f', '-d')
1273
Sachi King9f16d522016-03-16 12:20:45 +11001274 def create_commit(self, project):
1275 path = os.path.join(self.upstream_root, project)
1276 repo = git.Repo(path)
1277 repo.head.reference = repo.heads['master']
1278 file_name = os.path.join(path, 'README')
1279 with open(file_name, 'a') as f:
1280 f.write('creating fake commit\n')
1281 repo.index.add([file_name])
1282 commit = repo.index.commit('Creating a fake commit')
1283 return commit.hexsha
1284
James E. Blairb8c16472015-05-05 14:55:26 -07001285 def orderedRelease(self):
1286 # Run one build at a time to ensure non-race order:
1287 while len(self.builds):
1288 self.release(self.builds[0])
1289 self.waitUntilSettled()
1290
Clark Boylanb640e052014-04-03 16:41:46 -07001291 def release(self, job):
1292 if isinstance(job, FakeBuild):
1293 job.release()
1294 else:
1295 job.waiting = False
1296 self.log.debug("Queued job %s released" % job.unique)
1297 self.gearman_server.wakeConnections()
1298
1299 def getParameter(self, job, name):
1300 if isinstance(job, FakeBuild):
1301 return job.parameters[name]
1302 else:
1303 parameters = json.loads(job.arguments)
1304 return parameters[name]
1305
1306 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001308 while True:
1309 done = True
1310 for connection in self.gearman_server.active_connections:
1311 if (connection.functions and
1312 connection.client_id not in ['Zuul RPC Listener',
1313 'Zuul Merger']):
1314 done = False
1315 if done:
1316 break
1317 time.sleep(0)
1318 self.gearman_server.functions = set()
1319 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001320
1321 def haveAllBuildsReported(self):
1322 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001323 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001324 return False
1325 # Find out if every build that the worker has completed has been
1326 # reported back to Zuul. If it hasn't then that means a Gearman
1327 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001328 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001329 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001330 if not zbuild:
1331 # It has already been reported
1332 continue
1333 # It hasn't been reported yet.
1334 return False
1335 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001336 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001337 if connection.state == 'GRAB_WAIT':
1338 return False
1339 return True
1340
1341 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001342 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001343 for build in builds:
1344 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001345 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001346 for j in conn.related_jobs.values():
1347 if j.unique == build.uuid:
1348 client_job = j
1349 break
1350 if not client_job:
1351 self.log.debug("%s is not known to the gearman client" %
1352 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001354 if not client_job.handle:
1355 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001356 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001357 server_job = self.gearman_server.jobs.get(client_job.handle)
1358 if not server_job:
1359 self.log.debug("%s is not known to the gearman server" %
1360 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001361 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001362 if not hasattr(server_job, 'waiting'):
1363 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001364 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001365 if server_job.waiting:
1366 continue
James E. Blair17302972016-08-10 16:11:42 -07001367 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001368 self.log.debug("%s has not reported start" % build)
1369 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001370 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001371 if worker_build:
1372 if worker_build.isWaiting():
1373 continue
1374 else:
1375 self.log.debug("%s is running" % worker_build)
1376 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001377 else:
James E. Blair962220f2016-08-03 11:22:38 -07001378 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001379 return False
1380 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001381
Jan Hruban6b71aff2015-10-22 16:58:08 +02001382 def eventQueuesEmpty(self):
1383 for queue in self.event_queues:
1384 yield queue.empty()
1385
1386 def eventQueuesJoin(self):
1387 for queue in self.event_queues:
1388 queue.join()
1389
Clark Boylanb640e052014-04-03 16:41:46 -07001390 def waitUntilSettled(self):
1391 self.log.debug("Waiting until settled...")
1392 start = time.time()
1393 while True:
1394 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001395 self.log.debug("Queue status:")
1396 for queue in self.event_queues:
1397 self.log.debug(" %s: %s" % (queue, queue.empty()))
1398 self.log.debug("All builds waiting: %s" %
1399 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001400 self.log.debug("All builds reported: %s" %
1401 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001402 raise Exception("Timeout waiting for Zuul to settle")
1403 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001404
James E. Blaire1767bc2016-08-02 10:00:27 -07001405 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001406 # have all build states propogated to zuul?
1407 if self.haveAllBuildsReported():
1408 # Join ensures that the queue is empty _and_ events have been
1409 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001410 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001411 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001412 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001413 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001414 self.haveAllBuildsReported() and
1415 self.areAllBuildsWaiting()):
1416 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001417 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001418 self.log.debug("...settled.")
1419 return
1420 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001421 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001422 self.sched.wake_event.wait(0.1)
1423
1424 def countJobResults(self, jobs, result):
1425 jobs = filter(lambda x: x.result == result, jobs)
1426 return len(jobs)
1427
James E. Blair96c6bf82016-01-15 16:20:40 -08001428 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001429 for job in self.history:
1430 if (job.name == name and
1431 (project is None or
1432 job.parameters['ZUUL_PROJECT'] == project)):
1433 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001434 raise Exception("Unable to find job %s in history" % name)
1435
1436 def assertEmptyQueues(self):
1437 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001438 for tenant in self.sched.abide.tenants.values():
1439 for pipeline in tenant.layout.pipelines.values():
1440 for queue in pipeline.queues:
1441 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001442 print('pipeline %s queue %s contents %s' % (
1443 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001444 self.assertEqual(len(queue.queue), 0,
1445 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001446
1447 def assertReportedStat(self, key, value=None, kind=None):
1448 start = time.time()
1449 while time.time() < (start + 5):
1450 for stat in self.statsd.stats:
1451 pprint.pprint(self.statsd.stats)
1452 k, v = stat.split(':')
1453 if key == k:
1454 if value is None and kind is None:
1455 return
1456 elif value:
1457 if value == v:
1458 return
1459 elif kind:
1460 if v.endswith('|' + kind):
1461 return
1462 time.sleep(0.1)
1463
1464 pprint.pprint(self.statsd.stats)
1465 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001466
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001467 def assertBuilds(self, builds):
1468 """Assert that the running builds are as described.
1469
1470 The list of running builds is examined and must match exactly
1471 the list of builds described by the input.
1472
1473 :arg list builds: A list of dictionaries. Each item in the
1474 list must match the corresponding build in the build
1475 history, and each element of the dictionary must match the
1476 corresponding attribute of the build.
1477
1478 """
James E. Blair3158e282016-08-19 09:34:11 -07001479 try:
1480 self.assertEqual(len(self.builds), len(builds))
1481 for i, d in enumerate(builds):
1482 for k, v in d.items():
1483 self.assertEqual(
1484 getattr(self.builds[i], k), v,
1485 "Element %i in builds does not match" % (i,))
1486 except Exception:
1487 for build in self.builds:
1488 self.log.error("Running build: %s" % build)
1489 else:
1490 self.log.error("No running builds")
1491 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001492
James E. Blairb536ecc2016-08-31 10:11:42 -07001493 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001494 """Assert that the completed builds are as described.
1495
1496 The list of completed builds is examined and must match
1497 exactly the list of builds described by the input.
1498
1499 :arg list history: A list of dictionaries. Each item in the
1500 list must match the corresponding build in the build
1501 history, and each element of the dictionary must match the
1502 corresponding attribute of the build.
1503
James E. Blairb536ecc2016-08-31 10:11:42 -07001504 :arg bool ordered: If true, the history must match the order
1505 supplied, if false, the builds are permitted to have
1506 arrived in any order.
1507
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001508 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001509 def matches(history_item, item):
1510 for k, v in item.items():
1511 if getattr(history_item, k) != v:
1512 return False
1513 return True
James E. Blair3158e282016-08-19 09:34:11 -07001514 try:
1515 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001516 if ordered:
1517 for i, d in enumerate(history):
1518 if not matches(self.history[i], d):
1519 raise Exception(
1520 "Element %i in history does not match" % (i,))
1521 else:
1522 unseen = self.history[:]
1523 for i, d in enumerate(history):
1524 found = False
1525 for unseen_item in unseen:
1526 if matches(unseen_item, d):
1527 found = True
1528 unseen.remove(unseen_item)
1529 break
1530 if not found:
1531 raise Exception("No match found for element %i "
1532 "in history" % (i,))
1533 if unseen:
1534 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001535 except Exception:
1536 for build in self.history:
1537 self.log.error("Completed build: %s" % build)
1538 else:
1539 self.log.error("No completed builds")
1540 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001541
James E. Blair59fdbac2015-12-07 17:08:06 -08001542 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001543 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1544
1545 def updateConfigLayout(self, path):
1546 root = os.path.join(self.test_root, "config")
1547 os.makedirs(root)
1548 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1549 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001550- tenant:
1551 name: openstack
1552 source:
1553 gerrit:
1554 config-repos:
1555 - %s
1556 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001557 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001558 self.config.set('zuul', 'tenant_config',
1559 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001560
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001561 def addCommitToRepo(self, project, message, files,
1562 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001563 path = os.path.join(self.upstream_root, project)
1564 repo = git.Repo(path)
1565 repo.head.reference = branch
1566 zuul.merger.merger.reset_repo_to_head(repo)
1567 for fn, content in files.items():
1568 fn = os.path.join(path, fn)
1569 with open(fn, 'w') as f:
1570 f.write(content)
1571 repo.index.add([fn])
1572 commit = repo.index.commit(message)
1573 repo.heads[branch].commit = commit
1574 repo.head.reference = branch
1575 repo.git.clean('-x', '-f', '-d')
1576 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001577 if tag:
1578 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001579
James E. Blair7fc8daa2016-08-08 15:37:15 -07001580 def addEvent(self, connection, event):
1581 """Inject a Fake (Gerrit) event.
1582
1583 This method accepts a JSON-encoded event and simulates Zuul
1584 having received it from Gerrit. It could (and should)
1585 eventually apply to any connection type, but is currently only
1586 used with Gerrit connections. The name of the connection is
1587 used to look up the corresponding server, and the event is
1588 simulated as having been received by all Zuul connections
1589 attached to that server. So if two Gerrit connections in Zuul
1590 are connected to the same Gerrit server, and you invoke this
1591 method specifying the name of one of them, the event will be
1592 received by both.
1593
1594 .. note::
1595
1596 "self.fake_gerrit.addEvent" calls should be migrated to
1597 this method.
1598
1599 :arg str connection: The name of the connection corresponding
1600 to the gerrit server.
1601 :arg str event: The JSON-encoded event.
1602
1603 """
1604 specified_conn = self.connections.connections[connection]
1605 for conn in self.connections.connections.values():
1606 if (isinstance(conn, specified_conn.__class__) and
1607 specified_conn.server == conn.server):
1608 conn.addEvent(event)
1609
James E. Blair3f876d52016-07-22 13:07:14 -07001610
1611class AnsibleZuulTestCase(ZuulTestCase):
1612 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001613 run_ansible = True