blob: 2d1dd7d50a3e9ea489573feec832e98ab9e60e0a [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. Blaira92cbc82017-01-23 14:56:49 -0800679 self._test_root = kw.pop('_test_root', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800680 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700681 self.hold_jobs_in_build = False
682 self.lock = threading.Lock()
683 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700684 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700685 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700686 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800687
James E. Blaira5dba232016-08-08 15:53:24 -0700688 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700689 """Instruct the launcher to report matching builds as failures.
690
691 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700692 :arg Change change: The :py:class:`~tests.base.FakeChange`
693 instance which should cause the job to fail. This job
694 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700695
696 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700697 l = self.fail_tests.get(name, [])
698 l.append(change)
699 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800700
James E. Blair962220f2016-08-03 11:22:38 -0700701 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700702 """Release a held build.
703
704 :arg str regex: A regular expression which, if supplied, will
705 cause only builds with matching names to be released. If
706 not supplied, all builds will be released.
707
708 """
James E. Blair962220f2016-08-03 11:22:38 -0700709 builds = self.running_builds[:]
710 self.log.debug("Releasing build %s (%s)" % (regex,
711 len(self.running_builds)))
712 for build in builds:
713 if not regex or re.match(regex, build.name):
714 self.log.debug("Releasing build %s" %
715 (build.parameters['ZUUL_UUID']))
716 build.release()
717 else:
718 self.log.debug("Not releasing build %s" %
719 (build.parameters['ZUUL_UUID']))
720 self.log.debug("Done releasing builds %s (%s)" %
721 (regex, len(self.running_builds)))
722
James E. Blair17302972016-08-10 16:11:42 -0700723 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700724 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700726 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700727 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800728 args = json.loads(job.arguments)
729 args['zuul']['_test'] = dict(test_root=self._test_root)
730 job.arguments = json.dumps(args)
James E. Blair17302972016-08-10 16:11:42 -0700731 super(RecordingLaunchServer, self).launchJob(job)
732
733 def stopJob(self, job):
734 self.log.debug("handle stop")
735 parameters = json.loads(job.arguments)
736 uuid = parameters['uuid']
737 for build in self.running_builds:
738 if build.unique == uuid:
739 build.aborted = True
740 build.release()
741 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700742
743 def runAnsible(self, jobdir, job):
744 build = self.job_builds[job.unique]
745 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700746
747 if self._run_ansible:
748 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
749 else:
750 result = build.run()
751
752 self.lock.acquire()
753 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700754 BuildHistory(name=build.name, result=result, changes=build.changes,
755 node=build.node, uuid=build.unique,
756 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700757 pipeline=build.parameters['ZUUL_PIPELINE'])
758 )
James E. Blairab7132b2016-08-05 12:36:22 -0700759 self.running_builds.remove(build)
760 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700761 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800762 if build.run_error:
763 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700764 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800765
766
Clark Boylanb640e052014-04-03 16:41:46 -0700767class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700768 """A Gearman server for use in tests.
769
770 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
771 added to the queue but will not be distributed to workers
772 until released. This attribute may be changed at any time and
773 will take effect for subsequently enqueued jobs, but
774 previously held jobs will still need to be explicitly
775 released.
776
777 """
778
Clark Boylanb640e052014-04-03 16:41:46 -0700779 def __init__(self):
780 self.hold_jobs_in_queue = False
781 super(FakeGearmanServer, self).__init__(0)
782
783 def getJobForConnection(self, connection, peek=False):
784 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
785 for job in queue:
786 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500787 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700788 job.waiting = self.hold_jobs_in_queue
789 else:
790 job.waiting = False
791 if job.waiting:
792 continue
793 if job.name in connection.functions:
794 if not peek:
795 queue.remove(job)
796 connection.related_jobs[job.handle] = job
797 job.worker_connection = connection
798 job.running = True
799 return job
800 return None
801
802 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700803 """Release a held job.
804
805 :arg str regex: A regular expression which, if supplied, will
806 cause only jobs with matching names to be released. If
807 not supplied, all jobs will be released.
808 """
Clark Boylanb640e052014-04-03 16:41:46 -0700809 released = False
810 qlen = (len(self.high_queue) + len(self.normal_queue) +
811 len(self.low_queue))
812 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
813 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500814 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700815 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500816 parameters = json.loads(job.arguments)
817 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700818 self.log.debug("releasing queued job %s" %
819 job.unique)
820 job.waiting = False
821 released = True
822 else:
823 self.log.debug("not releasing queued job %s" %
824 job.unique)
825 if released:
826 self.wakeConnections()
827 qlen = (len(self.high_queue) + len(self.normal_queue) +
828 len(self.low_queue))
829 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
830
831
832class FakeSMTP(object):
833 log = logging.getLogger('zuul.FakeSMTP')
834
835 def __init__(self, messages, server, port):
836 self.server = server
837 self.port = port
838 self.messages = messages
839
840 def sendmail(self, from_email, to_email, msg):
841 self.log.info("Sending email from %s, to %s, with msg %s" % (
842 from_email, to_email, msg))
843
844 headers = msg.split('\n\n', 1)[0]
845 body = msg.split('\n\n', 1)[1]
846
847 self.messages.append(dict(
848 from_email=from_email,
849 to_email=to_email,
850 msg=msg,
851 headers=headers,
852 body=body,
853 ))
854
855 return True
856
857 def quit(self):
858 return True
859
860
861class FakeSwiftClientConnection(swiftclient.client.Connection):
862 def post_account(self, headers):
863 # Do nothing
864 pass
865
866 def get_auth(self):
867 # Returns endpoint and (unused) auth token
868 endpoint = os.path.join('https://storage.example.org', 'V1',
869 'AUTH_account')
870 return endpoint, ''
871
872
James E. Blairdce6cea2016-12-20 16:45:32 -0800873class FakeNodepool(object):
874 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800875 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800876
877 log = logging.getLogger("zuul.test.FakeNodepool")
878
879 def __init__(self, host, port, chroot):
880 self.client = kazoo.client.KazooClient(
881 hosts='%s:%s%s' % (host, port, chroot))
882 self.client.start()
883 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800884 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800885 self.thread = threading.Thread(target=self.run)
886 self.thread.daemon = True
887 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800888 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800889
890 def stop(self):
891 self._running = False
892 self.thread.join()
893 self.client.stop()
894 self.client.close()
895
896 def run(self):
897 while self._running:
898 self._run()
899 time.sleep(0.1)
900
901 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800902 if self.paused:
903 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800904 for req in self.getNodeRequests():
905 self.fulfillRequest(req)
906
907 def getNodeRequests(self):
908 try:
909 reqids = self.client.get_children(self.REQUEST_ROOT)
910 except kazoo.exceptions.NoNodeError:
911 return []
912 reqs = []
913 for oid in sorted(reqids):
914 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800915 try:
916 data, stat = self.client.get(path)
917 data = json.loads(data)
918 data['_oid'] = oid
919 reqs.append(data)
920 except kazoo.exceptions.NoNodeError:
921 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800922 return reqs
923
James E. Blaire18d4602017-01-05 11:17:28 -0800924 def getNodes(self):
925 try:
926 nodeids = self.client.get_children(self.NODE_ROOT)
927 except kazoo.exceptions.NoNodeError:
928 return []
929 nodes = []
930 for oid in sorted(nodeids):
931 path = self.NODE_ROOT + '/' + oid
932 data, stat = self.client.get(path)
933 data = json.loads(data)
934 data['_oid'] = oid
935 try:
936 lockfiles = self.client.get_children(path + '/lock')
937 except kazoo.exceptions.NoNodeError:
938 lockfiles = []
939 if lockfiles:
940 data['_lock'] = True
941 else:
942 data['_lock'] = False
943 nodes.append(data)
944 return nodes
945
James E. Blaira38c28e2017-01-04 10:33:20 -0800946 def makeNode(self, request_id, node_type):
947 now = time.time()
948 path = '/nodepool/nodes/'
949 data = dict(type=node_type,
950 provider='test-provider',
951 region='test-region',
952 az=None,
953 public_ipv4='127.0.0.1',
954 private_ipv4=None,
955 public_ipv6=None,
956 allocated_to=request_id,
957 state='ready',
958 state_time=now,
959 created_time=now,
960 updated_time=now,
961 image_id=None,
962 launcher='fake-nodepool')
963 data = json.dumps(data)
964 path = self.client.create(path, data,
965 makepath=True,
966 sequence=True)
967 nodeid = path.split("/")[-1]
968 return nodeid
969
James E. Blair6ab79e02017-01-06 10:10:17 -0800970 def addFailRequest(self, request):
971 self.fail_requests.add(request['_oid'])
972
James E. Blairdce6cea2016-12-20 16:45:32 -0800973 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800974 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800975 return
976 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800977 oid = request['_oid']
978 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800979
James E. Blair6ab79e02017-01-06 10:10:17 -0800980 if oid in self.fail_requests:
981 request['state'] = 'failed'
982 else:
983 request['state'] = 'fulfilled'
984 nodes = []
985 for node in request['node_types']:
986 nodeid = self.makeNode(oid, node)
987 nodes.append(nodeid)
988 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -0800989
James E. Blaira38c28e2017-01-04 10:33:20 -0800990 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800991 path = self.REQUEST_ROOT + '/' + oid
992 data = json.dumps(request)
993 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
994 self.client.set(path, data)
995
996
James E. Blair498059b2016-12-20 13:50:13 -0800997class ChrootedKazooFixture(fixtures.Fixture):
998 def __init__(self):
999 super(ChrootedKazooFixture, self).__init__()
1000
1001 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1002 if ':' in zk_host:
1003 host, port = zk_host.split(':')
1004 else:
1005 host = zk_host
1006 port = None
1007
1008 self.zookeeper_host = host
1009
1010 if not port:
1011 self.zookeeper_port = 2181
1012 else:
1013 self.zookeeper_port = int(port)
1014
1015 def _setUp(self):
1016 # Make sure the test chroot paths do not conflict
1017 random_bits = ''.join(random.choice(string.ascii_lowercase +
1018 string.ascii_uppercase)
1019 for x in range(8))
1020
1021 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1022 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1023
1024 # Ensure the chroot path exists and clean up any pre-existing znodes.
1025 _tmp_client = kazoo.client.KazooClient(
1026 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1027 _tmp_client.start()
1028
1029 if _tmp_client.exists(self.zookeeper_chroot):
1030 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1031
1032 _tmp_client.ensure_path(self.zookeeper_chroot)
1033 _tmp_client.stop()
1034 _tmp_client.close()
1035
1036 self.addCleanup(self._cleanup)
1037
1038 def _cleanup(self):
1039 '''Remove the chroot path.'''
1040 # Need a non-chroot'ed client to remove the chroot path
1041 _tmp_client = kazoo.client.KazooClient(
1042 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1043 _tmp_client.start()
1044 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1045 _tmp_client.stop()
1046
1047
Maru Newby3fe5f852015-01-13 04:22:14 +00001048class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001049 log = logging.getLogger("zuul.test")
1050
1051 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001052 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001053 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1054 try:
1055 test_timeout = int(test_timeout)
1056 except ValueError:
1057 # If timeout value is invalid do not set a timeout.
1058 test_timeout = 0
1059 if test_timeout > 0:
1060 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1061
1062 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1063 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1064 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1065 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1066 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1067 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1068 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1069 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1070 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1071 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001072 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001073 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1074 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001075 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1076 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001077 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1078 log_level = logging.WARNING
1079 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1080 log_level = logging.ERROR
1081 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1082 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001083 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001084 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001085 format='%(asctime)s %(name)-32s '
1086 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001087
James E. Blairdce6cea2016-12-20 16:45:32 -08001088 # NOTE(notmorgan): Extract logging overrides for specific libraries
1089 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1090 # each. This is used to limit the output during test runs from
1091 # libraries that zuul depends on such as gear.
1092 log_defaults_from_env = os.environ.get(
1093 'OS_LOG_DEFAULTS',
1094 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001095
James E. Blairdce6cea2016-12-20 16:45:32 -08001096 if log_defaults_from_env:
1097 for default in log_defaults_from_env.split(','):
1098 try:
1099 name, level_str = default.split('=', 1)
1100 level = getattr(logging, level_str, logging.DEBUG)
1101 self.useFixture(fixtures.FakeLogger(
1102 name=name,
1103 level=level,
1104 format='%(asctime)s %(name)-32s '
1105 '%(levelname)-8s %(message)s'))
1106 except ValueError:
1107 # NOTE(notmorgan): Invalid format of the log default,
1108 # skip and don't try and apply a logger for the
1109 # specified module
1110 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001111
Maru Newby3fe5f852015-01-13 04:22:14 +00001112
1113class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001114 """A test case with a functioning Zuul.
1115
1116 The following class variables are used during test setup and can
1117 be overidden by subclasses but are effectively read-only once a
1118 test method starts running:
1119
1120 :cvar str config_file: This points to the main zuul config file
1121 within the fixtures directory. Subclasses may override this
1122 to obtain a different behavior.
1123
1124 :cvar str tenant_config_file: This is the tenant config file
1125 (which specifies from what git repos the configuration should
1126 be loaded). It defaults to the value specified in
1127 `config_file` but can be overidden by subclasses to obtain a
1128 different tenant/project layout while using the standard main
1129 configuration.
1130
1131 The following are instance variables that are useful within test
1132 methods:
1133
1134 :ivar FakeGerritConnection fake_<connection>:
1135 A :py:class:`~tests.base.FakeGerritConnection` will be
1136 instantiated for each connection present in the config file
1137 and stored here. For instance, `fake_gerrit` will hold the
1138 FakeGerritConnection object for a connection named `gerrit`.
1139
1140 :ivar FakeGearmanServer gearman_server: An instance of
1141 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1142 server that all of the Zuul components in this test use to
1143 communicate with each other.
1144
1145 :ivar RecordingLaunchServer launch_server: An instance of
1146 :py:class:`~tests.base.RecordingLaunchServer` which is the
1147 Ansible launch server used to run jobs for this test.
1148
1149 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1150 representing currently running builds. They are appended to
1151 the list in the order they are launched, and removed from this
1152 list upon completion.
1153
1154 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1155 objects representing completed builds. They are appended to
1156 the list in the order they complete.
1157
1158 """
1159
James E. Blair83005782015-12-11 14:46:03 -08001160 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001161 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001162
1163 def _startMerger(self):
1164 self.merge_server = zuul.merger.server.MergeServer(self.config,
1165 self.connections)
1166 self.merge_server.start()
1167
Maru Newby3fe5f852015-01-13 04:22:14 +00001168 def setUp(self):
1169 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001170
1171 self.setupZK()
1172
James E. Blair97d902e2014-08-21 13:25:56 -07001173 if USE_TEMPDIR:
1174 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001175 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1176 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001177 else:
1178 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001179 self.test_root = os.path.join(tmp_root, "zuul-test")
1180 self.upstream_root = os.path.join(self.test_root, "upstream")
1181 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001182 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001183
1184 if os.path.exists(self.test_root):
1185 shutil.rmtree(self.test_root)
1186 os.makedirs(self.test_root)
1187 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001188 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001189
1190 # Make per test copy of Configuration.
1191 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001192 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001193 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001194 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001195 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001196 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001197
1198 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001199 # TODOv3(jeblair): remove these and replace with new git
1200 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001201 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001202 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001203 self.init_repo("org/project5")
1204 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.init_repo("org/one-job-project")
1206 self.init_repo("org/nonvoting-project")
1207 self.init_repo("org/templated-project")
1208 self.init_repo("org/layered-project")
1209 self.init_repo("org/node-project")
1210 self.init_repo("org/conflict-project")
1211 self.init_repo("org/noop-project")
1212 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001213 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001214
1215 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001216 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1217 # see: https://github.com/jsocol/pystatsd/issues/61
1218 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001219 os.environ['STATSD_PORT'] = str(self.statsd.port)
1220 self.statsd.start()
1221 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001222 reload_module(statsd)
1223 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001224
1225 self.gearman_server = FakeGearmanServer()
1226
1227 self.config.set('gearman', 'port', str(self.gearman_server.port))
1228
James E. Blaire511d2f2016-12-08 15:22:26 -08001229 gerritsource.GerritSource.replication_timeout = 1.5
1230 gerritsource.GerritSource.replication_retry_interval = 0.5
1231 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001232
Joshua Hesketh352264b2015-08-11 23:42:08 +10001233 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001234
1235 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1236 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001237
Clark Boylanb640e052014-04-03 16:41:46 -07001238 self.swift = zuul.lib.swift.Swift(self.config)
1239
Jan Hruban6b71aff2015-10-22 16:58:08 +02001240 self.event_queues = [
1241 self.sched.result_event_queue,
1242 self.sched.trigger_event_queue
1243 ]
1244
James E. Blairfef78942016-03-11 16:28:56 -08001245 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001246 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001247
Clark Boylanb640e052014-04-03 16:41:46 -07001248 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001249 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001250 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001251 return FakeURLOpener(self.upstream_root, *args, **kw)
1252
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001253 old_urlopen = urllib.request.urlopen
1254 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001255
James E. Blair3f876d52016-07-22 13:07:14 -07001256 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001257
James E. Blaire1767bc2016-08-02 10:00:27 -07001258 self.launch_server = RecordingLaunchServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001259 self.config, self.connections,
1260 _run_ansible=self.run_ansible,
1261 _test_root=self.test_root)
James E. Blaire1767bc2016-08-02 10:00:27 -07001262 self.launch_server.start()
1263 self.history = self.launch_server.build_history
1264 self.builds = self.launch_server.running_builds
1265
1266 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001267 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001268 self.merge_client = zuul.merger.client.MergeClient(
1269 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001270 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001271 self.zk = zuul.zk.ZooKeeper()
1272 self.zk.connect([self.zk_config])
1273
1274 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1275 self.zk_config.port,
1276 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001277
James E. Blaire1767bc2016-08-02 10:00:27 -07001278 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001279 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001280 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001281 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001282
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001283 self.webapp = zuul.webapp.WebApp(
1284 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001285 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001286
1287 self.sched.start()
1288 self.sched.reconfigure(self.config)
1289 self.sched.resume()
1290 self.webapp.start()
1291 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001292 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001293
Clark Boylanb640e052014-04-03 16:41:46 -07001294 self.addCleanup(self.shutdown)
1295
James E. Blaire18d4602017-01-05 11:17:28 -08001296 def tearDown(self):
1297 super(ZuulTestCase, self).tearDown()
1298 self.assertFinalState()
1299
James E. Blairfef78942016-03-11 16:28:56 -08001300 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001301 # Set up gerrit related fakes
1302 # Set a changes database so multiple FakeGerrit's can report back to
1303 # a virtual canonical database given by the configured hostname
1304 self.gerrit_changes_dbs = {}
1305
1306 def getGerritConnection(driver, name, config):
1307 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1308 con = FakeGerritConnection(driver, name, config,
1309 changes_db=db,
1310 upstream_root=self.upstream_root)
1311 self.event_queues.append(con.event_queue)
1312 setattr(self, 'fake_' + name, con)
1313 return con
1314
1315 self.useFixture(fixtures.MonkeyPatch(
1316 'zuul.driver.gerrit.GerritDriver.getConnection',
1317 getGerritConnection))
1318
1319 # Set up smtp related fakes
Joshua Hesketh352264b2015-08-11 23:42:08 +10001320 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001321
Joshua Hesketh352264b2015-08-11 23:42:08 +10001322 def FakeSMTPFactory(*args, **kw):
1323 args = [self.smtp_messages] + list(args)
1324 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001325
Joshua Hesketh352264b2015-08-11 23:42:08 +10001326 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001327
James E. Blaire511d2f2016-12-08 15:22:26 -08001328 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001329 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001330 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001331
James E. Blair83005782015-12-11 14:46:03 -08001332 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001333 # This creates the per-test configuration object. It can be
1334 # overriden by subclasses, but should not need to be since it
1335 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001336 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001337 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001338 if hasattr(self, 'tenant_config_file'):
1339 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001340 git_path = os.path.join(
1341 os.path.dirname(
1342 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1343 'git')
1344 if os.path.exists(git_path):
1345 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001346 project = reponame.replace('_', '/')
1347 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001348 os.path.join(git_path, reponame))
1349
James E. Blair498059b2016-12-20 13:50:13 -08001350 def setupZK(self):
1351 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001352 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1353 self.zk_chroot_fixture.zookeeper_host,
1354 self.zk_chroot_fixture.zookeeper_port,
1355 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001356
James E. Blair96c6bf82016-01-15 16:20:40 -08001357 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001358 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001359
1360 files = {}
1361 for (dirpath, dirnames, filenames) in os.walk(source_path):
1362 for filename in filenames:
1363 test_tree_filepath = os.path.join(dirpath, filename)
1364 common_path = os.path.commonprefix([test_tree_filepath,
1365 source_path])
1366 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1367 with open(test_tree_filepath, 'r') as f:
1368 content = f.read()
1369 files[relative_filepath] = content
1370 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001371 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001372
James E. Blaire18d4602017-01-05 11:17:28 -08001373 def assertNodepoolState(self):
1374 # Make sure that there are no pending requests
1375
1376 requests = self.fake_nodepool.getNodeRequests()
1377 self.assertEqual(len(requests), 0)
1378
1379 nodes = self.fake_nodepool.getNodes()
1380 for node in nodes:
1381 self.assertFalse(node['_lock'], "Node %s is locked" %
1382 (node['_oid'],))
1383
Clark Boylanb640e052014-04-03 16:41:46 -07001384 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001385 # Make sure that git.Repo objects have been garbage collected.
1386 repos = []
1387 gc.collect()
1388 for obj in gc.get_objects():
1389 if isinstance(obj, git.Repo):
1390 repos.append(obj)
1391 self.assertEqual(len(repos), 0)
1392 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001393 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001394 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001395 for tenant in self.sched.abide.tenants.values():
1396 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001397 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001398 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001399
1400 def shutdown(self):
1401 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001402 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001403 self.merge_server.stop()
1404 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001405 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001406 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.sched.stop()
1408 self.sched.join()
1409 self.statsd.stop()
1410 self.statsd.join()
1411 self.webapp.stop()
1412 self.webapp.join()
1413 self.rpc.stop()
1414 self.rpc.join()
1415 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001416 self.fake_nodepool.stop()
1417 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001418 threads = threading.enumerate()
1419 if len(threads) > 1:
1420 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001421 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001422
1423 def init_repo(self, project):
1424 parts = project.split('/')
1425 path = os.path.join(self.upstream_root, *parts[:-1])
1426 if not os.path.exists(path):
1427 os.makedirs(path)
1428 path = os.path.join(self.upstream_root, project)
1429 repo = git.Repo.init(path)
1430
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001431 with repo.config_writer() as config_writer:
1432 config_writer.set_value('user', 'email', 'user@example.com')
1433 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001434
Clark Boylanb640e052014-04-03 16:41:46 -07001435 repo.index.commit('initial commit')
1436 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001437
James E. Blair97d902e2014-08-21 13:25:56 -07001438 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001439 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001440 repo.git.clean('-x', '-f', '-d')
1441
James E. Blair97d902e2014-08-21 13:25:56 -07001442 def create_branch(self, project, branch):
1443 path = os.path.join(self.upstream_root, project)
1444 repo = git.Repo.init(path)
1445 fn = os.path.join(path, 'README')
1446
1447 branch_head = repo.create_head(branch)
1448 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001449 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001450 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001451 f.close()
1452 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001453 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001454
James E. Blair97d902e2014-08-21 13:25:56 -07001455 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001456 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001457 repo.git.clean('-x', '-f', '-d')
1458
Sachi King9f16d522016-03-16 12:20:45 +11001459 def create_commit(self, project):
1460 path = os.path.join(self.upstream_root, project)
1461 repo = git.Repo(path)
1462 repo.head.reference = repo.heads['master']
1463 file_name = os.path.join(path, 'README')
1464 with open(file_name, 'a') as f:
1465 f.write('creating fake commit\n')
1466 repo.index.add([file_name])
1467 commit = repo.index.commit('Creating a fake commit')
1468 return commit.hexsha
1469
James E. Blairb8c16472015-05-05 14:55:26 -07001470 def orderedRelease(self):
1471 # Run one build at a time to ensure non-race order:
1472 while len(self.builds):
1473 self.release(self.builds[0])
1474 self.waitUntilSettled()
1475
Clark Boylanb640e052014-04-03 16:41:46 -07001476 def release(self, job):
1477 if isinstance(job, FakeBuild):
1478 job.release()
1479 else:
1480 job.waiting = False
1481 self.log.debug("Queued job %s released" % job.unique)
1482 self.gearman_server.wakeConnections()
1483
1484 def getParameter(self, job, name):
1485 if isinstance(job, FakeBuild):
1486 return job.parameters[name]
1487 else:
1488 parameters = json.loads(job.arguments)
1489 return parameters[name]
1490
Clark Boylanb640e052014-04-03 16:41:46 -07001491 def haveAllBuildsReported(self):
1492 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001493 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001494 return False
1495 # Find out if every build that the worker has completed has been
1496 # reported back to Zuul. If it hasn't then that means a Gearman
1497 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001498 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001499 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001500 if not zbuild:
1501 # It has already been reported
1502 continue
1503 # It hasn't been reported yet.
1504 return False
1505 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001506 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001507 if connection.state == 'GRAB_WAIT':
1508 return False
1509 return True
1510
1511 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001512 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001513 for build in builds:
1514 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001515 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001516 for j in conn.related_jobs.values():
1517 if j.unique == build.uuid:
1518 client_job = j
1519 break
1520 if not client_job:
1521 self.log.debug("%s is not known to the gearman client" %
1522 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001523 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001524 if not client_job.handle:
1525 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001526 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001527 server_job = self.gearman_server.jobs.get(client_job.handle)
1528 if not server_job:
1529 self.log.debug("%s is not known to the gearman server" %
1530 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001531 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001532 if not hasattr(server_job, 'waiting'):
1533 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001534 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001535 if server_job.waiting:
1536 continue
James E. Blair17302972016-08-10 16:11:42 -07001537 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001538 self.log.debug("%s has not reported start" % build)
1539 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001540 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001541 if worker_build:
1542 if worker_build.isWaiting():
1543 continue
1544 else:
1545 self.log.debug("%s is running" % worker_build)
1546 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001547 else:
James E. Blair962220f2016-08-03 11:22:38 -07001548 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001549 return False
1550 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001551
James E. Blairdce6cea2016-12-20 16:45:32 -08001552 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001553 if self.fake_nodepool.paused:
1554 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001555 if self.sched.nodepool.requests:
1556 return False
1557 return True
1558
Jan Hruban6b71aff2015-10-22 16:58:08 +02001559 def eventQueuesEmpty(self):
1560 for queue in self.event_queues:
1561 yield queue.empty()
1562
1563 def eventQueuesJoin(self):
1564 for queue in self.event_queues:
1565 queue.join()
1566
Clark Boylanb640e052014-04-03 16:41:46 -07001567 def waitUntilSettled(self):
1568 self.log.debug("Waiting until settled...")
1569 start = time.time()
1570 while True:
James E. Blair71932482017-02-02 11:29:07 -08001571 if time.time() - start > 20:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001572 self.log.error("Timeout waiting for Zuul to settle")
1573 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001574 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001575 self.log.error(" %s: %s" % (queue, queue.empty()))
1576 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001577 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001578 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001579 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001580 self.log.error("All requests completed: %s" %
1581 (self.areAllNodeRequestsComplete(),))
1582 self.log.error("Merge client jobs: %s" %
1583 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001584 raise Exception("Timeout waiting for Zuul to settle")
1585 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001586
James E. Blaire1767bc2016-08-02 10:00:27 -07001587 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001588 # have all build states propogated to zuul?
1589 if self.haveAllBuildsReported():
1590 # Join ensures that the queue is empty _and_ events have been
1591 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001592 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001593 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001594 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001595 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001596 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001597 self.areAllBuildsWaiting() and
1598 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001599 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001600 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001601 self.log.debug("...settled.")
1602 return
1603 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001604 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001605 self.sched.wake_event.wait(0.1)
1606
1607 def countJobResults(self, jobs, result):
1608 jobs = filter(lambda x: x.result == result, jobs)
1609 return len(jobs)
1610
James E. Blair96c6bf82016-01-15 16:20:40 -08001611 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001612 for job in self.history:
1613 if (job.name == name and
1614 (project is None or
1615 job.parameters['ZUUL_PROJECT'] == project)):
1616 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001617 raise Exception("Unable to find job %s in history" % name)
1618
1619 def assertEmptyQueues(self):
1620 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001621 for tenant in self.sched.abide.tenants.values():
1622 for pipeline in tenant.layout.pipelines.values():
1623 for queue in pipeline.queues:
1624 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001625 print('pipeline %s queue %s contents %s' % (
1626 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001627 self.assertEqual(len(queue.queue), 0,
1628 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001629
1630 def assertReportedStat(self, key, value=None, kind=None):
1631 start = time.time()
1632 while time.time() < (start + 5):
1633 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001634 k, v = stat.split(':')
1635 if key == k:
1636 if value is None and kind is None:
1637 return
1638 elif value:
1639 if value == v:
1640 return
1641 elif kind:
1642 if v.endswith('|' + kind):
1643 return
1644 time.sleep(0.1)
1645
Clark Boylanb640e052014-04-03 16:41:46 -07001646 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001647
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001648 def assertBuilds(self, builds):
1649 """Assert that the running builds are as described.
1650
1651 The list of running builds is examined and must match exactly
1652 the list of builds described by the input.
1653
1654 :arg list builds: A list of dictionaries. Each item in the
1655 list must match the corresponding build in the build
1656 history, and each element of the dictionary must match the
1657 corresponding attribute of the build.
1658
1659 """
James E. Blair3158e282016-08-19 09:34:11 -07001660 try:
1661 self.assertEqual(len(self.builds), len(builds))
1662 for i, d in enumerate(builds):
1663 for k, v in d.items():
1664 self.assertEqual(
1665 getattr(self.builds[i], k), v,
1666 "Element %i in builds does not match" % (i,))
1667 except Exception:
1668 for build in self.builds:
1669 self.log.error("Running build: %s" % build)
1670 else:
1671 self.log.error("No running builds")
1672 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001673
James E. Blairb536ecc2016-08-31 10:11:42 -07001674 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001675 """Assert that the completed builds are as described.
1676
1677 The list of completed builds is examined and must match
1678 exactly the list of builds described by the input.
1679
1680 :arg list history: A list of dictionaries. Each item in the
1681 list must match the corresponding build in the build
1682 history, and each element of the dictionary must match the
1683 corresponding attribute of the build.
1684
James E. Blairb536ecc2016-08-31 10:11:42 -07001685 :arg bool ordered: If true, the history must match the order
1686 supplied, if false, the builds are permitted to have
1687 arrived in any order.
1688
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001689 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001690 def matches(history_item, item):
1691 for k, v in item.items():
1692 if getattr(history_item, k) != v:
1693 return False
1694 return True
James E. Blair3158e282016-08-19 09:34:11 -07001695 try:
1696 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001697 if ordered:
1698 for i, d in enumerate(history):
1699 if not matches(self.history[i], d):
1700 raise Exception(
1701 "Element %i in history does not match" % (i,))
1702 else:
1703 unseen = self.history[:]
1704 for i, d in enumerate(history):
1705 found = False
1706 for unseen_item in unseen:
1707 if matches(unseen_item, d):
1708 found = True
1709 unseen.remove(unseen_item)
1710 break
1711 if not found:
1712 raise Exception("No match found for element %i "
1713 "in history" % (i,))
1714 if unseen:
1715 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001716 except Exception:
1717 for build in self.history:
1718 self.log.error("Completed build: %s" % build)
1719 else:
1720 self.log.error("No completed builds")
1721 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001722
James E. Blair6ac368c2016-12-22 18:07:20 -08001723 def printHistory(self):
1724 """Log the build history.
1725
1726 This can be useful during tests to summarize what jobs have
1727 completed.
1728
1729 """
1730 self.log.debug("Build history:")
1731 for build in self.history:
1732 self.log.debug(build)
1733
James E. Blair59fdbac2015-12-07 17:08:06 -08001734 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001735 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1736
1737 def updateConfigLayout(self, path):
1738 root = os.path.join(self.test_root, "config")
1739 os.makedirs(root)
1740 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1741 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001742- tenant:
1743 name: openstack
1744 source:
1745 gerrit:
1746 config-repos:
1747 - %s
1748 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001749 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001750 self.config.set('zuul', 'tenant_config',
1751 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001752
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001753 def addCommitToRepo(self, project, message, files,
1754 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001755 path = os.path.join(self.upstream_root, project)
1756 repo = git.Repo(path)
1757 repo.head.reference = branch
1758 zuul.merger.merger.reset_repo_to_head(repo)
1759 for fn, content in files.items():
1760 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001761 try:
1762 os.makedirs(os.path.dirname(fn))
1763 except OSError:
1764 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001765 with open(fn, 'w') as f:
1766 f.write(content)
1767 repo.index.add([fn])
1768 commit = repo.index.commit(message)
1769 repo.heads[branch].commit = commit
1770 repo.head.reference = branch
1771 repo.git.clean('-x', '-f', '-d')
1772 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001773 if tag:
1774 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001775
James E. Blair7fc8daa2016-08-08 15:37:15 -07001776 def addEvent(self, connection, event):
1777 """Inject a Fake (Gerrit) event.
1778
1779 This method accepts a JSON-encoded event and simulates Zuul
1780 having received it from Gerrit. It could (and should)
1781 eventually apply to any connection type, but is currently only
1782 used with Gerrit connections. The name of the connection is
1783 used to look up the corresponding server, and the event is
1784 simulated as having been received by all Zuul connections
1785 attached to that server. So if two Gerrit connections in Zuul
1786 are connected to the same Gerrit server, and you invoke this
1787 method specifying the name of one of them, the event will be
1788 received by both.
1789
1790 .. note::
1791
1792 "self.fake_gerrit.addEvent" calls should be migrated to
1793 this method.
1794
1795 :arg str connection: The name of the connection corresponding
1796 to the gerrit server.
1797 :arg str event: The JSON-encoded event.
1798
1799 """
1800 specified_conn = self.connections.connections[connection]
1801 for conn in self.connections.connections.values():
1802 if (isinstance(conn, specified_conn.__class__) and
1803 specified_conn.server == conn.server):
1804 conn.addEvent(event)
1805
James E. Blair3f876d52016-07-22 13:07:14 -07001806
1807class AnsibleZuulTestCase(ZuulTestCase):
1808 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001809 run_ansible = True