blob: 1b6541644eeffa3e7f04f8aadfa5092773a88f1b [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
James E. Blaire511d2f2016-12-08 15:22:26 -080048import zuul.driver.gerrit.gerritsource as gerritsource
49import zuul.driver.gerrit.gerritconnection as gerritconnection
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
James E. Blairdce6cea2016-12-20 16:45:32 -080061import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070062
63FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
64 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070065USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070066
67logging.basicConfig(level=logging.DEBUG,
68 format='%(asctime)s %(name)-32s '
69 '%(levelname)-8s %(message)s')
70
71
72def repack_repo(path):
73 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
74 output = subprocess.Popen(cmd, close_fds=True,
75 stdout=subprocess.PIPE,
76 stderr=subprocess.PIPE)
77 out = output.communicate()
78 if output.returncode:
79 raise Exception("git repack returned %d" % output.returncode)
80 return out
81
82
83def random_sha1():
84 return hashlib.sha1(str(random.random())).hexdigest()
85
86
James E. Blaira190f3b2015-01-05 14:56:54 -080087def iterate_timeout(max_seconds, purpose):
88 start = time.time()
89 count = 0
90 while (time.time() < start + max_seconds):
91 count += 1
92 yield count
93 time.sleep(0)
94 raise Exception("Timeout waiting for %s" % purpose)
95
96
Clark Boylanb640e052014-04-03 16:41:46 -070097class ChangeReference(git.Reference):
98 _common_path_default = "refs/changes"
99 _points_to_commits_only = True
100
101
102class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700103 categories = {'approved': ('Approved', -1, 1),
104 'code-review': ('Code-Review', -2, 2),
105 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700106
107 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700108 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700109 self.gerrit = gerrit
110 self.reported = 0
111 self.queried = 0
112 self.patchsets = []
113 self.number = number
114 self.project = project
115 self.branch = branch
116 self.subject = subject
117 self.latest_patchset = 0
118 self.depends_on_change = None
119 self.needed_by_changes = []
120 self.fail_merge = False
121 self.messages = []
122 self.data = {
123 'branch': branch,
124 'comments': [],
125 'commitMessage': subject,
126 'createdOn': time.time(),
127 'id': 'I' + random_sha1(),
128 'lastUpdated': time.time(),
129 'number': str(number),
130 'open': status == 'NEW',
131 'owner': {'email': 'user@example.com',
132 'name': 'User Name',
133 'username': 'username'},
134 'patchSets': self.patchsets,
135 'project': project,
136 'status': status,
137 'subject': subject,
138 'submitRecords': [],
139 'url': 'https://hostname/%s' % number}
140
141 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.data['submitRecords'] = self.getSubmitRecords()
144 self.open = status == 'NEW'
145
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700146 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700147 path = os.path.join(self.upstream_root, self.project)
148 repo = git.Repo(path)
149 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
150 self.latest_patchset),
151 'refs/tags/init')
152 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700153 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700154 repo.git.clean('-x', '-f', '-d')
155
156 path = os.path.join(self.upstream_root, self.project)
157 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700158 for fn, content in files.items():
159 fn = os.path.join(path, fn)
160 with open(fn, 'w') as f:
161 f.write(content)
162 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700163 else:
164 for fni in range(100):
165 fn = os.path.join(path, str(fni))
166 f = open(fn, 'w')
167 for ci in range(4096):
168 f.write(random.choice(string.printable))
169 f.close()
170 repo.index.add([fn])
171
172 r = repo.index.commit(msg)
173 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700174 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700175 repo.git.clean('-x', '-f', '-d')
176 repo.heads['master'].checkout()
177 return r
178
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700179 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700180 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700182 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 data = ("test %s %s %s\n" %
184 (self.branch, self.number, self.latest_patchset))
185 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700186 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700187 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 ps_files = [{'file': '/COMMIT_MSG',
189 'type': 'ADDED'},
190 {'file': 'README',
191 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700193 ps_files.append({'file': f, 'type': 'ADDED'})
194 d = {'approvals': [],
195 'createdOn': time.time(),
196 'files': ps_files,
197 'number': str(self.latest_patchset),
198 'ref': 'refs/changes/1/%s/%s' % (self.number,
199 self.latest_patchset),
200 'revision': c.hexsha,
201 'uploader': {'email': 'user@example.com',
202 'name': 'User name',
203 'username': 'user'}}
204 self.data['currentPatchSet'] = d
205 self.patchsets.append(d)
206 self.data['submitRecords'] = self.getSubmitRecords()
207
208 def getPatchsetCreatedEvent(self, patchset):
209 event = {"type": "patchset-created",
210 "change": {"project": self.project,
211 "branch": self.branch,
212 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
213 "number": str(self.number),
214 "subject": self.subject,
215 "owner": {"name": "User Name"},
216 "url": "https://hostname/3"},
217 "patchSet": self.patchsets[patchset - 1],
218 "uploader": {"name": "User Name"}}
219 return event
220
221 def getChangeRestoredEvent(self):
222 event = {"type": "change-restored",
223 "change": {"project": self.project,
224 "branch": self.branch,
225 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
226 "number": str(self.number),
227 "subject": self.subject,
228 "owner": {"name": "User Name"},
229 "url": "https://hostname/3"},
230 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100231 "patchSet": self.patchsets[-1],
232 "reason": ""}
233 return event
234
235 def getChangeAbandonedEvent(self):
236 event = {"type": "change-abandoned",
237 "change": {"project": self.project,
238 "branch": self.branch,
239 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
240 "number": str(self.number),
241 "subject": self.subject,
242 "owner": {"name": "User Name"},
243 "url": "https://hostname/3"},
244 "abandoner": {"name": "User Name"},
245 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700246 "reason": ""}
247 return event
248
249 def getChangeCommentEvent(self, patchset):
250 event = {"type": "comment-added",
251 "change": {"project": self.project,
252 "branch": self.branch,
253 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
254 "number": str(self.number),
255 "subject": self.subject,
256 "owner": {"name": "User Name"},
257 "url": "https://hostname/3"},
258 "patchSet": self.patchsets[patchset - 1],
259 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700260 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700261 "description": "Code-Review",
262 "value": "0"}],
263 "comment": "This is a comment"}
264 return event
265
Joshua Hesketh642824b2014-07-01 17:54:59 +1000266 def addApproval(self, category, value, username='reviewer_john',
267 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700268 if not granted_on:
269 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000270 approval = {
271 'description': self.categories[category][0],
272 'type': category,
273 'value': str(value),
274 'by': {
275 'username': username,
276 'email': username + '@example.com',
277 },
278 'grantedOn': int(granted_on)
279 }
Clark Boylanb640e052014-04-03 16:41:46 -0700280 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
281 if x['by']['username'] == username and x['type'] == category:
282 del self.patchsets[-1]['approvals'][i]
283 self.patchsets[-1]['approvals'].append(approval)
284 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000285 'author': {'email': 'author@example.com',
286 'name': 'Patchset Author',
287 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700288 'change': {'branch': self.branch,
289 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
290 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000291 'owner': {'email': 'owner@example.com',
292 'name': 'Change Owner',
293 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700294 'project': self.project,
295 'subject': self.subject,
296 'topic': 'master',
297 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000298 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700299 'patchSet': self.patchsets[-1],
300 'type': 'comment-added'}
301 self.data['submitRecords'] = self.getSubmitRecords()
302 return json.loads(json.dumps(event))
303
304 def getSubmitRecords(self):
305 status = {}
306 for cat in self.categories.keys():
307 status[cat] = 0
308
309 for a in self.patchsets[-1]['approvals']:
310 cur = status[a['type']]
311 cat_min, cat_max = self.categories[a['type']][1:]
312 new = int(a['value'])
313 if new == cat_min:
314 cur = new
315 elif abs(new) > abs(cur):
316 cur = new
317 status[a['type']] = cur
318
319 labels = []
320 ok = True
321 for typ, cat in self.categories.items():
322 cur = status[typ]
323 cat_min, cat_max = cat[1:]
324 if cur == cat_min:
325 value = 'REJECT'
326 ok = False
327 elif cur == cat_max:
328 value = 'OK'
329 else:
330 value = 'NEED'
331 ok = False
332 labels.append({'label': cat[0], 'status': value})
333 if ok:
334 return [{'status': 'OK'}]
335 return [{'status': 'NOT_READY',
336 'labels': labels}]
337
338 def setDependsOn(self, other, patchset):
339 self.depends_on_change = other
340 d = {'id': other.data['id'],
341 'number': other.data['number'],
342 'ref': other.patchsets[patchset - 1]['ref']
343 }
344 self.data['dependsOn'] = [d]
345
346 other.needed_by_changes.append(self)
347 needed = other.data.get('neededBy', [])
348 d = {'id': self.data['id'],
349 'number': self.data['number'],
350 'ref': self.patchsets[patchset - 1]['ref'],
351 'revision': self.patchsets[patchset - 1]['revision']
352 }
353 needed.append(d)
354 other.data['neededBy'] = needed
355
356 def query(self):
357 self.queried += 1
358 d = self.data.get('dependsOn')
359 if d:
360 d = d[0]
361 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
362 d['isCurrentPatchSet'] = True
363 else:
364 d['isCurrentPatchSet'] = False
365 return json.loads(json.dumps(self.data))
366
367 def setMerged(self):
368 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000369 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700370 return
371 if self.fail_merge:
372 return
373 self.data['status'] = 'MERGED'
374 self.open = False
375
376 path = os.path.join(self.upstream_root, self.project)
377 repo = git.Repo(path)
378 repo.heads[self.branch].commit = \
379 repo.commit(self.patchsets[-1]['revision'])
380
381 def setReported(self):
382 self.reported += 1
383
384
James E. Blaire511d2f2016-12-08 15:22:26 -0800385class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700386 """A Fake Gerrit connection for use in tests.
387
388 This subclasses
389 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
390 ability for tests to add changes to the fake Gerrit it represents.
391 """
392
Joshua Hesketh352264b2015-08-11 23:42:08 +1000393 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700394
James E. Blaire511d2f2016-12-08 15:22:26 -0800395 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700396 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800397 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 connection_config)
399
James E. Blair7fc8daa2016-08-08 15:37:15 -0700400 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700401 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
402 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000403 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700404 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200405 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700406
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700407 def addFakeChange(self, project, branch, subject, status='NEW',
408 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700409 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700410 self.change_number += 1
411 c = FakeChange(self, self.change_number, project, branch, subject,
412 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700413 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700414 self.changes[self.change_number] = c
415 return c
416
Clark Boylanb640e052014-04-03 16:41:46 -0700417 def review(self, project, changeid, message, action):
418 number, ps = changeid.split(',')
419 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000420
421 # Add the approval back onto the change (ie simulate what gerrit would
422 # do).
423 # Usually when zuul leaves a review it'll create a feedback loop where
424 # zuul's review enters another gerrit event (which is then picked up by
425 # zuul). However, we can't mimic this behaviour (by adding this
426 # approval event into the queue) as it stops jobs from checking what
427 # happens before this event is triggered. If a job needs to see what
428 # happens they can add their own verified event into the queue.
429 # Nevertheless, we can update change with the new review in gerrit.
430
James E. Blair8b5408c2016-08-08 15:37:46 -0700431 for cat in action.keys():
432 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000433 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000434
James E. Blair8b5408c2016-08-08 15:37:46 -0700435 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000436 if 'label' in action:
437 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000438 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439
Clark Boylanb640e052014-04-03 16:41:46 -0700440 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000441
Clark Boylanb640e052014-04-03 16:41:46 -0700442 if 'submit' in action:
443 change.setMerged()
444 if message:
445 change.setReported()
446
447 def query(self, number):
448 change = self.changes.get(int(number))
449 if change:
450 return change.query()
451 return {}
452
James E. Blairc494d542014-08-06 09:23:52 -0700453 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700454 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700455 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800456 if query.startswith('change:'):
457 # Query a specific changeid
458 changeid = query[len('change:'):]
459 l = [change.query() for change in self.changes.values()
460 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700461 elif query.startswith('message:'):
462 # Query the content of a commit message
463 msg = query[len('message:'):].strip()
464 l = [change.query() for change in self.changes.values()
465 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800466 else:
467 # Query all open changes
468 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700469 return l
James E. Blairc494d542014-08-06 09:23:52 -0700470
Joshua Hesketh352264b2015-08-11 23:42:08 +1000471 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700472 pass
473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def getGitUrl(self, project):
475 return os.path.join(self.upstream_root, project.name)
476
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800477 def _getGitwebUrl(self, project, sha=None):
478 return self.getGitwebUrl(project, sha)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700486 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
487 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. Blair34776ee2016-08-25 13:53:54 -0700544 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700546 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700548 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700549 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700551 # TODOv3(jeblair): self.node is really "the image of the node
552 # assigned". We should rename it (self.node_image?) if we
553 # keep using it like this, or we may end up exposing more of
554 # the complexity around multi-node jobs here
555 # (self.nodes[0].image?)
556 self.node = None
557 if len(self.parameters.get('nodes')) == 1:
558 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700559 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700560 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700561 self.wait_condition = threading.Condition()
562 self.waiting = False
563 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500564 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700566 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700567 self.changes = None
568 if 'ZUUL_CHANGE_IDS' in self.parameters:
569 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700570
James E. Blair3158e282016-08-19 09:34:11 -0700571 def __repr__(self):
572 waiting = ''
573 if self.waiting:
574 waiting = ' [waiting]'
575 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
576
Clark Boylanb640e052014-04-03 16:41:46 -0700577 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700578 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700579 self.wait_condition.acquire()
580 self.wait_condition.notify()
581 self.waiting = False
582 self.log.debug("Build %s released" % self.unique)
583 self.wait_condition.release()
584
585 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700586 """Return whether this build is being held.
587
588 :returns: Whether the build is being held.
589 :rtype: bool
590 """
591
Clark Boylanb640e052014-04-03 16:41:46 -0700592 self.wait_condition.acquire()
593 if self.waiting:
594 ret = True
595 else:
596 ret = False
597 self.wait_condition.release()
598 return ret
599
600 def _wait(self):
601 self.wait_condition.acquire()
602 self.waiting = True
603 self.log.debug("Build %s waiting" % self.unique)
604 self.wait_condition.wait()
605 self.wait_condition.release()
606
607 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700608 self.log.debug('Running build %s' % self.unique)
609
James E. Blaire1767bc2016-08-02 10:00:27 -0700610 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700611 self.log.debug('Holding build %s' % self.unique)
612 self._wait()
613 self.log.debug("Build %s continuing" % self.unique)
614
Clark Boylanb640e052014-04-03 16:41:46 -0700615 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700616 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700617 result = 'FAILURE'
618 if self.aborted:
619 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500620 if self.requeue:
621 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700622
623 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700624 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700625
James E. Blaire1767bc2016-08-02 10:00:27 -0700626 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700627
James E. Blaira5dba232016-08-08 15:53:24 -0700628 def shouldFail(self):
629 changes = self.launch_server.fail_tests.get(self.name, [])
630 for change in changes:
631 if self.hasChanges(change):
632 return True
633 return False
634
James E. Blaire7b99a02016-08-05 14:27:34 -0700635 def hasChanges(self, *changes):
636 """Return whether this build has certain changes in its git repos.
637
638 :arg FakeChange changes: One or more changes (varargs) that
639 are expected to be present (in order) in the git repository of
640 the active project.
641
642 :returns: Whether the build has the indicated changes.
643 :rtype: bool
644
645 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800646 for change in changes:
647 path = os.path.join(self.jobdir.git_root, change.project)
648 try:
649 repo = git.Repo(path)
650 except NoSuchPathError as e:
651 self.log.debug('%s' % e)
652 return False
653 ref = self.parameters['ZUUL_REF']
654 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
655 commit_message = '%s-1' % change.subject
656 self.log.debug("Checking if build %s has changes; commit_message "
657 "%s; repo_messages %s" % (self, commit_message,
658 repo_messages))
659 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700660 self.log.debug(" messages do not match")
661 return False
662 self.log.debug(" OK")
663 return True
664
Clark Boylanb640e052014-04-03 16:41:46 -0700665
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000666class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700667 """An Ansible launcher to be used in tests.
668
669 :ivar bool hold_jobs_in_build: If true, when jobs are launched
670 they will report that they have started but then pause until
671 released before reporting completion. This attribute may be
672 changed at any time and will take effect for subsequently
673 launched builds, but previously held builds will still need to
674 be explicitly released.
675
676 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800677 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800679 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700680 self.hold_jobs_in_build = False
681 self.lock = threading.Lock()
682 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700683 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700685 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800686
James E. Blaira5dba232016-08-08 15:53:24 -0700687 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700688 """Instruct the launcher to report matching builds as failures.
689
690 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700691 :arg Change change: The :py:class:`~tests.base.FakeChange`
692 instance which should cause the job to fail. This job
693 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700694
695 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700696 l = self.fail_tests.get(name, [])
697 l.append(change)
698 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800699
James E. Blair962220f2016-08-03 11:22:38 -0700700 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700701 """Release a held build.
702
703 :arg str regex: A regular expression which, if supplied, will
704 cause only builds with matching names to be released. If
705 not supplied, all builds will be released.
706
707 """
James E. Blair962220f2016-08-03 11:22:38 -0700708 builds = self.running_builds[:]
709 self.log.debug("Releasing build %s (%s)" % (regex,
710 len(self.running_builds)))
711 for build in builds:
712 if not regex or re.match(regex, build.name):
713 self.log.debug("Releasing build %s" %
714 (build.parameters['ZUUL_UUID']))
715 build.release()
716 else:
717 self.log.debug("Not releasing build %s" %
718 (build.parameters['ZUUL_UUID']))
719 self.log.debug("Done releasing builds %s (%s)" %
720 (regex, len(self.running_builds)))
721
James E. Blair17302972016-08-10 16:11:42 -0700722 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700723 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700726 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700727 super(RecordingLaunchServer, self).launchJob(job)
728
729 def stopJob(self, job):
730 self.log.debug("handle stop")
731 parameters = json.loads(job.arguments)
732 uuid = parameters['uuid']
733 for build in self.running_builds:
734 if build.unique == uuid:
735 build.aborted = True
736 build.release()
737 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700738
739 def runAnsible(self, jobdir, job):
740 build = self.job_builds[job.unique]
741 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700742
743 if self._run_ansible:
744 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
745 else:
746 result = build.run()
747
748 self.lock.acquire()
749 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700750 BuildHistory(name=build.name, result=result, changes=build.changes,
751 node=build.node, uuid=build.unique,
752 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 pipeline=build.parameters['ZUUL_PIPELINE'])
754 )
James E. Blairab7132b2016-08-05 12:36:22 -0700755 self.running_builds.remove(build)
756 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700757 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800758 if build.run_error:
759 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700760 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800761
762
Clark Boylanb640e052014-04-03 16:41:46 -0700763class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700764 """A Gearman server for use in tests.
765
766 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
767 added to the queue but will not be distributed to workers
768 until released. This attribute may be changed at any time and
769 will take effect for subsequently enqueued jobs, but
770 previously held jobs will still need to be explicitly
771 released.
772
773 """
774
Clark Boylanb640e052014-04-03 16:41:46 -0700775 def __init__(self):
776 self.hold_jobs_in_queue = False
777 super(FakeGearmanServer, self).__init__(0)
778
779 def getJobForConnection(self, connection, peek=False):
780 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
781 for job in queue:
782 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500783 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700784 job.waiting = self.hold_jobs_in_queue
785 else:
786 job.waiting = False
787 if job.waiting:
788 continue
789 if job.name in connection.functions:
790 if not peek:
791 queue.remove(job)
792 connection.related_jobs[job.handle] = job
793 job.worker_connection = connection
794 job.running = True
795 return job
796 return None
797
798 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700799 """Release a held job.
800
801 :arg str regex: A regular expression which, if supplied, will
802 cause only jobs with matching names to be released. If
803 not supplied, all jobs will be released.
804 """
Clark Boylanb640e052014-04-03 16:41:46 -0700805 released = False
806 qlen = (len(self.high_queue) + len(self.normal_queue) +
807 len(self.low_queue))
808 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
809 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500810 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700811 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500812 parameters = json.loads(job.arguments)
813 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700814 self.log.debug("releasing queued job %s" %
815 job.unique)
816 job.waiting = False
817 released = True
818 else:
819 self.log.debug("not releasing queued job %s" %
820 job.unique)
821 if released:
822 self.wakeConnections()
823 qlen = (len(self.high_queue) + len(self.normal_queue) +
824 len(self.low_queue))
825 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
826
827
828class FakeSMTP(object):
829 log = logging.getLogger('zuul.FakeSMTP')
830
831 def __init__(self, messages, server, port):
832 self.server = server
833 self.port = port
834 self.messages = messages
835
836 def sendmail(self, from_email, to_email, msg):
837 self.log.info("Sending email from %s, to %s, with msg %s" % (
838 from_email, to_email, msg))
839
840 headers = msg.split('\n\n', 1)[0]
841 body = msg.split('\n\n', 1)[1]
842
843 self.messages.append(dict(
844 from_email=from_email,
845 to_email=to_email,
846 msg=msg,
847 headers=headers,
848 body=body,
849 ))
850
851 return True
852
853 def quit(self):
854 return True
855
856
857class FakeSwiftClientConnection(swiftclient.client.Connection):
858 def post_account(self, headers):
859 # Do nothing
860 pass
861
862 def get_auth(self):
863 # Returns endpoint and (unused) auth token
864 endpoint = os.path.join('https://storage.example.org', 'V1',
865 'AUTH_account')
866 return endpoint, ''
867
868
James E. Blairdce6cea2016-12-20 16:45:32 -0800869class FakeNodepool(object):
870 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800871 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800872
873 log = logging.getLogger("zuul.test.FakeNodepool")
874
875 def __init__(self, host, port, chroot):
876 self.client = kazoo.client.KazooClient(
877 hosts='%s:%s%s' % (host, port, chroot))
878 self.client.start()
879 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800880 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800881 self.thread = threading.Thread(target=self.run)
882 self.thread.daemon = True
883 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800884 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800885
886 def stop(self):
887 self._running = False
888 self.thread.join()
889 self.client.stop()
890 self.client.close()
891
892 def run(self):
893 while self._running:
894 self._run()
895 time.sleep(0.1)
896
897 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800898 if self.paused:
899 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800900 for req in self.getNodeRequests():
901 self.fulfillRequest(req)
902
903 def getNodeRequests(self):
904 try:
905 reqids = self.client.get_children(self.REQUEST_ROOT)
906 except kazoo.exceptions.NoNodeError:
907 return []
908 reqs = []
909 for oid in sorted(reqids):
910 path = self.REQUEST_ROOT + '/' + oid
911 data, stat = self.client.get(path)
912 data = json.loads(data)
913 data['_oid'] = oid
914 reqs.append(data)
915 return reqs
916
James E. Blaire18d4602017-01-05 11:17:28 -0800917 def getNodes(self):
918 try:
919 nodeids = self.client.get_children(self.NODE_ROOT)
920 except kazoo.exceptions.NoNodeError:
921 return []
922 nodes = []
923 for oid in sorted(nodeids):
924 path = self.NODE_ROOT + '/' + oid
925 data, stat = self.client.get(path)
926 data = json.loads(data)
927 data['_oid'] = oid
928 try:
929 lockfiles = self.client.get_children(path + '/lock')
930 except kazoo.exceptions.NoNodeError:
931 lockfiles = []
932 if lockfiles:
933 data['_lock'] = True
934 else:
935 data['_lock'] = False
936 nodes.append(data)
937 return nodes
938
James E. Blaira38c28e2017-01-04 10:33:20 -0800939 def makeNode(self, request_id, node_type):
940 now = time.time()
941 path = '/nodepool/nodes/'
942 data = dict(type=node_type,
943 provider='test-provider',
944 region='test-region',
945 az=None,
946 public_ipv4='127.0.0.1',
947 private_ipv4=None,
948 public_ipv6=None,
949 allocated_to=request_id,
950 state='ready',
951 state_time=now,
952 created_time=now,
953 updated_time=now,
954 image_id=None,
955 launcher='fake-nodepool')
956 data = json.dumps(data)
957 path = self.client.create(path, data,
958 makepath=True,
959 sequence=True)
960 nodeid = path.split("/")[-1]
961 return nodeid
962
James E. Blair6ab79e02017-01-06 10:10:17 -0800963 def addFailRequest(self, request):
964 self.fail_requests.add(request['_oid'])
965
James E. Blairdce6cea2016-12-20 16:45:32 -0800966 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800967 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800968 return
969 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800970 oid = request['_oid']
971 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800972
James E. Blair6ab79e02017-01-06 10:10:17 -0800973 if oid in self.fail_requests:
974 request['state'] = 'failed'
975 else:
976 request['state'] = 'fulfilled'
977 nodes = []
978 for node in request['node_types']:
979 nodeid = self.makeNode(oid, node)
980 nodes.append(nodeid)
981 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -0800982
James E. Blaira38c28e2017-01-04 10:33:20 -0800983 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800984 path = self.REQUEST_ROOT + '/' + oid
985 data = json.dumps(request)
986 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
987 self.client.set(path, data)
988
989
James E. Blair498059b2016-12-20 13:50:13 -0800990class ChrootedKazooFixture(fixtures.Fixture):
991 def __init__(self):
992 super(ChrootedKazooFixture, self).__init__()
993
994 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
995 if ':' in zk_host:
996 host, port = zk_host.split(':')
997 else:
998 host = zk_host
999 port = None
1000
1001 self.zookeeper_host = host
1002
1003 if not port:
1004 self.zookeeper_port = 2181
1005 else:
1006 self.zookeeper_port = int(port)
1007
1008 def _setUp(self):
1009 # Make sure the test chroot paths do not conflict
1010 random_bits = ''.join(random.choice(string.ascii_lowercase +
1011 string.ascii_uppercase)
1012 for x in range(8))
1013
1014 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1015 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1016
1017 # Ensure the chroot path exists and clean up any pre-existing znodes.
1018 _tmp_client = kazoo.client.KazooClient(
1019 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1020 _tmp_client.start()
1021
1022 if _tmp_client.exists(self.zookeeper_chroot):
1023 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1024
1025 _tmp_client.ensure_path(self.zookeeper_chroot)
1026 _tmp_client.stop()
1027 _tmp_client.close()
1028
1029 self.addCleanup(self._cleanup)
1030
1031 def _cleanup(self):
1032 '''Remove the chroot path.'''
1033 # Need a non-chroot'ed client to remove the chroot path
1034 _tmp_client = kazoo.client.KazooClient(
1035 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1036 _tmp_client.start()
1037 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1038 _tmp_client.stop()
1039
1040
Maru Newby3fe5f852015-01-13 04:22:14 +00001041class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001042 log = logging.getLogger("zuul.test")
1043
1044 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001045 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001046 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1047 try:
1048 test_timeout = int(test_timeout)
1049 except ValueError:
1050 # If timeout value is invalid do not set a timeout.
1051 test_timeout = 0
1052 if test_timeout > 0:
1053 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1054
1055 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1056 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1057 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1058 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1059 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1060 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1061 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1062 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1063 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1064 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001065 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001066 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1067 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001068 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1069 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001070 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1071 log_level = logging.WARNING
1072 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1073 log_level = logging.ERROR
1074 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1075 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001076 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001077 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001078 format='%(asctime)s %(name)-32s '
1079 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001080
James E. Blairdce6cea2016-12-20 16:45:32 -08001081 # NOTE(notmorgan): Extract logging overrides for specific libraries
1082 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1083 # each. This is used to limit the output during test runs from
1084 # libraries that zuul depends on such as gear.
1085 log_defaults_from_env = os.environ.get(
1086 'OS_LOG_DEFAULTS',
1087 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001088
James E. Blairdce6cea2016-12-20 16:45:32 -08001089 if log_defaults_from_env:
1090 for default in log_defaults_from_env.split(','):
1091 try:
1092 name, level_str = default.split('=', 1)
1093 level = getattr(logging, level_str, logging.DEBUG)
1094 self.useFixture(fixtures.FakeLogger(
1095 name=name,
1096 level=level,
1097 format='%(asctime)s %(name)-32s '
1098 '%(levelname)-8s %(message)s'))
1099 except ValueError:
1100 # NOTE(notmorgan): Invalid format of the log default,
1101 # skip and don't try and apply a logger for the
1102 # specified module
1103 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001104
Maru Newby3fe5f852015-01-13 04:22:14 +00001105
1106class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001107 """A test case with a functioning Zuul.
1108
1109 The following class variables are used during test setup and can
1110 be overidden by subclasses but are effectively read-only once a
1111 test method starts running:
1112
1113 :cvar str config_file: This points to the main zuul config file
1114 within the fixtures directory. Subclasses may override this
1115 to obtain a different behavior.
1116
1117 :cvar str tenant_config_file: This is the tenant config file
1118 (which specifies from what git repos the configuration should
1119 be loaded). It defaults to the value specified in
1120 `config_file` but can be overidden by subclasses to obtain a
1121 different tenant/project layout while using the standard main
1122 configuration.
1123
1124 The following are instance variables that are useful within test
1125 methods:
1126
1127 :ivar FakeGerritConnection fake_<connection>:
1128 A :py:class:`~tests.base.FakeGerritConnection` will be
1129 instantiated for each connection present in the config file
1130 and stored here. For instance, `fake_gerrit` will hold the
1131 FakeGerritConnection object for a connection named `gerrit`.
1132
1133 :ivar FakeGearmanServer gearman_server: An instance of
1134 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1135 server that all of the Zuul components in this test use to
1136 communicate with each other.
1137
1138 :ivar RecordingLaunchServer launch_server: An instance of
1139 :py:class:`~tests.base.RecordingLaunchServer` which is the
1140 Ansible launch server used to run jobs for this test.
1141
1142 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1143 representing currently running builds. They are appended to
1144 the list in the order they are launched, and removed from this
1145 list upon completion.
1146
1147 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1148 objects representing completed builds. They are appended to
1149 the list in the order they complete.
1150
1151 """
1152
James E. Blair83005782015-12-11 14:46:03 -08001153 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001154 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001155
1156 def _startMerger(self):
1157 self.merge_server = zuul.merger.server.MergeServer(self.config,
1158 self.connections)
1159 self.merge_server.start()
1160
Maru Newby3fe5f852015-01-13 04:22:14 +00001161 def setUp(self):
1162 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001163
1164 self.setupZK()
1165
James E. Blair97d902e2014-08-21 13:25:56 -07001166 if USE_TEMPDIR:
1167 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001168 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1169 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001170 else:
1171 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.test_root = os.path.join(tmp_root, "zuul-test")
1173 self.upstream_root = os.path.join(self.test_root, "upstream")
1174 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001175 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001176
1177 if os.path.exists(self.test_root):
1178 shutil.rmtree(self.test_root)
1179 os.makedirs(self.test_root)
1180 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001181 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001182
1183 # Make per test copy of Configuration.
1184 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001185 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001186 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001187 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001188 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001189 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001190
1191 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001192 # TODOv3(jeblair): remove these and replace with new git
1193 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001194 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001195 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001196 self.init_repo("org/project5")
1197 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001198 self.init_repo("org/one-job-project")
1199 self.init_repo("org/nonvoting-project")
1200 self.init_repo("org/templated-project")
1201 self.init_repo("org/layered-project")
1202 self.init_repo("org/node-project")
1203 self.init_repo("org/conflict-project")
1204 self.init_repo("org/noop-project")
1205 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001206 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001207
1208 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001209 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1210 # see: https://github.com/jsocol/pystatsd/issues/61
1211 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001212 os.environ['STATSD_PORT'] = str(self.statsd.port)
1213 self.statsd.start()
1214 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001215 reload_module(statsd)
1216 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001217
1218 self.gearman_server = FakeGearmanServer()
1219
1220 self.config.set('gearman', 'port', str(self.gearman_server.port))
1221
James E. Blaire511d2f2016-12-08 15:22:26 -08001222 gerritsource.GerritSource.replication_timeout = 1.5
1223 gerritsource.GerritSource.replication_retry_interval = 0.5
1224 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001225
Joshua Hesketh352264b2015-08-11 23:42:08 +10001226 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001227
1228 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1229 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001230
Clark Boylanb640e052014-04-03 16:41:46 -07001231 self.swift = zuul.lib.swift.Swift(self.config)
1232
Jan Hruban6b71aff2015-10-22 16:58:08 +02001233 self.event_queues = [
1234 self.sched.result_event_queue,
1235 self.sched.trigger_event_queue
1236 ]
1237
James E. Blairfef78942016-03-11 16:28:56 -08001238 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001239 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001240
Clark Boylanb640e052014-04-03 16:41:46 -07001241 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001242 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001243 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001244 return FakeURLOpener(self.upstream_root, *args, **kw)
1245
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001246 old_urlopen = urllib.request.urlopen
1247 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001248
James E. Blair3f876d52016-07-22 13:07:14 -07001249 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001250
James E. Blaire1767bc2016-08-02 10:00:27 -07001251 self.launch_server = RecordingLaunchServer(
1252 self.config, self.connections, _run_ansible=self.run_ansible)
1253 self.launch_server.start()
1254 self.history = self.launch_server.build_history
1255 self.builds = self.launch_server.running_builds
1256
1257 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001258 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001259 self.merge_client = zuul.merger.client.MergeClient(
1260 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001261 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001262 self.zk = zuul.zk.ZooKeeper()
1263 self.zk.connect([self.zk_config])
1264
1265 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1266 self.zk_config.port,
1267 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001268
James E. Blaire1767bc2016-08-02 10:00:27 -07001269 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001270 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001271 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001272 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001273
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001274 self.webapp = zuul.webapp.WebApp(
1275 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001276 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001277
1278 self.sched.start()
1279 self.sched.reconfigure(self.config)
1280 self.sched.resume()
1281 self.webapp.start()
1282 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001283 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001284
Clark Boylanb640e052014-04-03 16:41:46 -07001285 self.addCleanup(self.shutdown)
1286
James E. Blaire18d4602017-01-05 11:17:28 -08001287 def tearDown(self):
1288 super(ZuulTestCase, self).tearDown()
1289 self.assertFinalState()
1290
James E. Blairfef78942016-03-11 16:28:56 -08001291 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001292 # Set up gerrit related fakes
1293 # Set a changes database so multiple FakeGerrit's can report back to
1294 # a virtual canonical database given by the configured hostname
1295 self.gerrit_changes_dbs = {}
1296
1297 def getGerritConnection(driver, name, config):
1298 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1299 con = FakeGerritConnection(driver, name, config,
1300 changes_db=db,
1301 upstream_root=self.upstream_root)
1302 self.event_queues.append(con.event_queue)
1303 setattr(self, 'fake_' + name, con)
1304 return con
1305
1306 self.useFixture(fixtures.MonkeyPatch(
1307 'zuul.driver.gerrit.GerritDriver.getConnection',
1308 getGerritConnection))
1309
1310 # Set up smtp related fakes
Joshua Hesketh352264b2015-08-11 23:42:08 +10001311 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001312
Joshua Hesketh352264b2015-08-11 23:42:08 +10001313 def FakeSMTPFactory(*args, **kw):
1314 args = [self.smtp_messages] + list(args)
1315 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001316
Joshua Hesketh352264b2015-08-11 23:42:08 +10001317 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001318
James E. Blaire511d2f2016-12-08 15:22:26 -08001319 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001320 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001321 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001322
James E. Blair83005782015-12-11 14:46:03 -08001323 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001324 # This creates the per-test configuration object. It can be
1325 # overriden by subclasses, but should not need to be since it
1326 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001327 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001328 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001329 if hasattr(self, 'tenant_config_file'):
1330 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001331 git_path = os.path.join(
1332 os.path.dirname(
1333 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1334 'git')
1335 if os.path.exists(git_path):
1336 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001337 project = reponame.replace('_', '/')
1338 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001339 os.path.join(git_path, reponame))
1340
James E. Blair498059b2016-12-20 13:50:13 -08001341 def setupZK(self):
1342 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001343 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1344 self.zk_chroot_fixture.zookeeper_host,
1345 self.zk_chroot_fixture.zookeeper_port,
1346 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001347
James E. Blair96c6bf82016-01-15 16:20:40 -08001348 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001349 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001350
1351 files = {}
1352 for (dirpath, dirnames, filenames) in os.walk(source_path):
1353 for filename in filenames:
1354 test_tree_filepath = os.path.join(dirpath, filename)
1355 common_path = os.path.commonprefix([test_tree_filepath,
1356 source_path])
1357 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1358 with open(test_tree_filepath, 'r') as f:
1359 content = f.read()
1360 files[relative_filepath] = content
1361 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001362 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001363
James E. Blaire18d4602017-01-05 11:17:28 -08001364 def assertNodepoolState(self):
1365 # Make sure that there are no pending requests
1366
1367 requests = self.fake_nodepool.getNodeRequests()
1368 self.assertEqual(len(requests), 0)
1369
1370 nodes = self.fake_nodepool.getNodes()
1371 for node in nodes:
1372 self.assertFalse(node['_lock'], "Node %s is locked" %
1373 (node['_oid'],))
1374
Clark Boylanb640e052014-04-03 16:41:46 -07001375 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001376 # Make sure that git.Repo objects have been garbage collected.
1377 repos = []
1378 gc.collect()
1379 for obj in gc.get_objects():
1380 if isinstance(obj, git.Repo):
1381 repos.append(obj)
1382 self.assertEqual(len(repos), 0)
1383 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001384 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001385 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001386 for tenant in self.sched.abide.tenants.values():
1387 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001388 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001389 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001390
1391 def shutdown(self):
1392 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001393 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001394 self.merge_server.stop()
1395 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001396 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.sched.stop()
1399 self.sched.join()
1400 self.statsd.stop()
1401 self.statsd.join()
1402 self.webapp.stop()
1403 self.webapp.join()
1404 self.rpc.stop()
1405 self.rpc.join()
1406 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001407 self.fake_nodepool.stop()
1408 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001409 threads = threading.enumerate()
1410 if len(threads) > 1:
1411 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001412 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001413
1414 def init_repo(self, project):
1415 parts = project.split('/')
1416 path = os.path.join(self.upstream_root, *parts[:-1])
1417 if not os.path.exists(path):
1418 os.makedirs(path)
1419 path = os.path.join(self.upstream_root, project)
1420 repo = git.Repo.init(path)
1421
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001422 with repo.config_writer() as config_writer:
1423 config_writer.set_value('user', 'email', 'user@example.com')
1424 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001425
Clark Boylanb640e052014-04-03 16:41:46 -07001426 repo.index.commit('initial commit')
1427 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001428
James E. Blair97d902e2014-08-21 13:25:56 -07001429 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001430 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001431 repo.git.clean('-x', '-f', '-d')
1432
James E. Blair97d902e2014-08-21 13:25:56 -07001433 def create_branch(self, project, branch):
1434 path = os.path.join(self.upstream_root, project)
1435 repo = git.Repo.init(path)
1436 fn = os.path.join(path, 'README')
1437
1438 branch_head = repo.create_head(branch)
1439 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001440 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001441 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001442 f.close()
1443 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001444 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001445
James E. Blair97d902e2014-08-21 13:25:56 -07001446 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001447 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001448 repo.git.clean('-x', '-f', '-d')
1449
Sachi King9f16d522016-03-16 12:20:45 +11001450 def create_commit(self, project):
1451 path = os.path.join(self.upstream_root, project)
1452 repo = git.Repo(path)
1453 repo.head.reference = repo.heads['master']
1454 file_name = os.path.join(path, 'README')
1455 with open(file_name, 'a') as f:
1456 f.write('creating fake commit\n')
1457 repo.index.add([file_name])
1458 commit = repo.index.commit('Creating a fake commit')
1459 return commit.hexsha
1460
James E. Blairb8c16472015-05-05 14:55:26 -07001461 def orderedRelease(self):
1462 # Run one build at a time to ensure non-race order:
1463 while len(self.builds):
1464 self.release(self.builds[0])
1465 self.waitUntilSettled()
1466
Clark Boylanb640e052014-04-03 16:41:46 -07001467 def release(self, job):
1468 if isinstance(job, FakeBuild):
1469 job.release()
1470 else:
1471 job.waiting = False
1472 self.log.debug("Queued job %s released" % job.unique)
1473 self.gearman_server.wakeConnections()
1474
1475 def getParameter(self, job, name):
1476 if isinstance(job, FakeBuild):
1477 return job.parameters[name]
1478 else:
1479 parameters = json.loads(job.arguments)
1480 return parameters[name]
1481
Clark Boylanb640e052014-04-03 16:41:46 -07001482 def haveAllBuildsReported(self):
1483 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001484 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001485 return False
1486 # Find out if every build that the worker has completed has been
1487 # reported back to Zuul. If it hasn't then that means a Gearman
1488 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001489 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001490 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001491 if not zbuild:
1492 # It has already been reported
1493 continue
1494 # It hasn't been reported yet.
1495 return False
1496 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001497 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001498 if connection.state == 'GRAB_WAIT':
1499 return False
1500 return True
1501
1502 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001503 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001504 for build in builds:
1505 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001506 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001507 for j in conn.related_jobs.values():
1508 if j.unique == build.uuid:
1509 client_job = j
1510 break
1511 if not client_job:
1512 self.log.debug("%s is not known to the gearman client" %
1513 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001514 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001515 if not client_job.handle:
1516 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001517 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001518 server_job = self.gearman_server.jobs.get(client_job.handle)
1519 if not server_job:
1520 self.log.debug("%s is not known to the gearman server" %
1521 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001522 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001523 if not hasattr(server_job, 'waiting'):
1524 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001525 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001526 if server_job.waiting:
1527 continue
James E. Blair17302972016-08-10 16:11:42 -07001528 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001529 self.log.debug("%s has not reported start" % build)
1530 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001531 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001532 if worker_build:
1533 if worker_build.isWaiting():
1534 continue
1535 else:
1536 self.log.debug("%s is running" % worker_build)
1537 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001538 else:
James E. Blair962220f2016-08-03 11:22:38 -07001539 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001540 return False
1541 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001542
James E. Blairdce6cea2016-12-20 16:45:32 -08001543 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001544 if self.fake_nodepool.paused:
1545 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001546 if self.sched.nodepool.requests:
1547 return False
1548 return True
1549
Jan Hruban6b71aff2015-10-22 16:58:08 +02001550 def eventQueuesEmpty(self):
1551 for queue in self.event_queues:
1552 yield queue.empty()
1553
1554 def eventQueuesJoin(self):
1555 for queue in self.event_queues:
1556 queue.join()
1557
Clark Boylanb640e052014-04-03 16:41:46 -07001558 def waitUntilSettled(self):
1559 self.log.debug("Waiting until settled...")
1560 start = time.time()
1561 while True:
1562 if time.time() - start > 10:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001563 self.log.error("Timeout waiting for Zuul to settle")
1564 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001565 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001566 self.log.error(" %s: %s" % (queue, queue.empty()))
1567 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001568 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001569 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001570 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001571 self.log.error("All requests completed: %s" %
1572 (self.areAllNodeRequestsComplete(),))
1573 self.log.error("Merge client jobs: %s" %
1574 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001575 raise Exception("Timeout waiting for Zuul to settle")
1576 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001577
James E. Blaire1767bc2016-08-02 10:00:27 -07001578 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001579 # have all build states propogated to zuul?
1580 if self.haveAllBuildsReported():
1581 # Join ensures that the queue is empty _and_ events have been
1582 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001583 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001584 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001585 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001586 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001587 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001588 self.areAllBuildsWaiting() and
1589 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001590 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001591 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001592 self.log.debug("...settled.")
1593 return
1594 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001595 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001596 self.sched.wake_event.wait(0.1)
1597
1598 def countJobResults(self, jobs, result):
1599 jobs = filter(lambda x: x.result == result, jobs)
1600 return len(jobs)
1601
James E. Blair96c6bf82016-01-15 16:20:40 -08001602 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001603 for job in self.history:
1604 if (job.name == name and
1605 (project is None or
1606 job.parameters['ZUUL_PROJECT'] == project)):
1607 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001608 raise Exception("Unable to find job %s in history" % name)
1609
1610 def assertEmptyQueues(self):
1611 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001612 for tenant in self.sched.abide.tenants.values():
1613 for pipeline in tenant.layout.pipelines.values():
1614 for queue in pipeline.queues:
1615 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001616 print('pipeline %s queue %s contents %s' % (
1617 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001618 self.assertEqual(len(queue.queue), 0,
1619 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001620
1621 def assertReportedStat(self, key, value=None, kind=None):
1622 start = time.time()
1623 while time.time() < (start + 5):
1624 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001625 k, v = stat.split(':')
1626 if key == k:
1627 if value is None and kind is None:
1628 return
1629 elif value:
1630 if value == v:
1631 return
1632 elif kind:
1633 if v.endswith('|' + kind):
1634 return
1635 time.sleep(0.1)
1636
Clark Boylanb640e052014-04-03 16:41:46 -07001637 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001638
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001639 def assertBuilds(self, builds):
1640 """Assert that the running builds are as described.
1641
1642 The list of running builds is examined and must match exactly
1643 the list of builds described by the input.
1644
1645 :arg list builds: A list of dictionaries. Each item in the
1646 list must match the corresponding build in the build
1647 history, and each element of the dictionary must match the
1648 corresponding attribute of the build.
1649
1650 """
James E. Blair3158e282016-08-19 09:34:11 -07001651 try:
1652 self.assertEqual(len(self.builds), len(builds))
1653 for i, d in enumerate(builds):
1654 for k, v in d.items():
1655 self.assertEqual(
1656 getattr(self.builds[i], k), v,
1657 "Element %i in builds does not match" % (i,))
1658 except Exception:
1659 for build in self.builds:
1660 self.log.error("Running build: %s" % build)
1661 else:
1662 self.log.error("No running builds")
1663 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001664
James E. Blairb536ecc2016-08-31 10:11:42 -07001665 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001666 """Assert that the completed builds are as described.
1667
1668 The list of completed builds is examined and must match
1669 exactly the list of builds described by the input.
1670
1671 :arg list history: A list of dictionaries. Each item in the
1672 list must match the corresponding build in the build
1673 history, and each element of the dictionary must match the
1674 corresponding attribute of the build.
1675
James E. Blairb536ecc2016-08-31 10:11:42 -07001676 :arg bool ordered: If true, the history must match the order
1677 supplied, if false, the builds are permitted to have
1678 arrived in any order.
1679
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001680 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001681 def matches(history_item, item):
1682 for k, v in item.items():
1683 if getattr(history_item, k) != v:
1684 return False
1685 return True
James E. Blair3158e282016-08-19 09:34:11 -07001686 try:
1687 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001688 if ordered:
1689 for i, d in enumerate(history):
1690 if not matches(self.history[i], d):
1691 raise Exception(
1692 "Element %i in history does not match" % (i,))
1693 else:
1694 unseen = self.history[:]
1695 for i, d in enumerate(history):
1696 found = False
1697 for unseen_item in unseen:
1698 if matches(unseen_item, d):
1699 found = True
1700 unseen.remove(unseen_item)
1701 break
1702 if not found:
1703 raise Exception("No match found for element %i "
1704 "in history" % (i,))
1705 if unseen:
1706 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001707 except Exception:
1708 for build in self.history:
1709 self.log.error("Completed build: %s" % build)
1710 else:
1711 self.log.error("No completed builds")
1712 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001713
James E. Blair6ac368c2016-12-22 18:07:20 -08001714 def printHistory(self):
1715 """Log the build history.
1716
1717 This can be useful during tests to summarize what jobs have
1718 completed.
1719
1720 """
1721 self.log.debug("Build history:")
1722 for build in self.history:
1723 self.log.debug(build)
1724
James E. Blair59fdbac2015-12-07 17:08:06 -08001725 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001726 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1727
1728 def updateConfigLayout(self, path):
1729 root = os.path.join(self.test_root, "config")
1730 os.makedirs(root)
1731 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1732 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001733- tenant:
1734 name: openstack
1735 source:
1736 gerrit:
1737 config-repos:
1738 - %s
1739 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001740 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001741 self.config.set('zuul', 'tenant_config',
1742 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001743
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001744 def addCommitToRepo(self, project, message, files,
1745 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001746 path = os.path.join(self.upstream_root, project)
1747 repo = git.Repo(path)
1748 repo.head.reference = branch
1749 zuul.merger.merger.reset_repo_to_head(repo)
1750 for fn, content in files.items():
1751 fn = os.path.join(path, fn)
1752 with open(fn, 'w') as f:
1753 f.write(content)
1754 repo.index.add([fn])
1755 commit = repo.index.commit(message)
1756 repo.heads[branch].commit = commit
1757 repo.head.reference = branch
1758 repo.git.clean('-x', '-f', '-d')
1759 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001760 if tag:
1761 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001762
James E. Blair7fc8daa2016-08-08 15:37:15 -07001763 def addEvent(self, connection, event):
1764 """Inject a Fake (Gerrit) event.
1765
1766 This method accepts a JSON-encoded event and simulates Zuul
1767 having received it from Gerrit. It could (and should)
1768 eventually apply to any connection type, but is currently only
1769 used with Gerrit connections. The name of the connection is
1770 used to look up the corresponding server, and the event is
1771 simulated as having been received by all Zuul connections
1772 attached to that server. So if two Gerrit connections in Zuul
1773 are connected to the same Gerrit server, and you invoke this
1774 method specifying the name of one of them, the event will be
1775 received by both.
1776
1777 .. note::
1778
1779 "self.fake_gerrit.addEvent" calls should be migrated to
1780 this method.
1781
1782 :arg str connection: The name of the connection corresponding
1783 to the gerrit server.
1784 :arg str event: The JSON-encoded event.
1785
1786 """
1787 specified_conn = self.connections.connections[connection]
1788 for conn in self.connections.connections.values():
1789 if (isinstance(conn, specified_conn.__class__) and
1790 specified_conn.server == conn.server):
1791 conn.addEvent(event)
1792
James E. Blair3f876d52016-07-22 13:07:14 -07001793
1794class AnsibleZuulTestCase(ZuulTestCase):
1795 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001796 run_ansible = True