blob: db73a8d58bfa6f155f8efbb1b2cd82172f0b15d2 [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
James E. Blair0ef64f82017-02-02 11:25:16 -0800911 try:
912 data, stat = self.client.get(path)
913 data = json.loads(data)
914 data['_oid'] = oid
915 reqs.append(data)
916 except kazoo.exceptions.NoNodeError:
917 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800918 return reqs
919
James E. Blaire18d4602017-01-05 11:17:28 -0800920 def getNodes(self):
921 try:
922 nodeids = self.client.get_children(self.NODE_ROOT)
923 except kazoo.exceptions.NoNodeError:
924 return []
925 nodes = []
926 for oid in sorted(nodeids):
927 path = self.NODE_ROOT + '/' + oid
928 data, stat = self.client.get(path)
929 data = json.loads(data)
930 data['_oid'] = oid
931 try:
932 lockfiles = self.client.get_children(path + '/lock')
933 except kazoo.exceptions.NoNodeError:
934 lockfiles = []
935 if lockfiles:
936 data['_lock'] = True
937 else:
938 data['_lock'] = False
939 nodes.append(data)
940 return nodes
941
James E. Blaira38c28e2017-01-04 10:33:20 -0800942 def makeNode(self, request_id, node_type):
943 now = time.time()
944 path = '/nodepool/nodes/'
945 data = dict(type=node_type,
946 provider='test-provider',
947 region='test-region',
948 az=None,
949 public_ipv4='127.0.0.1',
950 private_ipv4=None,
951 public_ipv6=None,
952 allocated_to=request_id,
953 state='ready',
954 state_time=now,
955 created_time=now,
956 updated_time=now,
957 image_id=None,
958 launcher='fake-nodepool')
959 data = json.dumps(data)
960 path = self.client.create(path, data,
961 makepath=True,
962 sequence=True)
963 nodeid = path.split("/")[-1]
964 return nodeid
965
James E. Blair6ab79e02017-01-06 10:10:17 -0800966 def addFailRequest(self, request):
967 self.fail_requests.add(request['_oid'])
968
James E. Blairdce6cea2016-12-20 16:45:32 -0800969 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800970 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800971 return
972 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800973 oid = request['_oid']
974 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800975
James E. Blair6ab79e02017-01-06 10:10:17 -0800976 if oid in self.fail_requests:
977 request['state'] = 'failed'
978 else:
979 request['state'] = 'fulfilled'
980 nodes = []
981 for node in request['node_types']:
982 nodeid = self.makeNode(oid, node)
983 nodes.append(nodeid)
984 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -0800985
James E. Blaira38c28e2017-01-04 10:33:20 -0800986 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -0800987 path = self.REQUEST_ROOT + '/' + oid
988 data = json.dumps(request)
989 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
990 self.client.set(path, data)
991
992
James E. Blair498059b2016-12-20 13:50:13 -0800993class ChrootedKazooFixture(fixtures.Fixture):
994 def __init__(self):
995 super(ChrootedKazooFixture, self).__init__()
996
997 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
998 if ':' in zk_host:
999 host, port = zk_host.split(':')
1000 else:
1001 host = zk_host
1002 port = None
1003
1004 self.zookeeper_host = host
1005
1006 if not port:
1007 self.zookeeper_port = 2181
1008 else:
1009 self.zookeeper_port = int(port)
1010
1011 def _setUp(self):
1012 # Make sure the test chroot paths do not conflict
1013 random_bits = ''.join(random.choice(string.ascii_lowercase +
1014 string.ascii_uppercase)
1015 for x in range(8))
1016
1017 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1018 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1019
1020 # Ensure the chroot path exists and clean up any pre-existing znodes.
1021 _tmp_client = kazoo.client.KazooClient(
1022 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1023 _tmp_client.start()
1024
1025 if _tmp_client.exists(self.zookeeper_chroot):
1026 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1027
1028 _tmp_client.ensure_path(self.zookeeper_chroot)
1029 _tmp_client.stop()
1030 _tmp_client.close()
1031
1032 self.addCleanup(self._cleanup)
1033
1034 def _cleanup(self):
1035 '''Remove the chroot path.'''
1036 # Need a non-chroot'ed client to remove the chroot path
1037 _tmp_client = kazoo.client.KazooClient(
1038 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1039 _tmp_client.start()
1040 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1041 _tmp_client.stop()
1042
1043
Maru Newby3fe5f852015-01-13 04:22:14 +00001044class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001045 log = logging.getLogger("zuul.test")
1046
1047 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001048 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001049 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1050 try:
1051 test_timeout = int(test_timeout)
1052 except ValueError:
1053 # If timeout value is invalid do not set a timeout.
1054 test_timeout = 0
1055 if test_timeout > 0:
1056 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1057
1058 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1059 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1060 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1061 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1062 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1063 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1064 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1065 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1066 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1067 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001068 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001069 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1070 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001071 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1072 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001073 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1074 log_level = logging.WARNING
1075 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1076 log_level = logging.ERROR
1077 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1078 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001079 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001080 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001081 format='%(asctime)s %(name)-32s '
1082 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001083
James E. Blairdce6cea2016-12-20 16:45:32 -08001084 # NOTE(notmorgan): Extract logging overrides for specific libraries
1085 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1086 # each. This is used to limit the output during test runs from
1087 # libraries that zuul depends on such as gear.
1088 log_defaults_from_env = os.environ.get(
1089 'OS_LOG_DEFAULTS',
1090 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001091
James E. Blairdce6cea2016-12-20 16:45:32 -08001092 if log_defaults_from_env:
1093 for default in log_defaults_from_env.split(','):
1094 try:
1095 name, level_str = default.split('=', 1)
1096 level = getattr(logging, level_str, logging.DEBUG)
1097 self.useFixture(fixtures.FakeLogger(
1098 name=name,
1099 level=level,
1100 format='%(asctime)s %(name)-32s '
1101 '%(levelname)-8s %(message)s'))
1102 except ValueError:
1103 # NOTE(notmorgan): Invalid format of the log default,
1104 # skip and don't try and apply a logger for the
1105 # specified module
1106 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001107
Maru Newby3fe5f852015-01-13 04:22:14 +00001108
1109class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001110 """A test case with a functioning Zuul.
1111
1112 The following class variables are used during test setup and can
1113 be overidden by subclasses but are effectively read-only once a
1114 test method starts running:
1115
1116 :cvar str config_file: This points to the main zuul config file
1117 within the fixtures directory. Subclasses may override this
1118 to obtain a different behavior.
1119
1120 :cvar str tenant_config_file: This is the tenant config file
1121 (which specifies from what git repos the configuration should
1122 be loaded). It defaults to the value specified in
1123 `config_file` but can be overidden by subclasses to obtain a
1124 different tenant/project layout while using the standard main
1125 configuration.
1126
1127 The following are instance variables that are useful within test
1128 methods:
1129
1130 :ivar FakeGerritConnection fake_<connection>:
1131 A :py:class:`~tests.base.FakeGerritConnection` will be
1132 instantiated for each connection present in the config file
1133 and stored here. For instance, `fake_gerrit` will hold the
1134 FakeGerritConnection object for a connection named `gerrit`.
1135
1136 :ivar FakeGearmanServer gearman_server: An instance of
1137 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1138 server that all of the Zuul components in this test use to
1139 communicate with each other.
1140
1141 :ivar RecordingLaunchServer launch_server: An instance of
1142 :py:class:`~tests.base.RecordingLaunchServer` which is the
1143 Ansible launch server used to run jobs for this test.
1144
1145 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1146 representing currently running builds. They are appended to
1147 the list in the order they are launched, and removed from this
1148 list upon completion.
1149
1150 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1151 objects representing completed builds. They are appended to
1152 the list in the order they complete.
1153
1154 """
1155
James E. Blair83005782015-12-11 14:46:03 -08001156 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001157 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001158
1159 def _startMerger(self):
1160 self.merge_server = zuul.merger.server.MergeServer(self.config,
1161 self.connections)
1162 self.merge_server.start()
1163
Maru Newby3fe5f852015-01-13 04:22:14 +00001164 def setUp(self):
1165 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001166
1167 self.setupZK()
1168
James E. Blair97d902e2014-08-21 13:25:56 -07001169 if USE_TEMPDIR:
1170 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001171 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1172 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001173 else:
1174 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.test_root = os.path.join(tmp_root, "zuul-test")
1176 self.upstream_root = os.path.join(self.test_root, "upstream")
1177 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001178 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001179
1180 if os.path.exists(self.test_root):
1181 shutil.rmtree(self.test_root)
1182 os.makedirs(self.test_root)
1183 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001184 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001185
1186 # Make per test copy of Configuration.
1187 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001188 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001189 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001190 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001191 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001192 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001193
1194 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001195 # TODOv3(jeblair): remove these and replace with new git
1196 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001197 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001198 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001199 self.init_repo("org/project5")
1200 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001201 self.init_repo("org/one-job-project")
1202 self.init_repo("org/nonvoting-project")
1203 self.init_repo("org/templated-project")
1204 self.init_repo("org/layered-project")
1205 self.init_repo("org/node-project")
1206 self.init_repo("org/conflict-project")
1207 self.init_repo("org/noop-project")
1208 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001209 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001210
1211 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001212 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1213 # see: https://github.com/jsocol/pystatsd/issues/61
1214 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001215 os.environ['STATSD_PORT'] = str(self.statsd.port)
1216 self.statsd.start()
1217 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001218 reload_module(statsd)
1219 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001220
1221 self.gearman_server = FakeGearmanServer()
1222
1223 self.config.set('gearman', 'port', str(self.gearman_server.port))
1224
James E. Blaire511d2f2016-12-08 15:22:26 -08001225 gerritsource.GerritSource.replication_timeout = 1.5
1226 gerritsource.GerritSource.replication_retry_interval = 0.5
1227 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001228
Joshua Hesketh352264b2015-08-11 23:42:08 +10001229 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001230
1231 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1232 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001233
Clark Boylanb640e052014-04-03 16:41:46 -07001234 self.swift = zuul.lib.swift.Swift(self.config)
1235
Jan Hruban6b71aff2015-10-22 16:58:08 +02001236 self.event_queues = [
1237 self.sched.result_event_queue,
1238 self.sched.trigger_event_queue
1239 ]
1240
James E. Blairfef78942016-03-11 16:28:56 -08001241 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001242 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001243
Clark Boylanb640e052014-04-03 16:41:46 -07001244 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001245 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001246 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001247 return FakeURLOpener(self.upstream_root, *args, **kw)
1248
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001249 old_urlopen = urllib.request.urlopen
1250 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001251
James E. Blair3f876d52016-07-22 13:07:14 -07001252 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001253
James E. Blaire1767bc2016-08-02 10:00:27 -07001254 self.launch_server = RecordingLaunchServer(
1255 self.config, self.connections, _run_ansible=self.run_ansible)
1256 self.launch_server.start()
1257 self.history = self.launch_server.build_history
1258 self.builds = self.launch_server.running_builds
1259
1260 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001261 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001262 self.merge_client = zuul.merger.client.MergeClient(
1263 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001264 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001265 self.zk = zuul.zk.ZooKeeper()
1266 self.zk.connect([self.zk_config])
1267
1268 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1269 self.zk_config.port,
1270 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001271
James E. Blaire1767bc2016-08-02 10:00:27 -07001272 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001273 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001274 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001275 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001276
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001277 self.webapp = zuul.webapp.WebApp(
1278 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001279 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001280
1281 self.sched.start()
1282 self.sched.reconfigure(self.config)
1283 self.sched.resume()
1284 self.webapp.start()
1285 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001286 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001287
Clark Boylanb640e052014-04-03 16:41:46 -07001288 self.addCleanup(self.shutdown)
1289
James E. Blaire18d4602017-01-05 11:17:28 -08001290 def tearDown(self):
1291 super(ZuulTestCase, self).tearDown()
1292 self.assertFinalState()
1293
James E. Blairfef78942016-03-11 16:28:56 -08001294 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001295 # Set up gerrit related fakes
1296 # Set a changes database so multiple FakeGerrit's can report back to
1297 # a virtual canonical database given by the configured hostname
1298 self.gerrit_changes_dbs = {}
1299
1300 def getGerritConnection(driver, name, config):
1301 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1302 con = FakeGerritConnection(driver, name, config,
1303 changes_db=db,
1304 upstream_root=self.upstream_root)
1305 self.event_queues.append(con.event_queue)
1306 setattr(self, 'fake_' + name, con)
1307 return con
1308
1309 self.useFixture(fixtures.MonkeyPatch(
1310 'zuul.driver.gerrit.GerritDriver.getConnection',
1311 getGerritConnection))
1312
1313 # Set up smtp related fakes
Joshua Hesketh352264b2015-08-11 23:42:08 +10001314 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001315
Joshua Hesketh352264b2015-08-11 23:42:08 +10001316 def FakeSMTPFactory(*args, **kw):
1317 args = [self.smtp_messages] + list(args)
1318 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001319
Joshua Hesketh352264b2015-08-11 23:42:08 +10001320 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001321
James E. Blaire511d2f2016-12-08 15:22:26 -08001322 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001323 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001324 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001325
James E. Blair83005782015-12-11 14:46:03 -08001326 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001327 # This creates the per-test configuration object. It can be
1328 # overriden by subclasses, but should not need to be since it
1329 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001330 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001331 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001332 if hasattr(self, 'tenant_config_file'):
1333 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001334 git_path = os.path.join(
1335 os.path.dirname(
1336 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1337 'git')
1338 if os.path.exists(git_path):
1339 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001340 project = reponame.replace('_', '/')
1341 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001342 os.path.join(git_path, reponame))
1343
James E. Blair498059b2016-12-20 13:50:13 -08001344 def setupZK(self):
1345 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001346 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1347 self.zk_chroot_fixture.zookeeper_host,
1348 self.zk_chroot_fixture.zookeeper_port,
1349 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001350
James E. Blair96c6bf82016-01-15 16:20:40 -08001351 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001352 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001353
1354 files = {}
1355 for (dirpath, dirnames, filenames) in os.walk(source_path):
1356 for filename in filenames:
1357 test_tree_filepath = os.path.join(dirpath, filename)
1358 common_path = os.path.commonprefix([test_tree_filepath,
1359 source_path])
1360 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1361 with open(test_tree_filepath, 'r') as f:
1362 content = f.read()
1363 files[relative_filepath] = content
1364 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001365 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001366
James E. Blaire18d4602017-01-05 11:17:28 -08001367 def assertNodepoolState(self):
1368 # Make sure that there are no pending requests
1369
1370 requests = self.fake_nodepool.getNodeRequests()
1371 self.assertEqual(len(requests), 0)
1372
1373 nodes = self.fake_nodepool.getNodes()
1374 for node in nodes:
1375 self.assertFalse(node['_lock'], "Node %s is locked" %
1376 (node['_oid'],))
1377
Clark Boylanb640e052014-04-03 16:41:46 -07001378 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001379 # Make sure that git.Repo objects have been garbage collected.
1380 repos = []
1381 gc.collect()
1382 for obj in gc.get_objects():
1383 if isinstance(obj, git.Repo):
1384 repos.append(obj)
1385 self.assertEqual(len(repos), 0)
1386 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001387 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001388 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001389 for tenant in self.sched.abide.tenants.values():
1390 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001391 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001392 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001393
1394 def shutdown(self):
1395 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001397 self.merge_server.stop()
1398 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001399 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001400 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.sched.stop()
1402 self.sched.join()
1403 self.statsd.stop()
1404 self.statsd.join()
1405 self.webapp.stop()
1406 self.webapp.join()
1407 self.rpc.stop()
1408 self.rpc.join()
1409 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001410 self.fake_nodepool.stop()
1411 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001412 threads = threading.enumerate()
1413 if len(threads) > 1:
1414 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001415 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001416
1417 def init_repo(self, project):
1418 parts = project.split('/')
1419 path = os.path.join(self.upstream_root, *parts[:-1])
1420 if not os.path.exists(path):
1421 os.makedirs(path)
1422 path = os.path.join(self.upstream_root, project)
1423 repo = git.Repo.init(path)
1424
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001425 with repo.config_writer() as config_writer:
1426 config_writer.set_value('user', 'email', 'user@example.com')
1427 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001428
Clark Boylanb640e052014-04-03 16:41:46 -07001429 repo.index.commit('initial commit')
1430 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001431
James E. Blair97d902e2014-08-21 13:25:56 -07001432 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001433 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001434 repo.git.clean('-x', '-f', '-d')
1435
James E. Blair97d902e2014-08-21 13:25:56 -07001436 def create_branch(self, project, branch):
1437 path = os.path.join(self.upstream_root, project)
1438 repo = git.Repo.init(path)
1439 fn = os.path.join(path, 'README')
1440
1441 branch_head = repo.create_head(branch)
1442 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001443 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001444 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001445 f.close()
1446 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001447 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001448
James E. Blair97d902e2014-08-21 13:25:56 -07001449 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001450 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001451 repo.git.clean('-x', '-f', '-d')
1452
Sachi King9f16d522016-03-16 12:20:45 +11001453 def create_commit(self, project):
1454 path = os.path.join(self.upstream_root, project)
1455 repo = git.Repo(path)
1456 repo.head.reference = repo.heads['master']
1457 file_name = os.path.join(path, 'README')
1458 with open(file_name, 'a') as f:
1459 f.write('creating fake commit\n')
1460 repo.index.add([file_name])
1461 commit = repo.index.commit('Creating a fake commit')
1462 return commit.hexsha
1463
James E. Blairb8c16472015-05-05 14:55:26 -07001464 def orderedRelease(self):
1465 # Run one build at a time to ensure non-race order:
1466 while len(self.builds):
1467 self.release(self.builds[0])
1468 self.waitUntilSettled()
1469
Clark Boylanb640e052014-04-03 16:41:46 -07001470 def release(self, job):
1471 if isinstance(job, FakeBuild):
1472 job.release()
1473 else:
1474 job.waiting = False
1475 self.log.debug("Queued job %s released" % job.unique)
1476 self.gearman_server.wakeConnections()
1477
1478 def getParameter(self, job, name):
1479 if isinstance(job, FakeBuild):
1480 return job.parameters[name]
1481 else:
1482 parameters = json.loads(job.arguments)
1483 return parameters[name]
1484
Clark Boylanb640e052014-04-03 16:41:46 -07001485 def haveAllBuildsReported(self):
1486 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001487 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001488 return False
1489 # Find out if every build that the worker has completed has been
1490 # reported back to Zuul. If it hasn't then that means a Gearman
1491 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001492 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001493 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001494 if not zbuild:
1495 # It has already been reported
1496 continue
1497 # It hasn't been reported yet.
1498 return False
1499 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001500 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001501 if connection.state == 'GRAB_WAIT':
1502 return False
1503 return True
1504
1505 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001506 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001507 for build in builds:
1508 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001509 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001510 for j in conn.related_jobs.values():
1511 if j.unique == build.uuid:
1512 client_job = j
1513 break
1514 if not client_job:
1515 self.log.debug("%s is not known to the gearman client" %
1516 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001517 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001518 if not client_job.handle:
1519 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001520 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001521 server_job = self.gearman_server.jobs.get(client_job.handle)
1522 if not server_job:
1523 self.log.debug("%s is not known to the gearman server" %
1524 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001525 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001526 if not hasattr(server_job, 'waiting'):
1527 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001528 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001529 if server_job.waiting:
1530 continue
James E. Blair17302972016-08-10 16:11:42 -07001531 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001532 self.log.debug("%s has not reported start" % build)
1533 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001534 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001535 if worker_build:
1536 if worker_build.isWaiting():
1537 continue
1538 else:
1539 self.log.debug("%s is running" % worker_build)
1540 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001541 else:
James E. Blair962220f2016-08-03 11:22:38 -07001542 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001543 return False
1544 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001545
James E. Blairdce6cea2016-12-20 16:45:32 -08001546 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001547 if self.fake_nodepool.paused:
1548 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001549 if self.sched.nodepool.requests:
1550 return False
1551 return True
1552
Jan Hruban6b71aff2015-10-22 16:58:08 +02001553 def eventQueuesEmpty(self):
1554 for queue in self.event_queues:
1555 yield queue.empty()
1556
1557 def eventQueuesJoin(self):
1558 for queue in self.event_queues:
1559 queue.join()
1560
Clark Boylanb640e052014-04-03 16:41:46 -07001561 def waitUntilSettled(self):
1562 self.log.debug("Waiting until settled...")
1563 start = time.time()
1564 while True:
James E. Blair71932482017-02-02 11:29:07 -08001565 if time.time() - start > 20:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001566 self.log.error("Timeout waiting for Zuul to settle")
1567 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001568 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001569 self.log.error(" %s: %s" % (queue, queue.empty()))
1570 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001571 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001572 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001573 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001574 self.log.error("All requests completed: %s" %
1575 (self.areAllNodeRequestsComplete(),))
1576 self.log.error("Merge client jobs: %s" %
1577 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001578 raise Exception("Timeout waiting for Zuul to settle")
1579 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001580
James E. Blaire1767bc2016-08-02 10:00:27 -07001581 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001582 # have all build states propogated to zuul?
1583 if self.haveAllBuildsReported():
1584 # Join ensures that the queue is empty _and_ events have been
1585 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001586 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001587 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001588 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001589 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001590 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001591 self.areAllBuildsWaiting() and
1592 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001593 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001594 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001595 self.log.debug("...settled.")
1596 return
1597 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001598 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001599 self.sched.wake_event.wait(0.1)
1600
1601 def countJobResults(self, jobs, result):
1602 jobs = filter(lambda x: x.result == result, jobs)
1603 return len(jobs)
1604
James E. Blair96c6bf82016-01-15 16:20:40 -08001605 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001606 for job in self.history:
1607 if (job.name == name and
1608 (project is None or
1609 job.parameters['ZUUL_PROJECT'] == project)):
1610 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001611 raise Exception("Unable to find job %s in history" % name)
1612
1613 def assertEmptyQueues(self):
1614 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001615 for tenant in self.sched.abide.tenants.values():
1616 for pipeline in tenant.layout.pipelines.values():
1617 for queue in pipeline.queues:
1618 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001619 print('pipeline %s queue %s contents %s' % (
1620 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001621 self.assertEqual(len(queue.queue), 0,
1622 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001623
1624 def assertReportedStat(self, key, value=None, kind=None):
1625 start = time.time()
1626 while time.time() < (start + 5):
1627 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001628 k, v = stat.split(':')
1629 if key == k:
1630 if value is None and kind is None:
1631 return
1632 elif value:
1633 if value == v:
1634 return
1635 elif kind:
1636 if v.endswith('|' + kind):
1637 return
1638 time.sleep(0.1)
1639
Clark Boylanb640e052014-04-03 16:41:46 -07001640 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001641
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001642 def assertBuilds(self, builds):
1643 """Assert that the running builds are as described.
1644
1645 The list of running builds is examined and must match exactly
1646 the list of builds described by the input.
1647
1648 :arg list builds: A list of dictionaries. Each item in the
1649 list must match the corresponding build in the build
1650 history, and each element of the dictionary must match the
1651 corresponding attribute of the build.
1652
1653 """
James E. Blair3158e282016-08-19 09:34:11 -07001654 try:
1655 self.assertEqual(len(self.builds), len(builds))
1656 for i, d in enumerate(builds):
1657 for k, v in d.items():
1658 self.assertEqual(
1659 getattr(self.builds[i], k), v,
1660 "Element %i in builds does not match" % (i,))
1661 except Exception:
1662 for build in self.builds:
1663 self.log.error("Running build: %s" % build)
1664 else:
1665 self.log.error("No running builds")
1666 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001667
James E. Blairb536ecc2016-08-31 10:11:42 -07001668 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001669 """Assert that the completed builds are as described.
1670
1671 The list of completed builds is examined and must match
1672 exactly the list of builds described by the input.
1673
1674 :arg list history: A list of dictionaries. Each item in the
1675 list must match the corresponding build in the build
1676 history, and each element of the dictionary must match the
1677 corresponding attribute of the build.
1678
James E. Blairb536ecc2016-08-31 10:11:42 -07001679 :arg bool ordered: If true, the history must match the order
1680 supplied, if false, the builds are permitted to have
1681 arrived in any order.
1682
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001683 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001684 def matches(history_item, item):
1685 for k, v in item.items():
1686 if getattr(history_item, k) != v:
1687 return False
1688 return True
James E. Blair3158e282016-08-19 09:34:11 -07001689 try:
1690 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001691 if ordered:
1692 for i, d in enumerate(history):
1693 if not matches(self.history[i], d):
1694 raise Exception(
1695 "Element %i in history does not match" % (i,))
1696 else:
1697 unseen = self.history[:]
1698 for i, d in enumerate(history):
1699 found = False
1700 for unseen_item in unseen:
1701 if matches(unseen_item, d):
1702 found = True
1703 unseen.remove(unseen_item)
1704 break
1705 if not found:
1706 raise Exception("No match found for element %i "
1707 "in history" % (i,))
1708 if unseen:
1709 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001710 except Exception:
1711 for build in self.history:
1712 self.log.error("Completed build: %s" % build)
1713 else:
1714 self.log.error("No completed builds")
1715 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001716
James E. Blair6ac368c2016-12-22 18:07:20 -08001717 def printHistory(self):
1718 """Log the build history.
1719
1720 This can be useful during tests to summarize what jobs have
1721 completed.
1722
1723 """
1724 self.log.debug("Build history:")
1725 for build in self.history:
1726 self.log.debug(build)
1727
James E. Blair59fdbac2015-12-07 17:08:06 -08001728 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001729 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1730
1731 def updateConfigLayout(self, path):
1732 root = os.path.join(self.test_root, "config")
1733 os.makedirs(root)
1734 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1735 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001736- tenant:
1737 name: openstack
1738 source:
1739 gerrit:
1740 config-repos:
1741 - %s
1742 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001743 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001744 self.config.set('zuul', 'tenant_config',
1745 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001746
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001747 def addCommitToRepo(self, project, message, files,
1748 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001749 path = os.path.join(self.upstream_root, project)
1750 repo = git.Repo(path)
1751 repo.head.reference = branch
1752 zuul.merger.merger.reset_repo_to_head(repo)
1753 for fn, content in files.items():
1754 fn = os.path.join(path, fn)
1755 with open(fn, 'w') as f:
1756 f.write(content)
1757 repo.index.add([fn])
1758 commit = repo.index.commit(message)
1759 repo.heads[branch].commit = commit
1760 repo.head.reference = branch
1761 repo.git.clean('-x', '-f', '-d')
1762 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001763 if tag:
1764 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001765
James E. Blair7fc8daa2016-08-08 15:37:15 -07001766 def addEvent(self, connection, event):
1767 """Inject a Fake (Gerrit) event.
1768
1769 This method accepts a JSON-encoded event and simulates Zuul
1770 having received it from Gerrit. It could (and should)
1771 eventually apply to any connection type, but is currently only
1772 used with Gerrit connections. The name of the connection is
1773 used to look up the corresponding server, and the event is
1774 simulated as having been received by all Zuul connections
1775 attached to that server. So if two Gerrit connections in Zuul
1776 are connected to the same Gerrit server, and you invoke this
1777 method specifying the name of one of them, the event will be
1778 received by both.
1779
1780 .. note::
1781
1782 "self.fake_gerrit.addEvent" calls should be migrated to
1783 this method.
1784
1785 :arg str connection: The name of the connection corresponding
1786 to the gerrit server.
1787 :arg str event: The JSON-encoded event.
1788
1789 """
1790 specified_conn = self.connections.connections[connection]
1791 for conn in self.connections.connections.values():
1792 if (isinstance(conn, specified_conn.__class__) and
1793 specified_conn.server == conn.server):
1794 conn.addEvent(event)
1795
James E. Blair3f876d52016-07-22 13:07:14 -07001796
1797class AnsibleZuulTestCase(ZuulTestCase):
1798 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001799 run_ansible = True