blob: 603fba3e8e1e2ba1a81903cea441d85552430179 [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()
Clint Byrum69e47122016-12-02 16:40:35 -0800761 if build.run_error:
762 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700763 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800764
765
Clark Boylanb640e052014-04-03 16:41:46 -0700766class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700767 """A Gearman server for use in tests.
768
769 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
770 added to the queue but will not be distributed to workers
771 until released. This attribute may be changed at any time and
772 will take effect for subsequently enqueued jobs, but
773 previously held jobs will still need to be explicitly
774 released.
775
776 """
777
Clark Boylanb640e052014-04-03 16:41:46 -0700778 def __init__(self):
779 self.hold_jobs_in_queue = False
780 super(FakeGearmanServer, self).__init__(0)
781
782 def getJobForConnection(self, connection, peek=False):
783 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
784 for job in queue:
785 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500786 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700787 job.waiting = self.hold_jobs_in_queue
788 else:
789 job.waiting = False
790 if job.waiting:
791 continue
792 if job.name in connection.functions:
793 if not peek:
794 queue.remove(job)
795 connection.related_jobs[job.handle] = job
796 job.worker_connection = connection
797 job.running = True
798 return job
799 return None
800
801 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700802 """Release a held job.
803
804 :arg str regex: A regular expression which, if supplied, will
805 cause only jobs with matching names to be released. If
806 not supplied, all jobs will be released.
807 """
Clark Boylanb640e052014-04-03 16:41:46 -0700808 released = False
809 qlen = (len(self.high_queue) + len(self.normal_queue) +
810 len(self.low_queue))
811 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
812 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500813 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700814 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500815 parameters = json.loads(job.arguments)
816 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700817 self.log.debug("releasing queued job %s" %
818 job.unique)
819 job.waiting = False
820 released = True
821 else:
822 self.log.debug("not releasing queued job %s" %
823 job.unique)
824 if released:
825 self.wakeConnections()
826 qlen = (len(self.high_queue) + len(self.normal_queue) +
827 len(self.low_queue))
828 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
829
830
831class FakeSMTP(object):
832 log = logging.getLogger('zuul.FakeSMTP')
833
834 def __init__(self, messages, server, port):
835 self.server = server
836 self.port = port
837 self.messages = messages
838
839 def sendmail(self, from_email, to_email, msg):
840 self.log.info("Sending email from %s, to %s, with msg %s" % (
841 from_email, to_email, msg))
842
843 headers = msg.split('\n\n', 1)[0]
844 body = msg.split('\n\n', 1)[1]
845
846 self.messages.append(dict(
847 from_email=from_email,
848 to_email=to_email,
849 msg=msg,
850 headers=headers,
851 body=body,
852 ))
853
854 return True
855
856 def quit(self):
857 return True
858
859
860class FakeSwiftClientConnection(swiftclient.client.Connection):
861 def post_account(self, headers):
862 # Do nothing
863 pass
864
865 def get_auth(self):
866 # Returns endpoint and (unused) auth token
867 endpoint = os.path.join('https://storage.example.org', 'V1',
868 'AUTH_account')
869 return endpoint, ''
870
871
Maru Newby3fe5f852015-01-13 04:22:14 +0000872class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700873 log = logging.getLogger("zuul.test")
874
875 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000876 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700877 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
878 try:
879 test_timeout = int(test_timeout)
880 except ValueError:
881 # If timeout value is invalid do not set a timeout.
882 test_timeout = 0
883 if test_timeout > 0:
884 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
885
886 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
887 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
888 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
889 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
890 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
891 os.environ.get('OS_STDERR_CAPTURE') == '1'):
892 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
893 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
894 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
895 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -0700896 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100897 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
898 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -0700899 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
900 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100901 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
902 log_level = logging.WARNING
903 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
904 log_level = logging.ERROR
905 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
906 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -0700907 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100908 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -0700909 format='%(asctime)s %(name)-32s '
910 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000911
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700912 # NOTE(notmorgan): Extract logging overrides for specific libraries
913 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
914 # each. This is used to limit the output during test runs from
915 # libraries that zuul depends on such as gear.
916 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
917
918 if log_defaults_from_env:
919 for default in log_defaults_from_env.split(','):
920 try:
921 name, level_str = default.split('=', 1)
922 level = getattr(logging, level_str, logging.DEBUG)
923 self.useFixture(fixtures.FakeLogger(
924 name=name,
925 level=level,
926 format='%(asctime)s %(name)-32s '
927 '%(levelname)-8s %(message)s'))
928 except ValueError:
929 # NOTE(notmorgan): Invalid format of the log default,
930 # skip and don't try and apply a logger for the
931 # specified module
932 pass
933
Maru Newby3fe5f852015-01-13 04:22:14 +0000934
935class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700936 """A test case with a functioning Zuul.
937
938 The following class variables are used during test setup and can
939 be overidden by subclasses but are effectively read-only once a
940 test method starts running:
941
942 :cvar str config_file: This points to the main zuul config file
943 within the fixtures directory. Subclasses may override this
944 to obtain a different behavior.
945
946 :cvar str tenant_config_file: This is the tenant config file
947 (which specifies from what git repos the configuration should
948 be loaded). It defaults to the value specified in
949 `config_file` but can be overidden by subclasses to obtain a
950 different tenant/project layout while using the standard main
951 configuration.
952
953 The following are instance variables that are useful within test
954 methods:
955
956 :ivar FakeGerritConnection fake_<connection>:
957 A :py:class:`~tests.base.FakeGerritConnection` will be
958 instantiated for each connection present in the config file
959 and stored here. For instance, `fake_gerrit` will hold the
960 FakeGerritConnection object for a connection named `gerrit`.
961
962 :ivar FakeGearmanServer gearman_server: An instance of
963 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
964 server that all of the Zuul components in this test use to
965 communicate with each other.
966
967 :ivar RecordingLaunchServer launch_server: An instance of
968 :py:class:`~tests.base.RecordingLaunchServer` which is the
969 Ansible launch server used to run jobs for this test.
970
971 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
972 representing currently running builds. They are appended to
973 the list in the order they are launched, and removed from this
974 list upon completion.
975
976 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
977 objects representing completed builds. They are appended to
978 the list in the order they complete.
979
980 """
981
James E. Blair83005782015-12-11 14:46:03 -0800982 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700983 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700984
985 def _startMerger(self):
986 self.merge_server = zuul.merger.server.MergeServer(self.config,
987 self.connections)
988 self.merge_server.start()
989
Maru Newby3fe5f852015-01-13 04:22:14 +0000990 def setUp(self):
991 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700992 if USE_TEMPDIR:
993 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000994 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
995 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700996 else:
997 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700998 self.test_root = os.path.join(tmp_root, "zuul-test")
999 self.upstream_root = os.path.join(self.test_root, "upstream")
1000 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001001 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001002
1003 if os.path.exists(self.test_root):
1004 shutil.rmtree(self.test_root)
1005 os.makedirs(self.test_root)
1006 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001007 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001008
1009 # Make per test copy of Configuration.
1010 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001011 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001012 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001013 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001014 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001015 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001016
1017 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001018 # TODOv3(jeblair): remove these and replace with new git
1019 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001020 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001021 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001022 self.init_repo("org/project5")
1023 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001024 self.init_repo("org/one-job-project")
1025 self.init_repo("org/nonvoting-project")
1026 self.init_repo("org/templated-project")
1027 self.init_repo("org/layered-project")
1028 self.init_repo("org/node-project")
1029 self.init_repo("org/conflict-project")
1030 self.init_repo("org/noop-project")
1031 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001032 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001033
1034 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001035 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1036 # see: https://github.com/jsocol/pystatsd/issues/61
1037 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001038 os.environ['STATSD_PORT'] = str(self.statsd.port)
1039 self.statsd.start()
1040 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001041 reload_module(statsd)
1042 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001043
1044 self.gearman_server = FakeGearmanServer()
1045
1046 self.config.set('gearman', 'port', str(self.gearman_server.port))
1047
Joshua Hesketh352264b2015-08-11 23:42:08 +10001048 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1049 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1050 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001051
Joshua Hesketh352264b2015-08-11 23:42:08 +10001052 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001053
1054 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1055 FakeSwiftClientConnection))
1056 self.swift = zuul.lib.swift.Swift(self.config)
1057
Jan Hruban6b71aff2015-10-22 16:58:08 +02001058 self.event_queues = [
1059 self.sched.result_event_queue,
1060 self.sched.trigger_event_queue
1061 ]
1062
James E. Blairfef78942016-03-11 16:28:56 -08001063 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001064 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001065
Clark Boylanb640e052014-04-03 16:41:46 -07001066 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001067 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001068 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001069 return FakeURLOpener(self.upstream_root, *args, **kw)
1070
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001071 old_urlopen = urllib.request.urlopen
1072 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001073
James E. Blair3f876d52016-07-22 13:07:14 -07001074 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001075
James E. Blaire1767bc2016-08-02 10:00:27 -07001076 self.launch_server = RecordingLaunchServer(
1077 self.config, self.connections, _run_ansible=self.run_ansible)
1078 self.launch_server.start()
1079 self.history = self.launch_server.build_history
1080 self.builds = self.launch_server.running_builds
1081
1082 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001083 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001084 self.merge_client = zuul.merger.client.MergeClient(
1085 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001086 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001087
James E. Blaire1767bc2016-08-02 10:00:27 -07001088 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001089 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001090 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001091
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001092 self.webapp = zuul.webapp.WebApp(
1093 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001094 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001095
1096 self.sched.start()
1097 self.sched.reconfigure(self.config)
1098 self.sched.resume()
1099 self.webapp.start()
1100 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001101 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001102
1103 self.addCleanup(self.assertFinalState)
1104 self.addCleanup(self.shutdown)
1105
James E. Blairfef78942016-03-11 16:28:56 -08001106 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001107 # Register connections from the config
1108 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001109
Joshua Hesketh352264b2015-08-11 23:42:08 +10001110 def FakeSMTPFactory(*args, **kw):
1111 args = [self.smtp_messages] + list(args)
1112 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001113
Joshua Hesketh352264b2015-08-11 23:42:08 +10001114 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001115
Joshua Hesketh352264b2015-08-11 23:42:08 +10001116 # Set a changes database so multiple FakeGerrit's can report back to
1117 # a virtual canonical database given by the configured hostname
1118 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001119 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001120
Joshua Hesketh352264b2015-08-11 23:42:08 +10001121 for section_name in self.config.sections():
1122 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1123 section_name, re.I)
1124 if not con_match:
1125 continue
1126 con_name = con_match.group(2)
1127 con_config = dict(self.config.items(section_name))
1128
1129 if 'driver' not in con_config:
1130 raise Exception("No driver specified for connection %s."
1131 % con_name)
1132
1133 con_driver = con_config['driver']
1134
1135 # TODO(jhesketh): load the required class automatically
1136 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001137 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1138 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001139 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001140 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001141 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001142 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001143 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001144 self.event_queues.append(
1145 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001146 setattr(self, 'fake_' + con_name,
1147 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001148 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001149 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001150 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1151 else:
1152 raise Exception("Unknown driver, %s, for connection %s"
1153 % (con_config['driver'], con_name))
1154
1155 # If the [gerrit] or [smtp] sections still exist, load them in as a
1156 # connection named 'gerrit' or 'smtp' respectfully
1157
1158 if 'gerrit' in self.config.sections():
1159 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001160 self.event_queues.append(
1161 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001162 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001163 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001164 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001165
1166 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001167 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001168 zuul.connection.smtp.SMTPConnection(
1169 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001170
James E. Blair83005782015-12-11 14:46:03 -08001171 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001172 # This creates the per-test configuration object. It can be
1173 # overriden by subclasses, but should not need to be since it
1174 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001176 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001177 if hasattr(self, 'tenant_config_file'):
1178 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001179 git_path = os.path.join(
1180 os.path.dirname(
1181 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1182 'git')
1183 if os.path.exists(git_path):
1184 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001185 project = reponame.replace('_', '/')
1186 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001187 os.path.join(git_path, reponame))
1188
1189 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001190 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001191
1192 files = {}
1193 for (dirpath, dirnames, filenames) in os.walk(source_path):
1194 for filename in filenames:
1195 test_tree_filepath = os.path.join(dirpath, filename)
1196 common_path = os.path.commonprefix([test_tree_filepath,
1197 source_path])
1198 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1199 with open(test_tree_filepath, 'r') as f:
1200 content = f.read()
1201 files[relative_filepath] = content
1202 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001203 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001204
Clark Boylanb640e052014-04-03 16:41:46 -07001205 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001206 # Make sure that git.Repo objects have been garbage collected.
1207 repos = []
1208 gc.collect()
1209 for obj in gc.get_objects():
1210 if isinstance(obj, git.Repo):
1211 repos.append(obj)
1212 self.assertEqual(len(repos), 0)
1213 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001214 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001215 for tenant in self.sched.abide.tenants.values():
1216 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001217 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001218 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001219
1220 def shutdown(self):
1221 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001223 self.merge_server.stop()
1224 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001225 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001226 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001227 self.sched.stop()
1228 self.sched.join()
1229 self.statsd.stop()
1230 self.statsd.join()
1231 self.webapp.stop()
1232 self.webapp.join()
1233 self.rpc.stop()
1234 self.rpc.join()
1235 self.gearman_server.shutdown()
1236 threads = threading.enumerate()
1237 if len(threads) > 1:
1238 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001239
1240 def init_repo(self, project):
1241 parts = project.split('/')
1242 path = os.path.join(self.upstream_root, *parts[:-1])
1243 if not os.path.exists(path):
1244 os.makedirs(path)
1245 path = os.path.join(self.upstream_root, project)
1246 repo = git.Repo.init(path)
1247
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001248 with repo.config_writer() as config_writer:
1249 config_writer.set_value('user', 'email', 'user@example.com')
1250 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001251
Clark Boylanb640e052014-04-03 16:41:46 -07001252 repo.index.commit('initial commit')
1253 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001254
James E. Blair97d902e2014-08-21 13:25:56 -07001255 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001256 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001257 repo.git.clean('-x', '-f', '-d')
1258
James E. Blair97d902e2014-08-21 13:25:56 -07001259 def create_branch(self, project, branch):
1260 path = os.path.join(self.upstream_root, project)
1261 repo = git.Repo.init(path)
1262 fn = os.path.join(path, 'README')
1263
1264 branch_head = repo.create_head(branch)
1265 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001266 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001267 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001268 f.close()
1269 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001270 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001271
James E. Blair97d902e2014-08-21 13:25:56 -07001272 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001273 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001274 repo.git.clean('-x', '-f', '-d')
1275
Sachi King9f16d522016-03-16 12:20:45 +11001276 def create_commit(self, project):
1277 path = os.path.join(self.upstream_root, project)
1278 repo = git.Repo(path)
1279 repo.head.reference = repo.heads['master']
1280 file_name = os.path.join(path, 'README')
1281 with open(file_name, 'a') as f:
1282 f.write('creating fake commit\n')
1283 repo.index.add([file_name])
1284 commit = repo.index.commit('Creating a fake commit')
1285 return commit.hexsha
1286
James E. Blairb8c16472015-05-05 14:55:26 -07001287 def orderedRelease(self):
1288 # Run one build at a time to ensure non-race order:
1289 while len(self.builds):
1290 self.release(self.builds[0])
1291 self.waitUntilSettled()
1292
Clark Boylanb640e052014-04-03 16:41:46 -07001293 def release(self, job):
1294 if isinstance(job, FakeBuild):
1295 job.release()
1296 else:
1297 job.waiting = False
1298 self.log.debug("Queued job %s released" % job.unique)
1299 self.gearman_server.wakeConnections()
1300
1301 def getParameter(self, job, name):
1302 if isinstance(job, FakeBuild):
1303 return job.parameters[name]
1304 else:
1305 parameters = json.loads(job.arguments)
1306 return parameters[name]
1307
Clark Boylanb640e052014-04-03 16:41:46 -07001308 def haveAllBuildsReported(self):
1309 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001311 return False
1312 # Find out if every build that the worker has completed has been
1313 # reported back to Zuul. If it hasn't then that means a Gearman
1314 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001315 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001316 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001317 if not zbuild:
1318 # It has already been reported
1319 continue
1320 # It hasn't been reported yet.
1321 return False
1322 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001323 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001324 if connection.state == 'GRAB_WAIT':
1325 return False
1326 return True
1327
1328 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001329 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001330 for build in builds:
1331 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001332 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001333 for j in conn.related_jobs.values():
1334 if j.unique == build.uuid:
1335 client_job = j
1336 break
1337 if not client_job:
1338 self.log.debug("%s is not known to the gearman client" %
1339 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001340 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001341 if not client_job.handle:
1342 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001343 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001344 server_job = self.gearman_server.jobs.get(client_job.handle)
1345 if not server_job:
1346 self.log.debug("%s is not known to the gearman server" %
1347 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001348 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001349 if not hasattr(server_job, 'waiting'):
1350 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001351 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001352 if server_job.waiting:
1353 continue
James E. Blair17302972016-08-10 16:11:42 -07001354 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001355 self.log.debug("%s has not reported start" % build)
1356 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001357 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001358 if worker_build:
1359 if worker_build.isWaiting():
1360 continue
1361 else:
1362 self.log.debug("%s is running" % worker_build)
1363 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001364 else:
James E. Blair962220f2016-08-03 11:22:38 -07001365 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001366 return False
1367 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001368
Jan Hruban6b71aff2015-10-22 16:58:08 +02001369 def eventQueuesEmpty(self):
1370 for queue in self.event_queues:
1371 yield queue.empty()
1372
1373 def eventQueuesJoin(self):
1374 for queue in self.event_queues:
1375 queue.join()
1376
Clark Boylanb640e052014-04-03 16:41:46 -07001377 def waitUntilSettled(self):
1378 self.log.debug("Waiting until settled...")
1379 start = time.time()
1380 while True:
1381 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001382 self.log.debug("Queue status:")
1383 for queue in self.event_queues:
1384 self.log.debug(" %s: %s" % (queue, queue.empty()))
1385 self.log.debug("All builds waiting: %s" %
1386 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001387 self.log.debug("All builds reported: %s" %
1388 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001389 raise Exception("Timeout waiting for Zuul to settle")
1390 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001391
James E. Blaire1767bc2016-08-02 10:00:27 -07001392 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001393 # have all build states propogated to zuul?
1394 if self.haveAllBuildsReported():
1395 # Join ensures that the queue is empty _and_ events have been
1396 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001397 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001399 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001400 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.haveAllBuildsReported() and
1402 self.areAllBuildsWaiting()):
1403 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001404 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001405 self.log.debug("...settled.")
1406 return
1407 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001408 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001409 self.sched.wake_event.wait(0.1)
1410
1411 def countJobResults(self, jobs, result):
1412 jobs = filter(lambda x: x.result == result, jobs)
1413 return len(jobs)
1414
James E. Blair96c6bf82016-01-15 16:20:40 -08001415 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001416 for job in self.history:
1417 if (job.name == name and
1418 (project is None or
1419 job.parameters['ZUUL_PROJECT'] == project)):
1420 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001421 raise Exception("Unable to find job %s in history" % name)
1422
1423 def assertEmptyQueues(self):
1424 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001425 for tenant in self.sched.abide.tenants.values():
1426 for pipeline in tenant.layout.pipelines.values():
1427 for queue in pipeline.queues:
1428 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001429 print('pipeline %s queue %s contents %s' % (
1430 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001431 self.assertEqual(len(queue.queue), 0,
1432 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001433
1434 def assertReportedStat(self, key, value=None, kind=None):
1435 start = time.time()
1436 while time.time() < (start + 5):
1437 for stat in self.statsd.stats:
1438 pprint.pprint(self.statsd.stats)
1439 k, v = stat.split(':')
1440 if key == k:
1441 if value is None and kind is None:
1442 return
1443 elif value:
1444 if value == v:
1445 return
1446 elif kind:
1447 if v.endswith('|' + kind):
1448 return
1449 time.sleep(0.1)
1450
1451 pprint.pprint(self.statsd.stats)
1452 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001453
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001454 def assertBuilds(self, builds):
1455 """Assert that the running builds are as described.
1456
1457 The list of running builds is examined and must match exactly
1458 the list of builds described by the input.
1459
1460 :arg list builds: A list of dictionaries. Each item in the
1461 list must match the corresponding build in the build
1462 history, and each element of the dictionary must match the
1463 corresponding attribute of the build.
1464
1465 """
James E. Blair3158e282016-08-19 09:34:11 -07001466 try:
1467 self.assertEqual(len(self.builds), len(builds))
1468 for i, d in enumerate(builds):
1469 for k, v in d.items():
1470 self.assertEqual(
1471 getattr(self.builds[i], k), v,
1472 "Element %i in builds does not match" % (i,))
1473 except Exception:
1474 for build in self.builds:
1475 self.log.error("Running build: %s" % build)
1476 else:
1477 self.log.error("No running builds")
1478 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001479
James E. Blairb536ecc2016-08-31 10:11:42 -07001480 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001481 """Assert that the completed builds are as described.
1482
1483 The list of completed builds is examined and must match
1484 exactly the list of builds described by the input.
1485
1486 :arg list history: A list of dictionaries. Each item in the
1487 list must match the corresponding build in the build
1488 history, and each element of the dictionary must match the
1489 corresponding attribute of the build.
1490
James E. Blairb536ecc2016-08-31 10:11:42 -07001491 :arg bool ordered: If true, the history must match the order
1492 supplied, if false, the builds are permitted to have
1493 arrived in any order.
1494
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001495 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001496 def matches(history_item, item):
1497 for k, v in item.items():
1498 if getattr(history_item, k) != v:
1499 return False
1500 return True
James E. Blair3158e282016-08-19 09:34:11 -07001501 try:
1502 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001503 if ordered:
1504 for i, d in enumerate(history):
1505 if not matches(self.history[i], d):
1506 raise Exception(
1507 "Element %i in history does not match" % (i,))
1508 else:
1509 unseen = self.history[:]
1510 for i, d in enumerate(history):
1511 found = False
1512 for unseen_item in unseen:
1513 if matches(unseen_item, d):
1514 found = True
1515 unseen.remove(unseen_item)
1516 break
1517 if not found:
1518 raise Exception("No match found for element %i "
1519 "in history" % (i,))
1520 if unseen:
1521 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001522 except Exception:
1523 for build in self.history:
1524 self.log.error("Completed build: %s" % build)
1525 else:
1526 self.log.error("No completed builds")
1527 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001528
James E. Blair59fdbac2015-12-07 17:08:06 -08001529 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001530 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1531
1532 def updateConfigLayout(self, path):
1533 root = os.path.join(self.test_root, "config")
1534 os.makedirs(root)
1535 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1536 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001537- tenant:
1538 name: openstack
1539 source:
1540 gerrit:
1541 config-repos:
1542 - %s
1543 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001544 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001545 self.config.set('zuul', 'tenant_config',
1546 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001547
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001548 def addCommitToRepo(self, project, message, files,
1549 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001550 path = os.path.join(self.upstream_root, project)
1551 repo = git.Repo(path)
1552 repo.head.reference = branch
1553 zuul.merger.merger.reset_repo_to_head(repo)
1554 for fn, content in files.items():
1555 fn = os.path.join(path, fn)
1556 with open(fn, 'w') as f:
1557 f.write(content)
1558 repo.index.add([fn])
1559 commit = repo.index.commit(message)
1560 repo.heads[branch].commit = commit
1561 repo.head.reference = branch
1562 repo.git.clean('-x', '-f', '-d')
1563 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001564 if tag:
1565 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001566
James E. Blair7fc8daa2016-08-08 15:37:15 -07001567 def addEvent(self, connection, event):
1568 """Inject a Fake (Gerrit) event.
1569
1570 This method accepts a JSON-encoded event and simulates Zuul
1571 having received it from Gerrit. It could (and should)
1572 eventually apply to any connection type, but is currently only
1573 used with Gerrit connections. The name of the connection is
1574 used to look up the corresponding server, and the event is
1575 simulated as having been received by all Zuul connections
1576 attached to that server. So if two Gerrit connections in Zuul
1577 are connected to the same Gerrit server, and you invoke this
1578 method specifying the name of one of them, the event will be
1579 received by both.
1580
1581 .. note::
1582
1583 "self.fake_gerrit.addEvent" calls should be migrated to
1584 this method.
1585
1586 :arg str connection: The name of the connection corresponding
1587 to the gerrit server.
1588 :arg str event: The JSON-encoded event.
1589
1590 """
1591 specified_conn = self.connections.connections[connection]
1592 for conn in self.connections.connections.values():
1593 if (isinstance(conn, specified_conn.__class__) and
1594 specified_conn.server == conn.server):
1595 conn.addEvent(event)
1596
James E. Blair3f876d52016-07-22 13:07:14 -07001597
1598class AnsibleZuulTestCase(ZuulTestCase):
1599 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001600 run_ansible = True