blob: 6d2c64d8b7efead06d9a5c9909fa59b32d28c230 [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
1308 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001309 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001310 while True:
1311 done = True
1312 for connection in self.gearman_server.active_connections:
1313 if (connection.functions and
1314 connection.client_id not in ['Zuul RPC Listener',
1315 'Zuul Merger']):
1316 done = False
1317 if done:
1318 break
1319 time.sleep(0)
1320 self.gearman_server.functions = set()
1321 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001322
1323 def haveAllBuildsReported(self):
1324 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001325 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001326 return False
1327 # Find out if every build that the worker has completed has been
1328 # reported back to Zuul. If it hasn't then that means a Gearman
1329 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001330 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001331 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001332 if not zbuild:
1333 # It has already been reported
1334 continue
1335 # It hasn't been reported yet.
1336 return False
1337 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001338 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001339 if connection.state == 'GRAB_WAIT':
1340 return False
1341 return True
1342
1343 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001344 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001345 for build in builds:
1346 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001347 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001348 for j in conn.related_jobs.values():
1349 if j.unique == build.uuid:
1350 client_job = j
1351 break
1352 if not client_job:
1353 self.log.debug("%s is not known to the gearman client" %
1354 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001355 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001356 if not client_job.handle:
1357 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001358 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001359 server_job = self.gearman_server.jobs.get(client_job.handle)
1360 if not server_job:
1361 self.log.debug("%s is not known to the gearman server" %
1362 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001363 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001364 if not hasattr(server_job, 'waiting'):
1365 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001366 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001367 if server_job.waiting:
1368 continue
James E. Blair17302972016-08-10 16:11:42 -07001369 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001370 self.log.debug("%s has not reported start" % build)
1371 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001372 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001373 if worker_build:
1374 if worker_build.isWaiting():
1375 continue
1376 else:
1377 self.log.debug("%s is running" % worker_build)
1378 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001379 else:
James E. Blair962220f2016-08-03 11:22:38 -07001380 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001381 return False
1382 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001383
Jan Hruban6b71aff2015-10-22 16:58:08 +02001384 def eventQueuesEmpty(self):
1385 for queue in self.event_queues:
1386 yield queue.empty()
1387
1388 def eventQueuesJoin(self):
1389 for queue in self.event_queues:
1390 queue.join()
1391
Clark Boylanb640e052014-04-03 16:41:46 -07001392 def waitUntilSettled(self):
1393 self.log.debug("Waiting until settled...")
1394 start = time.time()
1395 while True:
1396 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001397 self.log.debug("Queue status:")
1398 for queue in self.event_queues:
1399 self.log.debug(" %s: %s" % (queue, queue.empty()))
1400 self.log.debug("All builds waiting: %s" %
1401 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001402 self.log.debug("All builds reported: %s" %
1403 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001404 raise Exception("Timeout waiting for Zuul to settle")
1405 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001406
James E. Blaire1767bc2016-08-02 10:00:27 -07001407 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001408 # have all build states propogated to zuul?
1409 if self.haveAllBuildsReported():
1410 # Join ensures that the queue is empty _and_ events have been
1411 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001412 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001414 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001415 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.haveAllBuildsReported() and
1417 self.areAllBuildsWaiting()):
1418 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001419 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001420 self.log.debug("...settled.")
1421 return
1422 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001423 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001424 self.sched.wake_event.wait(0.1)
1425
1426 def countJobResults(self, jobs, result):
1427 jobs = filter(lambda x: x.result == result, jobs)
1428 return len(jobs)
1429
James E. Blair96c6bf82016-01-15 16:20:40 -08001430 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001431 for job in self.history:
1432 if (job.name == name and
1433 (project is None or
1434 job.parameters['ZUUL_PROJECT'] == project)):
1435 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001436 raise Exception("Unable to find job %s in history" % name)
1437
1438 def assertEmptyQueues(self):
1439 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001440 for tenant in self.sched.abide.tenants.values():
1441 for pipeline in tenant.layout.pipelines.values():
1442 for queue in pipeline.queues:
1443 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001444 print('pipeline %s queue %s contents %s' % (
1445 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001446 self.assertEqual(len(queue.queue), 0,
1447 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001448
1449 def assertReportedStat(self, key, value=None, kind=None):
1450 start = time.time()
1451 while time.time() < (start + 5):
1452 for stat in self.statsd.stats:
1453 pprint.pprint(self.statsd.stats)
1454 k, v = stat.split(':')
1455 if key == k:
1456 if value is None and kind is None:
1457 return
1458 elif value:
1459 if value == v:
1460 return
1461 elif kind:
1462 if v.endswith('|' + kind):
1463 return
1464 time.sleep(0.1)
1465
1466 pprint.pprint(self.statsd.stats)
1467 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001468
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001469 def assertBuilds(self, builds):
1470 """Assert that the running builds are as described.
1471
1472 The list of running builds is examined and must match exactly
1473 the list of builds described by the input.
1474
1475 :arg list builds: A list of dictionaries. Each item in the
1476 list must match the corresponding build in the build
1477 history, and each element of the dictionary must match the
1478 corresponding attribute of the build.
1479
1480 """
James E. Blair3158e282016-08-19 09:34:11 -07001481 try:
1482 self.assertEqual(len(self.builds), len(builds))
1483 for i, d in enumerate(builds):
1484 for k, v in d.items():
1485 self.assertEqual(
1486 getattr(self.builds[i], k), v,
1487 "Element %i in builds does not match" % (i,))
1488 except Exception:
1489 for build in self.builds:
1490 self.log.error("Running build: %s" % build)
1491 else:
1492 self.log.error("No running builds")
1493 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001494
James E. Blairb536ecc2016-08-31 10:11:42 -07001495 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001496 """Assert that the completed builds are as described.
1497
1498 The list of completed builds is examined and must match
1499 exactly the list of builds described by the input.
1500
1501 :arg list history: A list of dictionaries. Each item in the
1502 list must match the corresponding build in the build
1503 history, and each element of the dictionary must match the
1504 corresponding attribute of the build.
1505
James E. Blairb536ecc2016-08-31 10:11:42 -07001506 :arg bool ordered: If true, the history must match the order
1507 supplied, if false, the builds are permitted to have
1508 arrived in any order.
1509
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001510 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001511 def matches(history_item, item):
1512 for k, v in item.items():
1513 if getattr(history_item, k) != v:
1514 return False
1515 return True
James E. Blair3158e282016-08-19 09:34:11 -07001516 try:
1517 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001518 if ordered:
1519 for i, d in enumerate(history):
1520 if not matches(self.history[i], d):
1521 raise Exception(
1522 "Element %i in history does not match" % (i,))
1523 else:
1524 unseen = self.history[:]
1525 for i, d in enumerate(history):
1526 found = False
1527 for unseen_item in unseen:
1528 if matches(unseen_item, d):
1529 found = True
1530 unseen.remove(unseen_item)
1531 break
1532 if not found:
1533 raise Exception("No match found for element %i "
1534 "in history" % (i,))
1535 if unseen:
1536 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001537 except Exception:
1538 for build in self.history:
1539 self.log.error("Completed build: %s" % build)
1540 else:
1541 self.log.error("No completed builds")
1542 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001543
James E. Blair59fdbac2015-12-07 17:08:06 -08001544 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001545 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1546
1547 def updateConfigLayout(self, path):
1548 root = os.path.join(self.test_root, "config")
1549 os.makedirs(root)
1550 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1551 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001552- tenant:
1553 name: openstack
1554 source:
1555 gerrit:
1556 config-repos:
1557 - %s
1558 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001559 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001560 self.config.set('zuul', 'tenant_config',
1561 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001562
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001563 def addCommitToRepo(self, project, message, files,
1564 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001565 path = os.path.join(self.upstream_root, project)
1566 repo = git.Repo(path)
1567 repo.head.reference = branch
1568 zuul.merger.merger.reset_repo_to_head(repo)
1569 for fn, content in files.items():
1570 fn = os.path.join(path, fn)
1571 with open(fn, 'w') as f:
1572 f.write(content)
1573 repo.index.add([fn])
1574 commit = repo.index.commit(message)
1575 repo.heads[branch].commit = commit
1576 repo.head.reference = branch
1577 repo.git.clean('-x', '-f', '-d')
1578 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001579 if tag:
1580 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001581
James E. Blair7fc8daa2016-08-08 15:37:15 -07001582 def addEvent(self, connection, event):
1583 """Inject a Fake (Gerrit) event.
1584
1585 This method accepts a JSON-encoded event and simulates Zuul
1586 having received it from Gerrit. It could (and should)
1587 eventually apply to any connection type, but is currently only
1588 used with Gerrit connections. The name of the connection is
1589 used to look up the corresponding server, and the event is
1590 simulated as having been received by all Zuul connections
1591 attached to that server. So if two Gerrit connections in Zuul
1592 are connected to the same Gerrit server, and you invoke this
1593 method specifying the name of one of them, the event will be
1594 received by both.
1595
1596 .. note::
1597
1598 "self.fake_gerrit.addEvent" calls should be migrated to
1599 this method.
1600
1601 :arg str connection: The name of the connection corresponding
1602 to the gerrit server.
1603 :arg str event: The JSON-encoded event.
1604
1605 """
1606 specified_conn = self.connections.connections[connection]
1607 for conn in self.connections.connections.values():
1608 if (isinstance(conn, specified_conn.__class__) and
1609 specified_conn.server == conn.server):
1610 conn.addEvent(event)
1611
James E. Blair3f876d52016-07-22 13:07:14 -07001612
1613class AnsibleZuulTestCase(ZuulTestCase):
1614 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001615 run_ansible = True