blob: 9e3c07bfdacf344516ab81d587f69dbdade0ebc2 [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.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
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
James E. Blair498059b2016-12-20 13:50:13 -080042import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080043import kazoo.exceptions
Clark Boylanb640e052014-04-03 16:41:46 -070044import statsd
45import testtools
Clint Byrum3343e3e2016-11-15 16:05:03 -080046from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070047
Joshua Hesketh352264b2015-08-11 23:42:08 +100048import zuul.connection.gerrit
49import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070050import zuul.scheduler
51import zuul.webapp
52import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100053import zuul.launcher.server
54import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080056import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070058import zuul.merger.merger
59import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070060import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.reporter.gerrit
62import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110063import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.trigger.gerrit
65import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070066import zuul.trigger.zuultrigger
James E. Blairdce6cea2016-12-20 16:45:32 -080067import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070068
69FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
70 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070071USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070072
73logging.basicConfig(level=logging.DEBUG,
74 format='%(asctime)s %(name)-32s '
75 '%(levelname)-8s %(message)s')
76
77
78def repack_repo(path):
79 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
80 output = subprocess.Popen(cmd, close_fds=True,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE)
83 out = output.communicate()
84 if output.returncode:
85 raise Exception("git repack returned %d" % output.returncode)
86 return out
87
88
89def random_sha1():
90 return hashlib.sha1(str(random.random())).hexdigest()
91
92
James E. Blaira190f3b2015-01-05 14:56:54 -080093def iterate_timeout(max_seconds, purpose):
94 start = time.time()
95 count = 0
96 while (time.time() < start + max_seconds):
97 count += 1
98 yield count
99 time.sleep(0)
100 raise Exception("Timeout waiting for %s" % purpose)
101
102
Clark Boylanb640e052014-04-03 16:41:46 -0700103class ChangeReference(git.Reference):
104 _common_path_default = "refs/changes"
105 _points_to_commits_only = True
106
107
108class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700109 categories = {'approved': ('Approved', -1, 1),
110 'code-review': ('Code-Review', -2, 2),
111 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700112
113 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700114 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700115 self.gerrit = gerrit
116 self.reported = 0
117 self.queried = 0
118 self.patchsets = []
119 self.number = number
120 self.project = project
121 self.branch = branch
122 self.subject = subject
123 self.latest_patchset = 0
124 self.depends_on_change = None
125 self.needed_by_changes = []
126 self.fail_merge = False
127 self.messages = []
128 self.data = {
129 'branch': branch,
130 'comments': [],
131 'commitMessage': subject,
132 'createdOn': time.time(),
133 'id': 'I' + random_sha1(),
134 'lastUpdated': time.time(),
135 'number': str(number),
136 'open': status == 'NEW',
137 'owner': {'email': 'user@example.com',
138 'name': 'User Name',
139 'username': 'username'},
140 'patchSets': self.patchsets,
141 'project': project,
142 'status': status,
143 'subject': subject,
144 'submitRecords': [],
145 'url': 'https://hostname/%s' % number}
146
147 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700148 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700149 self.data['submitRecords'] = self.getSubmitRecords()
150 self.open = status == 'NEW'
151
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700152 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700153 path = os.path.join(self.upstream_root, self.project)
154 repo = git.Repo(path)
155 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
156 self.latest_patchset),
157 'refs/tags/init')
158 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700159 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700160 repo.git.clean('-x', '-f', '-d')
161
162 path = os.path.join(self.upstream_root, self.project)
163 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700164 for fn, content in files.items():
165 fn = os.path.join(path, fn)
166 with open(fn, 'w') as f:
167 f.write(content)
168 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700169 else:
170 for fni in range(100):
171 fn = os.path.join(path, str(fni))
172 f = open(fn, 'w')
173 for ci in range(4096):
174 f.write(random.choice(string.printable))
175 f.close()
176 repo.index.add([fn])
177
178 r = repo.index.commit(msg)
179 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700180 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700181 repo.git.clean('-x', '-f', '-d')
182 repo.heads['master'].checkout()
183 return r
184
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700185 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700186 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700187 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700188 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700189 data = ("test %s %s %s\n" %
190 (self.branch, self.number, self.latest_patchset))
191 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700192 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700194 ps_files = [{'file': '/COMMIT_MSG',
195 'type': 'ADDED'},
196 {'file': 'README',
197 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700198 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700199 ps_files.append({'file': f, 'type': 'ADDED'})
200 d = {'approvals': [],
201 'createdOn': time.time(),
202 'files': ps_files,
203 'number': str(self.latest_patchset),
204 'ref': 'refs/changes/1/%s/%s' % (self.number,
205 self.latest_patchset),
206 'revision': c.hexsha,
207 'uploader': {'email': 'user@example.com',
208 'name': 'User name',
209 'username': 'user'}}
210 self.data['currentPatchSet'] = d
211 self.patchsets.append(d)
212 self.data['submitRecords'] = self.getSubmitRecords()
213
214 def getPatchsetCreatedEvent(self, patchset):
215 event = {"type": "patchset-created",
216 "change": {"project": self.project,
217 "branch": self.branch,
218 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
219 "number": str(self.number),
220 "subject": self.subject,
221 "owner": {"name": "User Name"},
222 "url": "https://hostname/3"},
223 "patchSet": self.patchsets[patchset - 1],
224 "uploader": {"name": "User Name"}}
225 return event
226
227 def getChangeRestoredEvent(self):
228 event = {"type": "change-restored",
229 "change": {"project": self.project,
230 "branch": self.branch,
231 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
232 "number": str(self.number),
233 "subject": self.subject,
234 "owner": {"name": "User Name"},
235 "url": "https://hostname/3"},
236 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100237 "patchSet": self.patchsets[-1],
238 "reason": ""}
239 return event
240
241 def getChangeAbandonedEvent(self):
242 event = {"type": "change-abandoned",
243 "change": {"project": self.project,
244 "branch": self.branch,
245 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
246 "number": str(self.number),
247 "subject": self.subject,
248 "owner": {"name": "User Name"},
249 "url": "https://hostname/3"},
250 "abandoner": {"name": "User Name"},
251 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700252 "reason": ""}
253 return event
254
255 def getChangeCommentEvent(self, patchset):
256 event = {"type": "comment-added",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "patchSet": self.patchsets[patchset - 1],
265 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700266 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700267 "description": "Code-Review",
268 "value": "0"}],
269 "comment": "This is a comment"}
270 return event
271
Joshua Hesketh642824b2014-07-01 17:54:59 +1000272 def addApproval(self, category, value, username='reviewer_john',
273 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700274 if not granted_on:
275 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000276 approval = {
277 'description': self.categories[category][0],
278 'type': category,
279 'value': str(value),
280 'by': {
281 'username': username,
282 'email': username + '@example.com',
283 },
284 'grantedOn': int(granted_on)
285 }
Clark Boylanb640e052014-04-03 16:41:46 -0700286 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
287 if x['by']['username'] == username and x['type'] == category:
288 del self.patchsets[-1]['approvals'][i]
289 self.patchsets[-1]['approvals'].append(approval)
290 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000291 'author': {'email': 'author@example.com',
292 'name': 'Patchset Author',
293 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700294 'change': {'branch': self.branch,
295 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
296 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000297 'owner': {'email': 'owner@example.com',
298 'name': 'Change Owner',
299 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700300 'project': self.project,
301 'subject': self.subject,
302 'topic': 'master',
303 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000304 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700305 'patchSet': self.patchsets[-1],
306 'type': 'comment-added'}
307 self.data['submitRecords'] = self.getSubmitRecords()
308 return json.loads(json.dumps(event))
309
310 def getSubmitRecords(self):
311 status = {}
312 for cat in self.categories.keys():
313 status[cat] = 0
314
315 for a in self.patchsets[-1]['approvals']:
316 cur = status[a['type']]
317 cat_min, cat_max = self.categories[a['type']][1:]
318 new = int(a['value'])
319 if new == cat_min:
320 cur = new
321 elif abs(new) > abs(cur):
322 cur = new
323 status[a['type']] = cur
324
325 labels = []
326 ok = True
327 for typ, cat in self.categories.items():
328 cur = status[typ]
329 cat_min, cat_max = cat[1:]
330 if cur == cat_min:
331 value = 'REJECT'
332 ok = False
333 elif cur == cat_max:
334 value = 'OK'
335 else:
336 value = 'NEED'
337 ok = False
338 labels.append({'label': cat[0], 'status': value})
339 if ok:
340 return [{'status': 'OK'}]
341 return [{'status': 'NOT_READY',
342 'labels': labels}]
343
344 def setDependsOn(self, other, patchset):
345 self.depends_on_change = other
346 d = {'id': other.data['id'],
347 'number': other.data['number'],
348 'ref': other.patchsets[patchset - 1]['ref']
349 }
350 self.data['dependsOn'] = [d]
351
352 other.needed_by_changes.append(self)
353 needed = other.data.get('neededBy', [])
354 d = {'id': self.data['id'],
355 'number': self.data['number'],
356 'ref': self.patchsets[patchset - 1]['ref'],
357 'revision': self.patchsets[patchset - 1]['revision']
358 }
359 needed.append(d)
360 other.data['neededBy'] = needed
361
362 def query(self):
363 self.queried += 1
364 d = self.data.get('dependsOn')
365 if d:
366 d = d[0]
367 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
368 d['isCurrentPatchSet'] = True
369 else:
370 d['isCurrentPatchSet'] = False
371 return json.loads(json.dumps(self.data))
372
373 def setMerged(self):
374 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000375 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700376 return
377 if self.fail_merge:
378 return
379 self.data['status'] = 'MERGED'
380 self.open = False
381
382 path = os.path.join(self.upstream_root, self.project)
383 repo = git.Repo(path)
384 repo.heads[self.branch].commit = \
385 repo.commit(self.patchsets[-1]['revision'])
386
387 def setReported(self):
388 self.reported += 1
389
390
Joshua Hesketh352264b2015-08-11 23:42:08 +1000391class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700392 """A Fake Gerrit connection for use in tests.
393
394 This subclasses
395 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
396 ability for tests to add changes to the fake Gerrit it represents.
397 """
398
Joshua Hesketh352264b2015-08-11 23:42:08 +1000399 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700400
Joshua Hesketh352264b2015-08-11 23:42:08 +1000401 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700402 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000403 super(FakeGerritConnection, self).__init__(connection_name,
404 connection_config)
405
James E. Blair7fc8daa2016-08-08 15:37:15 -0700406 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700407 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
408 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000409 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700410 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200411 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700412
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700413 def addFakeChange(self, project, branch, subject, status='NEW',
414 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700415 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700416 self.change_number += 1
417 c = FakeChange(self, self.change_number, project, branch, subject,
418 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700419 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700420 self.changes[self.change_number] = c
421 return c
422
Clark Boylanb640e052014-04-03 16:41:46 -0700423 def review(self, project, changeid, message, action):
424 number, ps = changeid.split(',')
425 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000426
427 # Add the approval back onto the change (ie simulate what gerrit would
428 # do).
429 # Usually when zuul leaves a review it'll create a feedback loop where
430 # zuul's review enters another gerrit event (which is then picked up by
431 # zuul). However, we can't mimic this behaviour (by adding this
432 # approval event into the queue) as it stops jobs from checking what
433 # happens before this event is triggered. If a job needs to see what
434 # happens they can add their own verified event into the queue.
435 # Nevertheless, we can update change with the new review in gerrit.
436
James E. Blair8b5408c2016-08-08 15:37:46 -0700437 for cat in action.keys():
438 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000439 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000440
James E. Blair8b5408c2016-08-08 15:37:46 -0700441 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442 if 'label' in action:
443 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000444 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000445
Clark Boylanb640e052014-04-03 16:41:46 -0700446 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000447
Clark Boylanb640e052014-04-03 16:41:46 -0700448 if 'submit' in action:
449 change.setMerged()
450 if message:
451 change.setReported()
452
453 def query(self, number):
454 change = self.changes.get(int(number))
455 if change:
456 return change.query()
457 return {}
458
James E. Blairc494d542014-08-06 09:23:52 -0700459 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700460 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700461 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800462 if query.startswith('change:'):
463 # Query a specific changeid
464 changeid = query[len('change:'):]
465 l = [change.query() for change in self.changes.values()
466 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700467 elif query.startswith('message:'):
468 # Query the content of a commit message
469 msg = query[len('message:'):].strip()
470 l = [change.query() for change in self.changes.values()
471 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800472 else:
473 # Query all open changes
474 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700475 return l
James E. Blairc494d542014-08-06 09:23:52 -0700476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700478 pass
479
Joshua Hesketh352264b2015-08-11 23:42:08 +1000480 def getGitUrl(self, project):
481 return os.path.join(self.upstream_root, project.name)
482
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800483 def _getGitwebUrl(self, project, sha=None):
484 return self.getGitwebUrl(project, sha)
485
Clark Boylanb640e052014-04-03 16:41:46 -0700486
487class BuildHistory(object):
488 def __init__(self, **kw):
489 self.__dict__.update(kw)
490
491 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700492 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
493 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700494
495
496class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200497 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700498 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700499 self.url = url
500
501 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700502 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700503 path = res.path
504 project = '/'.join(path.split('/')[2:-2])
505 ret = '001e# service=git-upload-pack\n'
506 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
507 'multi_ack thin-pack side-band side-band-64k ofs-delta '
508 'shallow no-progress include-tag multi_ack_detailed no-done\n')
509 path = os.path.join(self.upstream_root, project)
510 repo = git.Repo(path)
511 for ref in repo.refs:
512 r = ref.object.hexsha + ' ' + ref.path + '\n'
513 ret += '%04x%s' % (len(r) + 4, r)
514 ret += '0000'
515 return ret
516
517
Clark Boylanb640e052014-04-03 16:41:46 -0700518class FakeStatsd(threading.Thread):
519 def __init__(self):
520 threading.Thread.__init__(self)
521 self.daemon = True
522 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
523 self.sock.bind(('', 0))
524 self.port = self.sock.getsockname()[1]
525 self.wake_read, self.wake_write = os.pipe()
526 self.stats = []
527
528 def run(self):
529 while True:
530 poll = select.poll()
531 poll.register(self.sock, select.POLLIN)
532 poll.register(self.wake_read, select.POLLIN)
533 ret = poll.poll()
534 for (fd, event) in ret:
535 if fd == self.sock.fileno():
536 data = self.sock.recvfrom(1024)
537 if not data:
538 return
539 self.stats.append(data[0])
540 if fd == self.wake_read:
541 return
542
543 def stop(self):
544 os.write(self.wake_write, '1\n')
545
546
James E. Blaire1767bc2016-08-02 10:00:27 -0700547class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700548 log = logging.getLogger("zuul.test")
549
James E. Blair34776ee2016-08-25 13:53:54 -0700550 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700551 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700552 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700553 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700554 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700555 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700556 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700557 # TODOv3(jeblair): self.node is really "the image of the node
558 # assigned". We should rename it (self.node_image?) if we
559 # keep using it like this, or we may end up exposing more of
560 # the complexity around multi-node jobs here
561 # (self.nodes[0].image?)
562 self.node = None
563 if len(self.parameters.get('nodes')) == 1:
564 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700566 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700567 self.wait_condition = threading.Condition()
568 self.waiting = False
569 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500570 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700571 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700572 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700573 self.changes = None
574 if 'ZUUL_CHANGE_IDS' in self.parameters:
575 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700576
James E. Blair3158e282016-08-19 09:34:11 -0700577 def __repr__(self):
578 waiting = ''
579 if self.waiting:
580 waiting = ' [waiting]'
581 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
582
Clark Boylanb640e052014-04-03 16:41:46 -0700583 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700584 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700585 self.wait_condition.acquire()
586 self.wait_condition.notify()
587 self.waiting = False
588 self.log.debug("Build %s released" % self.unique)
589 self.wait_condition.release()
590
591 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700592 """Return whether this build is being held.
593
594 :returns: Whether the build is being held.
595 :rtype: bool
596 """
597
Clark Boylanb640e052014-04-03 16:41:46 -0700598 self.wait_condition.acquire()
599 if self.waiting:
600 ret = True
601 else:
602 ret = False
603 self.wait_condition.release()
604 return ret
605
606 def _wait(self):
607 self.wait_condition.acquire()
608 self.waiting = True
609 self.log.debug("Build %s waiting" % self.unique)
610 self.wait_condition.wait()
611 self.wait_condition.release()
612
613 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700614 self.log.debug('Running build %s' % self.unique)
615
James E. Blaire1767bc2016-08-02 10:00:27 -0700616 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700617 self.log.debug('Holding build %s' % self.unique)
618 self._wait()
619 self.log.debug("Build %s continuing" % self.unique)
620
Clark Boylanb640e052014-04-03 16:41:46 -0700621 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700622 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700623 result = 'FAILURE'
624 if self.aborted:
625 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500626 if self.requeue:
627 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700628
629 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700630 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700631
James E. Blaire1767bc2016-08-02 10:00:27 -0700632 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700633
James E. Blaira5dba232016-08-08 15:53:24 -0700634 def shouldFail(self):
635 changes = self.launch_server.fail_tests.get(self.name, [])
636 for change in changes:
637 if self.hasChanges(change):
638 return True
639 return False
640
James E. Blaire7b99a02016-08-05 14:27:34 -0700641 def hasChanges(self, *changes):
642 """Return whether this build has certain changes in its git repos.
643
644 :arg FakeChange changes: One or more changes (varargs) that
645 are expected to be present (in order) in the git repository of
646 the active project.
647
648 :returns: Whether the build has the indicated changes.
649 :rtype: bool
650
651 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800652 for change in changes:
653 path = os.path.join(self.jobdir.git_root, change.project)
654 try:
655 repo = git.Repo(path)
656 except NoSuchPathError as e:
657 self.log.debug('%s' % e)
658 return False
659 ref = self.parameters['ZUUL_REF']
660 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
661 commit_message = '%s-1' % change.subject
662 self.log.debug("Checking if build %s has changes; commit_message "
663 "%s; repo_messages %s" % (self, commit_message,
664 repo_messages))
665 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700666 self.log.debug(" messages do not match")
667 return False
668 self.log.debug(" OK")
669 return True
670
Clark Boylanb640e052014-04-03 16:41:46 -0700671
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000672class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700673 """An Ansible launcher to be used in tests.
674
675 :ivar bool hold_jobs_in_build: If true, when jobs are launched
676 they will report that they have started but then pause until
677 released before reporting completion. This attribute may be
678 changed at any time and will take effect for subsequently
679 launched builds, but previously held builds will still need to
680 be explicitly released.
681
682 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800683 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800685 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700686 self.hold_jobs_in_build = False
687 self.lock = threading.Lock()
688 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700689 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700690 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700691 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800692
James E. Blaira5dba232016-08-08 15:53:24 -0700693 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700694 """Instruct the launcher to report matching builds as failures.
695
696 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700697 :arg Change change: The :py:class:`~tests.base.FakeChange`
698 instance which should cause the job to fail. This job
699 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700700
701 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700702 l = self.fail_tests.get(name, [])
703 l.append(change)
704 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800705
James E. Blair962220f2016-08-03 11:22:38 -0700706 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700707 """Release a held build.
708
709 :arg str regex: A regular expression which, if supplied, will
710 cause only builds with matching names to be released. If
711 not supplied, all builds will be released.
712
713 """
James E. Blair962220f2016-08-03 11:22:38 -0700714 builds = self.running_builds[:]
715 self.log.debug("Releasing build %s (%s)" % (regex,
716 len(self.running_builds)))
717 for build in builds:
718 if not regex or re.match(regex, build.name):
719 self.log.debug("Releasing build %s" %
720 (build.parameters['ZUUL_UUID']))
721 build.release()
722 else:
723 self.log.debug("Not releasing build %s" %
724 (build.parameters['ZUUL_UUID']))
725 self.log.debug("Done releasing builds %s (%s)" %
726 (regex, len(self.running_builds)))
727
James E. Blair17302972016-08-10 16:11:42 -0700728 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700729 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700730 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700731 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700732 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700733 super(RecordingLaunchServer, self).launchJob(job)
734
735 def stopJob(self, job):
736 self.log.debug("handle stop")
737 parameters = json.loads(job.arguments)
738 uuid = parameters['uuid']
739 for build in self.running_builds:
740 if build.unique == uuid:
741 build.aborted = True
742 build.release()
743 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700744
745 def runAnsible(self, jobdir, job):
746 build = self.job_builds[job.unique]
747 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700748
749 if self._run_ansible:
750 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
751 else:
752 result = build.run()
753
754 self.lock.acquire()
755 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700756 BuildHistory(name=build.name, result=result, changes=build.changes,
757 node=build.node, uuid=build.unique,
758 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700759 pipeline=build.parameters['ZUUL_PIPELINE'])
760 )
James E. Blairab7132b2016-08-05 12:36:22 -0700761 self.running_builds.remove(build)
762 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700763 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800764 if build.run_error:
765 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700766 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800767
768
Clark Boylanb640e052014-04-03 16:41:46 -0700769class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700770 """A Gearman server for use in tests.
771
772 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
773 added to the queue but will not be distributed to workers
774 until released. This attribute may be changed at any time and
775 will take effect for subsequently enqueued jobs, but
776 previously held jobs will still need to be explicitly
777 released.
778
779 """
780
Clark Boylanb640e052014-04-03 16:41:46 -0700781 def __init__(self):
782 self.hold_jobs_in_queue = False
783 super(FakeGearmanServer, self).__init__(0)
784
785 def getJobForConnection(self, connection, peek=False):
786 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
787 for job in queue:
788 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500789 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700790 job.waiting = self.hold_jobs_in_queue
791 else:
792 job.waiting = False
793 if job.waiting:
794 continue
795 if job.name in connection.functions:
796 if not peek:
797 queue.remove(job)
798 connection.related_jobs[job.handle] = job
799 job.worker_connection = connection
800 job.running = True
801 return job
802 return None
803
804 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700805 """Release a held job.
806
807 :arg str regex: A regular expression which, if supplied, will
808 cause only jobs with matching names to be released. If
809 not supplied, all jobs will be released.
810 """
Clark Boylanb640e052014-04-03 16:41:46 -0700811 released = False
812 qlen = (len(self.high_queue) + len(self.normal_queue) +
813 len(self.low_queue))
814 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
815 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500816 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700817 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500818 parameters = json.loads(job.arguments)
819 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700820 self.log.debug("releasing queued job %s" %
821 job.unique)
822 job.waiting = False
823 released = True
824 else:
825 self.log.debug("not releasing queued job %s" %
826 job.unique)
827 if released:
828 self.wakeConnections()
829 qlen = (len(self.high_queue) + len(self.normal_queue) +
830 len(self.low_queue))
831 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
832
833
834class FakeSMTP(object):
835 log = logging.getLogger('zuul.FakeSMTP')
836
837 def __init__(self, messages, server, port):
838 self.server = server
839 self.port = port
840 self.messages = messages
841
842 def sendmail(self, from_email, to_email, msg):
843 self.log.info("Sending email from %s, to %s, with msg %s" % (
844 from_email, to_email, msg))
845
846 headers = msg.split('\n\n', 1)[0]
847 body = msg.split('\n\n', 1)[1]
848
849 self.messages.append(dict(
850 from_email=from_email,
851 to_email=to_email,
852 msg=msg,
853 headers=headers,
854 body=body,
855 ))
856
857 return True
858
859 def quit(self):
860 return True
861
862
863class FakeSwiftClientConnection(swiftclient.client.Connection):
864 def post_account(self, headers):
865 # Do nothing
866 pass
867
868 def get_auth(self):
869 # Returns endpoint and (unused) auth token
870 endpoint = os.path.join('https://storage.example.org', 'V1',
871 'AUTH_account')
872 return endpoint, ''
873
874
James E. Blairdce6cea2016-12-20 16:45:32 -0800875class FakeNodepool(object):
876 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800877 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800878
879 log = logging.getLogger("zuul.test.FakeNodepool")
880
881 def __init__(self, host, port, chroot):
882 self.client = kazoo.client.KazooClient(
883 hosts='%s:%s%s' % (host, port, chroot))
884 self.client.start()
885 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800886 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800887 self.thread = threading.Thread(target=self.run)
888 self.thread.daemon = True
889 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800890 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800891
892 def stop(self):
893 self._running = False
894 self.thread.join()
895 self.client.stop()
896 self.client.close()
897
898 def run(self):
899 while self._running:
900 self._run()
901 time.sleep(0.1)
902
903 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800904 if self.paused:
905 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800906 for req in self.getNodeRequests():
907 self.fulfillRequest(req)
908
909 def getNodeRequests(self):
910 try:
911 reqids = self.client.get_children(self.REQUEST_ROOT)
912 except kazoo.exceptions.NoNodeError:
913 return []
914 reqs = []
915 for oid in sorted(reqids):
916 path = self.REQUEST_ROOT + '/' + oid
917 data, stat = self.client.get(path)
918 data = json.loads(data)
919 data['_oid'] = oid
920 reqs.append(data)
921 return reqs
922
James E. Blaire18d4602017-01-05 11:17:28 -0800923 def getNodes(self):
924 try:
925 nodeids = self.client.get_children(self.NODE_ROOT)
926 except kazoo.exceptions.NoNodeError:
927 return []
928 nodes = []
929 for oid in sorted(nodeids):
930 path = self.NODE_ROOT + '/' + oid
931 data, stat = self.client.get(path)
932 data = json.loads(data)
933 data['_oid'] = oid
934 try:
935 lockfiles = self.client.get_children(path + '/lock')
936 except kazoo.exceptions.NoNodeError:
937 lockfiles = []
938 if lockfiles:
939 data['_lock'] = True
940 else:
941 data['_lock'] = False
942 nodes.append(data)
943 return nodes
944
James E. Blaira38c28e2017-01-04 10:33:20 -0800945 def makeNode(self, request_id, node_type):
946 now = time.time()
947 path = '/nodepool/nodes/'
948 data = dict(type=node_type,
949 provider='test-provider',
950 region='test-region',
951 az=None,
952 public_ipv4='127.0.0.1',
953 private_ipv4=None,
954 public_ipv6=None,
955 allocated_to=request_id,
956 state='ready',
957 state_time=now,
958 created_time=now,
959 updated_time=now,
960 image_id=None,
961 launcher='fake-nodepool')
962 data = json.dumps(data)
963 path = self.client.create(path, data,
964 makepath=True,
965 sequence=True)
966 nodeid = path.split("/")[-1]
967 return nodeid
968
James E. Blair6ab79e02017-01-06 10:10:17 -0800969 def addFailRequest(self, request):
970 self.fail_requests.add(request['_oid'])
971
James E. Blairdce6cea2016-12-20 16:45:32 -0800972 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800973 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800974 return
975 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800976 oid = request['_oid']
977 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800978
James E. Blair6ab79e02017-01-06 10:10:17 -0800979 if oid in self.fail_requests:
980 request['state'] = 'failed'
981 else:
982 request['state'] = 'fulfilled'
983 nodes = []
984 for node in request['node_types']:
985 nodeid = self.makeNode(oid, node)
986 nodes.append(nodeid)
987 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -0800988
James E. Blaira38c28e2017-01-04 10:33:20 -0800989 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800990 path = self.REQUEST_ROOT + '/' + oid
991 data = json.dumps(request)
992 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
993 self.client.set(path, data)
994
995
James E. Blair498059b2016-12-20 13:50:13 -0800996class ChrootedKazooFixture(fixtures.Fixture):
997 def __init__(self):
998 super(ChrootedKazooFixture, self).__init__()
999
1000 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1001 if ':' in zk_host:
1002 host, port = zk_host.split(':')
1003 else:
1004 host = zk_host
1005 port = None
1006
1007 self.zookeeper_host = host
1008
1009 if not port:
1010 self.zookeeper_port = 2181
1011 else:
1012 self.zookeeper_port = int(port)
1013
1014 def _setUp(self):
1015 # Make sure the test chroot paths do not conflict
1016 random_bits = ''.join(random.choice(string.ascii_lowercase +
1017 string.ascii_uppercase)
1018 for x in range(8))
1019
1020 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1021 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1022
1023 # Ensure the chroot path exists and clean up any pre-existing znodes.
1024 _tmp_client = kazoo.client.KazooClient(
1025 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1026 _tmp_client.start()
1027
1028 if _tmp_client.exists(self.zookeeper_chroot):
1029 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1030
1031 _tmp_client.ensure_path(self.zookeeper_chroot)
1032 _tmp_client.stop()
1033 _tmp_client.close()
1034
1035 self.addCleanup(self._cleanup)
1036
1037 def _cleanup(self):
1038 '''Remove the chroot path.'''
1039 # Need a non-chroot'ed client to remove the chroot path
1040 _tmp_client = kazoo.client.KazooClient(
1041 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1042 _tmp_client.start()
1043 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1044 _tmp_client.stop()
1045
1046
Maru Newby3fe5f852015-01-13 04:22:14 +00001047class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001048 log = logging.getLogger("zuul.test")
1049
1050 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001051 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001052 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1053 try:
1054 test_timeout = int(test_timeout)
1055 except ValueError:
1056 # If timeout value is invalid do not set a timeout.
1057 test_timeout = 0
1058 if test_timeout > 0:
1059 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1060
1061 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1062 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1063 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1064 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1065 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1066 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1067 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1068 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1069 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1070 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001071 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001072 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1073 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001074 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1075 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001076 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1077 log_level = logging.WARNING
1078 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1079 log_level = logging.ERROR
1080 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1081 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001082 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001083 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001084 format='%(asctime)s %(name)-32s '
1085 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001086
James E. Blairdce6cea2016-12-20 16:45:32 -08001087 # NOTE(notmorgan): Extract logging overrides for specific libraries
1088 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1089 # each. This is used to limit the output during test runs from
1090 # libraries that zuul depends on such as gear.
1091 log_defaults_from_env = os.environ.get(
1092 'OS_LOG_DEFAULTS',
1093 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001094
James E. Blairdce6cea2016-12-20 16:45:32 -08001095 if log_defaults_from_env:
1096 for default in log_defaults_from_env.split(','):
1097 try:
1098 name, level_str = default.split('=', 1)
1099 level = getattr(logging, level_str, logging.DEBUG)
1100 self.useFixture(fixtures.FakeLogger(
1101 name=name,
1102 level=level,
1103 format='%(asctime)s %(name)-32s '
1104 '%(levelname)-8s %(message)s'))
1105 except ValueError:
1106 # NOTE(notmorgan): Invalid format of the log default,
1107 # skip and don't try and apply a logger for the
1108 # specified module
1109 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001110
Maru Newby3fe5f852015-01-13 04:22:14 +00001111
1112class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001113 """A test case with a functioning Zuul.
1114
1115 The following class variables are used during test setup and can
1116 be overidden by subclasses but are effectively read-only once a
1117 test method starts running:
1118
1119 :cvar str config_file: This points to the main zuul config file
1120 within the fixtures directory. Subclasses may override this
1121 to obtain a different behavior.
1122
1123 :cvar str tenant_config_file: This is the tenant config file
1124 (which specifies from what git repos the configuration should
1125 be loaded). It defaults to the value specified in
1126 `config_file` but can be overidden by subclasses to obtain a
1127 different tenant/project layout while using the standard main
1128 configuration.
1129
1130 The following are instance variables that are useful within test
1131 methods:
1132
1133 :ivar FakeGerritConnection fake_<connection>:
1134 A :py:class:`~tests.base.FakeGerritConnection` will be
1135 instantiated for each connection present in the config file
1136 and stored here. For instance, `fake_gerrit` will hold the
1137 FakeGerritConnection object for a connection named `gerrit`.
1138
1139 :ivar FakeGearmanServer gearman_server: An instance of
1140 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1141 server that all of the Zuul components in this test use to
1142 communicate with each other.
1143
1144 :ivar RecordingLaunchServer launch_server: An instance of
1145 :py:class:`~tests.base.RecordingLaunchServer` which is the
1146 Ansible launch server used to run jobs for this test.
1147
1148 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1149 representing currently running builds. They are appended to
1150 the list in the order they are launched, and removed from this
1151 list upon completion.
1152
1153 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1154 objects representing completed builds. They are appended to
1155 the list in the order they complete.
1156
1157 """
1158
James E. Blair83005782015-12-11 14:46:03 -08001159 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001160 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001161
1162 def _startMerger(self):
1163 self.merge_server = zuul.merger.server.MergeServer(self.config,
1164 self.connections)
1165 self.merge_server.start()
1166
Maru Newby3fe5f852015-01-13 04:22:14 +00001167 def setUp(self):
1168 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001169
1170 self.setupZK()
1171
James E. Blair97d902e2014-08-21 13:25:56 -07001172 if USE_TEMPDIR:
1173 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001174 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1175 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001176 else:
1177 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001178 self.test_root = os.path.join(tmp_root, "zuul-test")
1179 self.upstream_root = os.path.join(self.test_root, "upstream")
1180 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001181 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001182
1183 if os.path.exists(self.test_root):
1184 shutil.rmtree(self.test_root)
1185 os.makedirs(self.test_root)
1186 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001187 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001188
1189 # Make per test copy of Configuration.
1190 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001191 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001192 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001193 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001194 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001195 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001196
1197 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001198 # TODOv3(jeblair): remove these and replace with new git
1199 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001200 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001201 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001202 self.init_repo("org/project5")
1203 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001204 self.init_repo("org/one-job-project")
1205 self.init_repo("org/nonvoting-project")
1206 self.init_repo("org/templated-project")
1207 self.init_repo("org/layered-project")
1208 self.init_repo("org/node-project")
1209 self.init_repo("org/conflict-project")
1210 self.init_repo("org/noop-project")
1211 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001212 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001213
1214 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001215 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1216 # see: https://github.com/jsocol/pystatsd/issues/61
1217 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001218 os.environ['STATSD_PORT'] = str(self.statsd.port)
1219 self.statsd.start()
1220 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001221 reload_module(statsd)
1222 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001223
1224 self.gearman_server = FakeGearmanServer()
1225
1226 self.config.set('gearman', 'port', str(self.gearman_server.port))
1227
Joshua Hesketh352264b2015-08-11 23:42:08 +10001228 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1229 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1230 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001231
Joshua Hesketh352264b2015-08-11 23:42:08 +10001232 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001233
1234 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1235 FakeSwiftClientConnection))
1236 self.swift = zuul.lib.swift.Swift(self.config)
1237
Jan Hruban6b71aff2015-10-22 16:58:08 +02001238 self.event_queues = [
1239 self.sched.result_event_queue,
1240 self.sched.trigger_event_queue
1241 ]
1242
James E. Blairfef78942016-03-11 16:28:56 -08001243 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001244 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001245
Clark Boylanb640e052014-04-03 16:41:46 -07001246 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001247 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001248 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001249 return FakeURLOpener(self.upstream_root, *args, **kw)
1250
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001251 old_urlopen = urllib.request.urlopen
1252 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001253
James E. Blair3f876d52016-07-22 13:07:14 -07001254 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001255
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 self.launch_server = RecordingLaunchServer(
1257 self.config, self.connections, _run_ansible=self.run_ansible)
1258 self.launch_server.start()
1259 self.history = self.launch_server.build_history
1260 self.builds = self.launch_server.running_builds
1261
1262 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001263 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001264 self.merge_client = zuul.merger.client.MergeClient(
1265 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001266 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001267 self.zk = zuul.zk.ZooKeeper()
1268 self.zk.connect([self.zk_config])
1269
1270 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1271 self.zk_config.port,
1272 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001273
James E. Blaire1767bc2016-08-02 10:00:27 -07001274 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001275 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001276 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001277 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001278
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001279 self.webapp = zuul.webapp.WebApp(
1280 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001281 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001282
1283 self.sched.start()
1284 self.sched.reconfigure(self.config)
1285 self.sched.resume()
1286 self.webapp.start()
1287 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001289
Clark Boylanb640e052014-04-03 16:41:46 -07001290 self.addCleanup(self.shutdown)
1291
James E. Blaire18d4602017-01-05 11:17:28 -08001292 def tearDown(self):
1293 super(ZuulTestCase, self).tearDown()
1294 self.assertFinalState()
1295
James E. Blairfef78942016-03-11 16:28:56 -08001296 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001297 # Register connections from the config
1298 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001299
Joshua Hesketh352264b2015-08-11 23:42:08 +10001300 def FakeSMTPFactory(*args, **kw):
1301 args = [self.smtp_messages] + list(args)
1302 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001303
Joshua Hesketh352264b2015-08-11 23:42:08 +10001304 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001305
Joshua Hesketh352264b2015-08-11 23:42:08 +10001306 # Set a changes database so multiple FakeGerrit's can report back to
1307 # a virtual canonical database given by the configured hostname
1308 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001309 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001310
Joshua Hesketh352264b2015-08-11 23:42:08 +10001311 for section_name in self.config.sections():
1312 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1313 section_name, re.I)
1314 if not con_match:
1315 continue
1316 con_name = con_match.group(2)
1317 con_config = dict(self.config.items(section_name))
1318
1319 if 'driver' not in con_config:
1320 raise Exception("No driver specified for connection %s."
1321 % con_name)
1322
1323 con_driver = con_config['driver']
1324
1325 # TODO(jhesketh): load the required class automatically
1326 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001327 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1328 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001329 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001330 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001331 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001332 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001333 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001334 self.event_queues.append(
1335 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001336 setattr(self, 'fake_' + con_name,
1337 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001338 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001339 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001340 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1341 else:
1342 raise Exception("Unknown driver, %s, for connection %s"
1343 % (con_config['driver'], con_name))
1344
1345 # If the [gerrit] or [smtp] sections still exist, load them in as a
1346 # connection named 'gerrit' or 'smtp' respectfully
1347
1348 if 'gerrit' in self.config.sections():
1349 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001350 self.event_queues.append(
1351 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001352 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001353 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001354 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001355
1356 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001357 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001358 zuul.connection.smtp.SMTPConnection(
1359 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001360
James E. Blair83005782015-12-11 14:46:03 -08001361 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001362 # This creates the per-test configuration object. It can be
1363 # overriden by subclasses, but should not need to be since it
1364 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001365 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001366 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001367 if hasattr(self, 'tenant_config_file'):
1368 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001369 git_path = os.path.join(
1370 os.path.dirname(
1371 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1372 'git')
1373 if os.path.exists(git_path):
1374 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001375 project = reponame.replace('_', '/')
1376 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001377 os.path.join(git_path, reponame))
1378
James E. Blair498059b2016-12-20 13:50:13 -08001379 def setupZK(self):
1380 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001381 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1382 self.zk_chroot_fixture.zookeeper_host,
1383 self.zk_chroot_fixture.zookeeper_port,
1384 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001385
James E. Blair96c6bf82016-01-15 16:20:40 -08001386 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001387 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001388
1389 files = {}
1390 for (dirpath, dirnames, filenames) in os.walk(source_path):
1391 for filename in filenames:
1392 test_tree_filepath = os.path.join(dirpath, filename)
1393 common_path = os.path.commonprefix([test_tree_filepath,
1394 source_path])
1395 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1396 with open(test_tree_filepath, 'r') as f:
1397 content = f.read()
1398 files[relative_filepath] = content
1399 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001400 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001401
James E. Blaire18d4602017-01-05 11:17:28 -08001402 def assertNodepoolState(self):
1403 # Make sure that there are no pending requests
1404
1405 requests = self.fake_nodepool.getNodeRequests()
1406 self.assertEqual(len(requests), 0)
1407
1408 nodes = self.fake_nodepool.getNodes()
1409 for node in nodes:
1410 self.assertFalse(node['_lock'], "Node %s is locked" %
1411 (node['_oid'],))
1412
Clark Boylanb640e052014-04-03 16:41:46 -07001413 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001414 # Make sure that git.Repo objects have been garbage collected.
1415 repos = []
1416 gc.collect()
1417 for obj in gc.get_objects():
1418 if isinstance(obj, git.Repo):
1419 repos.append(obj)
1420 self.assertEqual(len(repos), 0)
1421 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001422 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001423 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001424 for tenant in self.sched.abide.tenants.values():
1425 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001426 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001427 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001428
1429 def shutdown(self):
1430 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001431 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001432 self.merge_server.stop()
1433 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001434 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001435 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001436 self.sched.stop()
1437 self.sched.join()
1438 self.statsd.stop()
1439 self.statsd.join()
1440 self.webapp.stop()
1441 self.webapp.join()
1442 self.rpc.stop()
1443 self.rpc.join()
1444 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001445 self.fake_nodepool.stop()
1446 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001447 threads = threading.enumerate()
1448 if len(threads) > 1:
1449 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001450 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001451
1452 def init_repo(self, project):
1453 parts = project.split('/')
1454 path = os.path.join(self.upstream_root, *parts[:-1])
1455 if not os.path.exists(path):
1456 os.makedirs(path)
1457 path = os.path.join(self.upstream_root, project)
1458 repo = git.Repo.init(path)
1459
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001460 with repo.config_writer() as config_writer:
1461 config_writer.set_value('user', 'email', 'user@example.com')
1462 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001463
Clark Boylanb640e052014-04-03 16:41:46 -07001464 repo.index.commit('initial commit')
1465 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001466
James E. Blair97d902e2014-08-21 13:25:56 -07001467 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001468 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001469 repo.git.clean('-x', '-f', '-d')
1470
James E. Blair97d902e2014-08-21 13:25:56 -07001471 def create_branch(self, project, branch):
1472 path = os.path.join(self.upstream_root, project)
1473 repo = git.Repo.init(path)
1474 fn = os.path.join(path, 'README')
1475
1476 branch_head = repo.create_head(branch)
1477 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001478 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001479 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001480 f.close()
1481 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001482 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001483
James E. Blair97d902e2014-08-21 13:25:56 -07001484 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001485 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001486 repo.git.clean('-x', '-f', '-d')
1487
Sachi King9f16d522016-03-16 12:20:45 +11001488 def create_commit(self, project):
1489 path = os.path.join(self.upstream_root, project)
1490 repo = git.Repo(path)
1491 repo.head.reference = repo.heads['master']
1492 file_name = os.path.join(path, 'README')
1493 with open(file_name, 'a') as f:
1494 f.write('creating fake commit\n')
1495 repo.index.add([file_name])
1496 commit = repo.index.commit('Creating a fake commit')
1497 return commit.hexsha
1498
James E. Blairb8c16472015-05-05 14:55:26 -07001499 def orderedRelease(self):
1500 # Run one build at a time to ensure non-race order:
1501 while len(self.builds):
1502 self.release(self.builds[0])
1503 self.waitUntilSettled()
1504
Clark Boylanb640e052014-04-03 16:41:46 -07001505 def release(self, job):
1506 if isinstance(job, FakeBuild):
1507 job.release()
1508 else:
1509 job.waiting = False
1510 self.log.debug("Queued job %s released" % job.unique)
1511 self.gearman_server.wakeConnections()
1512
1513 def getParameter(self, job, name):
1514 if isinstance(job, FakeBuild):
1515 return job.parameters[name]
1516 else:
1517 parameters = json.loads(job.arguments)
1518 return parameters[name]
1519
Clark Boylanb640e052014-04-03 16:41:46 -07001520 def haveAllBuildsReported(self):
1521 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001522 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001523 return False
1524 # Find out if every build that the worker has completed has been
1525 # reported back to Zuul. If it hasn't then that means a Gearman
1526 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001527 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001528 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001529 if not zbuild:
1530 # It has already been reported
1531 continue
1532 # It hasn't been reported yet.
1533 return False
1534 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001535 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001536 if connection.state == 'GRAB_WAIT':
1537 return False
1538 return True
1539
1540 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001541 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001542 for build in builds:
1543 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001544 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001545 for j in conn.related_jobs.values():
1546 if j.unique == build.uuid:
1547 client_job = j
1548 break
1549 if not client_job:
1550 self.log.debug("%s is not known to the gearman client" %
1551 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001552 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001553 if not client_job.handle:
1554 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001555 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001556 server_job = self.gearman_server.jobs.get(client_job.handle)
1557 if not server_job:
1558 self.log.debug("%s is not known to the gearman server" %
1559 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001560 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001561 if not hasattr(server_job, 'waiting'):
1562 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001563 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001564 if server_job.waiting:
1565 continue
James E. Blair17302972016-08-10 16:11:42 -07001566 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001567 self.log.debug("%s has not reported start" % build)
1568 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001569 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001570 if worker_build:
1571 if worker_build.isWaiting():
1572 continue
1573 else:
1574 self.log.debug("%s is running" % worker_build)
1575 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001576 else:
James E. Blair962220f2016-08-03 11:22:38 -07001577 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001578 return False
1579 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001580
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001582 if self.fake_nodepool.paused:
1583 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001584 if self.sched.nodepool.requests:
1585 return False
1586 return True
1587
Jan Hruban6b71aff2015-10-22 16:58:08 +02001588 def eventQueuesEmpty(self):
1589 for queue in self.event_queues:
1590 yield queue.empty()
1591
1592 def eventQueuesJoin(self):
1593 for queue in self.event_queues:
1594 queue.join()
1595
Clark Boylanb640e052014-04-03 16:41:46 -07001596 def waitUntilSettled(self):
1597 self.log.debug("Waiting until settled...")
1598 start = time.time()
1599 while True:
1600 if time.time() - start > 10:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001601 self.log.error("Timeout waiting for Zuul to settle")
1602 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001603 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001604 self.log.error(" %s: %s" % (queue, queue.empty()))
1605 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001606 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001607 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001608 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001609 self.log.error("All requests completed: %s" %
1610 (self.areAllNodeRequestsComplete(),))
1611 self.log.error("Merge client jobs: %s" %
1612 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001613 raise Exception("Timeout waiting for Zuul to settle")
1614 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001615
James E. Blaire1767bc2016-08-02 10:00:27 -07001616 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001617 # have all build states propogated to zuul?
1618 if self.haveAllBuildsReported():
1619 # Join ensures that the queue is empty _and_ events have been
1620 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001621 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001622 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001623 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001624 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001625 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001626 self.areAllBuildsWaiting() and
1627 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001628 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001629 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001630 self.log.debug("...settled.")
1631 return
1632 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001633 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001634 self.sched.wake_event.wait(0.1)
1635
1636 def countJobResults(self, jobs, result):
1637 jobs = filter(lambda x: x.result == result, jobs)
1638 return len(jobs)
1639
James E. Blair96c6bf82016-01-15 16:20:40 -08001640 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001641 for job in self.history:
1642 if (job.name == name and
1643 (project is None or
1644 job.parameters['ZUUL_PROJECT'] == project)):
1645 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001646 raise Exception("Unable to find job %s in history" % name)
1647
1648 def assertEmptyQueues(self):
1649 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001650 for tenant in self.sched.abide.tenants.values():
1651 for pipeline in tenant.layout.pipelines.values():
1652 for queue in pipeline.queues:
1653 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001654 print('pipeline %s queue %s contents %s' % (
1655 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001656 self.assertEqual(len(queue.queue), 0,
1657 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001658
1659 def assertReportedStat(self, key, value=None, kind=None):
1660 start = time.time()
1661 while time.time() < (start + 5):
1662 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001663 k, v = stat.split(':')
1664 if key == k:
1665 if value is None and kind is None:
1666 return
1667 elif value:
1668 if value == v:
1669 return
1670 elif kind:
1671 if v.endswith('|' + kind):
1672 return
1673 time.sleep(0.1)
1674
Clark Boylanb640e052014-04-03 16:41:46 -07001675 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001676
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001677 def assertBuilds(self, builds):
1678 """Assert that the running builds are as described.
1679
1680 The list of running builds is examined and must match exactly
1681 the list of builds described by the input.
1682
1683 :arg list builds: A list of dictionaries. Each item in the
1684 list must match the corresponding build in the build
1685 history, and each element of the dictionary must match the
1686 corresponding attribute of the build.
1687
1688 """
James E. Blair3158e282016-08-19 09:34:11 -07001689 try:
1690 self.assertEqual(len(self.builds), len(builds))
1691 for i, d in enumerate(builds):
1692 for k, v in d.items():
1693 self.assertEqual(
1694 getattr(self.builds[i], k), v,
1695 "Element %i in builds does not match" % (i,))
1696 except Exception:
1697 for build in self.builds:
1698 self.log.error("Running build: %s" % build)
1699 else:
1700 self.log.error("No running builds")
1701 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001702
James E. Blairb536ecc2016-08-31 10:11:42 -07001703 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001704 """Assert that the completed builds are as described.
1705
1706 The list of completed builds is examined and must match
1707 exactly the list of builds described by the input.
1708
1709 :arg list history: A list of dictionaries. Each item in the
1710 list must match the corresponding build in the build
1711 history, and each element of the dictionary must match the
1712 corresponding attribute of the build.
1713
James E. Blairb536ecc2016-08-31 10:11:42 -07001714 :arg bool ordered: If true, the history must match the order
1715 supplied, if false, the builds are permitted to have
1716 arrived in any order.
1717
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001718 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001719 def matches(history_item, item):
1720 for k, v in item.items():
1721 if getattr(history_item, k) != v:
1722 return False
1723 return True
James E. Blair3158e282016-08-19 09:34:11 -07001724 try:
1725 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001726 if ordered:
1727 for i, d in enumerate(history):
1728 if not matches(self.history[i], d):
1729 raise Exception(
1730 "Element %i in history does not match" % (i,))
1731 else:
1732 unseen = self.history[:]
1733 for i, d in enumerate(history):
1734 found = False
1735 for unseen_item in unseen:
1736 if matches(unseen_item, d):
1737 found = True
1738 unseen.remove(unseen_item)
1739 break
1740 if not found:
1741 raise Exception("No match found for element %i "
1742 "in history" % (i,))
1743 if unseen:
1744 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001745 except Exception:
1746 for build in self.history:
1747 self.log.error("Completed build: %s" % build)
1748 else:
1749 self.log.error("No completed builds")
1750 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001751
James E. Blair6ac368c2016-12-22 18:07:20 -08001752 def printHistory(self):
1753 """Log the build history.
1754
1755 This can be useful during tests to summarize what jobs have
1756 completed.
1757
1758 """
1759 self.log.debug("Build history:")
1760 for build in self.history:
1761 self.log.debug(build)
1762
James E. Blair59fdbac2015-12-07 17:08:06 -08001763 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001764 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1765
1766 def updateConfigLayout(self, path):
1767 root = os.path.join(self.test_root, "config")
1768 os.makedirs(root)
1769 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1770 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001771- tenant:
1772 name: openstack
1773 source:
1774 gerrit:
1775 config-repos:
1776 - %s
1777 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001778 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001779 self.config.set('zuul', 'tenant_config',
1780 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001781
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001782 def addCommitToRepo(self, project, message, files,
1783 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001784 path = os.path.join(self.upstream_root, project)
1785 repo = git.Repo(path)
1786 repo.head.reference = branch
1787 zuul.merger.merger.reset_repo_to_head(repo)
1788 for fn, content in files.items():
1789 fn = os.path.join(path, fn)
1790 with open(fn, 'w') as f:
1791 f.write(content)
1792 repo.index.add([fn])
1793 commit = repo.index.commit(message)
1794 repo.heads[branch].commit = commit
1795 repo.head.reference = branch
1796 repo.git.clean('-x', '-f', '-d')
1797 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001798 if tag:
1799 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001800
James E. Blair7fc8daa2016-08-08 15:37:15 -07001801 def addEvent(self, connection, event):
1802 """Inject a Fake (Gerrit) event.
1803
1804 This method accepts a JSON-encoded event and simulates Zuul
1805 having received it from Gerrit. It could (and should)
1806 eventually apply to any connection type, but is currently only
1807 used with Gerrit connections. The name of the connection is
1808 used to look up the corresponding server, and the event is
1809 simulated as having been received by all Zuul connections
1810 attached to that server. So if two Gerrit connections in Zuul
1811 are connected to the same Gerrit server, and you invoke this
1812 method specifying the name of one of them, the event will be
1813 received by both.
1814
1815 .. note::
1816
1817 "self.fake_gerrit.addEvent" calls should be migrated to
1818 this method.
1819
1820 :arg str connection: The name of the connection corresponding
1821 to the gerrit server.
1822 :arg str event: The JSON-encoded event.
1823
1824 """
1825 specified_conn = self.connections.connections[connection]
1826 for conn in self.connections.connections.values():
1827 if (isinstance(conn, specified_conn.__class__) and
1828 specified_conn.server == conn.server):
1829 conn.addEvent(event)
1830
James E. Blair3f876d52016-07-22 13:07:14 -07001831
1832class AnsibleZuulTestCase(ZuulTestCase):
1833 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001834 run_ansible = True